Measuring
Session replay
Off until you turn it on — twice
Nothing is recorded unless both of these are true:
- An admin switched replay on for the site (dashboard → Replays → settings). Members can watch recordings; only admins can enable, change or delete.
- The page opts in with
data-replayon 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 page | In the recording |
|---|---|
| Any text | masked: every non-space character becomes *. Length and line breaks survive, words do not. |
| Any input, textarea or select value | masked 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 codes | never captured — grey box |
Anything inside data-vitrus-block | never captured — grey box |
| iframes, canvas, video, audio | grey box |
| Images | recorded, unless data-replay-block-media="true" or the site setting hides them |
| placeholder, title, alt, aria-label | masked |
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 URLs | query string and fragment removed; mailto: and tel: links dropped |
Scripts, on* handlers, javascript: URLs | never 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, nosessionStorage. 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-patternspages are not recorded;data-mask-patternsapply 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
| Setting | Default | Range |
|---|---|---|
| Enabled | off | — |
| Share of visitors recorded | 100% | 1–100%, decided per visitor per day |
| Maximum length of a recording | 30 minutes | 1–120 minutes |
| Retention | 30 days | 1–90 days |
| Hide images | off | — |
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
insertRuleafter 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
| Endpoint | What it does |
|---|---|
GET /api/replay/config?site= | public; whether this visitor is recorded |
POST /api/replay/chunk | public; 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.