Skip to content

Privacy

How to Mask Sensitive Data in Session Recordings

A hands-on guide to masking inputs, hiding on-page PII and blocking elements from session recordings on the web and in React Native, plus what is always masked.

Published · Updated · 7 min read

The right time to remove sensitive data from a session recording is before it leaves the browser. This guide shows how to do that with AnyReplay: what is masked without any configuration, how to mask or remove anything else, how it works on React Native, and how to check that it worked. It is practical rather than legal — for the legal side, read session replay and GDPR.

Three levels of protection

There are three things you can do to an element in a recording, and it helps to name them before configuring anything.

LevelWhat the reviewer seesWhen to use it
MaskThe element, with its text replaced by asterisksInputs, names, emails, amounts — the shape matters, the value does not
BlockAn empty box of the same size; nothing inside is recordedChat transcripts, documents, anything where even the layout is sensitive
RecordEverything, as the user saw itNavigation, product copy, error messages — the reason you have replay

Masking keeps the replay useful: you can still see that a user filled in the second field, hesitated, and cleared it. Blocking is for the cases where knowing where the text was is itself a leak.

What is masked with no configuration, and what is not

The browser SDK masks form fields by default: maskAllInputs defaults to true, so a replay of a form shows the form being filled in with asterisks of the same length. What it never records, in any configuration, is the short list the SDK enforces for you: passwords and one-time codes; card numbers, security codes, expiry dates and cardholder names, recognised from autocomplete="cc-…" or from a name, label or placeholder that reads like one; IBANs, routing numbers and sort codes; any value of 13–19 digits that passes the card-number check; and a half-typed card number past its sixth digit. Those are replaced with asterisks of the same length before the event is serialised, so the original characters are never in the payload. type="email" and type="tel" are not on that list: they are ordinary fields, masked by default and recorded when you turn field masking off, unless you mark them with ar-mask.

Turning field masking off is a single option, and it is a project-wide decision: maskAllInputs: false records what visitors type, while maskAllTyping: true also masks rich-text editors, which are recorded by default. Only a real boolean counts — the string 'false', which is what a tag manager variable usually hands over, is ignored, so fields stay masked. If you opt out but part of a form is still sensitive, mark those fields with ar-mask, which masks a field's value as well as text.

anyreplay('init', {
  projectKey: 'ar_pk_live_…',
  maskAllInputs: false,  // opt out: record what visitors type (passwords and cards stay masked)
  maskClass: 'ar-mask',  // elements with this class have their text masked
  blockClass: 'ar-block' // elements with this class are not recorded
});
Recording field values. Only the project key is required; the rest is optional.

Masking text that is not in an input

Most PII on a page was never typed during the session. Account pages show an email address; order confirmations show a shipping address; dashboards show balances. Add the ar-mask class to any element and its text content, and the text of everything inside it, is masked at record time.

<dl class="account-summary">
  <dt>Email</dt>
  <dd class="ar-mask">[email protected]</dd>
  <dt>Plan</dt>
  <dd>Team</dd>
</dl>

The reviewer sees the definition list with a masked email and a visible plan name. The masking applies to the initial snapshot and to every later mutation: if the email changes during the session, the new value is masked too.

Blocking an element entirely

Add ar-block and the element's contents are not serialised at all; the replay shows an empty box of the same size. Use it for a chat widget, an embedded document viewer, a canvas that draws customer data, or a third-party iframe whose contents you do not control.

<section class="support-chat ar-block">
  <!-- nothing in here reaches the recording -->
</section>

Blocking removes information from the replay, so prefer masking when the layout is harmless. A blocked checkout form tells the reviewer nothing about where the user got stuck; a masked one shows every step with the values hidden.

Choosing the class names

ar-mask and ar-block are defaults, not requirements. If your design system already has a class for sensitive fields, point the SDK at it with maskClass and blockClass. One caution: pick a class that is applied server-side or in the initial render. A class added by JavaScript after the first snapshot still works for later mutations, but the snapshot itself was taken with the element unmarked.

React Native

The mobile SDK records the React element tree rather than pixels, so masking is decided per element on the device. It works the same way round as the browser: maskAllInputs defaults to true, so what a person types into a TextInput is masked unless you say otherwise. maskAllInputs: false records what people type, maskAllTyping: true masks every TextInput and cannot be weakened, maskTestID (default ar-mask) masks a view and everything inside it, and maskImages: true records no image URLs. What is masked without being asked is a floor of its own:

  • Anything with secureTextEntry, whatever the component calls itself.
  • Fields whose textContentType, autoComplete, keyboardType, placeholder, accessibilityLabel, testID or nativeID marks them as a credential or payment field.
  • Any value of 13–19 digits that passes the card-number check, wherever it was typed — and, while one is being typed, every digit after the sixth.
<Text testID="ar-mask balance">{formatMoney(balance)}</Text>
Masking an on-screen value in React Native.

A masked element keeps its box and loses its words. The value is replaced before serialisation; it is not sent and then hidden. The honest limit applies once you opt out with maskAllInputs: false: a bare <TextInput /> with no props says nothing about itself, so it is then recorded, exactly as an unmarked input is on the web. Post codes, email addresses, telephone numbers and usernames are not on the mobile floor either. Give a sensitive field a textContentType, an autoComplete or an ar-mask testID, or keep the default and stop thinking about it. Details on the React Native feature page.

Common mistakes

  • Masking the input but not the confirmation screen. The user types a card number into a masked field, then the next page prints the last four digits and the cardholder name in plain text. Mark the confirmation elements too.
  • Relying on `type="password"`. AnyReplay masks it either way, but a tool that masks only password fields will happily record a one-time code typed into a text input.
  • Forgetting attributes. A placeholder or title can carry data. AnyReplay treats placeholders as interface text, which is what they usually are; if yours contain user data, mask the element.
  • Blocking too much. A blocked page is a page you cannot debug. Block what is sensitive in shape; mask what is sensitive in value.
  • Testing only on the happy path. Error states often echo the user's input back ("No account for [email protected]"). Mask the error container.

Checking that it worked

Do not trust the configuration; watch a recording. Install the SDK on a staging site, walk through the flows that handle personal data, then open the session in the dashboard and read the rebuilt page. AnyReplay's own verification did this in a real browser: masked inputs replayed as asterisks, with no original value anywhere in the rebuilt page.

For an extra layer, search the raw chunk payloads in your ingest logs for a known test value while recording a staging session. If the string is not in the payload, it was masked at the source.

Masking and translation

Masking and session translation compose naturally. Translation works on the interface strings in the recorded event stream — headings, buttons, labels — and a masked value is already asterisks by the time the stream exists, so nothing masked is ever sent to a model. The reviewer gets a page in their language with the user's data still hidden.

Summary

Inputs are masked unless you set maskAllInputs: false, and passwords and payment fields are never recorded. ar-mask hides text and field values; ar-block hides elements. React Native works the same way: typing is masked unless you opt out, while secureTextEntry, credential and payment fields, and ar-mask in a testID are masked on the device. Mark the confirmation screens, mark the error messages, and verify with a real recording. The privacy features page collects everything above alongside consent, retention and access control; the Free plan on the pricing page is enough to verify your masking on a staging site before rolling out.

Frequently asked questions

Are inputs masked by default in AnyReplay?

Yes. Every form field value is masked by default. Set maskAllInputs: false to record what people type, and mark any field that should stay hidden with the ar-mask class. Passwords and payment fields — card number, CVV, expiry, cardholder name — are masked in every configuration and no option records them.

What is the difference between ar-mask and ar-block?

ar-mask keeps the element and replaces its text with asterisks. ar-block removes the element and everything inside it from the recording entirely.

Can I use my own class names?

Yes. Set maskClass and blockClass in the init options to whatever your codebase already uses for sensitive content.

Is masked text sent to the translation model?

No. Masking happens in the browser before the event stream exists, so translation only ever sees asterisks where masked values were.

Start free — 1 project, 500 sessions a month

Install the snippet, record a session, and watch it back in your own language.