Session replay
DOM-reconstruction session replay with privacy masking, capture modes, and checkpoints — opt-in per session and always mindful of PII.
Session replay reconstructs the DOM — it does not take screenshots — so playback is crisp, searchable, and small on the wire. It is off by default; enable it explicitly.
Enable replay#
await ScaleBun.init({ clientKey, apiBaseUrl, features: { replay: true }, replay: { mode: 'sampled', sampleRate: 1 },});Modes: sampled, onError (keep a rolling buffer, promote it when something
breaks), always, and off.
Controls#
ScaleBun.replay.checkpoint('before-payment'); // mark a momentScaleBun.replay.flush(); // promote the pre-roll buffer to liveScaleBun.replay.pause();ScaleBun.replay.resume();ScaleBun.replay.stop(); // stop recording for this sessionScaleBun.replay.setPrivacy({ maskAllText: true });ScaleBun.replay.stats(); // null when not recordingstats() is the fastest way to answer "why is there no replay for this
session" — it returns the mode, the sample bucket, the adaptive quality level and
the dropped/shed counts, or null when this session was simply not sampled.
Sampling options#
| Option | Default | What it does |
|---|---|---|
mode | sampled | off · onError · sampled · always. |
sampleRate | 1 | Fraction of sessions recorded in sampled mode. |
onErrorSampleRate | 1 | Gates onError mode. |
persistBuffer | false | Mirror the pre-roll to IndexedDB so a tab-killing crash still yields it on the next load. A clean, error-free close discards it. |
privacyLevel | 'standard' | 'strict' adds heuristic redaction of emails, card numbers and SSNs to captured text. |
The sampling decision is deterministic per session — the SDK buckets the session id with the same hash the backend uses, so the server can re-derive it and does not have to trust the client. A session that is not sampled installs nothing.
Privacy#
Because replay reconstructs the DOM, masking is precise. Typed form inputs are always masked and passwords are always blanked, regardless of settings. Beyond that you control text masking:
privacy.maskReplayText— mask all visible page text.privacy.maskSelectors— mask text inside elements matching CSS selectors.data-scalebun-mask— tag any element to always mask its subtree.contenteditableregions are always masked.
Cross-origin iframes#
A cross-origin iframe is invisible to the parent page by default. It can be stitched into one replay if both sides run the SDK and each lists the other's origin:
// parent page, app.example.comreplayFrameOrigins: ['https://embed.example.com']
// the embedded app, embed.example.comreplayFrameOrigins: ['https://app.example.com']One side alone does nothing — the allowlist is what authorises the bridge in each direction.
Next#
Privacy & consent — consent gating and CSP.
Errors & crashes — errors link straight to the replay.