Mobile integration
Session Replay for Expo Apps (Works in Expo Go)
Add session replay to an Expo app with no config plugin or prebuild: npx expo install, Expo Router screen tracking, EXPO_PUBLIC_ keys, consent and a test run.
Updated · 4 min read
Expo session replay with AnyReplay is a JavaScript install: run npx expo install @anyreplay/react-native, call init in your root layout and wrap the app in <AnyReplay>. There is no config plugin and no prebuild, and it works in Expo Go as well as in development and production builds, as long as the app runs on the New Architecture.
Why add session replay to an Expo app
Expo teams ship fast, often over the air, and a report that says "checkout froze" is hard to act on without seeing the screens before it. An Expo session recording shows the screens a person moved through, their taps, and rage taps where they hit the same spot three times within a second. The replay is rebuilt from the element tree as a wireframe with real text, not a screenshot, and mobile sessions sit next to your web sessions in one list. Because the SDK has no native code, you can try it in Expo Go before changing a build. For how the recording works, see session replay for React Native.
How to install session replay in Expo
npx expo install @anyreplay/react-native @react-native-async-storage/async-storageAsyncStorage is optional. With it, a relaunch within 30 minutes resumes the same session and sampling stays stable for the same person; without it, ids are kept in memory. The requirements are the same as the React Native SDK: React Native 0.72 or later, React 18 or later, and the New Architecture to record the screen, which is on by default from Expo SDK 52. On the old architecture the SDK still installs and runs: it logs one warning and records taps, screen names and events, but not what is on screen, so the replay has no picture.
How do I record screens with Expo Router?
Expo Router manages the navigation container for you, so instead of trackNavigation you report each path to screen() from the root layout. init goes at module level so it runs once.
import AsyncStorage from '@react-native-async-storage/async-storage';
import { AnyReplay, init, screen } from '@anyreplay/react-native';
import { Stack, usePathname } from 'expo-router';
import { useEffect } from 'react';
init({
projectKey: process.env.EXPO_PUBLIC_ANYREPLAY_KEY!,
storage: AsyncStorage,
});
export default function RootLayout() {
const pathname = usePathname();
useEffect(() => {
screen(pathname);
}, [pathname]);
return (
<AnyReplay>
<Stack />
</AnyReplay>
);
}screen() does not filter duplicates: each call is a screen change and raises the page count. Keying the effect on pathname means it runs only when the path changes. If your Expo app uses React Navigation directly with a classic App.tsx, use trackNavigation on the NavigationContainer as the navigation tracking guide shows.
Should I record route patterns or ids?
The path from usePathname() includes real values, so /orders/[id] is reported as /orders/123. Screen names are not masked. If ids in your paths are sensitive, such as account numbers or patient ids, report the route pattern instead, for example from useSegments() joined into /orders/[id]. Never put an email or a name into a screen name.
How do I set the project key?
Put EXPO_PUBLIC_ANYREPLAY_KEY=YOUR_PROJECT_KEY in .env. Expo inlines EXPO_PUBLIC_ variables into the JavaScript bundle; the project key is public, so that is fine. If the project lists allowed domains, add the app's bundle id under Allowed apps and pass it as appId, because an app sends no Origin header.
How do I keep sensitive data out of recordings?
- What people type into a
TextInputis masked by default, as it is on the web. SetmaskAllInputs: falseto record typing, ormaskAllTyping: truefor masking no other option can weaken. - Masked whatever you configure:
secureTextEntry, any field whosetextContentType,autoComplete,keyboardType,placeholder,accessibilityLabel,testIDornativeIDmarks it as a password, one-time code or payment field, and any card number, whole or typed past its sixth digit. - Give an element a
testIDcontainingar-maskto mask it and everything inside it. - Set
maskImages: trueto record no image URLs.
One limit is worth stating plainly: with maskAllInputs: false, a bare <TextInput /> with no props says nothing about itself, so it is recorded, exactly as an <input> with no attributes is on the web, and only the card-number check on the value can catch it. Give sensitive fields a textContentType, an autoComplete or an ar-mask testID, or keep the default.
To wait for consent, pass requireConsent: true to init and call consent(true) from @anyreplay/react-native when the person agrees. Nothing is recorded before that. AnyReplay does not store the choice, so save it yourself and call consent(true) on each launch for people who already agreed, once init has resolved, because consent does nothing before the recorder exists. See consent.
How do I check it is working?
- Add
debug: truetoinitand watch the Metro log for warnings: an unmounted<AnyReplay>or a refused request. The old-architecture warning appears even without it. - Open the app in Expo Go or a development build, move between two screens, then send the app to the background to flush the buffer.
- Look for the session under Sessions. If requests are refused with 403, the bundle id is missing from Allowed apps.
The Expo install guide has the complete setup. Known limits are the same as for React Native: lists record rendered rows only, custom native components record as empty boxes, maps and videos as labelled boxes, and the last seconds can be lost if the OS kills the app without backgrounding it.
Frequently asked questions
Does session replay work in Expo Go?
Yes. The SDK is pure JavaScript with no native module, so it runs in Expo Go as well as in development and production builds. The screen is recorded on the New Architecture, which is on by default from Expo SDK 52.
Do I need a config plugin or expo prebuild?
No. Install the package with npx expo install and add init and the AnyReplay wrapper in JavaScript. Nothing changes in the native projects.
How are Expo Router screens named in recordings?
By whatever you pass to screen. The usual pattern passes the pathname from usePathname in the root layout; report the route pattern instead if paths contain sensitive ids.
Can I record custom events in an Expo app?
Yes. Call `track('coupon_applied', { code })` from `@anyreplay/react-native`; it works as in the browser SDK, and the event appears on the session timeline.