Session replay

See exactly what the user saw, without seeing what they typed

The browser SDK buffers a recording continuously but uploads nothing until an error actually happens; masked by default, played back in a sandboxed frame that can never execute anything it reconstructs.

Only uploaded when something actually breaks

The recorder keeps a rolling buffer of the page's own DOM snapshot plus every mutation, click, scroll, and (throttled) mousemove since the last rebaseline, entirely in memory, never uploaded on its own. Nothing leaves the browser unless the SDK's own error handler actually fires; a page that never errors never uploads a single recording, however long a visitor stays on it.

That buffer is bounded on two independent axes, not just one: a rebaseline every 15 seconds collapses it back down to a fresh snapshot plus whatever's happened since, and a hard cap of 300 buffered events forces an early rebaseline if a burst of activity would otherwise get there first. Either one keeps a long-lived tab's own memory use flat over time, rather than growing for as long as the page stays open.


Masked by default, opted out per element

Every text node's content is replaced with bullets before it's ever recorded, one bullet per character, so the recording still shows roughly how much text was there without ever carrying the text itself. An input or textarea's value is masked differently, and more conservatively: a single fixed-length placeholder regardless of the real value's own length, since a password field's character count is itself a real signal worth not leaking, unlike an ordinary line of body text.

An element (and everything inside it) can opt back out entirely with a plain data-forgeops-unmask="true" attribute, for the specific parts of a page where showing the real content is the point, a public marketing page embedded in the app, say. Nothing is unmasked by default; this is an explicit, per-element choice the page itself has to make.


Linked up after the fact, never blocking either upload

The SDK stamps the same client-generated replay id onto both the error event it reports and the recording it uploads, but the two are separate requests with no ordering guaranteed between them: the event might get processed first, or the recording might. A background job resolves the link afterward, retrying until the matching event actually shows up, so neither upload ever has to wait on the other one finishing first just to know what it belongs to.


Replayed in a frame that can't run anything

The recording is decompressed and rebuilt entirely client-side, reconstructing the page's own DOM node by node into a sandboxed iframe with sandbox="allow-same-origin" and nothing else, allow-scripts deliberately never included. That's what actually makes replaying someone else's page contents safe: an inline onclick attribute or a javascript: href, if a recorded page happened to carry one, is inert data in a sandbox with script execution turned off, the same as it would be in any other sandboxed iframe, never something that can run in this app's own origin.

Playback itself is a real transport: play, pause, scrub to any point, and step through at 1×, 2×, or 4× speed, with a persistent cursor trail at the most recent recorded pointer position and a brief marker flashing at each recorded click.

The session replay player mid-playback, showing a reconstructed page with masked cardholder-name and card-number fields and a cursor resting on a Save changes button

The reconstructed page, masked fields included, played back right on the issue that needed it.


Kept only as long as the event it belongs to

A recording doesn't get its own separate retention setting to keep track of; it shares the exact same window as the error event it was captured for, your plan's own event retention (7 to 365 days depending on plan), purged automatically once that event ages out. There's nothing to configure separately, and nothing left behind afterward: purging removes the recording from object storage itself, not just its row in the app's own database.


Enterprise plan only

Enforced the same unbypassable, model-layer way every other gated feature here is, checked on every ingestion request rather than only when a recording is first created: a real recording sitting in object storage is an ongoing cost that only grows the longer it's kept, the same class of further-infrastructure reasoning that already put on-call scheduling, audit log SIEM streaming, and IP allowlisting at this tier instead of Business.


See it on your own data

Free plan included, no credit card required.

Get started free