Skip to content
OTFotf
All posts

Session replay scoping that keeps production privacy and signal

D
DaveAuthor
7 min read
Session replay scoping that keeps production privacy and signal

Session replay is useful in production only when you decide what gets recorded, how often, and what never leaves the browser. If you turn it on at 100% with masking relaxed "just for a week," you buy noise and privacy risk. If you leave defaults alone and pick sample rates from real traffic, you get the sessions that explain bugs — without shipping PII.

This post is about scoping: replaysSessionSampleRate, replaysOnErrorSampleRate, privacy masking defaults, and error-first capture. It is not a crash triage walkthrough, not LLM tracing, and not release health. For those, start with crash triage that ships fixes, React Native error tracking in production, release health for AI-built apps, and LLM observability.

What session replay actually records

Sentry Session Replay reconstructs a video-like view of the DOM: clicks, scrolls, navigations, network activity, and console entries around an issue. On the web SDK path, recording runs in the browser before anything is uploaded. Defaults are deliberately strict: mask all DOM text, block media, and mask input values before send. That is the privacy floor you should treat as production-safe until you have tested every unmask.

A session starts when the Replay SDK initializes. It keeps capturing across pageloads and same-tab navigations on the same domain until idle for 5 minutes without interaction, until 60 minutes elapse, or until the tab closes. Interactions that reset idle are clicks and browser navigations. Those lifetime rules matter when you size storage and when you explain why a long support call did not produce one continuous replay.

Error-only capture is a first-class mode, not a fallback hack. When a session is not selected by replaysSessionSampleRate, the SDK still buffers roughly one minute of events in memory. If an error fires and replaysOnErrorSampleRate selects the session, that buffer plus the rest of the session uploads. If nothing errors (or the error path does not sample), the buffer is discarded and nothing is sent. That is the core scoping lever for production: pay for continuous sessions on a fraction of traffic, and keep full fidelity when something breaks.

Sample rates that match traffic, not vibes

Two knobs, checked in order:

  1. replaysSessionSampleRate — fraction of sessions recorded fully and sent in real time.
  2. replaysOnErrorSampleRate — fraction of non-session-sampled sessions that still upload when an error occurs (with the ~60s pre-error buffer).

Sentry's documented production guidance (use these as starting points, then adjust from your quota and support load):

Traffic volumeSession rateError rate
High (100k+ sessions/day)0.01 (1%)1.0
Medium (10k–100k/day)0.1 (10%)1.0
Low (under 10k/day)0.25 (25%)1.0

Keep the error path at 1.0 in production unless you have a measured reason not to. Error sessions are where replay pays for itself. Lowering session rate is how you control volume; lowering error rate is how you lose the debugging signal.

During local verification, set replaysSessionSampleRate: 1.0 so every session uploads. Drop it before you ship. Leaving 100% session sampling in production is the most common scoping failure on young AI-built apps: the product still works, the bill and the review queue do not.

Session sample vs error-sample vs discard decision diagram

11 production screens. Login, database, payments — all wired.

The SaaS Dashboard Kit ships everything already connected. Nothing to set up. Live demo at saas.otf-kit.dev.

See the live demo

Production init that keeps privacy defaults

Minimal production-shaped init (JavaScript browser SDK pattern from Sentry docs). Replace the DSN and pick the session rate from the table above:

import * as Sentry from "@sentry/browser";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,

  // Medium traffic example — change to 0.01 or 0.25 from the table
  replaysSessionSampleRate: 0.1,

  // Keep error-triggered capture complete in production
  replaysOnErrorSampleRate: 1.0,

  integrations: [
    Sentry.replayIntegration({
      // Defaults — keep true unless you have tested every screen
      maskAllText: true,
      blockAllMedia: true,
      maskAllInputs: true,
    }),
  ],
});

Privacy options you will reach for after the defaults:

  • mask / unmask — CSS selectors (plus defaults like .sentry-mask / [data-sentry-mask]).
  • block / unblock — replace elements with same-size placeholders (.sentry-block / [data-sentry-block]).
  • ignore — drop input events for matching fields (.sentry-ignore / [data-sentry-ignore]).
  • maskFn — custom character masking (default repeats * to string length).

Do not set maskAllText: false or blockAllMedia: false for a marketing site "because there is no PII" without a pass over auth, search, support chat, and any user-generated content. Sentry's privacy docs are explicit: if you change masking, or if your UI framework changes how text and media render, re-test before production. If you find a leak, stop shipping replay until it is fixed.

From SDK v8 onward, unblock and unmask no longer inject older default selectors automatically. If you rely on data-sentry-unmask / sentry-unmask in components, opt in:

Sentry.replayIntegration({
  maskAllText: true,
  blockAllMedia: true,
  unmask: [".sentry-unmask", "[data-sentry-unmask]"],
  unblock: [".sentry-unblock", "[data-sentry-unblock]"],
});

Bake privacy into shared UI primitives (avatar, username, free-text input) with data-sentry-mask / data-sentry-block so every screen inherits the rule. Prefer that over hunting pages after launch. Avatars rendered as CSS background-image on a div are a common miss: default media blocking targets real media elements, so mark those wrappers with data-sentry-block.

Error-first capture as the default product stance

For many AI-built apps under 10k sessions/day, a practical stance is:

  • Session sample at 0.25 (or lower once support can still reproduce issues).
  • Error sample at 1.0.
  • Masking left at defaults.
  • Network request/response bodies left opt-in and limited to non-PII routes.

That combination means most quiet sessions never leave the device, while almost every errored session still gives you the minute before the failure. Pair it with solid issue triage (crash triage that ships fixes) so replay is attached to a fix queue, not a curiosity tab.

If your traffic is already high, jump straight to 0.01 / 1.0. Do not "warm up" at 100% session sampling in production to "see what we get." You already know what you get: more of everything, including sessions you will never watch.

Byte and Nova adjusting sample-rate dials beside an error replay buffer

Scope decisions that bite later

Canvas recording. Canvas replay is opt-in via replayCanvasIntegration(). There is currently no PII scrubbing in canvas recordings. Treat that as a hard scope decision: enable only if the canvas cannot show secrets, messages, or identifiers — or keep it off.

CSP and the compression worker. Replay uses a Web Worker for compression. Your Content-Security-Policy needs worker-src 'self' blob: (and child-src 'self' blob: for older Safari). Missing this shows up as "replay mysteriously empty," not as a clear privacy win.

Network bodies. Capturing request/response bodies is opt-in for a reason. Prefer allowlists of safe endpoints over global capture. Server-side scrubbing exists as a backstop; it is not a license to record auth payloads.

Lazy-loading Replay. You can init Sentry without Replay, then addIntegration(replayIntegration()) later to shrink the critical path. Scoping rules (sample rates + privacy) still apply the moment Replay is added — lazy load is not a privacy control.

Checklist before you flip production on

Use this as a PR checklist for AI-built apps:

  1. Pick session rate from traffic band (0.01 / 0.1 / 0.25) — not from a tutorial that used 1.0.
  2. Set replaysOnErrorSampleRate: 1.0 unless you have quota math that says otherwise.
  3. Leave maskAllText, blockAllMedia, and input masking on; unmask only named safe selectors you tested.
  4. Mark design-system PII surfaces with data-sentry-mask / data-sentry-block (avatars, names, message bodies).
  5. Decide canvas explicitly — off unless you accept no PII scrubbing.
  6. Add CSP worker-src blob: (and child-src if you still support old Safari).
  7. Keep network body capture opt-in and route-scoped.
  8. Verify in staging with session rate 1.0, then lower before prod promote.
  9. Confirm idle/max session rules match how support expects replays to look (5 min idle / 60 min max).
  10. Link replay to your error and release process — do not treat it as a separate product from error tracking or release health.

Where OTF kits fit

If you start from an OTF kit, you get owned source plus AI-tool configs (Cursor, Claude, and related agents) so the agent extends your repo instead of a throwaway sandbox. Observability still has to be scoped on purpose: paste the init above into the web client, set rates from your traffic band, and keep masking defaults until a privacy review says otherwise. One-time payment kits do not remove the need for production sample-rate math — they just mean the agent is editing code you keep.

Sources

ai-toolsarchitecturebackend
OTF SaaS Dashboard Kit

Ship the product, not the setup.

  • 11 production screens — auth, billing, team, analytics, settings
  • Real database, payments, and login — all wired on day 1
  • AI configs pre-tuned so your agent extends instead of regenerates