Skip to content

React Native

Session Replay for React Native Without Screenshots

Why screenshot-based session replay fails on React Native, how recording the committed element tree captures real text and layout, and what it costs.

Published · Updated · 7 min read

On the web, session replay is a solved shape: snapshot the DOM, record mutations, rebuild on replay. On a phone there is no DOM, and the tooling has mostly answered that by filming the screen. This post explains why that answer is poor for React Native specifically, and what recording the React element tree instead looks like in practice — what it captures, what it cannot, and what it costs.

Why pixel capture fails on React Native

Native replay SDKs for iOS and Android walk the platform view hierarchy through native code. React Native apps have that hierarchy too, but the JavaScript side cannot reach it without a native module — which means a rebuild of the app, a dependency the Expo Go workflow cannot use, and a separate code path per platform. Faced with that, most replay vendors record React Native apps as screenshots taken on a timer.

Screenshots have three problems that no amount of engineering fixes:

  • Cost. A screenshot is a picture, every time, whether or not anything changed. Idle screens cost the same as busy ones, and a session is measured in megabytes rather than kilobytes.
  • Masking is a guess. A recorder that only has pixels has to find the password field in the image before it can hide it. Masking at the source needs to know what the element is.
  • Pixels cannot be translated. A Turkish user's screen, photographed, stays Turkish. For a product whose reason to exist is showing a session in the reviewer's language, throwing the words away on the way in is not an option.

Recording the element tree instead

The layer JavaScript can reach is the one React itself runs: the tree of elements React has committed. While a recording is running, AnyReplay's React Native SDK walks that tree on each tick and notes, for each host element (View, Text, TextInput, Image, and so on), what it is and what it says. Then it measures where that element sits on screen, which needs React Native's New Architecture.

Neither half is enough alone. React knows what is on screen but not where; the measurement knows where but not what. Together they yield the React Native element tree: a set of boxes with positions, sizes, roles and real text — a wireframe of the screen rather than a photograph of it. The player draws that wireframe. It is honest about what was captured, and because the text is text, it can be translated exactly like a web session.

import { AnyReplay, init, trackNavigation } from '@anyreplay/react-native';

init({ projectKey: 'ar_pk_live_…' });

export default function Root() {
  return (
    <AnyReplay>
      <NavigationContainer ref={(ref) => { if (ref) trackNavigation(ref); }}>
        <App />
      </NavigationContainer>
    </AnyReplay>
  );
}
The whole installation. No native module, no rebuild, works in Expo Go on SDK 52 and later.

It installs like any other package: no pod install, no Gradle change, nothing that breaks when Expo upgrades.

What this is called: semantic recording

We call the approach semantic recording: capture what things are rather than how they look. It is the mobile counterpart of DOM recording on the web, and it has the same consequences. Small payloads, because structure compresses better than pixels. Masking at the source, because the recorder knows a TextInput is a TextInput. And a recording a machine can read — which is what makes translation, search and future analysis possible.

It also has a visual cost. A wireframe is not a screenshot. Brand imagery, custom drawing, and the exact typography of a screen are not reproduced. A reviewer following what a user did rarely needs them; a designer checking pixel fidelity needs another tool.

Performance and data volume

The first frame of a screen is a few kilobytes — the element tree serialised once. Every frame after it carries only what changed since the last one, which on a typical screen is a handful of numbers: a scroll offset, a toggled state, a text node. An idle screen sends nothing at all. That is the property screenshot recorders cannot have, and it is what makes recording every user, rather than a sample, plausible on mobile.

The main cost lever is snapshotIntervalMs, which defaults to 500. It bounds how often the recorder looks for changes: slower is cheaper and coarser. Sampling works the same way as on the web — sampleRate between 0 and 1, decided once per visitor so a recorded person stays recorded — and if you hand the SDK an AsyncStorage instance it can recognise a returning visitor across launches.

OptionDefaultWhat it does
snapshotIntervalMs500How often changes are captured. The main cost lever.
sampleRate1Fraction of visitors recorded, decided once per visitor.
maskAllInputstrueMasks every TextInput. Set it to false to record typing.
requireConsentfalseRecord nothing until consent(true) is called.
storagein memoryPass AsyncStorage to recognise a returning visitor.

Masking happens on the device

Whatever is masked is masked before anything leaves the phone, and the SDK matches the web: maskAllInputs defaults to true, so typing is masked unless you set maskAllInputs: false. maskAllTyping: true is the stricter switch and cannot be weakened; an ar-mask testID masks a view and everything inside it; maskImages: true records no image URLs.

Under that sits a floor no configuration lifts: secureTextEntry, anything whose textContentType, autoComplete, keyboardType, placeholder, accessibilityLabel, testID or nativeID marks it as a credential or payment field, any 13–19 digit value that passes the card-number check, and a half-typed card number past its sixth digit. A masked element keeps its box and loses its words, replaced on the device rather than sent and then hidden.

If you opt out with maskAllInputs: false, a bare <TextInput /> with no props says nothing about itself, so it is recorded, exactly as an unmarked input is on the web. Email addresses, telephone numbers and post codes are not on the floor either. Give sensitive fields a textContentType, an autoComplete or an ar-mask testID, or keep the default. How to mask sensitive data covers both.

Screen changes are the chapters of a mobile session, and a tap is recorded when the finger lifts, so a scroll is not a tap. trackNavigation(ref) follows a react-navigation container automatically; if you use something else, call screen(name) yourself. identify({ userId, email }) attaches an identity so a support agent can find the session behind a ticket (user identification), and track(name, properties) tags a moment on the timeline. consent(granted) grants or withdraws consent at runtime, flush() sends what is buffered, and getSessionId() returns the current id for correlating with your own logs. init is safe to call twice; the second call is ignored.

What it does not do yet

Know these limits before you install.

  • Lists are recorded as what is on screen. A FlatList reports the rows it has rendered, so scrolling back through a replay shows the rows that existed at that moment, not the whole list.
  • Some contents are boxes. Images bundled with the app are uploaded once and drawn, but maps, web views and videos are labelled boxes whose contents are not captured.
  • Custom native components are recorded as Unknown — a box in the right place with no text, because there is nothing readable to capture.
  • No native SDK yet. There is no iOS or Android SDK, so an app written in Swift or Kotlin cannot be recorded. Flutter apps have their own SDK, which records the same way.

Who this is for

Teams shipping a React Native app who want to know what users actually did — support reproducing a "the button does nothing" ticket, QA chasing an intermittent state bug, product watching the first hundred sessions after a launch. The mobile app teams page walks through those workflows, and the React Native feature page has the summary. If your users and your reviewers speak different languages, reviewing sessions in another language is where the element-tree approach pays off most.

Ready to install? Session replay for React Native and session replay for Expo walk through setup, and React Native session replay tools compares this approach with screenshot-based SDKs.

The Free plan — one project, 500 sessions a month — covers a mobile app as easily as a website; see pricing.

Frequently asked questions

Does AnyReplay for React Native need a native module?

No. The SDK is pure JavaScript: it reads the tree React has committed, so there is no rebuild, and it works in Expo Go on SDK 52 and later, where the New Architecture is on.

Does the replay look like the real app?

It is a wireframe with real text and layout, not a screenshot. Maps and videos are labelled boxes and custom native components are unlabelled boxes. It shows what the user did, not exact pixels.

How much data does a mobile session produce?

The first frame of a screen is a few kilobytes; subsequent frames carry only what changed, and an idle screen sends nothing. The snapshotIntervalMs option (default 500) is the main cost lever.

Are passwords masked on mobile?

Yes, on the device. secureTextEntry fields, fields whose textContentType or autoComplete names a password, one-time code or card field, and anything whose testID contains ar-mask are masked before the recording leaves the phone. Ordinary TextInputs are masked too unless you set maskAllInputs: false.

Start free — 1 project, 500 sessions a month

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