Measuring

Session replay

Off until you turn it on — twice

Nothing is recorded unless both of these are true:

  1. An admin switched replay on for the site (dashboard → Replays → settings). Members can watch recordings; only admins can enable, change or delete.
  2. The page opts in with data-replay on the tracker tag:
<script defer data-site="SITE_ID" data-replay src="https://vitrus.dev/v.js"></script>

The recorder is a separate file (/r.js, under 5 KB gzipped — a build gate holds it there), loaded only on pages with that attribute. The core tracker stays under its own size budget, and a site without replay downloads none of it. Before recording anything the recorder asks the server whether the site has replay enabled and whether this visitor is sampled in; any other answer, or no answer, means nothing is recorded.

What is masked, and what is never captured

On the pageIn the recording
Any textmasked: every non-space character becomes *. Length and line breaks survive, words do not.
Any input, textarea or select valuemasked the same way — always, even inside an unmasked element
<input type="password">never captured — a grey box of the same size, no keystrokes, no length
Card fields (autocomplete="cc-*") and one-time codesnever captured — grey box
Anything inside data-vitrus-blocknever captured — grey box
iframes, canvas, video, audiogrey box
Imagesrecorded, unless data-replay-block-media="true" or the site setting hides them
placeholder, title, alt, aria-labelmasked
data-* values that look like identifiers (an @, five digits in a row, long values)masked; short state tokens like open are kept so the page still styles correctly
Link URLsquery string and fragment removed; mailto: and tel: links dropped
Scripts, on* handlers, javascript: URLsnever recorded

Unmasking, deliberately

Put data-vitrus-unmask on an element whose text is public — a headline, a navigation menu, a button label — and its text is recorded as written. It never unmasks form values. Mark anything sensitive the page shows (an account number, an address) with data-vitrus-block and it is never captured at all.

<nav data-vitrus-unmask>…</nav>
<address data-vitrus-block>…</address>

Consent and identifiers

  • Do Not Track and Global Privacy Control are always honoured for recordings — the data-do-not-track="false" override that exists for aggregate counts does not reach replay. Checked in the browser and again on the server.
  • No cookie, no localStorage, no sessionStorage. The recording is grouped on the server with the same daily-salted visitor hash a pageview uses, so several pages of one visit become one replay without anything stored in the browser.
  • No raw IP is stored. data-skip-patterns pages are not recorded; data-mask-patterns apply to the recorded path.
  • The server re-masks every input value on arrival and strips scripts and handlers, so an edited recorder cannot store a typed value in the input channel. It cannot re-mask page text — only the page knows which parts it unmasked — so text masking is a guarantee of the recorder.

Settings

SettingDefaultRange
Enabledoff—
Share of visitors recorded100%1–100%, decided per visitor per day
Maximum length of a recording30 minutes1–120 minutes
Retention30 days1–90 days
Hide imagesoff—

Out-of-range values are refused with the field named, never silently clamped. Recordings past retention disappear from the list immediately and are deleted by an hourly sweep. Switching replay off keeps existing recordings until their retention runs out, unless you also tick "delete every existing recording".

The player

Recordings are listed with a generated name (the same visitor hash, so it changes daily and names nobody), length, pages, clicks, errors, country, device and browser, and can be filtered by length, pages, device, browser, country and errors. The player has play/pause, 1x–8x speed, skip-inactivity, a timeline with markers for page loads, clicks, errors and the session's custom events, and an activity list you can click to jump. Every recording can be deleted individually.

The recording is rebuilt inside an iframe with sandbox and without allow-scripts, plus a content security policy that forbids scripts, frames and forms: nothing in a recording can execute. The page's stylesheets, fonts and images load from your site's own URLs so the replay looks right.

Limits

The hosted plans meter recordings that start in a month (Free 500, Starter 5,000, Pro 50,000). Past the limit there is the same 20% grace as for events; after that a new recording is refused and counted — the Replays page shows how many were refused. A recording in progress is never cut off by the limit.

Known blind spots

  • Shadow DOM contents, cross-origin iframes and what is drawn on a canvas are not recorded.
  • CSS rules added through CSSOM insertRule after the first snapshot (some CSS-in-JS libraries do this) are missed, so a late-styled element can look unstyled.
  • Timing across page loads is pinned to server arrival time, so a page boundary can be off by one chunk's network latency.
  • When the tab closes, the last few seconds go out uncompressed in a single beacon; what it cannot carry is lost, and the recording ends there.

API

EndpointWhat it does
GET /api/replay/config?site=public; whether this visitor is recorded
POST /api/replay/chunkpublic; recorder upload (gzip when the browser has CompressionStream)
GET /api/replays?site=list, with the SQL behind the count
GET /api/replays/:id?site=playback data and the session's events
DELETE /api/replays/:id?site=delete one recording
GET / PUT /api/replay/settings?site=read or change the settings above

Self-hosted, all of this works the same: the recorder, the player and the endpoints are in the Apache-2.0 packages.