> ## Documentation Index
> Fetch the complete documentation index at: https://mixpanel-edb78807-docs-session-replay-wireframes-beta.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Implement Session Replay (React Native)

Mixpanel's React Native Session Replay SDK enables you to capture and analyze user interactions in your mobile applications. Built as a Turbo Module for React Native's New Architecture, it provides native implementations for both iOS and Android with a unified JavaScript API.

## Features

* **📱 Cross-Platform Support** - Unified API for iOS and Android
* **🎥 Session Recording** - Capture user interactions and screen recordings
* **🔒 Privacy-First** - Built-in data masking for sensitive information
* **⚡ High Performance** - Native implementation with minimal JavaScript bridge overhead
* **🎯 Selective Recording** - Configurable sampling rates and recording controls
* **🚀 New Architecture Ready** - Built as a Turbo Module with full type safety
* **🛡️ Granular Privacy Controls** - Auto-masking and manual masking via wrapper components

## Requirements

* React Native >= 0.70
* iOS >= 13.0
* Android API Level >= 21
* New Architecture support (backward compatible with old architecture)

## Installation

```sh theme={"system"}
npm install @mixpanel/react-native-session-replay
```

or

```sh theme={"system"}
yarn add @mixpanel/react-native-session-replay
```

### Platform Setup

#### iOS

The SDK dependencies are automatically added via CocoaPods. Your project must target iOS 13 or later.

```sh theme={"system"}
cd ios && pod install
```

#### Android

Dependencies are automatically added through Gradle. Requirements:

* Minimum Android SDK 21+
* Kotlin support enabled

## Quick Start

Here's a minimal example to get started with Session Replay:

```typescript theme={"system"}
import {
  MPSessionReplay,
  MPSessionReplayConfig,
  MPSessionReplayMask,
} from "@mixpanel/react-native-session-replay";

// Initialize session replay
const config = new MPSessionReplayConfig({
  wifiOnly: false,
  recordingSessionsPercent: 100,
  autoStartRecording: true,
  autoMaskedViews: [MPSessionReplayMask.Image, MPSessionReplayMask.Text],
  flushInterval: 5,
  enableLogging: true,
});

await MPSessionReplay.initialize(token, distinctId, config).catch((error) => {
  console.error("Initialization error:", error);
});

// Control recording
await MPSessionReplay.startRecording();
await MPSessionReplay.stopRecording();

// Check recording status
const recording = await MPSessionReplay.isRecording();
```

## Data Residency

<Info>
  Available in React Native Session Replay SDK version `1.3.0` and later.
</Info>

If your Mixpanel project lives in the EU or India data center, or you route all Mixpanel traffic through a self-hosted proxy, set `serverURL` on `MPSessionReplayConfig`. The SDK exposes a `MPDataResidency` constant set with the managed region URLs so you don't have to hardcode them.

| Region       | `MPDataResidency` constant | URL                           |
| ------------ | -------------------------- | ----------------------------- |
| US (default) | `MPDataResidency.US`       | `https://api.mixpanel.com`    |
| EU           | `MPDataResidency.EU`       | `https://api-eu.mixpanel.com` |
| India        | `MPDataResidency.IN`       | `https://api-in.mixpanel.com` |

**Example Usage**

```typescript theme={"system"}
import {
  MPSessionReplay,
  MPSessionReplayConfig,
  MPDataResidency,
} from "@mixpanel/react-native-session-replay";

const config = new MPSessionReplayConfig({
  recordingSessionsPercent: 100,
  serverURL: MPDataResidency.EU,
});

await MPSessionReplay.initialize(token, distinctId, config);
```

You can also pass any fully-qualified HTTPS URL — useful when routing replay traffic through a self-hosted proxy:

```typescript theme={"system"}
const config = new MPSessionReplayConfig({
  serverURL: "https://mixpanel-proxy.yourcompany.com",
});
```

Learn more about [EU Data Residency](/docs/privacy/eu-residency) and [India Data Residency](/docs/privacy/in-residency).

## Configuration

The `MPSessionReplayConfig` class provides comprehensive control over session replay behavior:

```typescript theme={"system"}
const config = new MPSessionReplayConfig({
  wifiOnly: boolean,                // Default: true
  autoStartRecording: boolean,      // Default: true
  recordingSessionsPercent: number, // Default: 100 (range: 0-100)
  autoMaskedViews: MPSessionReplayMask[], // Default: all types
  flushInterval: number,            // Default: 10 (seconds)
  enableLogging: boolean,           // Default: false
});
```

### Configuration Options

| Option                     | Type                              | Default    | Description                                                                                                   |
| -------------------------- | --------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------- |
| `wifiOnly`                 | boolean                           | `true`     | Only transmit recordings over WiFi                                                                            |
| `autoStartRecording`       | boolean                           | `true`     | Automatically start recording on initialization                                                               |
| `recordingSessionsPercent` | number                            | `100`      | Percentage of sessions to record (0-100)                                                                      |
| `autoMaskedViews`          | MPSessionReplayMask\[]            | All types  | View types to automatically mask                                                                              |
| `flushInterval`            | number                            | `10`       | Interval in seconds to flush recordings                                                                       |
| `enableLogging`            | boolean                           | `false`    | Enable debug logging                                                                                          |
| `remoteSettingsMode`       | MPSessionReplayRemoteSettingsMode | `Disabled` | Setting for handling remote configuration during SDK initialization. Can be `Disabled`/ `Fallback`/ `Strict`. |

### Auto-Masked View Types

The `MPSessionReplayMask` enum defines view types that can be automatically masked:

```typescript theme={"system"}
enum MPSessionReplayMask {
  Text = "text", // Text inputs and labels
  Web = "web", // WebView content
  Map = "map", // Map views (iOS only)
  Image = "image", // Image components
}
```

**Example - Custom Auto-Masking:**

```typescript theme={"system"}
import {
  MPSessionReplayConfig,
  MPSessionReplayMask,
} from "mixpanel-react-native-session-replay";

// Only mask text inputs and images
const config = new MPSessionReplayConfig({
  autoMaskedViews: [MPSessionReplayMask.Text, MPSessionReplayMask.Image],
});
```

## API Reference

### initialize

Initialize the Session Replay SDK with your configuration.

```typescript theme={"system"}
initialize(
  token: string,
  distinctId: string,
  config: MPSessionReplayConfig
): Promise<void>
```

**Parameters:**

* `token` (required) - Your Mixpanel project token
* `distinctId` (required) - User identifier for the session
* `config` (required) - Session replay configuration

**Validation:**

* Token must be a non-empty string
* distinctId must be a non-empty string
* recordingSessionsPercent must be between 0 and 100

**Example:**

```typescript theme={"system"}
const config = new MPSessionReplayConfig({
  autoStartRecording: true,
  enableLogging: __DEV__, // Enable logging in development
});

try {
  await MPSessionReplay.initialize("YOUR_TOKEN", "user-123", config);
  console.log("Session Replay initialized");
} catch (error) {
  console.error("Failed to initialize:", error);
}
```

### startRecording

Start recording user interactions.

```typescript theme={"system"}
startRecording(): Promise<void>
```

**Example:**

```typescript theme={"system"}
await MPSessionReplay.startRecording();
```

### stopRecording

Stop recording user interactions.

```typescript theme={"system"}
stopRecording(): Promise<void>
```

**Example:**

```typescript theme={"system"}
await MPSessionReplay.stopRecording();
```

### isRecording

Check if session recording is currently active.

```typescript theme={"system"}
isRecording(): Promise<boolean>
```

**Returns:** Boolean indicating recording status

**Example:**

```typescript theme={"system"}
const recording = await MPSessionReplay.isRecording();
if (recording) {
  console.log("Session is being recorded");
}
```

### identify

Update the user identifier for the current recording session.

```typescript theme={"system"}
identify(distinctId: string): Promise<void>
```

**Parameters:**

* `distinctId` (required) - New user identifier

**Example:**

```typescript theme={"system"}
// Update user ID after authentication
await MPSessionReplay.identify("authenticated-user-456");
```

## Remote Configuration

<Note>
  Available in React Native Session Replay SDK version `1.2.0` and later. Requires a paid Session Replay add-on.
</Note>

You can set the `remote_settings_mode` for your project in Mixpanel under `Settings > Organization Settings > Session Replay`. Using this, you can quickly set SDK options remotely without needing to update your app. This allows you to adjust recording settings, such as sampling rates, on the fly based on your needs.

Three modes are available:

* `Disabled`: Do not use remote configuration and proceed to use the hardcoded initial options provided by the user during initialization. This is the default behavior.
* `Fallback`: Attempt to retrieve remote configuration and proceed with those settings. If there is failure or timeout (500 ms), we will use the previously cached remote settings (if one exists from the last successful fetch). If no existing remote settings are cached, we will use the values from the SDK initialization config.
* `Strict`: Requires successful remote configuration fetch for SDK initialization. If there is failure or timeout (500 ms), SDK initialization will fail and the Session Replay features will be unavailable for that app launch.

You can use this setting to quickly update and adjust configurations to your liking.

List of currently supported remote settings:

* `recordingSessionsPercent`

Settings not yet supported by remote configuration will use the value provided during initialization, or the default value if none was provided

## Privacy & Data Masking

Session Replay provides two approaches to protect sensitive data: automatic masking and manual masking.

### Automatic Masking

Configure which view types are automatically masked during initialization:

```typescript theme={"system"}
const config = new MPSessionReplayConfig({
  autoMaskedViews: [
    MPSessionReplayMask.Text, // Masks all text inputs
    MPSessionReplayMask.Image, // Masks all images
    MPSessionReplayMask.Web, // Masks all WebViews
    MPSessionReplayMask.Map, // Masks map views (iOS only)
  ],
});
```

**Default Behavior:** All view types are masked by default for maximum privacy.

### Manual Masking with MPSessionReplayView

Use the `MPSessionReplayView` wrapper component for granular control over what gets masked:

```typescript theme={"system"}
import { MPSessionReplayView } from 'mixpanel-react-native-session-replay';

// Mask sensitive content
<MPSessionReplayView sensitive={true}>
  <TextInput
    value={password}
    onChangeText={setPassword}
    secureTextEntry
  />
  <Text>Social Security Number: {ssn}</Text>
</MPSessionReplayView>

// Explicitly mark content as safe (not masked)
<MPSessionReplayView sensitive={false}>
  <Text>Public information that should always be visible</Text>
</MPSessionReplayView>
```

### Complete Masking Example

```typescript theme={"system"}
import React, { useState } from "react";
import { View, TextInput, Image, Text } from "react-native";
import { MPSessionReplayView } from "mixpanel-react-native-session-replay";
import { WebView } from "react-native-webview";

function ProfileScreen() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  return (
    <View>
      {/* Public information - not masked */}
      <Text>Welcome to Your Profile</Text>

      {/* Sensitive user data - masked */}
      <MPSessionReplayView sensitive={true}>
        <View>
          <Text>Email: {email}</Text>
          <TextInput
            value={email}
            onChangeText={setEmail}
            placeholder="email@example.com"
          />

          <Text>Password:</Text>
          <TextInput
            value={password}
            onChangeText={setPassword}
            secureTextEntry
          />

          <Image source={{ uri: profilePhotoUrl }} />
        </View>
      </MPSessionReplayView>

      {/* WebView masked by default if Web type is in autoMaskedViews */}
      <WebView source={{ uri: "https://example.com/terms" }} />
    </View>
  );
}
```

## Wireframes (Beta)

<Note>
  **Beta.** Wireframes are in beta. Before shipping to production, inspect the wireframes your app produces with `debugOptions.wireframeEmitter` and confirm that no sensitive information is captured.
</Note>

Wireframes add AI intelligence to our Mobile Session Replay product by gathering and sending text-based representations of the user’s screen, which we feed into the agent context.

### Enable wireframe capture

Set `wireframesOptions` on `MPSessionReplayConfig`.

```typescript theme={"system"}
import {
  MPSessionReplay,
  MPSessionReplayConfig,
  MPWireframesOptions,
} from "@mixpanel/react-native-session-replay";

const config = new MPSessionReplayConfig({
  wireframesOptions: new MPWireframesOptions(),
});

await MPSessionReplay.initialize(token, distinctId, config);
```

`MPWireframesOptions` takes two options:

| Option                          | Description                                                                                                                                                                                                                 | Default |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `sensitiveRules`                | Content-level strip and redact rules applied to element text, in the order you declare them. Elements are always kept; only their text is affected.                                                                         | `[]`    |
| `useAccessibilityLabelFallback` | Whether an element with no text of its own may fall back to its accessibility label (`accessibilityLabel` / `accessible` props, resolved by the native SDKs). Off by default — see [Risks to mitigate](#risks-to-mitigate). | `false` |

### How masking applies to wireframe text

Wireframe text passes through four layers, in order. Each layer only sees what survived the one before it. An element is **never removed** by masking — its role and bounds always ship, so the shape of the screen is preserved. Only its text is affected.

| Evaluation Order | Layer                         | What it does                                                                                                                                                                                                                                                                                   | Direction          |
| ---------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| 1                | **View-level masking**        | The existing Session Replay masking decision, made during the same traversal that builds the screenshot. If something is masked in the video, its text is dropped from the wireframe. Covers `autoMaskedViews`, anything wrapped in `<MPSessionReplayView sensitive>`, and `TextInput` fields. | Removes            |
| 2                | **Geometric leak prevention** | Any element whose bounds intersect a mask region the screenshot painted loses its text, even if that element was never marked sensitive itself.                                                                                                                                                | Removes            |
| 3                | **Declared text**             | Text you supply with `wireframeText` replaces whatever the first two layers produced. Because you authored it rather than the SDK scraping it off the screen, it is sent even for an element you masked in the video.                                                                          | Adds               |
| 4                | **Content rules**             | `sensitiveRules` — your own strip and redact patterns — applied in declared order to whatever text remains, including text you declared in Layer 3.                                                                                                                                            | Removes / rewrites |

The short version: **the SDK does its best to keep the wireframe consistent with what the replay video shows**, plus two escape hatches you control — one to add a safe description back (Layer 3), one to catch sensitive content by pattern (Layer 4).

#### Declare text for an element

`wireframeText` lets you supply the text for an element yourself — useful for custom components, canvas-drawn content, and masked screens you still want named. Use it to say what a screen is *for* — `"Checkout summary"`, `"Card number"` — without revealing what is on it.

```tsx theme={"system"}
<MPSessionReplayView sensitive wireframeText="Card number">
  <TextInput value={cardNumber} onChangeText={setCardNumber} />
</MPSessionReplayView>
```

The `wireframeText` prop is independent of `sensitive`, which controls pixels. Set both when you want an element masked in the video but named in the wireframe.

Declared text is sent verbatim, even on a masked element, so never interpolate sensitive user data into it.

#### Redact by pattern

Some text is sensitive because of what it *contains* rather than which view it came from — an account number inside a custom view the SDK has no way to recognize, for example. `sensitiveRules` match on the text itself and run last, after every other masking layer.

There are two behaviors — **redact** and **strip** — each available as a literal or a regex match. Redact allows you to replace sensitive text; strip omits all text from the element.

| Rule                                              | What it does                                                                                   |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `MPSensitiveRule.redact(text, replacement)`       | Replaces case-insensitive matches of `text` with `replacement` (`[REDACTED]` by default).      |
| `MPSensitiveRule.redactRegex(regex, replacement)` | Replaces every match of `regex` with `replacement`.                                            |
| `MPSensitiveRule.strip(text)`                     | Omits all text from the element if it contains `text`, case-insensitively. No later rule runs. |
| `MPSensitiveRule.stripRegex(regex)`               | Omits all text from the element if `regex` matches anywhere in it. No later rule runs.         |

```typescript theme={"system"}
import {
  MPSensitiveRule,
  MPWireframesOptions,
} from "@mixpanel/react-native-session-replay";

new MPWireframesOptions({
  sensitiveRules: [
    MPSensitiveRule.redactRegex(/\d{3}-\d{2}-\d{4}/, "[SSN]"),
    MPSensitiveRule.strip("Bearer "),
  ],
});
```

Only the `i`, `m`, and `s` regex flags carry over to native; others are ignored with a warning. A pattern the native platform cannot compile causes `initialize` to reject rather than silently redacting nothing.

Treat content rules as a **backstop, not a boundary**. A regex only catches what it matches, on the text the SDK already read. View-level masking is the stronger control; reach for rules when there is no view to mask.

### Verify what you are sending

`debugOptions.wireframeEmitter` hands you each wireframe as it is captured, with a `maskDecision` per element explaining why its text was kept, rewritten, or dropped.

```typescript theme={"system"}
import {
  MPDebugOptions,
  MPSessionReplayConfig,
  MPWireframesOptions,
} from "@mixpanel/react-native-session-replay";

const config = new MPSessionReplayConfig({
  wireframesOptions: new MPWireframesOptions(),
  debugOptions: new MPDebugOptions({
    overlayColors: null,
    wireframeEmitter: (snapshot) => console.log(JSON.stringify(snapshot)),
  }),
});
```

| Decision      | Meaning                                                                                                                                 |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `NONE`        | Text sent as-is.                                                                                                                        |
| `DECLARED`    | Your `wireframeText`. Sent verbatim, even on a masked element.                                                                          |
| `EXPLICIT`    | Wrapped in an `MPSessionReplayView` marked `sensitive`. Text dropped.                                                                   |
| `AUTO`        | Auto-masked by `autoMaskedViews`. Text dropped.                                                                                         |
| `TEXT_ENTRY`  | A `TextInput`. The value the user typed is always dropped and cannot be unmasked — declare `wireframeText` if you want to provide text. |
| `GEOMETRIC`   | Bounds intersected a mask region the screenshot painted. Text dropped.                                                                  |
| `RULE_STRIP`  | Matched one of your strip rules. Text dropped.                                                                                          |
| `RULE_REDACT` | Matched one of your redact rules. Text sent, rewritten.                                                                                 |

A few things worth knowing:

* **The emitter observes; it does not enable.** Setting it without `wireframesOptions` captures nothing.
* **Use it in debug builds only.** The emitter runs wherever you configure it, so leave it out of the release builds you ship.
* **The snapshot shape is not a stable contract.** It is for interactive debugging.

### Risks to mitigate

Layers 1 and 2 remove anything the video masked. They do not cover every way content can be invisible to the person looking at the screen. These are the specific gaps to check for in your own app before you enable wireframes in production.

<AccordionGroup>
  <Accordion title="Text behind an opaque view is still read">
    Layer 2 strips text under regions the screenshot **masked**, not under an ordinary opaque view you placed on top. So when a splash view, a bottom sheet, or a loading scrim covers content, the wireframe may describe text the screenshot does not show.

    Usually that is fine — content you never marked sensitive is content you were happy to capture either way. It only matters if something sensitive is sitting behind the cover, in which case mask it or give it a `wireframeText`.
  </Accordion>

  <Accordion title="Accessibility labels can describe more than the screen shows">
    An element with no text of its own can fall back to its accessibility label (`accessibilityLabel` / `accessible` props, resolved by the native SDKs). That is what lets an icon-only button ship as `button:Search` instead of an anonymous shell.

    The risk is that a label is not drawn on screen, so — unlike visible text — you cannot confirm what it holds by watching the replay. An avatar whose label is the user's full name ships that name with no visual cue that there was anything there to mask.

    Because of that, `useAccessibilityLabelFallback` is **off by default**. Turn it on only after auditing what your labels contain, and verify with the debug emitter — label-derived text is reported like any other text, with its own `maskDecision`.
  </Accordion>

  <Accordion title="Text the user cannot fully see">
    The SDK reads the string a view was handed, which is not always the string a user can see on-screen. Ellipsized labels, lines clipped by their container, and values scrolled out of frame all report their full value. Element text is capped at 50 characters, which bounds but does not eliminate this.

    **Mitigate:** mask or declare text for anything whose full value is more sensitive than its rendered form.
  </Accordion>

  <Accordion title="Declared text bypasses view masking by design">
    `wireframeText` is exempt from Layers 1 and 2 — that is the whole point of it. Content rules still run over it, but nothing else does.

    **Mitigate:** treat it as a static string you wrote at build time. Never interpolate sensitive user data into it.
  </Accordion>

  <Accordion title="Content the SDK cannot classify is not auto-masked">
    Automatic masking works by native view type. Custom components that draw their own content are not text components, so `autoMaskedViews` does not cover them — the same as for screenshots today, but now they can also carry text.

    **Mitigate:** mark custom components sensitive explicitly, and confirm coverage with the debug emitter rather than assuming.
  </Accordion>

  <Accordion title="Plain <Text> on the legacy architecture, on iOS">
    On the legacy (Paper) architecture on iOS, a plain `<Text>` publishes its string only through an accessibility label, so it needs `useAccessibilityLabelFallback` — or `wireframeText` — to carry text in the wireframe. Roles and bounds are unaffected either way.

    That means turning the fallback on to get readable text also opts every other label-derived string in, so audit them together. Fabric, the default since React Native 0.76, reads text exactly and needs neither. Android is unaffected on both architectures.
  </Accordion>
</AccordionGroup>
