Session replay model
Session replay is the asynchronous twin of live co-browse. Co-browse mirrors a visitor’s DOM to an agent over a scoped data channel while the page is open; session replay records the same rrweb event stream in the visitor’s browser, uploads it in batches, and stores it against the conversation so an agent can play it back later — after the visitor has left, often on a closed thread. This page is the conceptual model behind that subsystem; the operator walkthrough lives in the session replay guide, and the request/response shapes are in the Session Replay API reference.Recording lifecycle
A recording is a stamped object on the conversation —conversations.metadata.session_replay — that moves through a small state machine:
recording— created when the widget’s recorder starts (the visitor opt-in call on the widget capture endpoints). The widget buffers events in-browser and uploads them in batches of at most 500 events to the capture endpoint; each append either extends the inline head or spills to durable storage, as described below. The widget stamps the returnedrecording_idon every subsequent batch.ended— the terminal, healthy state. The widget finalizes the recording on page unload, navigation, or explicit stop, stampingended_at. A finalized recording is immutable — the append path returns it unchanged.capped— the terminal, abnormal state. Only the abuse backstop (see below) can set it; normal sessions never reach it.
ended recording is a no-op, and starting a second recording reuses an existing non-ended one rather than duplicating it.
Two-tier storage model
The design constraint is that a conversation row must never bloat with an unbounded event array, while a long session must not be silently truncated either. The model resolves this with two tiers of storage on one recording:
Every incoming event lands in the inline head until either bound (5,000 events or 4 MB of serialized JSON) is hit. From that moment every further event becomes overflow, written as one chunk object per upload batch:
seq (its position in the append order), its path, and its event_count / size_bytes. Once anything has spilled, nothing returns to the head — append order stays contiguous-from-start in the head and contiguous-after-head in the chunks. This ordering property is what makes the reassembled rrweb stream replayable: rrweb’s incremental mutations reference the full snapshot at the head, so dropping events from the middle would corrupt playback.
Total counters (total_event_count, total_size_bytes) track head plus chunks, and the recording carries offloaded: true once any chunk exists. The list endpoint summarizes on those totals so you can gauge per-recording footprint without fetching chunk pointers.
Playback reassembly
For an inline-only recording, playback is the head — no storage round-trip. For an offloaded recording, the playback endpoint downloads every chunk concurrently and returnshead ++ chunks in seq order as one event array. Chunk upload is idempotent on (recording_id, seq): a widget retry of the same batch re-writes the same object path rather than duplicating events.
Abuse backstop
Independent of the inline cap, a recording is hard-bounded at roughly 1 GiB of serialized events or 5 million events across both tiers. Only this ceiling flipsstatus to capped and sets truncated: true; the inline head cap never truncates — it only redirects where events live. A pathological recorder (for example a page script in a rapid DOM-mutation loop) is the only realistic way to trip it, so treat capped as a signal about the page, not a storage limit you need to raise.
Failure model
Reassembly happens on the request path, so its failures are designed to degrade, not error:- Head-only fallback. If a chunk download fails during playback (a transient object-storage read error), the response serves the inline head rather than failing — flagged by
offloaded: trueon the recording andtruncated: trueif the backstop also fired. Retry usually returns the full stream; persistent head-only responses on an offloaded recording are worth a support ticket. - Chunk upload failure. If writing an overflow chunk fails, the capture endpoint asks the widget to retry that batch; idempotency on
(recording_id, seq)makes the retry safe. - Corrupt recording payload. Every read path runs the decoded JSON through a narrowing guard before it is used; a drifted or malformed stored shape fails loudly instead of being appended to or played from.
Purge semantics
Purging a recording is ordered so the row pointer is dropped only after the durable data is gone:- Durable chunks first — best-effort deletion of every
blob_chunksobject; a missing object or transient storage error is logged and skipped so erasure still completes. - Then the pointer — the
session_replayvalue is removed from the conversation metadata. - Evidence — an audit entry (
session_replay.deleted) records the erasure.
Privacy boundary
Masking is decided in the visitor’s browser, before any event is uploaded — masked content never leaves the page, and the server stores exactly what the browser sent. The default posture is fail-closed: every input value is masked and the built-in PII heuristics apply until you explicitly relax them. The recorder treats event payloads as opaque bytes the server never interprets, and playback hands them back to the agent’s player unchanged. The tenant-owned controls map one-to-one onto the recorder’s options:
The policy is account-wide and takes effect from the next widget bootstrap — it never retroactively alters stored recordings. Choosing what to mask or block, and documenting that choice with your DPO, is the control that keeps replay aligned with your compliance posture. The same capture-time-redaction contract also governs live co-browse, so the two capture surfaces share one masking posture rather than diverging.
Tenant isolation
Every recording is stamped inside one tenant’s schema (the{schema} segment in the chunk object path is the same schema reference the query layer resolves), so recordings, chunk pointers, and chunk objects can never cross tenants — the isolation is structural, not a runtime filter. Visitor tokens resolve to exactly one tenant before any append is accepted.
See also
- Session replay guide — operator walkthrough: retention, playback in the inbox, purge, masking panel
- Session Replay API reference — endpoint shapes and error codes
- Co-browse session model — the live counterpart that shares this masking contract
- Conversations — the entity a recording is stamped on