โ† All posts
RELEASE August 19, 2026 ยท 4 min read

Session replay that records almost nothing

SR
Sentrinel Team
Product & engineering

Most session replay works like a security camera: record everything, store it all, and hope you never need it. That is expensive in a way that scales with your traffic rather than with your problems, and the recordings you keep are overwhelmingly of people having a perfectly good time.

Sentrinel's records continuously and uploads almost none of it. A session that never fails sends nothing at all.

The trade

Replay is the largest thing a monitoring platform stores per unit of value. A single recording is megabytes. Multiply by every session and you are paying, forever, for footage of successful checkouts.

But you do not want a recording of a successful checkout. You want the thirty seconds before the crash โ€” the click that caused it, the form state, the screen they were looking at when it broke.

So the recorder keeps a rolling buffer in memory and throws it away continuously. When an error fires, that buffer gets uploaded:

initSentrinelBrowser({
  endpoint: "/api/_sentrinel",
  replay: {
    enabled: true,
    bufferSeconds: 30,   // how much history to keep
    tailSeconds: 5,      // keep recording after the error
  },
});

Cost is paid when something goes wrong, and what you get is the part you wanted.

The tail matters more than it looks. Uploading the instant an error fires gives you everything up to the failure and nothing after it โ€” no error screen, no stuck spinner, no second thing that broke. Five more seconds of recording is usually where the actual symptom is.

Everything is masked, and that is the only safe default

Text renders as blocks. Input values are never recorded. You opt individual elements in:

<div data-sentrinel-unmask>Order #1042 โ€” shipped</div>

This is the opposite of what most tools default to, and we think the other direction is indefensible. Nobody enables replay expecting to ship customer names, addresses and card details to a vendor. If the default is "record everything legible", then the first time someone turns it on in production they have created a data-protection problem they do not know about yet.

Masked-by-default means the worst case of forgetting to configure it is a replay that is less useful than it could be. Unmasked-by-default means the worst case is a breach.

When we tested this against a real checkout page, the recording came back with the title as ****** **** * ******** and every price, name and card number as asterisks. Layout, clicks and navigation โ€” everything you need to see what happened โ€” came through intact.

Two things that broke while building it

Worth writing down, because both were silent.

The recorder trims its own buffer, and the first version trimmed too well. rrweb rebuilds a page by taking a full DOM snapshot and then applying mutations forward. Drop the snapshot and every mutation after it is a diff against nothing. The buffer therefore never trims past its most recent snapshot, however old that makes the window.

The viewport is a separate event, and it comes first. rrweb emits a Meta event carrying the recorded window size before the first snapshot. Our trim rule โ€” "keep from the last snapshot onward" โ€” threw it away. The recording still rebuilt perfectly; it rendered into a zero-width frame. The data was fine and the player looked broken, which is the worst combination to debug. Meta is now held outside the buffer and re-attached to every chunk.

Neither would have failed a test that only checked "did a recording arrive".

Where it stops

It needs ClickHouse. Recordings are far too large for the Postgres telemetry store, so ingest answers 501 rather than accepting and dropping them. Retention is its own, shorter clock โ€” 14 days by default. Nobody watches a replay from six weeks ago.

A crash in the final moment before navigation may be lost. The upload needs the page to still be alive. sendBeacon is the only transport a browser guarantees during unload and it cannot carry megabytes. Errors mid-session โ€” nearly all of them โ€” upload fine.

It is web only. rrweb records the DOM, and a Flutter or native app does not have one. Mobile replay is a different technology entirely โ€” screen and view-hierarchy capture โ€” not a port of this.

Two things since

A replay cannot come from a phone. Keys are now bound to one integration, and a Mobile app key cannot post a replay at all โ€” the surface is web-only, and a key that ships in a bundle should not be able to reach it. The backend plugin's server key is the one that forwards the browser SDK's uploads. One key, one integration.

The request behind the recording is a call away. A replay carries the request and trace it captured, and an AI agent with a read-only key can pull both โ€” so the fix can start from the recording without anyone copying ids out of the dashboard. Your coding agent can read production now.

The bit that makes it useful

A recording is not filed on its own. It carries the session it belongs to and the consumer who caused it, so from a customer's record in Sentrinel you can watch what they saw when they hit the error you are already looking at.

That link is the difference between "we have replay" and "I can see what happened to this person." Every signal in Sentrinel โ€” requests, errors, logs, traces, replays โ€” stores who it belonged to rather than resolving it by joining back through a request. Those tables expire on different clocks, and a trail assembled by join quietly gets shorter as the requests behind it age out.

A partial answer to "show me everything this user did" is worse than no answer, because it looks complete.

Details in the browser guide.