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):
| Setting | Default | Meaning |
|---|---|---|
| Record sessions | Off | Load the recorder on sampled tabs. |
| Sample rate | 100 % | The share of browser tabs that are recorded (0 to 100 %): each tab is sampled on its own, not each session. |
| Mask all inputs | On | Replace what is typed in inputs with asterisks. Strongly recommended. |
| Mask text selector | None | CSS selectors (up to 512 characters) of elements whose text is masked, for example .private, [data-mask]. |
| Block selector | None | CSS selectors (up to 512 characters) of elements that are not recorded at all, for example .no-record. |
| Minimum duration | 0 seconds | Shorter 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:1recorded,0not). 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. WheresessionStorageis 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_IDevery 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, a503, or no answer within 30 seconds in browsers withAbortSignal.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
| What | How it is handled |
|---|---|
| Input values | Masked by default (Mask all inputs). Password fields stay masked even with that setting off. |
| Hidden and file inputs | Never recorded. |
| CSRF tokens | Every <meta> whose name contains csrf (any case) is never recorded. |
Editable text ([contenteditable]) | Always masked. |
| Canvas | Not recorded. |
| Elements matching Mask text selector | Their text, and their descendants' text, is masked. |
| Elements matching Block selector | Replaced 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-skippages. Recording stops at once on a page that the tag'sdata-skipcovers, 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=trueor 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
| Limit | Value |
|---|---|
| One chunk | 1 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/rec | 5 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_DAYSdays (30 by default; see Configuration). - Watching recordings needs the
replay:readpermission (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-srcmust allowr.js(list PilotFox's origin, or use a nonce: the recorder's tag gets the script tag's nonce, so'strict-dynamic'works), andconnect-srcmust allow/api/rec. See Content Security Policy. - One recorder per page: with two PilotFox tags on a page (two
data-namespacevalues), the recorder the first one starts serves both, with the first tag's site and settings. Keep replay to one tag per page.