ObservabilityGENERALPLATFORM-SPECIFICSIMPLIFIED

Session Replay and the Privacy It Costs

Replay records whatever was on the screen — including things you did not intend to record. Masking is opt-out shaped and fails open, so the governance question comes before the engineering one.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

Before recording a user's session, what exactly am I collecting, who is it about, and what happens when the masking is wrong?

The user intent

A user reports that checkout did not work and cannot say what they did. An engineer wants to watch it happen. That is a reasonable wish, and it is also a request to record a person.

The obvious build

Add the replay snippet, record every session, and when a bug comes in, watch the replay. It is only DOM and events, not video, so there is nothing sensitive in it.

Why it breaks

It is not "only DOM". The DOM is the content — the account balance, the diagnosis, the message thread, the name of a child on a school portal. A faithful reconstruction of the DOM is a faithful reconstruction of what was on the person's screen.

How it breaks in a real browser
  • It is not "only DOM". The DOM is the content — the account balance, the diagnosis, the message thread, the name of a child on a school portal. A faithful reconstruction of the DOM is a faithful reconstruction of what was on the person's screen.
  • Text typed into an unmasked field is captured keystroke by keystroke. A card number pasted into the wrong input, a password typed into a username box, a private note drafted and deleted — all of it is in the stream, including the parts the user chose not to submit.
  • The screen routinely contains other people's data. A support agent's session shows a customer's record; a clinician's session shows a patient. Consent from the person being recorded does not cover the people on their screen.
  • Masking works by matching selectors. A new field ships, no rule matches it, and it is captured in full — the failure is silent, and it is discovered by an audit rather than by a test.
  • The recording exists indefinitely once it is in a vendor's system, and a subject-access or deletion request now has to reach it. Most teams cannot say what a given replay contains.
  • Some categories — health, financial, biometric, information about children — carry legal obligations that attach the moment the data is collected, not the moment someone looks at it.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Replay is not video. It takes an initial serialised snapshot of the DOM, then records a stream of mutations, input events, scroll positions, pointer movements, viewport changes and network metadata, and replays them into an iframe to reconstruct the page.
  • That reconstruction fidelity is the point and the problem: anything rendered is in the snapshot or the mutation stream, including content that was on screen for a moment and content that was never submitted anywhere.
  • Masking is applied at capture: matching nodes have their text replaced with placeholder characters before serialisation. It is a filter over a stream that already contains everything, and it is only as complete as its rules.
  • Masking policies come in two shapes, and the difference is the whole lesson. A blocklist captures by default and hides what matches — so anything new is captured. An allowlist hides by default and reveals only what matches — so anything new is hidden. Only the second one fails safe (Fail Open vs Fail Closed in Security Engineering).
  • Input values, value attributes, placeholder text, title and aria-label all carry text. A rule that masks visible text nodes but not accessible names leaks through the accessibility surface, which is easy to forget precisely because it is not visible.
  • Consent is a mechanism, not a banner: recording must not start until consent exists, because a snapshot taken before the answer cannot be un-taken.
  • Retention is the other half. Data that exists can be requested, subpoenaed, breached or mistakenly exposed. Deletion on a short schedule is the only control that reduces that exposure over time, and the strongest control of all is not collecting it.

What this makes the browser do

And which of it is avoidable.

  • The initial snapshot serialises the whole document, which on a large page is real main-thread work at exactly the moment the page is trying to become interactive (Long Tasks).
  • A MutationObserver over the whole document fires for every DOM change your application makes. On a busy list or an animation driven through the DOM, that is a continuous stream of callbacks competing with rendering (What a Mutation Costs).
  • Pointer-move capture is high frequency by nature and must be throttled, or the recorder becomes the largest consumer of the main thread during exactly the interactions people complain about.
  • The event stream must be buffered, compressed and uploaded. Doing that work on the main thread rather than in a worker is a self-inflicted responsiveness problem in a tool bought to diagnose responsiveness problems (Web Workers and the DOM Boundary).
  • Recording is not free for the user's data plan or battery either, which is a cost you are imposing on someone for your benefit.

What a recording actually contains

Before any engineering discussion, be concrete about the artefact. A session replay is a reconstruction of a person's screen, assembled from a full serialisation of the document plus every change to it. If it were less than that, it would not be able to show you the bug.

The consequence is that the question "is there anything sensitive in our replays" has the same answer as "is there anything sensitive on our screens". For nearly every product that handles accounts, messages, money, health or other people's records, the answer is yes, and it is yes on pages nobody thought of as sensitive.

Capture, and what each stage can expose
  1. 1
    Initial DOM snapshot

    Serialises the whole document — text, attributes, computed styles, referenced assets — as the starting frame.

    fails by Capturing whatever was already rendered, including content shown before consent was resolved, and accessible names that masking rules aimed only at visible text will miss.

  2. 2
    Mutation stream

    Records every DOM change so the replay can reconstruct the page over time.

    fails by Capturing content that appeared for a moment and was removed — a validation message quoting an entered value, a toast containing a name, a modal opened by mistake.

  3. 3
    Input and interaction events

    Records focus, keystrokes, scroll, pointer movement and viewport changes.

    fails by Recording what was typed and then deleted, and what was pasted into the wrong field, neither of which was ever submitted anywhere.

  4. 4
    Network metadata

    Records request URLs, timings and sometimes bodies, to align the replay with what the application was doing.

    fails by Capturing tokens and identifiers in query strings, and response bodies containing data far beyond what the screen showed.

  5. 5
    Masking filter

    Replaces matching text with placeholders before serialisation.

    fails by Matching selectors that no longer exist after a redesign — silent, undetected, and effective from the moment of the deploy.

  6. 6
    Upload, storage, retention

    Streams the session to a processor that stores it for a configured period.

    fails by Retention set once and never reviewed; recordings shared into chat; a vendor breach making every one of them somebody else's.

Every stage is a place where data enters. Only the last two are places where it can be removed, and both of them run after the data already exists.

Masking fails open unless you invert it

This is the engineering heart of the lesson. A masking configuration is a policy about defaults, and the default decides what a mistake costs you. Under a blocklist, forgetting a rule means data is disclosed. Under an allowlist, forgetting a rule means a box in a replay is blank and somebody files a ticket.

The asymmetry matters because forgetting is certain. Fields are added, components are reused in new contexts, class names change during a redesign, and a third-party widget starts rendering content it did not render last quarter. A policy that requires nobody to ever forget is not a policy.

Two masking configurations, one redesign later
Blocklist: capture by default
recorder.init({
  maskTextSelector: '.pii, .card-number, #email, [data-sensitive]',
  maskAllInputs: false,          // capture what users type
})

// Sprint 14 ships <input class="member-note"> for free-text notes.
// No rule matches it.
// => every note anyone types is now in the recording stream.
// Nothing errors. No test fails. Discovered in an audit, months later,
// with the whole retention window already full of it.
Allowlist: hide by default
recorder.init({
  maskAllText: true,             // everything is a placeholder
  maskAllInputs: true,           // no typed content, ever
  unmaskTextSelector: '.replay-safe',   // opt IN, node by node
  blockSelector: '.payment, .messages, .patient-record',  // not captured at all
})

// Sprint 14 ships <input class="member-note">.
// No rule matches it.
// => it is masked, because masked is the default.
// The cost of the mistake is a blank box and a ticket.

Both configurations are wrong in the same way — a rule was not written for a new field. Only the direction of the default differs, and that direction decides whether the inevitable omission produces a disclosure or an inconvenience. A control that fails safe is worth more than a control that is merely thorough today.

Decide whether to record at all

PLATFORM-SPECIFICWhere the line falls between these options is set by your jurisdiction and sector rather than by engineering judgement: an explicit prior-consent regime, a health or financial data classification, or a product used by children each move the acceptable option several rows up this list, and an internal admin tool showing customer records is usually excluded outright.

The option most often missing from the discussion is the one at the top of this list. Replay is usually adopted to answer a question — "what did the user do before it broke" — and that question frequently has a cheaper answer with a fraction of the obligation attached.

Work down the list only as far as the question requires. The safest data is the data you never collected, and every step further down adds a standing obligation that outlives the investigation that motivated it.

What is the least data that answers the question?

The user hit a bug and cannot describe it. What do you deploy?

Nothing new — error reports and breadcrumbs

when You need to know the sequence of routes, clicks and requests that preceded a failure

cost Less fidelity than a replay: you get the shape of the journey, not what was on screen. Answers a large majority of real investigations (Frontend Error Tracking).

Structured product events

when You need to know which steps of a flow people complete and where they drop out

cost Requires deciding what to instrument in advance, and answers only the questions you thought to ask (Analytics Events That Answer a Question).

Replay, errored sessions only, allowlist masking

when A specific, reproducible-in-principle failure is resisting every cheaper approach

cost Buffering in memory to decide at the end, a much smaller but still real privacy surface, and a masking configuration that must be tested and maintained.

Replay, sampled, allowlist masking, short retention

when A consumer product with no regulated data, where UX research also needs the signal

cost A standing obligation: consent, retention enforcement, access logging, deletion handling, and a vendor inside your trust boundary.

Replay everything, blocklist masking

when It is difficult to construct a case for this on a product that handles personal data

cost Maximum exposure with a control that fails open. Every field added after the rules were written is captured by default.

Ask the user

when A single reproducible report from a reachable person

cost Slow, and it does not scale — and it is still the right answer more often than the tooling discussion admits.

How to build it

Most important first.

  • Start with the governance question: what is the narrowest data that answers the question you actually have? Very often an error report with breadcrumbs answers it, and no replay is needed at all (Frontend Error Tracking).
  • Default to not recording. Make replay opt-in per surface, and never enable it globally on an application that handles regulated data.
  • Mask by allowlist. Everything is hidden unless a rule explicitly reveals it, so a new field ships hidden and a mistake costs you a blank box instead of a disclosure.
  • Block entire regions, not individual fields, where sensitive content lives — payment sections, message bodies, medical records, anything showing a third party's data. A blocked subtree is not captured at all rather than captured and hidden.
  • Never capture input values by default. Record that a field was focused, changed and blurred; the content of what someone typed is rarely what the investigation needed.
  • Obtain consent before the recorder initialises, and make refusing it genuinely free of consequence.
  • Set the shortest retention that still answers a support ticket, and enforce it in the vendor's configuration rather than in a document.
  • Write down who may view recordings, log every view, and treat that log as an audit trail — replay access is access to personal data (Audit Logs for Privileged Actions in Security Engineering).
  • Test the masking as a first-class test. Take a recording of a page containing known sensitive strings, and assert those strings are absent from the payload (End-to-End Testing).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Replay records what was on screen and does not record what was announced. A screen-reader user's experience is largely invisible to it — focus order, live-region announcements and reading order do not appear in a visual reconstruction, so replay is not a way to observe accessibility (Accessibility Testing).
  • Recorders hook input handling, and a hook that mishandles an event can break keyboard operation, swallow a default action, or interfere with an assistive technology's synthetic events. That is a functional regression caused by an observation tool, and it lands on the users least able to work around it (Keyboard Operability).
  • Masking must not remove accessible names. A rule that blanks an aria-label along with visible text degrades the experience for a screen-reader user in order to protect a recording — mask what is transmitted, never what is rendered.
  • The consent interface is itself an interface: it must be keyboard-operable, announced, and dismissable without a pointer, or you have made the privacy control inaccessible to the people it protects (Focus Management).
  • The recorder's main-thread cost delays announcements and focus movement exactly as it delays paint (What the Main Thread Owns).

What can go wrong

Failure modes
  • A redesign renames a class and the masking selector stops matching. Capture continues; nothing errors; the leak is silent and dated from the day of the deploy.
  • A third-party widget renders inside your page and is captured with it — a payment iframe's surroundings, an embedded chat, a support tool showing another customer.
  • Text that is masked visually but present in an aria-label, a title, a data- attribute or a value attribute, and therefore in the stream.
  • The recorder itself becomes the performance regression, and is diagnosed last because it is the diagnostic tool.
  • Consent is collected but the recorder started at page load, so a snapshot of the pre-consent page already exists.
  • Retention configured in one environment and not another, so the staging replays live forever and contain production data copied in for testing.
  • A replay is shared into a chat channel and now exists outside every control you configured.
  • A user exercises a deletion right and nobody can locate their recordings because sessions were keyed by an identifier that cannot be joined back to the person.
What can arrive out of order
  • The recorder can initialise before the consent state is known, capturing a snapshot that should never have existed. Gate initialisation on consent rather than filtering afterwards.
  • The DOM snapshot and the mutation stream can disagree if a mutation lands during serialisation, producing a replay showing a state that never existed on screen.
  • Network metadata and DOM mutations arrive on separate paths, so the reconstructed ordering of "request sent" against "spinner appeared" may be wrong — a real hazard when the replay is being used to reason about a race.
  • A masking rule applied asynchronously after a node is inserted leaves a window in which the unmasked node was already serialised into the stream.
Security
  • Session replay is the highest-risk telemetry a frontend team can deploy, and treating it as ordinary analytics is the mistake this lesson exists to prevent. Assume every recording contains personal data until a test proves otherwise.
  • Masking fails open by construction: the stream contains everything, and the filter decides what to remove. An allowlist inverts the default so that a missed rule costs visibility instead of disclosure.
  • Data you never collected cannot leak, cannot be subpoenaed, cannot be exposed by a vendor breach and cannot be mishandled by a colleague. Minimisation is the only control with no failure mode (Sensitive Data Classification in Security Engineering).
  • The recorder runs with your page's full authority. A third-party replay script can read the DOM, cookies accessible to script, and anything else on the page — you are extending your trust boundary to that vendor (Third-Party Scripts and the Supply Chain).
  • Recordings of internal-tool sessions expose your own systems: admin interfaces, internal identifiers, support workflows and the data of the customers being served.
  • Sensitive fields sometimes leak through the URL rather than the DOM. Strip query strings and report route patterns (URL Parameters).
  • Cross-tenant exposure is a real risk in multi-tenant products: a support session that spans several customers puts their data in one recording (Multi-Tenant Isolation in Security Engineering).
Misreads
  • "It is only the DOM, so there is nothing sensitive." The DOM is the content of the screen. If it were not, replay would be useless.
  • "We mask passwords, so we are covered." Passwords are the easiest case. The hard cases are free-text fields, rendered account data, other people's information, and every field added after the rules were written.
  • "The vendor is compliant, so we are compliant." A processor's certifications do not decide what you collect, from whom, on what basis, or for how long. Those are your decisions.
  • "Nobody looks at most recordings, so the risk is low." The risk is created by the data existing, not by anyone watching it.
  • "Consent is a banner." Consent is a state that must gate the recorder's initialisation. A banner shown after the snapshot was taken is theatre.
  • "We can always turn masking on later." Everything recorded before then is already collected, and it is now your problem to find and delete.

Measuring it, and what changes in the field

How you would see this
  • An automated masking test in CI: render pages seeded with known sensitive markers, capture, and assert none of the markers appear in the payload. This is the only measurement that tells you the masking still works after a redesign.
  • The size and event rate of the recorded stream per session — a proxy for both cost and how much you are capturing.
  • The recorder's own main-thread contribution, measured with it enabled and disabled on the same journey (Measure Before Optimising).
  • An access log of who viewed which recording, reviewed like any other access to personal data.
  • A retention report: how many recordings exist, how old the oldest is, and whether that matches the policy you published.
  • The count of blocked and allowed regions per page, so an unexpected drop after a deploy is visible rather than silent.
Slow device, slow network, large data, old tab
  • On a slow device, the recorder is a much larger share of a much scarcer main thread, so the sessions it degrades most are the ones already having the worst experience.
  • On a metered or slow connection, uploading a session stream competes with the application's own requests and consumes the user's data allowance (Network Failures Only the Client Can See).
  • In a long-lived tab, the mutation stream grows without bound; recorders cap it, which means the part of the session you most wanted may already have been dropped (Long-Lived Clients and Version Skew).
  • On internal tools and admin interfaces, every session contains other people's data by definition, which changes the answer from "mask carefully" to "do not record".
  • Under a regulated regime — health, finance, children's services, or any jurisdiction with an explicit consent requirement — the default answer is no recording, and the exception needs a named owner and a legal basis.
What this costs
  • Replay genuinely shortens some investigations, particularly the ones where the user cannot describe what they did. That value is real, and it is bought with a permanent standing risk that exists whether or not anyone watches a recording.
  • Allowlist masking is safe and makes many recordings less useful, because the interesting content is often the content you hid. That is the correct trade and it should be made consciously.
  • Short retention limits exposure and means the recording for a bug reported three weeks later no longer exists.
  • Recording only sessions that errored reduces volume and privacy surface sharply, and requires buffering the session in memory in order to decide, which is its own cost.
  • Not deploying replay at all costs you a debugging tool and removes an entire class of obligation. For many products, that is the right answer and it is rarely presented as an option.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe capture mechanism — an initial DOM serialisation plus a MutationObserver stream and input events — is the same across Chromium, Gecko and WebKit because it is built entirely on standard DOM APIs; what differs between engines is only the cost of observing a large document, not what ends up in the recording.
  • PLATFORM-SPECIFICThe legal and organisational obligations attached to this data vary by jurisdiction, sector and the kind of information your product handles: a consumer app in one region, a clinical tool in another and an internal admin console for a multi-tenant product face materially different requirements, and none of them can be settled by an engineering default.
  • SIMPLIFIEDReal recorders add canvas and media handling, shadow-DOM traversal, cross-iframe stitching and asset proxying, each of which widens what is captured; the snapshot-plus-mutations model here is enough to reason about the privacy properties but understates how much surface a production recorder actually touches.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Software Design — the difference between a control that is thorough and a control that fails safe, and why default-deny is a design property rather than a configuration detail.