# RUM script

The RUM script (`rum.js`) collects Core Web Vitals, JavaScript errors and user engagement from the real visitors of your site. It feeds RUM Monitoring. RUM means Real User Monitoring: measurement in the browsers of real visitors, not in a test lab.

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

## The RUM snippet [#the-rum-snippet]

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

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

The RUM 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** > **Real User Monitoring (RUM)** > **Install snippet**. The RUM script and the [Web Analytics script](/connect/analytics-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 RUM script reads these `data-*` attributes on its script tag:

| Attribute | Default | What it does |
|-----------|---------|--------------|
| `data-key` | required | The tracking ID of your domain. |
| `data-sample-rate` | `1.0` | The share of visitors to measure, from `0.0` to `1.0`. `0.5` measures half of the visitors. |
| `data-engagement` | `true` | Tracks clicks, scroll depth, time on page and form interactions. Set `false` to stop it. |
| `data-consent` | none | Set `granted` when your consent banner has the visitor's acceptance. See [Consent mode](#consent-mode). |
| `data-debug` | `false` | Writes log messages to the browser console. |
| `data-mask-text` | `false` | Removes the visible text of the elements recorded for Core Web Vitals attribution. |
| `data-mask-selectors` | `false` | Removes the CSS selectors recorded for LCP, CLS and INP attribution and for engagement events. |
| `data-filter-query-params` | `false` | Removes query parameters from the tracked URLs. |
| `data-track-non-production` | `false` | Sends data from hosts that the script skips by default. See [Non-production hosts](#non-production-hosts). |

The script reads `data-consent` first. If it is not set, the script reads the older attribute `data-storage`, so old snippets continue to work. `data-storage` takes `none`, `session` or `persistent`. With `session`, the script keeps only the visit ID, in session storage. Any other value means no consent. New snippets use `data-consent`.

## RUM options in Settings > Tracking [#rum-options-in-settings--tracking]

The **Real User Monitoring (RUM)** card in domain **Settings** > **Tracking** builds the snippet for you. Open **Options** to change these settings:

| Option | Choices | Snippet attribute |
|--------|---------|-------------------|
| **Consent mode** | **No consent** (default) or **Consent given** | `data-consent="granted"` for **Consent given** |
| **Sample rate** | 100%, 75%, 50%, 25%, 10%, 5% or 1% | `data-sample-rate` below 100% |
| **Engagement tracking** > **Enable tracking** | On (default) or off | `data-engagement="false"` when off |
| **Debug mode** > **Enable console logging** | Off (default) or on | `data-debug="true"` when on |

The **Options** line shows what differs from the defaults, for example **Consent given** or **50% sample**. VitalSentinel saves the first three options for everyone on the domain. Debug mode stays in your browser only ("Debug logging, for development only. Kept in this browser.").

<Callout type="warn">
  A change to an option 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>

## SPA support [#spa-support]

A single-page app (SPA) changes the page in the browser without a full page load. The RUM script finds these page changes on its own through the History API.

For a custom router, hash-based navigation or other state changes, start the page change by hand:

```javascript
window.VitalSentinelRUM.startSoftNavigation();
```

## Consent mode [#consent-mode]

The consent mode sets what the script can read from the browser and what it can keep on the visitor's device. The RUM script never sets a cookie.

### No consent (the default) [#no-consent-the-default]

In the **No consent** mode, the script saves nothing on the device: no cookies, no local storage, no session storage. It reads no device details except the window size, rounded to the nearest 50 pixels. It also checks a flag for headless automation and the Do Not Track and Global Privacy Control settings, so that it can obey them.

You still get Core Web Vitals, JavaScript errors, user engagement and the element behind each layout shift. You also get the country, browser, operating system and device type. The app says "No consent banner needed for storage."

If an earlier consent left IDs or the `vs_cg` key on the device, the script deletes them about 5 seconds after the page loads. It does not delete them if your banner grants consent first.

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

- **Screen size** - screen size and pixel ratio are missing from the per-page-view detail.
- **Device memory and processor** - "You cannot tell whether a slow page is really just a slow device."
- **Connection speed** - "A slow page and a weak network look the same."

### Consent given [#consent-given]

Use the **Consent given** mode for visitors who accepted your consent banner. It adds screen size, device pixel ratio, device memory, CPU cores, touch support and connection speed. With these, you can tell a slow page from a slow device or a weak network.

The script also keeps a visit ID and a visitor ID in the browser's local storage, so page loads link into visits. It sets no cookie. The Web Analytics script uses the same keys:

| Key | What it holds | How long it stays |
|-----|---------------|-------------------|
| `vs_sid`, `vs_sid_ts` | The visit ID and its time | A new visit starts after 30 minutes without activity. |
| `vs_vid`, `vs_vid_exp` | The visitor ID and its end date | 180 days from creation. Visits do not extend it. |
| `vs_cg` | A note that your banner called `grantConsent()`. The script writes it only after that call. | 180 days from the latest `grantConsent()` call. `revokeConsent()` deletes it earlier. |

<Callout type="warn">
  Use **Consent given** only when your banner has the visitor's consent. The app says "Only use this once your banner has the visitor's consent."
</Callout>

To declare consent on the script tag, add `data-consent="granted"`:

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

### Change the consent mode from your consent banner [#change-the-consent-mode-from-your-consent-banner]

You can also change the mode at runtime from your consent banner. This needs no page reload. The section **Using a consent banner?** in the RUM options shows these calls:

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

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

The `window.VitalSentinelRUM` object exists only after `rum.js` has loaded, so make the calls after that. If the script is not ready yet, it keeps the calls in a queue. Add both calls to your banner: the first when a visitor accepts, the second when the visitor refuses or later changes their mind.

When a visitor accepts, the script uses the visit ID and the visitor ID that are already on the device. A withdrawal applies from that moment on. The script sends events that are already in its queue with the context that they had when it collected them. Then it deletes the visit ID and the visitor ID from the device.

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

The RUM script obeys a Do Not Track or Global Privacy Control signal from the browser. You do not need to block the tag for these visitors yourself. For these visitors:

- The script reads no device details except the window size, whatever consent mode you declared.
- User engagement tracking does not run.
- The script keeps nothing on the device, and VitalSentinel makes no visit ID or visitor ID.
- VitalSentinel keeps only the country. The region, the city and the time zone stay empty.
- The script still measures Core Web Vitals and page views, 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 mode from the call. A mode that you write in the snippet, with `data-consent` or `data-storage`, 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 the same storage as the visit ID. The key lasts 180 days from the latest `grantConsent()` call. On a later page load, the script reads this key only if the snippet declares a consent mode. If the snippet keeps **No consent**, your banner must call `grantConsent()` again on each page load. `revokeConsent()` deletes the key, and the browser signal applies again.

User engagement tracking starts when the grant call comes. If the visitor withdraws consent, `revokeConsent()` stops it again.

## What the RUM script collects [#what-the-rum-script-collects]

### Core Web Vitals and loading metrics [#core-web-vitals-and-loading-metrics]

The RUM script tracks all Core Web Vitals and the other loading metrics:

- **LCP** (Largest Contentful Paint): loading performance
- **CLS** (Cumulative Layout Shift): visual stability
- **INP** (Interaction to Next Paint): response to interactions
- **FCP** (First Contentful Paint): the first render
- **TTFB** (Time to First Byte): server response time

It also records navigation timing: DNS lookup, TCP connection, request and response, DOM load and full page load.

### User engagement [#user-engagement]

When engagement tracking is on (the default), the RUM script tracks:

- Scroll depth
- Time on page and active time
- Page visibility changes
- Clicks on interactive elements
- Rage clicks: three or more clicks on the same element within two seconds
- Form engagement: focus, changes, submissions and abandonment

### JavaScript errors [#javascript-errors]

- JavaScript errors with stack traces
- Unhandled promise rejections
- Resources that fail to load, such as images, scripts and stylesheets

### Device information [#device-information]

In every mode, the script collects the window size, rounded to the nearest 50 pixels. In every mode, VitalSentinel also finds these details on its servers, from the request headers that every browser sends:

- Country, browser, operating system and device type
- If the device is a phone or a tablet
- If the visitor has data saver turned on

With **Consent given** only, the script adds screen size, device pixel ratio, device memory, CPU cores, connection type and speed, and touch support.

## Privacy by default [#privacy-by-default]

- No cookies in any mode. In the default mode, also no local storage and no session storage.
- No tracking across sites.
- In the default mode, the script makes a new random session ID on every page load. To link the page loads of one visit, VitalSentinel makes a short-lived ID on its servers. See [Data and privacy](/help/data-privacy#privacy-by-default).
- The script removes record identifiers from page paths before VitalSentinel stores a URL.

A long number, a UUID, a hex string or a long random token becomes `:id` or `:uuid`. For example, `/orders/44172/invoice` becomes `/orders/:id/invoice`. The shape of the path stays, so these page views group as one page. Dates, short segments and word slugs stay as they are.

### Privacy attributes [#privacy-attributes]

```html
<script
  src="https://rum.vitalsentinel.com/rum.js"
  data-key="YOUR_TRACKING_ID"
  data-mask-text="true"
  data-mask-selectors="true"
  data-filter-query-params="true"
  async
></script>
```

- `data-mask-text` removes the visible text recorded for the LCP, CLS and INP attribution elements. Engagement events carry no element text, so this attribute does not change them.
- `data-mask-selectors` removes the CSS selectors recorded for LCP, CLS and INP attribution and for engagement events. Use it if your element IDs or class names contain record identifiers, such as `#order-88213`.
- `data-filter-query-params` removes query parameters from the tracked URLs.

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

VitalSentinel discards a RUM hit from a host that cannot be a public production site. A reload on a developer's `localhost` gives Core Web Vitals that no real visitor gets. It also uses your page-view allowance.

Unless the host is a domain that you added to VitalSentinel, VitalSentinel discards hits from:

- `localhost`
- Any bare IP address, private or public, IPv4 or IPv6
- Hostnames on suffixes for testing and local development, such as `.localhost`, `.local`, `.test`, `.internal`, `.invalid` and `.example`

VitalSentinel keeps all other hosts, also a host that is not the domain of the tracking ID. A domain that you added is always accepted, also when it has one of the forms above.

### Why the RUM rule is less strict than the Web Analytics rule [#why-the-rum-rule-is-less-strict-than-the-web-analytics-rule]

Web Analytics accepts only the exact host of the domain. It drops `shop.example.com` and `staging.example.com` under the tracking ID of `example.com`. RUM keeps them, for two reasons.

First, the `data-track-non-production` setting stays in the browser and never reaches VitalSentinel. A strict host rule on the server would delete the data of every site that uses it. Second, real traffic can arrive on other hosts. Examples are Google Translate readers on `*.translate.goog`, or a second site that shares one tracking ID.

For a subdomain with its own audience, add it as its own domain and use its tracking ID. This keeps the numbers of the two sites apart. See [Add a Domain](/getting-started/domain-setup/add-domain).

### Non-production hosts [#non-production-hosts]

The RUM script also has its own, wider check in the browser. It sends nothing from `localhost`, loopback and private addresses, reserved test domains, and `staging`, `dev` and `test` subdomains. Thus, test visits do not mix with real visits.

If your production site uses such a hostname, set `data-track-non-production="true"`:

```html
<script
  src="https://rum.vitalsentinel.com/rum.js"
  data-key="YOUR_TRACKING_ID"
  data-track-non-production="true"
  async
></script>
```

<Callout type="warn">
  This attribute stays in the browser, so it removes only the browser check. It works for a name like `staging.example.com`, which the server keeps. It cannot make `localhost` or a bare IP address work, because the server discards them. The only exception is a host that you added as its own domain.
</Callout>

## JavaScript API [#javascript-api]

The RUM script has a JavaScript API on `window.VitalSentinelRUM`. The shorter name `window.VSRUM` is the same object.

### Track a custom event [#track-a-custom-event]

```javascript
window.VitalSentinelRUM.trackCustomEvent('purchase', {
  productId: '12345',
  value: 99.99,
  currency: 'USD'
});
```

### Mark and measure points in time [#mark-and-measure-points-in-time]

```javascript
window.VitalSentinelRUM.mark('hero-start');
window.VitalSentinelRUM.mark('hero-loaded');

// Measure the time between two marks
window.VitalSentinelRUM.measure('hero-time', { start: 'hero-start', end: 'hero-loaded' });
```

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

```javascript
window.VitalSentinelRUM.addData('plan', 'premium');
```

The script adds this key and value to all events after the call.

### Listen to RUM events [#listen-to-rum-events]

```javascript
window.VitalSentinelRUM.on('mark', (eventType, data) => {
  console.log('Mark created:', data.name, data.startTime);
});

window.VitalSentinelRUM.on('measure', (eventType, data) => {
  console.log('Measure recorded:', data.name, data.duration);
});

window.VitalSentinelRUM.on('soft_navigation', (eventType, data) => {
  console.log('SPA navigation:', data);
});
```

### Send queued events now [#send-queued-events-now]

```javascript
window.VitalSentinelRUM.forceFlush();
```

### Queue commands [#queue-commands]

The script makes its own command queue when it loads. Use `cmd()` to queue a command:

```javascript
window.VitalSentinelRUM.cmd(['mark', 'app-start']);

// trackCustomEvent cannot go in the queue. Call it directly.
window.VitalSentinelRUM.trackCustomEvent('page_intent', { category: 'pricing' });
```

## 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 the RUM script not load? [#why-does-the-rum-script-not-load]

If the browser console shows an error for `rum.js`, read it first. If an ad blocker runs, turn it off for the test. Make sure that the `data-key` value is the tracking ID of this domain.

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

### Why does RUM Monitoring show no data? [#why-does-rum-monitoring-show-no-data]

- If you test from `localhost`, a private address or a `staging`, `dev` or `test` subdomain, the script sends nothing by default.
- If `data-sample-rate` is very low, few visits are measured.
- If the page host is a bare IP address, the server discards the hit.
- If you open the page from a file on your computer (`file://`), the script does nothing.
- If the script tag has no `data-key`, the script does not start.

Numbers appear within minutes of the first real visit.

### Why does the console say "Non-production host, tracking disabled"? [#why-does-the-console-say-non-production-host-tracking-disabled]

With debug mode on, the RUM script writes `[VitalSentinel RUM] Non-production host, tracking disabled.` when it skips the host of the page. The RUM script skips these hosts by default:

- `localhost`, loopback and private addresses
- Reserved test domains, such as `.test`, `.local`, `.localhost`, `.internal`, `.example` and `.invalid`
- Hosts with three or more labels that start with `staging`, `stage`, `dev`, `development`, `test`, `testing`, `qa`, `uat`, `sandbox`, `preview` or `local`

Thus, `dev.example.com` is skipped, but `dev.to` is not. If your real site uses such a host, add `data-track-non-production="true"`. See [Non-production hosts](#non-production-hosts).

### Why does the console say "Session not sampled"? [#why-does-the-console-say-session-not-sampled]

With debug mode on, the RUM script writes `[VitalSentinel RUM] Session not sampled` when the **Sample rate** leaves out this page view. The script decides again on each page load. At a sample rate of 10%, about nine page loads in ten send nothing. For tests, set the **Sample rate** to 100%, copy the snippet again and replace it on your site.

### Why do my automated tests send no RUM data? [#why-do-my-automated-tests-send-no-rum-data]

The RUM script does nothing in a browser that automation controls, such as Selenium, Puppeteer or Playwright. Such a browser tells the page that a program controls it. VitalSentinel also drops batches from known crawlers on its servers. To test the script, open your site in a normal browser.

### Why does RUM Monitoring show no engagement for some visitors? [#why-does-rum-monitoring-show-no-engagement-for-some-visitors]

The RUM script does not track engagement for visitors who send Do Not Track or Global Privacy Control. It still measures their Core Web Vitals and JavaScript errors. If `data-engagement="false"` is on the tag, or **Engagement tracking** is off in the RUM options, the script tracks no engagement for any visitor.

### Why do some page addresses show ":id" or ":uuid"? [#why-do-some-page-addresses-show-id-or-uuid]

The RUM script replaces record identifiers in page paths before VitalSentinel stores a URL. For example, `/orders/44172/invoice` becomes `/orders/:id/invoice`. Thus, all order pages group as one page. This is expected. See [Privacy by default](#privacy-by-default).

### Why does the RUM script not start a second time? [#why-does-the-rum-script-not-start-a-second-time]

The RUM script starts only once on a page. If the page has two copies of the snippet, the second copy does nothing. The console can show `[VitalSentinel] Already initialized, ignoring duplicate init call`. Remove the extra copy, so that the right tracking ID measures the page.

### How do I see what the RUM script does? [#how-do-i-see-what-the-rum-script-does]

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

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

Remove debug mode when you are done. It is for development only.

## 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
- [RUM Monitoring](/rum-monitoring/overview) - what the RUM data shows
- [Metrics and Thresholds](/help/metrics-and-thresholds) - good, needs improvement and poor values
- [Data Privacy](/help/data-privacy) - how VitalSentinel handles visitor data
