# Web Analytics script

The Web Analytics script (`analytics.js`) counts visitors, page views, traffic sources, events and ecommerce on your site. It feeds Web Analytics. It finds ecommerce events on its own on Shopify, WooCommerce, Magento and Squarespace.

For the install flow and the install check, see [Tracking Scripts](/connect/tracking-scripts).

## The Web Analytics snippet [#the-web-analytics-snippet]

Add this tag to the `<head>` of every page:

```html
<script
  src="https://analytics.vitalsentinel.com/analytics.js"
  data-key="YOUR_TRACKING_ID"
  async
></script>
```

The script is small and loads with `async`, so it does not block your page. Copy the snippet with your real tracking ID from domain **Settings** > **Tracking** > **Web Analytics** > **Install snippet**. The Web Analytics script and the RUM script use the same tracking ID.

The **Install snippet** has two forms: **HTML** ("Paste into your site's `<head>`.") and **Google Tag Manager** ("Paste into a Custom HTML tag in GTM.").

## Data attributes [#data-attributes]

The Web Analytics script reads these `data-*` attributes on its script tag:

| Attribute | Default | What it does |
|-----------|---------|--------------|
| `data-key` | required | The tracking ID of your domain. |
| `data-storage` | `none` | The storage level: `none`, `session` or `persistent`. See [Storage mode](#storage-mode). |
| `data-spa` | `false` | Page tracking in single-page apps: `true`, `false` or `auto`. See [SPA support](#spa-support). |
| `data-cookie-domain` | none | The cookie scope for sites on more than one subdomain, with `persistent` storage. |
| `data-debug` | `false` | Writes log messages to the browser console. |

## Web Analytics options in Settings > Tracking [#web-analytics-options-in-settings--tracking]

The **Web Analytics** card in domain **Settings** > **Tracking** builds the snippet for you. Open **Options** to change these settings:

| Option | Choices | Snippet attribute |
|--------|---------|-------------------|
| **Storage mode** | **None** (default), **Session** or **Persistent** | `data-storage` for **Session** or **Persistent** |
| **Single page app (SPA) mode** | **Disabled** (default), **Enabled** or **Auto-detect** | `data-spa` for **Enabled** or **Auto-detect** |
| **Debug mode** > **Enable console logging** | Off (default) or on | `data-debug="true"` when on |

VitalSentinel saves **Storage mode** and **Single page app (SPA) mode** for everyone on the domain. Debug mode stays in your browser only ("For development only. Kept in this browser.").

<Callout type="warn">
  A change to these options does not change the snippet that is live on your site. Copy the snippet again and put it on your site after each change.
</Callout>

### Page titles, search terms and article authors [#page-titles-search-terms-and-article-authors]

The section **Page titles, search terms and article authors** in **Options** has two checkboxes. They are not part of the snippet. VitalSentinel applies them when the data arrives, so they save at once and need no change to your site.

- **Do not store page titles or site-search terms**: VitalSentinel stops storing both within about a minute. New traffic then shows pages by URL only, and you lose what visitors searched for. Use it if your pages or search box can show health, legal or other sensitive details about a visitor.
- **Do not store article authors**: VitalSentinel stops storing the byline within about a minute. The Authors report then stops filling in.

Text that VitalSentinel collected before stays in your reports until it ages out after three years. Only workspace owners and admins see these checkboxes.

## Storage mode [#storage-mode]

The **Storage mode** controls what the script saves on the visitor's device. Select the lowest level that gives you the data you need. Lower levels collect less personal data and need less consent.

The script accepts only `session` and `persistent`. A missing attribute, or any other value, falls back to `none`. A typo such as `persistant` also falls back to `none`.

| Level | What it saves | Consent |
|-------|---------------|---------|
| **None** (default) | Nothing: no cookies, no local storage, no session storage. No device details are sent. | No consent banner needed for storage. |
| **Session** | One entry in the browser's session storage, which the browser clears when the visitor closes it. No cookies. | Requires visitor consent before you enable it. |
| **Persistent** | A few entries in the browser's local storage, so you can recognize returning visitors. Also collects device details such as screen and window size. | Requires visitor consent before you enable it. |

When you select **Session** or **Persistent**, the card says "You're the data controller for this snippet."

### What the None mode does not collect [#what-the-none-mode-does-not-collect]

In the **None** mode, VitalSentinel joins the page views of one visit into one session on its servers. It keeps nothing on the device. The result is pseudonymous, not anonymous.

The **Not collected in this mode** line lists what you do not get:

- **Returning visitors**: "We cannot tell a first visit from a repeat one, so New visitors reads zero and each day's visitors are counted fresh."
- **First campaign**: the campaign that first brought a visitor "cannot be remembered between visits."
- **Screen size**: no screen size, window size, pixel ratio or touch support breakdown.
- **Connection speed**: no split between visitors on fast and slow connections.
- **Shopify cart items**: see [Ecommerce tracking](#ecommerce-tracking).
- **Exact site-search terms**: "Emails and long number runs are replaced before storage, which also affects long product codes."

You still get visitors, sessions, page views, entry and exit pages, traffic sources, campaigns, events and conversions. You also get the country, browser, operating system, device type and language.

### Session and Persistent storage details [#session-and-persistent-storage-details]

**Session** collects the same data as **None**, and also Shopify cart items. The main difference is where the visit is kept: in **None**, VitalSentinel joins the page views on its servers. In **Session**, the script keeps the session ID in the browser tab, with a 30-minute timeout. Thus, **Session** needs consent. With **Persistent**, a visitor ID recognizes returning visitors, and first-touch attribution works. VitalSentinel keeps the visitor ID for 180 days.

**Persistent** is the only level where the script reads device details: screen size, window size, pixel ratio, touch support and connection quality. The snippet from the app writes no cookies. The script writes a first-party cookie only if you add `data-cookie-domain`. That cookie also expires after 180 days.

### Cookie domain for subdomains [#cookie-domain-for-subdomains]

If your site uses more than one subdomain, such as `www.example.com` and `app.example.com`, and you use `persistent` storage, add `data-cookie-domain="example.com"`. The script then recognizes the same visitor on both subdomains. Without it, each subdomain tracks visitors separately.

```html
<script
  src="https://analytics.vitalsentinel.com/analytics.js"
  data-key="YOUR_TRACKING_ID"
  data-storage="persistent"
  data-cookie-domain="example.com"
  async
></script>
```

## Consent [#consent]

<Callout type="warn">
  `session` and `persistent` both store data on the visitor's device. Get the visitor's consent before you turn on either level. `none` stores nothing, so it needs no consent banner for storage.
</Callout>

### Grant and withdraw consent from your banner [#grant-and-withdraw-consent-from-your-banner]

Keep **None** in the snippet and load the snippet first, on every page and for every visitor. The script stores nothing, so it counts the visit from the first page load. Then call the script from your consent banner. The section **Using a consent banner?** in the options shows these calls:

```javascript
// Visitor accepted
window.VitalSentinelAnalytics.grantConsent('persistent');

// Visitor rejected, or withdrew a previous acceptance
window.VitalSentinelAnalytics.revokeConsent();
```

Each call changes the script on the page now. You do not need to change the snippet or reload the page.

### How grant and revoke work [#how-grant-and-revoke-work]

The script does not remember the consent level between page loads. Your consent tool must call `grantConsent()` on every page load.

`revokeConsent()` sends the events that are already in the queue. It then goes back to `none`, clears the device and network details, and deletes the identifiers that it stored. Events that the script collected before still go out with the level that they had.

To read the current level, call `getConsentLevel()`. It returns `none`, `session` or `persistent`.

### The built-in consent event [#the-built-in-consent-event]

The script also listens for the `vs-analytics-consent` browser event. Your banner can send it in place of a direct call:

```javascript
window.dispatchEvent(new CustomEvent('vs-analytics-consent', {
  detail: { level: 'persistent' }
}));
```

### Do Not Track and Global Privacy Control [#do-not-track-and-global-privacy-control]

The script reads the Do Not Track and Global Privacy Control signals of the browser. Any value that is present and is not `0`, `false` or `unspecified` counts as an opt-out. For these visitors:

- The script reads and writes no storage, whatever `data-storage` says.
- VitalSentinel keeps only the country. The region, the city and the time zone stay empty.
- No engagement measurement runs: no scroll depth, active time, engagement score or rage clicks.
- The script still counts the page view, so the traffic stays in your reports.

If the visitor accepts your consent banner, call `grantConsent()`. The visitor's own choice wins over the browser signal, so the script uses the level from the call. A level that you write in the snippet does not win over the signal. While the grant applies, each batch that the script sends carries `cg: 1`. Then the VitalSentinel servers also ignore the `DNT` and `Sec-GPC` headers for that visitor.

To remember the grant, the script keeps the key `vs_cg`: in session storage for `session`, in local storage for `persistent`. The key lasts 180 days from the latest `grantConsent()` call. On a later page load, the script reads this key only if `data-storage` in the snippet is `session` or `persistent`. If the snippet keeps `none`, your banner must call `grantConsent()` again on each page load. `revokeConsent()` deletes the key, and the browser signal applies again.

Engagement measurement starts when the grant call comes. If the visitor withdraws consent, `revokeConsent()` stops it again.

### Opt out and opt in [#opt-out-and-opt-in]

```javascript
// Stop all tracking on this page load
window.VitalSentinelAnalytics.optOut();

// Start tracking again
window.VitalSentinelAnalytics.optIn();
```

`optOut()` applies to the current page load only. Call it again on each page load.

## What the Web Analytics script collects [#what-the-web-analytics-script-collects]

### Page views and sessions [#page-views-and-sessions]

- URL and page title
- Referrer
- Time on page and scroll depth
- The article author, on pages that publish one (see [Authors](#article-authors))
- A session ID (pseudonymous), pages per session and session duration
- A visitor ID, only with `persistent` storage

A page that the browser restores from its back/forward cache counts as a page view. A return to a listing page with the Back button is a real view of that page. The engagement timer starts again for the new view.

### Traffic sources [#traffic-sources]

- UTM parameters: source, medium, campaign, term and content
- The referrer domain, and the referrer path without the query string
- The traffic source type, which VitalSentinel finds on its servers: direct, search, social, referral, email, newsletter, paid, paid search, paid social, AI search or internal
- First-touch attribution, only with `persistent` storage

### Visitor behavior [#visitor-behavior]

- Scroll depth milestones: 25%, 50%, 75%, 90% and 100%
- Time on page, active and total
- Rage clicks: three or more clicks within one second and 100 pixels
- Clicks on outbound links
- File downloads
- Site-search queries

Before VitalSentinel stores a search term, it replaces email addresses, JWTs, phone numbers, card numbers, IBANs, national ID numbers and long runs of digits. It does this in every storage mode. Short product codes such as `ABC-12345` stay as they are.

### Device information [#device-information]

With `session` or `persistent` storage, or after `grantConsent()`, the script reads the screen resolution, window size, device pixel ratio, touch support and connection type.

In every mode, VitalSentinel finds the language, browser, operating system and device type on its servers. It uses the request headers that every browser sends. From the IP address, it finds the country, the region, the city and the time zone. It also marks traffic from data centers and hosting networks, so that it can remove bots. It does not store the IP address. The region, the city and the time zone do not show in the reports or the API yet.

VitalSentinel also makes a visitor key that changes every day.

## Custom events [#custom-events]

### Track an event with JavaScript [#track-an-event-with-javascript]

```javascript
window.VitalSentinelAnalytics.track('signup', {
  plan: 'premium',
  source: 'homepage'
});
```

The JavaScript API is on `window.VitalSentinelAnalytics`. The shorter name `window.VSAnalytics` is the same object.

### Track an event with data attributes [#track-an-event-with-data-attributes]

You can track clicks without JavaScript. Add `data-vs-event` with the event name to an element. The script sends every other `data-vs-*` attribute as a property of the event.

```html
<button
  data-vs-event="button_click"
  data-vs-button-id="cta-header"
>
  Sign Up Now
</button>
```

When a visitor clicks the element, the script sends the event.

### Add properties to all events [#add-properties-to-all-events]

```javascript
window.VitalSentinelAnalytics.setGlobalProperties({
  environment: 'production',
  appVersion: '2.1.0'
});
```

The script adds these properties to all events after the call.

### Other API methods [#other-api-methods]

- `trackPageView({ path, title })` sends a page view by hand.
- `flush()` sends all queued events now.
- `identify()` stays in the API, so old code on your site continues to work. It stores nothing and sends nothing, because VitalSentinel does not identify persons.

## Conversions [#conversions]

A conversion goal counts the visits that do one thing at least once. You set up goals in Web Analytics, not in the script. In **Web Analytics** > **Conversions**, click **Add a goal**. In **A visit converts when**, select one of these:

- **A page is viewed**: enter the page address. Select **Is exactly** or **Starts with**.
- **A custom event is sent**: enter the **Event name**. This is "The name your site sends with the tracking script", the first argument of `track()` or the value of `data-vs-event`.

You can also enter **What one conversion is worth**. See [Conversions](/web-analytics/conversions).

## Ecommerce tracking [#ecommerce-tracking]

The Web Analytics script finds ecommerce events on its own on these platforms:

- Shopify
- WooCommerce
- Magento
- Squarespace

On Shopify and Squarespace, orders need one more step. On Shopify, add the custom pixel from VitalSentinel. On Squarespace, paste the snippet in the order page fields. See [Shopify](/connect/platforms#shopify-ecommerce-tracking) and [Squarespace](/connect/platforms#squarespace-ecommerce-tracking).

### Ecommerce events [#ecommerce-events]

| Event | When it is sent |
|-------|-----------------|
| `product_view` | A visitor views a product page. |
| `add_to_cart` | A visitor adds an item to the cart. |
| `view_cart` | A visitor views the cart page. |
| `begin_checkout` | A visitor starts the checkout. |
| `purchase` | A visitor completes an order. |

For each event, the script sends the product ID and SKU, the product name and category, the price and quantity, and the cart total. It also sends the currency of your store, and the order ID for a purchase.

### Currency [#currency]

The script reads the currency from your store. It never guesses a currency. It looks for the currency in this order:

1. The currency code that your shop platform puts on the page, for example `EUR`.
2. The currency in the price meta tags or the product data of the page.
3. A currency symbol in the price that has only one meaning, for example **€**, **£**, **CHF**, **R$**, **A$** or **US$**.

A symbol with more than one meaning gives no currency. Examples are a plain **$**, a plain **¥**, **kr** and **Rp**. If the script finds no currency, it sends the event without one, and the reports use the currency of the domain. Set your currency in domain **Settings** > **Site setup** > **Currency**, so that the revenue figures always have a currency.

### Platform notes [#platform-notes]

- **Shopify**: the script reads the cart from the visitor's own cart, which is on the visitor's device. Thus, it reads the cart only with `session` or `persistent` storage. With `none`, it still counts `view_cart`, but without items, quantities or values. `product_view` and `add_to_cart` work in every mode. The custom pixel sends `begin_checkout` and `purchase` without items, and it does not use the storage mode.
- **Magento, WooCommerce and Squarespace**: the script reads the cart from the page, so the storage mode does not change what they send.
- **Squarespace orders**: the script reads orders from the Squarespace order confirmation, when the snippet is in the order page fields. It records an order only when it can read an order ID. Without an ID, VitalSentinel cannot prevent double counts, so it drops the order.

See [Ecommerce](/web-analytics/ecommerce) for the reports.

## Article authors [#article-authors]

On article pages, the script reads the author from the structured data that the page publishes. It reads a JSON-LD article node, or a `<meta name="author">` tag on a page that also says that it is an article.

The script reads nothing from CSS class names. A page that does not say that it is an article sends no author. Examples are a homepage or a category list. Co-authors stay together as one byline.

The author data feeds the Authors report. To stop it, tick **Do not store article authors** in the Web Analytics options. See [Authors](/web-analytics/authors).

## SPA support [#spa-support]

A single-page app (SPA) changes the page in the browser without a full page load. SPA tracking is off by default (`data-spa="false"`): the script records only the first page load. In the options, this is **Disabled** ("Traditional multi-page navigation (default)").

To track page changes in an SPA, select **Enabled** ("Single-page app – track in-app route changes as pageviews"), or set `data-spa="true"`. The script then finds page changes from the History API, the popstate and hashchange events, and the Navigation API in modern browsers. A change of only the `#` part of an address on the same path does not count as a new page view. If your site uses a hash-based router, call `trackPageView()` on each route change.

**Auto-detect** (`data-spa="auto"`) is "Heuristic detection (legacy – not recommended)". It can give wrong results on server-rendered sites.

### Track page views by hand [#track-page-views-by-hand]

```javascript
window.VitalSentinelAnalytics.trackPageView({
  path: '/new-page',
  title: 'New Page Title'
});
```

## Tracking IDs and hostnames [#tracking-ids-and-hostnames]

A tracking ID belongs to one domain. Web Analytics accepts a hit only from the hostname of that domain. VitalSentinel discards a hit from any other host before it gets to your reports.

This rule uses the exact host. If your workspace tracks `example.com`, then `staging.example.com`, `docs.example.com` and `shop.example.com` are different sites, and VitalSentinel drops their hits.

| Tracked domain | Hit from | Result |
|----------------|----------|--------|
| `example.com` | `example.com` | Recorded |
| `example.com` | `www.example.com` | Recorded: `www` counts as the same site in both directions |
| `www.example.com` | `example.com` | Recorded |
| `example.com` | `staging.example.com` | Discarded |
| `example.com` | `shop.example.com` | Discarded |
| `example.com` | `example-com.translate.goog` | Discarded |

To measure a subdomain, add it as its own domain in VitalSentinel and install its tracking ID. VitalSentinel keeps a hit when it cannot read the hostname of the page URL. It also keeps a hit when it cannot look up the domain of the tracking ID.

<Callout type="warn">
  Visitors who read your site through Google Translate arrive on `*.translate.goog`. Web Analytics does not count these page views, but [RUM Monitoring](/rum-monitoring/overview) does. A funnel, signup or checkout step can be on its own subdomain with the tracking ID of the main site. Web Analytics then does not show that step.
</Callout>

## Bot traffic [#bot-traffic]

VitalSentinel removes bot traffic from your analytics: known bots, headless browsers and automated test tools. The check runs on the VitalSentinel servers, from the `User-Agent` header that every browser sends, so it works in every storage mode.

In the browser, the script checks for automation in every mode. It reads `navigator.userAgent` only with `session` or `persistent` storage.

## Troubleshooting [#troubleshooting]

Some problems affect both scripts: a Content Security Policy, caching plugins, Cloudflare Rocket Loader, consent tools, ad blockers and Google Tag Manager. For these, see [Tracking Scripts](/connect/tracking-scripts#troubleshooting).

### Why does Web Analytics show no page views? [#why-does-web-analytics-show-no-page-views]

If the browser console shows an error for `analytics.js`, read it first. Make sure that the `data-key` value is the tracking ID of this domain. In the browser's **Network** tab, make sure that the script loads. If an ad blocker runs, turn it off for the test. If the page is on another host than the domain, see [Tracking IDs and hostnames](#tracking-ids-and-hostnames).

### Why does Web Analytics not load with my Content Security Policy? [#why-does-web-analytics-not-load-with-my-content-security-policy]

If your site has a Content Security Policy, the Web Analytics script needs two hosts. Add `https://analytics.vitalsentinel.com` to `script-src`, so that `analytics.js` can load. Add `https://app.vitalsentinel.com` to `connect-src`, so that the script can send its data. If `analytics.js` loads but no request goes to `app.vitalsentinel.com`, the `connect-src` directive is usually the cause.

### Why does the console say "Missing data-key attribute"? [#why-does-the-console-say-missing-data-key-attribute]

The Web Analytics script writes `[VitalSentinel Analytics] Missing data-key attribute` when its own script tag has no tracking ID. The script then does nothing. Common causes:

- You pasted the **HTML** form of the snippet into Google Tag Manager. Use the **Google Tag Manager** form.
- A plugin combined or rewrote the script tag. Exclude `analytics.vitalsentinel.com` from JavaScript combine, minify and delay settings.
- The `data-key` attribute is on another tag, or has a typing error.

### Why does the console say "Tracking disabled (bot)"? [#why-does-the-console-say-tracking-disabled-bot]

With debug mode on, the script writes `[VitalSentinel Analytics] Tracking disabled (bot)` when it finds an automated browser. Selenium, Puppeteer, Playwright and other headless browsers send no data. VitalSentinel also removes known bots on its servers. To test the script, open your site in a normal browser.

### Why does Web Analytics count only one page for each visit? [#why-does-web-analytics-count-only-one-page-for-each-visit]

Your site is probably a single-page app (SPA), and SPA tracking is off. Then the script records only the first page load. Select **Enabled** in **Single page app (SPA) mode**, or set `data-spa="true"`. Copy the snippet again and replace it on your site. See [SPA support](#spa-support).

### Why do I see duplicate page views? [#why-do-i-see-duplicate-page-views]

In an SPA, do not call `trackPageView()` by hand when SPA tracking is on. Use `data-spa="false"` and track by hand, or use `data-spa="true"` and let the script find the page changes.

### Why does ecommerce tracking not work? [#why-does-ecommerce-tracking-not-work]

Make sure that your store runs on Shopify, WooCommerce, Magento or Squarespace. Other platforms send no shop events. Make sure that the product pages use the standard structure of the platform. On Shopify, cart contents need `session` or `persistent` storage.

Turn on debug mode and open a product page. The console shows `Ecommerce platform detected:` and the platform name, or `No ecommerce platform detected`. For a custom store, send your own events. See [Custom events](#custom-events).

### Why are orders missing in Ecommerce? [#why-are-orders-missing-in-ecommerce]

The script records an order only on the order page of your store, and only when it can read an order ID. Without an ID, VitalSentinel cannot prevent double counts, so it drops the order.

- On Shopify, add the custom pixel in **Customer events**. Shopify checkout pages do not use your theme code.
- On Squarespace, paste the snippet in the **Order status page** and **Order confirmation page** fields.
- On WooCommerce and Magento, make sure that the snippet runs on the order pages, not only on the shop pages.

See [Platform Guides](/connect/platforms).

### Why does New visitors show zero? [#why-does-new-visitors-show-zero]

In the **None** storage mode, VitalSentinel cannot tell a first visit from a repeat visit, so New visitors reads zero. To recognize returning visitors, use **Persistent** storage with the visitor's consent.

### Why does my storage setting not work? [#why-does-my-storage-setting-not-work]

The script accepts only `session` and `persistent` in `data-storage`. Any other value, such as the typing error `persistant`, falls back to `none`. Copy the snippet again from domain **Settings** > **Tracking**, so that the value is correct. Do Not Track and Global Privacy Control also force `none` for that visitor, until your consent banner calls `grantConsent()`.

### Why are visits from a subdomain or Google Translate missing? [#why-are-visits-from-a-subdomain-or-google-translate-missing]

Web Analytics accepts hits only from the exact host of the domain and its `www` form. It discards hits from `shop.example.com`, `staging.example.com` and `*.translate.goog` under the tracking ID of `example.com`. To measure a subdomain, add it as its own domain. See [Tracking IDs and hostnames](#tracking-ids-and-hostnames).

### How do I see what the Web Analytics script does? [#how-do-i-see-what-the-web-analytics-script-does]

Add `data-debug="true"` to the script tag, or tick **Enable console logging** in the options and copy the snippet again. Then open the browser console and look for messages that start with `[VitalSentinel Analytics]`. Remove debug mode when you are done.

## Related [#related]

- [Tracking Scripts](/connect/tracking-scripts) - install both scripts and check the installation
- [Platform Guides](/connect/platforms) - install steps for each CMS and framework
- [Web Analytics](/web-analytics/overview) - what the Web Analytics data shows
- [Conversions](/web-analytics/conversions) - goals and conversion reports
- [Data Privacy](/help/data-privacy) - how VitalSentinel handles visitor data
