August 28, 2026
Privacy at Capture
The data-privacy attribute lets a page tell any capture SDK what to mask, ignore, block or unmask.
Rogier Trimpe
@roggernautPrivacy rules for session recording usually live in SDK configuration or vendor-specific CSS classes. That makes a change of recorder a privacy migration.
Privacy at Capture is now open for review in rrweb. It proposes a DOM attribute that the page owns, backed by a versioned policy that other web and mobile capture SDKs can implement.
A shared capture standard lets a privacy decision made once in application markup be enforced by every recorder that implements it. Protection then survives migrations and SDK rewrites, while component authors can ship privacy intent with the component itself.
<div data-privacy="mask">Account balance: €12,480.30</div>rrweb keeps the element and replaces its content with stars while serialising the page. The recording backend never receives the account balance.
One attribute
The proposal defines three values:
<section data-privacy="mask">Keep the layout and mask the content.</section>
<section data-privacy="block">
Replace this subtree with a same-sized placeholder.
</section>
<input data-privacy="ignore" name="one-time-code" />
<nav data-privacy="unmask">This navigation is safe to record.</nav>maskkeeps the element's shape and obfuscates its content.ignoremasks the content and emits no input events at all — not even the value's length. It succeeds rrweb's.rr-ignoreclass, which only suppresses events and was never a masking control.blockreplaces the whole subtree with a placeholder of the same size. Descendants cannot unmask it.unmaskexposes content inside a masked region when the policy permits it.
Password and other protected inputs stay masked. unmask cannot expose a protected input or restore media removed by the strict preset. When conflicting values match the same element, the strictest wins: the ladder runs unmask, mask, ignore, block.
A value rrweb does not recognise is treated as mask. A typo in the attribute therefore over-protects, and never exposes content by accident.
We chose data-privacy over a bare privacy attribute because HTML defines data-* for application-owned annotations. It works on any element, is available through element.dataset.privacy and avoids a collision if HTML gains a native privacy attribute later. Leaving out a recorder prefix lets the annotation survive a change of tool.
Policy
Markup covers components a team can edit. Policy rules cover generated interfaces, third-party widgets and safe defaults for the rest of the page.
import { record } from 'rrweb';
record({
emit(event) {
// The event is already masked here.
},
privacyPolicy: {
version: 1,
preset: 'balanced',
rules: [
{
target: { type: 'selector', selector: '.support-widget' },
action: 'block',
},
],
},
});balanced masks every form input and sensitive descriptive attributes such as title, placeholder and aria-label. strict also masks page text, removes media sources and disables canvas recording. minimal leaves masking to the classic recording options you configure yourself, and records exactly as rrweb does today with one deliberate exception: password and payment fields always come out masked, because those are too sensitive to be configurable.
minimal is a permanent tier. It is not a staging step on the way to a managed preset, and teams that have tuned their own masking can stay on it indefinitely. It carries one floor that cannot be configured away: password and payment fields always record as stars, because those values are too sensitive to leave to configuration. That floor applies in every preset, including recordings started without a policy.
rrweb compiles the policy into the selectors, flags and sets already used by its serializer. The initial snapshot and later DOM mutations use the same masking decisions. There is no second rule-engine walk over every node.
Prior art
Privacy at Capture draws on MIT-licensed work in rrweb, PostHog, Highlight, Sentry, Amplitude, Mixpanel, Datadog and New Relic.
Those projects already use the main techniques in this proposal: broad masking with explicit escapes, selector merging, permanent protection for sensitive inputs, centralised attribute decisions, dimension-preserving image blocks and canvas-region masking.
Privacy at Capture packages those techniques behind one attribute, policy schema and precedence model. That contract is the part under review.
Three optional features remain experimental:
| Feature | Behaviour | Open question |
|---|---|---|
| Text detectors | Mask a whole text node after an email, phone, Luhn-valid card, SSN-like or IPv4 match | The fixed pattern set and live mutation path need production evidence |
| URL sanitisation | Remove credentials and mask selected query parameters before recording href, src and related attributes |
The default parameter list and replacement format need review |
| Unmask probe | Skip ancestor checks when a snapshot contains no possible unmask match | The optimisation needs evaluation around shadow DOM boundaries |
Keeping these features separate lets maintainers review the structural policy without taking on unproven detection and optimisation work.
Migration
Pages already contain privacy classes from their current recorder. Under the balanced and strict presets, rrweb always honours data-privacy and its own .rr-* classes.
Conventions belonging to other tools are opt-in, and selectable per vendor. vendorCompat: ['posthog'] recognises only PostHog's mask and block tokens; vendorCompat: true recognises the full registry of twenty-five tools. Each recognised token maps to the closest treatment rrweb's own verbs express, verified against the vendor's SDK or documentation, and never a less protective one. Escape tokens are the deliberate exception: no foreign unmask or allow marker is ever honoured, under any setting -- not even the previous vendor's own. Recognition can only add masking, so a stale class can over-protect a page but never expose one.
The default is off because recognition changes recording behaviour based on markup the embedding application may not control. A class that arrived with a third-party widget should not silently change what rrweb records, so turning it on is an explicit decision.
A migration, then, has three steps. Start recording with vendorCompat set to your previous vendor's id, so every annotation that protected the page yesterday protects it today; if the old markup used the vendor's unmask escape, re-declare those few elements explicitly with data-privacy="unmask" or an unmask rule in the policy -- reveal decisions belong in your own configuration, not a compat flag. Convert annotations to data-privacy as you touch each component. When the old classes are gone, remove the flag and the policy stands on the standard alone.
Errors
Runtime errors and invalid policy configuration need different handling.
During capture, each failure has a defined result. A custom masking function that throws or returns an invalid type falls back to the built-in star mask. If an unmask selector throws, it creates no match, so inherited masking stays in place. When mask and unmask both match, mask wins. An unrecognised data-privacy value is read as mask. The optional URL sanitiser drops an attribute when it cannot parse the value.
Selector syntax is checked once, when recording starts. rrweb drops a malformed selector and reports a warning. Other selectors continue to work. If the bad selector was supposed to mask content, that content is no longer protected by the rule. Applications should treat the warning as a configuration error.
Canvas and mobile
A DOM attribute cannot hide pixels already painted onto a canvas. The proposal adds a region provider for editors, maps and design tools. An application returns rectangles in CSS pixels for each sampled frame. rrweb maps them to backing-store coordinates and masks those regions before encoding the frame.
If the application cannot determine safe regions, it can skip the frame. Provider errors and invalid rectangles also skip capture. Strict mode disables canvas recording entirely.
The same terms can be used by Swift and Kotlin SDKs. Native implementations can apply mask, block and unmask during view traversal, while drawing surfaces expose protected regions through platform APIs. The current pull request covers the web recorder. A cross-platform conformance suite should follow once the DOM contract settles.
Detectors
The optional detector scans text content only: text nodes in the initial snapshot and live text mutations after it. A match masks the whole text node. It does not scan attributes or accept custom regular expressions.
Typed input is never scanned. While the detector is loaded, rrweb occludes every input value to its length from the first event. Waiting for a complete pattern would record the characters typed before an email address or card number becomes recognisable. A clean scan also says nothing about passport numbers, dates of birth, account IDs or sensitive free text, so a scanned value that looks safe is not evidence that it is.
Use annotations and policy rules for known data. Detectors cover text that appears somewhere the application did not expect.
What integrators should know
A few consequences reach the code that reads or replays the events.
- A serialised attribute value can now be
null, which means the attribute was dropped, so event consumers should handle a null alongside a string. - A masked image, video or other media source is replaced by an inline SVG data URI that keeps the original dimensions, so consumers should expect a
data:URI where a remote URL used to be. - Hidden inputs and payment fields record as stars on every tier, including
minimaland recordings started without a policy. - The
strictpreset disables canvas recording, so no canvas mutation events arrive for a session recorded under it. - Loading the detectors plugin occludes every input value to its length for the whole session, whether or not a pattern ever matches.
- Every ambiguous case resolves to masking, including unrecognised attribute values, failed selectors and masking callbacks that throw.
Credits
This proposal reuses and adapts MIT-licensed recorder work. The source licences and copyright notices remain with that code. The names below come from the relevant files and commit history.
Anna Garcia built PostHog's canvas-region masking. PostHog's attribute pipeline was directed and hardened by Manoel Aranda Neto, Arno Hillen and the replay team. Datadog's privacy levels include work by John Gracey, Benoît Zugmeyer, Congyao Zheng and Seth Fowler.
Jakub Grzegorzewski wrote Mixpanel's masking layer. At Amplitude, Joseph Jin built attribute masking and Lew Gordon built URL-aware privacy configuration. Highlight's text obfuscation and detectors include work by Jay Khatri, Vadim Korolik and Spencer Amarantides. New Relic's protected-input and configuration work includes contributions from Jordan Porter and ptang-nr.
Commit history is an imperfect record. Please send corrections or missing attribution to the pull request.
Review
The Privacy at Capture pull request contains the implementation, provenance notes, performance tests and a list of experimental boundaries. The code is MIT licensed with rrweb.
Convergence gives capture SDKs a common safety floor for precedence, protected inputs and failure handling. Vendors can keep their own serializers while testing them against the same privacy cases.
We need feedback from people who own site markup, people who maintain capture SDKs and privacy teams that review these systems. In particular:
- Do
mask,blockandunmaskcover the decisions a component needs to express? - Can another serializer implement the precedence rules without adopting rrweb internals?
- Which failure case or immutable protection is missing from the tests?
If the contract works for your SDK, say so in the rrweb review. Bring the first case it cannot express.
Tested in rrweb Cloud
rrweb Cloud is the live test bed for these settings. Running the policy against real applications gives us evidence about defaults, browser edge cases, performance and downstream event compatibility. We use those findings to improve rrweb's core implementation and public tests before asking other capture SDKs to adopt the contract.
Cloud provides the production environment needed to harden the recorder. The privacy contract and the resulting core improvements remain open to every downstream project. This is the best way for us to improve the quality of the shared library while the standard is still taking shape.
FAQ
Is it released?
The implementation is open for review in the upstream rrweb repository. Applications should pin the pull-request branch for evaluation until it is merged and released.
Will existing recordings change?
The default still uses the minimal policy path, and existing masking options continue to work. Password and hidden inputs, along with fields whose autocomplete value is cc-*, current-password, new-password or one-time-code, stay masked even if your own options would expose them. Invalid selector strings are reported and dropped individually.
Which attribute should a new component use?
Use data-privacy when evaluating the proposal. It describes the component's privacy requirement without tying it to a recorder.
Can unmask expose a password?
No. unmask only escapes text and attribute masking where the policy permits it. Protected inputs, blocked subtrees, sanitised URLs and media removed by strict mode remain closed.
Are the detectors enough?
No. They scan rendered page text for a fixed set of patterns. Presets, annotations and selector rules remain the dependable controls for known sensitive content.
Written by Rogier Trimpe
@roggernaut on GitHubMore in Updates
Get started
Ready to try rrweb Cloud?
Free tier, no credit card required. Start recording sessions in minutes.
Sign up free