Guide · Frontend
Session Replay Explained: How DOM Recording Masks Passwords and PII
19 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Session replay lets you watch a past visit to your website as if it were a video. It is really a recording of page structure and user actions, rebuilt later in a browser.
- The recorder takes one full snapshot of the DOM (the browser's live tree of page elements). Then it logs every change, click and keystroke as small JSON events (JSON is a plain text data format).
- Masking means swapping sensitive text for asterisks before an event leaves the browser. In rrweb 2.2.0, the open-source recorder we tested, only password inputs are masked by default.
- If a replay can identify a person, India's Digital Personal Data Protection (DPDP) Act 2023 treats it as personal data. If consent is your basis, record only after a clear yes, stop on withdrawal and delete old sessions.
- Beginners can stop after the section on which masking options to turn on. Engineers get our test results: one masking setting that silently unmasks passwords, attributes that masking never touches, and measured payload sizes.
What goes wrong when a user reports a bug you cannot reproduce?
You waste days guessing. The user says the Pay button did nothing, your logs show no error, and the page works perfectly on your laptop. Session replay fixes this by letting you watch what that user actually did.
Picture a common setup. A small online store gets support tickets saying checkout freezes on some phones. The setup is made up, but the bug pattern is real. The team adds a replay tool and watches a failing visit. They see the user tap Pay twice while a slow coupon box covers the button.
The same recording can also capture the user's phone number, email address and anything they typed. If the team is careless, it can even capture the password. That double edge is the whole subject of this guide.
Which words do you need before learning session replay?
Six words come up again and again. Each one is explained here once, in plain words.
| Word | What it means in plain words |
|---|---|
| DOM | The Document Object Model: the browser's live tree of everything on the page, such as headings, buttons and input boxes. |
| MutationObserver | A browser feature that tells your code whenever the DOM changes, for example when a node is added or an attribute changes. |
| Snapshot | A full copy of the DOM at one moment, written out as JSON (a plain text data format). |
| Event | One small JSON record of something that happened, such as a click, a keystroke or a DOM change. |
| PII | Personally identifiable information: data that can point to a real person, such as a name, phone number, email or account number. |
| Payload | The bytes the recorder sends to your server. Bigger payloads cost bandwidth, storage and money. |
What is session replay and how does it work?
Session replay is a recording of a user's visit made from DOM snapshots and change events, not from screen pixels. A small script on your page records, and a separate player rebuilds the page from that record later.
We tested the open-source rrweb project, which its package describes as a tool to record and replay the web. As of 25 October 2026 the latest release is 2.2.0, published on 24 October 2026. Its guide now recommends the packages @rrweb/record and @rrweb/replay and calls the older all-in-one rrweb package deprecated.
Step one: a full snapshot of the page
When recording starts, rrweb walks the whole DOM and writes it out as a JSON tree. Every node gets a numeric id so later events can point at it. In our test page with a small sign-in form, this snapshot was 4,506 bytes.
The snapshot also stores input values, which plain HTML does not show. Script code is not stored. rrweb swaps the code inside script tags for a placeholder, because the player never runs your JavaScript.
Step two: incremental events from MutationObserver
After the snapshot, rrweb only records changes. MDN describes MutationObserver as the interface that provides the ability to watch for changes being made to the DOM tree. rrweb's design notes say the observer's callback arrives in bulk, with an array of change records.
rrweb turns those records into Mutation events. It also listens for mouse moves, clicks, scrolls, window resizes and input changes. Each becomes an IncrementalSnapshot event with a source number that says which kind it is.
Step three: the player rebuilds the page in an iframe
The player creates an iframe (a page inside a page), builds the snapshot into it, then applies each event at its recorded time. When we replayed our recording, the page held one iframe and zero video elements. The rebuilt input boxes held the recorded values.
How a session replay is recorded and played back
Drag to exploreNotice that nothing here is a video. Masking happens in the user's browser before events are sent, so the server never sees masked values.
Diagram connections:
- User's page to Full snapshot (On start)
- User's page to MutationObserver (Every change)
- Full snapshot to Masking step
- MutationObserver to Masking step
- Masking step to Event batch (Masked events)
- Event batch to Replay storage (Upload)
- Replay storage to Replay player (Load and apply in order)
- User's page: Live DOM with forms and text
- Full snapshot: Whole DOM as a JSON tree, once
- MutationObserver: Reports DOM changes in batches
- Masking step: Swaps sensitive text for asterisks
- Event batch: Your code compresses, then uploads
- Replay storage: One file per session, with expiry
- Replay player: Rebuilds the DOM in an iframe
Why is session replay not a screen recording?
Because it stores structure, not pixels. That one choice explains both the benefits and the risks.
- It is small. In our test, one attribute change event was about 150 bytes of JSON.
- It is searchable. You can find every session where a given button was clicked, because the button is a node, not a patch of colour.
- It can be masked at the source. The recorder knows which node is a password box, so it can hide the value before sending anything.
- It misses what is not plain DOM. In rrweb 2.2.0, recording canvas drawings and cross-origin iframes is off by default, so their contents are not captured.
Think of a play script instead of a film. A film stores every frame. A script stores the set at the start, then each entrance, line and prop change, and a new cast performs it later.
The set is the full snapshot, the stage directions are the events, and the cast is the replay player. Masking is crossing out lines before the script is posted. Where the picture breaks: the player improvises nothing and runs none of your code.
Does session replay record passwords by default?
In rrweb 2.2.0, no: password fields are masked by default. Everything else a user types into text, email or phone fields is recorded in plain text unless you turn on more masking.
We checked this by recording a sign-in page in headless Chromium with Playwright. The page had phone, name, email and password fields, a greeting with a fake name, and a fake card number. We typed fake values, then searched the recorded JSON for each one.
| Setting | Password | Name, email, phone | Greeting text | Class rr-mask text |
|---|---|---|---|---|
| No options (default) | No | Yes | Yes | No |
| maskAllInputs: true | No | No | Yes | No |
| maskAllInputs: false | No | Yes | Yes | No |
| maskInputOptions: email only | Yes | Name and phone yes, email no | Yes | No |
| maskTextSelector: '.pii' | No | Yes | No | No |
| maskAllInputs plus maskTextSelector | No | No | No | No |
The older rrweb package gave the same results. Two details passed that a careless recorder might fail. A show-password button that turns the field into a text box stays masked, because rrweb marks the field as a former password field.
A script that writes the field's value attribute is masked too. In the default run, that attribute showed as twelve asterisks.
Which rrweb masking options should you turn on?
Turn on maskAllInputs, mask text that shows personal details, and block widgets you never need to see. That combination leaked none of our test values.
| Option | Default | What it does |
|---|---|---|
| maskAllInputs | false | Masks text-like input types such as text, email, tel, number and date, plus textarea and select. |
| maskInputOptions | password only | Masks only the input types you list. Your list replaces the default list. |
| maskTextClass and maskTextSelector | rr-mask and none | Replace the page text inside matching elements with asterisks. |
| blockClass and blockSelector | rr-block and none | Do not record the element at all; replay shows an empty box of the same size. |
| ignoreClass and ignoreSelector | rr-ignore and none | Do not record input events from matching elements. |
| maskInputFn and maskTextFn | none | Your own function decides the masked text. |
Here is the recorder setup we ran in Chromium. The page loaded @rrweb/record 2.2.0 through an import map, which tells the browser where a package name lives.
import { record } from '@rrweb/record';
const events = [];
const stopRecording = record({
emit(event) {
events.push(event);
},
maskAllInputs: true,
maskTextSelector: '.pii',
blockClass: 'rr-block',
sampling: { input: 'last' },
});The class pii is our own name for elements that show personal details, such as a greeting with the user's name. The sampling line keeps only the final value of each input instead of one event per keystroke. In our test, typing 20 characters gave 20 input events by default and 1 with this line.
Check it worked: search the recording for your test values
Never trust a masking config you have not tested. Type known fake values on a staging page, record, then search the JSON. This check printed No test values found in our run.
const raw = JSON.stringify(events);
const leaked = ['[email protected]', 'Hunter2-Secret', 'Greetingname', '4111']
.filter(function (value) { return raw.includes(value); });
console.log(leaked.length === 0 ? 'No test values found' : 'LEAK: ' + leaked.join(', '));List every sensitive field
Walk your sign-up, login, profile and payment pages. Note inputs, greetings, addresses, order details and account numbers.
Set masking to deny by default
Turn on maskAllInputs. Add your own class to any element that prints personal details, and pass it as maskTextSelector.
Block whole widgets
Add rr-block to payment iframes, chat widgets and anything you never need to watch.
Record a test session with fake values
Use values that cannot be real, such as [email protected], so a search hit is clearly a leak.
Search the JSON and fail the build on a hit
Run the check above in an automated browser test, so a new page cannot quietly add a leak.
Can one masking setting accidentally unmask passwords?
Yes. In rrweb 2.2.0, passing maskInputOptions with only email turned on switches off password masking, and our recording held the password in plain text.
The source explains why. The record function uses your maskInputOptions object as the whole list, and falls back to password only when you pass nothing. So password: true must appear in any list you write yourself.
The rrweb guide does show the password-only default in its options table. It does not warn that a custom list replaces it, so this is undocumented as of 25 October 2026; pin your version. Version 2.1.7 has the same code.
What does session replay masking still leak?
Masking covers input values and text, not everything else. Attributes, the page address and the shape of the page still travel to your server.
We put a title, a data attribute, an aria-label and an image with alt text inside an element covered by maskTextSelector. The visible text was masked. Every attribute value, including a fake email in the data attribute, appeared in plain text.
- Attributes: title, aria-label, alt, data attributes and image addresses are recorded as written. Use blockClass on such elements, or keep personal data out of attributes.
- The page address: the first Meta event stores the full URL. Tokens or emails in query strings end up in your replay storage.
- Length and layout: asterisks keep the length, and a blocked box keeps its size. Anyone watching the replay still learns how long a password or PIN was.
- Unmarked text: a name printed in a table cell is plain text unless that cell matches your mask selector.
What does India's DPDP Act 2023 mean for session replay?
If a replay can be linked to a person, it holds personal data under the Digital Personal Data Protection Act, 2023. This section quotes the Act; it is not legal advice, so check with your legal team.
The Act defines personal data as any data about an individual who is identifiable by or in relation to such data. A replay of a signed-in user, or one showing their typed address, clearly fits.
Where you rely on consent, section 6(1) says it must be free, specific, informed, unconditional and unambiguous, given with a clear affirmative action. It must be limited to personal data necessary for the specified purpose.
- Notice first: section 5(1) says a consent request must come with a notice of the personal data and the purpose for processing it.
- Plain language: section 6(3) asks for clear and plain language, with an option of English or a language in the Eighth Schedule of the Constitution.
- Easy withdrawal: section 6(4) gives the right to withdraw consent at any time, as easily as it was given.
- Security: section 8(5) requires reasonable security safeguards to prevent a personal data breach.
- Erasure: section 8(7) requires erasing personal data when consent is withdrawn or the purpose is no longer served, unless a law requires keeping it.
The government notified the DPDP Rules, 2025 on 14 November 2025, according to a Press Information Bureau document. That document says the Rules bring an eighteen-month period for phased compliance. Check the current dates as of when you read this.
In code, the safest pattern is to not load the recorder until consent is recorded. Avoid pre-ticked boxes, since the Act asks for a clear affirmative action. When consent is withdrawn, call the stop function that record returns, stop uploads and queue a deletion.
Where the consent check sits
Drag to exploreThe recorder script loads only after a yes. Withdrawal stops recording and starts deletion, not just hiding the banner.
Diagram connections:
- Page loads to Notice and choice
- Notice and choice to User said yes?
- User said yes? to No recording (No)
- User said yes? to Start recorder (Yes)
- Start recorder to Consent withdrawn (Later)
- Consent withdrawn to Stop and erase
- Page loads: No recorder script yet
- Notice and choice: Says what is recorded and why
- User said yes?: Clear action, not a pre-ticked box
- No recording: Site works the same
- Start recorder: Masking on, consent id attached
- Consent withdrawn: As easy as saying yes
- Stop and erase: Stop function, then delete stored sessions
How big is a session replay recording in 2026?
Small in bytes per event, but it adds up fast. Our 2.8-second sign-in test produced about 100 events and about 39.9 KB of raw JSON, which gzip shrank to about 3.5 KB.
| What we recorded | Events | Raw JSON | Gzipped |
|---|---|---|---|
| Sign-in test, default options (about 2.8 s) | about 100 | about 39.9 KB | about 3.5 KB |
| Its full snapshot alone | 1 | 4,506 bytes | not measured |
| Its one batch adding 100 list rows | 1 (200 nodes) | about 24.6 KB | not measured |
| About 41.7 s of steady mouse movement | 86 (652 positions) | about 38.6 KB | about 3.5 KB |
| Same, with mousemove sampling off | 2 | 4,658 bytes | about 0.8 KB |
Three lessons came out of the numbers. First, DOM changes dominate: adding 100 rows cost more than every click and keystroke combined. Second, compressing the whole batch works far better than compressing each event.
rrweb's storage guide says the same, and recommends compressing the whole session on the backend. In our run, gzip on the full batch gave about 3.5 KB. The @rrweb/packer per-event compressor gave about 27.3 KB, because each event is compressed separately.
Third, mouse movement is cheap per second but never stops. rrweb 2.2.0 samples the pointer every 50 ms and sends positions every 500 ms by default, according to its source.
Long sessions bring one more cost. The rrweb guide says that, by default, every emitted event is needed to replay a session. So you cannot keep only the last few minutes. The checkoutEveryNth and checkoutEveryNms options take fresh full snapshots on a schedule. With checkoutEveryNth set to 10, our short test produced 4 full snapshots in 39 events.
How do you keep session replay from slowing down your page?
Record less, send in batches, and measure on a slow phone. The recorder runs inside your page, so every change it observes costs your page some work.
- Block noisy areas. rrweb's storage guide lists long lists, complex SVG, JavaScript animations and canvas animations as common sources of many events.
- Sample. Turn off mousemove if you do not need cursor trails, slow down scroll events, and use input 'last'.
- Batch uploads. Collect events in memory, compress, and send every few seconds or when the tab is hidden.
- Record a fraction. Consider recording only a sample of consenting sessions, rather than every visit.
- Measure it. Compare your real user monitoring numbers with and without the recorder before rolling out.
How is session replay different from real user monitoring?
Real user monitoring sends a few numbers per page, while session replay sends the whole interaction. Error monitoring sends stack traces when something breaks. They answer different questions and carry different privacy weight.
| Signal | What it sends | Question it answers | Privacy weight |
|---|---|---|---|
| Real user monitoring | Timing numbers such as load time, per page view | Is the site fast for real users? | Low, if URLs are cleaned |
| Error monitoring | Error message, stack trace, browser details | What broke and where in the code? | Medium, messages can hold data |
| Session replay | Full DOM snapshot plus every change and input | What exactly did this user see and do? | High, it can show anything on screen |
Linking them is where replay shines. OpenTelemetry, the open standard for traces, metrics and logs, defines a session.id attribute as a unique ID to identify a session. As of 25 October 2026 it is marked Development, meaning it may still change.
Put the same session id on error reports and replay files. Then an engineer can jump from a stack trace straight to the moment it happened.
How do you explain session replay in a system design interview?
Lead with the mechanism, then spend most of your time on privacy and cost. Interviewers want to hear that you know it is not a video.
Define it in one sentence
A browser script records one DOM snapshot and then a stream of change and input events, and a player rebuilds the page from them.
Draw the pipeline
Recorder, masking, batching and compression in the browser, then an ingest service, object storage and a replay player.
Put privacy first
Mask all inputs by default, block payment and chat widgets, keep personal data out of attributes, and test with fake values in CI (automated build checks).
Gate it on consent
Load the recorder only after clear consent, stop it on withdrawal, and delete stored sessions with an expiry policy.
Size the storage
Estimate sessions per day times compressed size per session, and say that DOM changes, not clicks, drive the size.
Link it to other signals
Share one session id with error monitoring and real user monitoring, so engineers jump from an alert to the replay.
Questions people ask
- Is session replay the same as recording a video of the screen?
- No, session replay records the page structure and user actions, not pixels. rrweb takes one JSON snapshot of the DOM, then logs each change, click and keystroke. A player rebuilds the page inside an iframe and applies the events in order. In our test, the replay page had one iframe and no video element.
- Does rrweb mask passwords by default?
- Yes, in rrweb 2.2.0 password inputs are masked by default, as of 25 October 2026. Text, email and phone inputs are recorded in plain text unless you turn on maskAllInputs or list them. Be careful with maskInputOptions: a custom list replaces the default, so leave out password and passwords are recorded.
- What is the difference between maskAllInputs and maskTextSelector?
- maskAllInputs hides what users type into form fields, while maskTextSelector hides text the page itself displays. maskAllInputs covers text-like input types such as text, email, tel and number, plus textarea and select. maskTextSelector takes a CSS selector, such as a class you add to greetings or addresses. You usually need both.
- Do I need consent for session replay in India?
- If the replay can identify a person, it holds personal data under the DPDP Act 2023, so you need a lawful basis such as consent. Section 6(1) requires consent that is free, specific, informed, unconditional and unambiguous, with a clear affirmative action. Ask your legal team which basis fits your case.
- How much storage does session replay need?
- It depends mostly on how much your DOM changes. Our 2.8-second sign-in test with rrweb 2.2.0 produced about 39.9 KB of raw JSON and about 3.5 KB gzipped. Adding 100 list rows alone cost about 24.6 KB raw. Measure your own pages, then multiply by sessions per day and your retention period.
- Can session replay see what is inside a payment iframe?
- Not by default in rrweb 2.2.0, if the iframe comes from another origin. Recording cross-origin iframes is off unless you set recordCrossOriginIframes. In our test, the iframe element was recorded but none of its text or input values were. Still add rr-block to it, so it stays a blank box if settings change.
Sources and further reading
- MDN Web Docs: MutationObserver
- rrweb guide at tag [email protected] (options table, privacy and checkout sections)
- rrweb design notes: incremental snapshots and MutationObserver ([email protected])
- rrweb design notes: serialization ([email protected])
- rrweb recipe: optimize the storage size ([email protected])
- rrweb source: record options and maskInputOptions default ([email protected])
- rrweb source: maskInputValue in rrweb-snapshot utils ([email protected])
- rrweb source: mouse movement sampling defaults in observer.ts ([email protected])
- npm registry metadata for @rrweb/record 2.2.0
- OpenTelemetry semantic conventions: Session (session.id)
- The Digital Personal Data Protection Act, 2023 (MeitY official text)
- PIB: DPDP Rules, 2025 Notified (17 November 2025)