PilotFoxDocs

Session replay

Turn on session replay for a site, what is recorded and masked, and where recordings are stored.

Session replay records how a page changed (its structure, scrolling, mouse movement and interactions) so you can watch a visit back in the Replays report. It is off until you turn it on for a site.

Turn it on

Site settings → Session replay (editors and above):

SettingDefaultMeaning
Record sessionsOffLoad the recorder on sampled tabs.
Sample rate100 %The share of browser tabs that are recorded (0 to 100 %): each tab is sampled on its own, not each session.
Mask all inputsOnReplace what is typed in inputs with asterisks. Strongly recommended.
Mask text selectorNoneCSS selectors (up to 512 characters) of elements whose text is masked, for example .private, [data-mask].
Block selectorNoneCSS selectors (up to 512 characters) of elements that are not recorded at all, for example .no-record.
Minimum duration0 secondsShorter recordings are hidden from the Replays list.

Recordings are kept in object storage (Cloudflare R2 or another S3-compatible store). An instance without object storage reports replay as off to the script and records nothing; on a self-hosted instance, see Configuration.

No change to your pages is needed: the tracking script loads the recorder itself.

How recording works

  • Sampling. Each browser tab is sampled once, when the script starts on a replay-enabled site, and the decision is kept for the tab in sessionStorage (pilotfox:rs: 1 recorded, 0 not). A recorded tab stays recorded across its page loads. A visitor's session can span several tabs, so with a sample rate below 100 % a session may be recorded in some of its tabs only. Where sessionStorage is blocked, the decision is made again on each page load.
  • The recorder. On a recorded tab, the script loads /api/r.js (rrweb 2.2.0, about 27 KB compressed) from its own directory, so it works through your own domain too. Browsers cache it for a day. Tabs that are not recorded never load it.
  • What is captured. A snapshot of the page, then its changes; mouse movement (sampled every 50 ms), scrolling (every 150 ms), clicks and input events; a new full snapshot every 5 minutes or 1 000 events. Canvas content is not recorded.
  • Sending. The recorder sends a chunk to /api/rec/SITE_ID every 5 seconds, or sooner after 200 events or about 256 KB, compressed with gzip where the browser supports it, and a last one, uncompressed, when the page is hidden. A send that fails (a network error, a 503, or no answer within 30 seconds in browsers with AbortSignal.timeout) is retried once, a second later. While a send is in flight, the recorder keeps at most 5 000 events, or about 1 MiB of them besides its latest full snapshot; past that it drops them and starts again from a fresh snapshot, so a slow network never makes it grow without bound.
  • Sessions. A recording belongs to the visitor's current session, and a chunk is stored only while that session is live: recording never starts a session or keeps one alive. Pageviews and events keep it going as usual.

What is masked and never recorded

WhatHow it is handled
Input valuesMasked by default (Mask all inputs). Password fields stay masked even with that setting off.
Hidden and file inputsNever recorded.
CSRF tokensEvery <meta> whose name contains csrf (any case) is never recorded.
Editable text ([contenteditable])Always masked.
CanvasNot recorded.
Elements matching Mask text selectorTheir text, and their descendants' text, is masked.
Elements matching Block selectorReplaced by an empty box of the same size.

Your own markup can use rrweb's classes:

<p class="rr-mask">Masked text: the visitor's balance is $1,234</p>
<div class="rr-block">Not recorded: replaced by an empty box</div>
<input class="rr-ignore" name="search" />  <!-- typing in this field is not recorded -->

Settings refuse a malformed selector list. If a visitor's browser cannot parse part of a saved list, the recorder keeps the selectors it can parse and drops the rest; if it cannot even check the built-in ones, it records nothing.

Pages that are never recorded

  • data-skip pages. Recording stops at once on a page that the tag's data-skip covers, and starts again, from a fresh snapshot, on the next page it does not cover.
  • Excluded pages. The site's path exclusions, and host names outside Allowed hostnames, stop recording the same way. A re: path entry saved through the API, which events ignore, is matched here as a regular expression, and one the browser cannot compile keeps replay off every page.
  • Opted-out browsers. optOut(), ?pilotfox_ignore=true or Global Privacy Control stop recording at once. An opt-out in one tab also stops the recording in every other open tab of the site, and what was recorded but not yet sent is discarded. Before each send, the recorder checks again and stops for good if the visitor has opted out.
  • Prerendered pages start recording only when they are shown.

The page address in a recording goes through data-mask and the Query strings setting, like events, and loses its fragment. The URL is checked on every route change of a single-page app, with Single-page app navigation off too.

Exclusions become public while replay is on

So that the recorder can stay off those pages, the script's settings (GET /api/c/SITE_ID) carry the site's path exclusions and allowed host names while replay is on. Anyone can read that response. The other exclusions (IP addresses, countries, networks, user agents) are never sent.

Control recording from your code

// Record this tab even if it was not sampled (it stays recorded for the tab):
window.pilotfox?.startReplay();

// Stop recording this tab and keep it off for the tab (what is buffered is sent first):
window.pilotfox?.stopReplay();

Both work only on a site with replay turned on, and neither records an opted-out browser. Calls made before the site's settings have arrived wait for them.

Limits

LimitValue
One chunk1 MiB as sent, 8 MiB uncompressed, 5 000 events
One recording (one session)2 000 chunks, 200 MiB as sent, 4 hours from its first chunk
Requests to /api/rec5 per second per address, in bursts of up to 50

A chunk over the size limits is refused with 413, and one with more than 5 000 events with 400. Chunks past a recording's limits are dropped without an error (204). A recording appears in the Replays list once it holds a full snapshot and at least two events.

Storage, access and deletion

  • Chunks are stored compressed in the instance's object storage, one object per chunk, and indexed in its database. An instance keeps recordings for REPLAY_RETENTION_DAYS days (30 by default; see Configuration).
  • Watching recordings needs the replay:read permission (viewers and above). A share link includes replays only if it was created with them; a public dashboard never does.
  • Editors and above delete a recording from its page, or with DELETE /api/v1/sites/{siteId}/replays/{sessionId} (replay:delete). Chunks that a tab still recording that session sends afterwards are dropped, until 4 hours 30 minutes pass without one, so the deleted recording does not start again.
  • Erasing a user's data deletes their recordings, and deleting a site deletes all of its recordings.

Notes

  • Content Security Policy: script-src must allow r.js (list PilotFox's origin, or use a nonce: the recorder's tag gets the script tag's nonce, so 'strict-dynamic' works), and connect-src must allow /api/rec. See Content Security Policy.
  • One recorder per page: with two PilotFox tags on a page (two data-namespace values), the recorder the first one starts serves both, with the first tag's site and settings. Keep replay to one tag per page.

On this page