> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sazabi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sazabi React Native SDK

> Capture mobile sessions with the Sazabi React Native SDK, including screens, touches, errors, and network calls with trace context.

[All Sazabi React Native SDK integration capabilities](https://docs.sazabi.com/catalogs/integrations/sazabi-react-native-sdk)

**Early preview**

The Sazabi React Native SDK is an early preview. Options, event attributes,
and capture behavior may change between releases, so pin the version you
install and read the changelog before you upgrade.

Capture what happens in your mobile app with `@sazabi/react-native`, the official Sazabi React Native SDK. It emits a session-scoped stream of screen changes, touches, console output, JavaScript errors, and network calls as OTLP log records, and injects W3C trace context into your API requests so a symptom in the app and its backend cause share a trace id.

## About

`@sazabi/react-native` is a pure JavaScript library you install in your own React Native or Expo app. No native module, no config plugin, no `pod install`; it runs in Expo Go. It lets you:

* Follow a single user's session from launch to error, across app launches when you give it a storage adapter
* Catch JavaScript errors that reach React Native's global error handler, fatal ones included, errors thrown while React renders, and unhandled promise rejections on Hermes, with stack traces
* See every touch as an episode, with taps and rage taps derived from them
* Join mobile network calls to backend logs on a shared `trace_id`
* Use AI to investigate a mobile session end to end

The SDK is configured with your intake URL alone. The URL embeds a write-only public key, which is why it is safe to ship in an app bundle: it can write telemetry into your project and never read from it.

### What gets captured

Every event carries `session.id`, `session.window_id` (one app launch), `session.distinct_id`, `app.screen.name`, `app.screen.url`, and an `app.event_type` naming the kind of event. The same keys the [browser SDK](https://docs.sazabi.com/catalogs/log-sources/send-to-an-endpoint/sazabi-browser-sdk.md) emits, so one query reads both; the platform is on the resource (`os.name`, `os.version`, `telemetry.sdk.name: "@sazabi/react-native"`, `telemetry.sdk.language: "javascript"`).

| `app.event_type` | What it records |
| - | - |
| `navigation` | Screen changes from your React Navigation or expo-router container: route name, path, and param keys |
| `pointer` | One episode per touch, from down to up, with coordinates, distance, duration, pressure, concurrent touches, and a sampled path; long touches emit partial rows |
| `click`, `rage_click` | Taps derived from short, still touches, and the repeated taps that signal frustration |
| `network` | `fetch` and `XMLHttpRequest` calls with method, status, duration, and trace context; sensitive query parameter values in `url.full` become `[redacted]`. Intake uploads are excluded; in development, so are Metro dev-server traffic and URLs containing `/symbolicate` |
| `error` | Errors with `exception.type`, `exception.message`, `exception.stacktrace`, and `app.error.kind`: `uncaught` or `fatal` from React Native's own `isFatal` flag (a render error caught by `<SazabiRoot>` is always `fatal`), or `unhandledrejection`. One row per error |
| `log` | Lines your app emits through the SDK's `log()` |
| `custom` | Marks you emit with `addEvent()` |
| `lifecycle` | The app going to the background and coming back |

By default the SDK also mirrors `console.error` and `console.warn` into the stream, leaving the original console behavior untouched.

A development build reports every uncaught error as fatal, so expect `fatal` rows there. The `console.error` line React Native prints for a reported error is not mirrored as a second `log` row. Rejection tracking needs the Hermes engine; an app on JavaScriptCore records no `unhandledrejection` rows. In a development build the SDK prints the same "Possible unhandled promise rejection" warning React Native's own tracker would. Hermes has one rejection-tracker slot; if another crash reporter owns it, see [Other crash reporters](https://docs.sazabi.com/reference/react-native-sdk.md#other-crash-reporters). Errors thrown while React renders are recorded by `<SazabiRoot>`, which must be the outermost component; it records the error as `fatal`, then rethrows it, so the app still crashes as it would without the SDK.

Every row of a crash batch delivered from the crash outbox carries `app.outbox.id`, one id per batch. An app on AsyncStorage can deliver a crash batch twice; deduplicate on that id.

`app.screen.url` and `url.path` carry the screen's path as your router reports it. When a screen opens from a deep link, its query string is kept, values included, as the browser SDK keeps the page URL. Only the `navigation` row's param list is reduced to keys.

Native crashes, ANRs, app-start and frame metrics, and session replay are not captured. Release builds ship Hermes bytecode, and the SDK does not upload source maps, so production stack traces are bytecode offsets.

### Configuration

| Option | Default | Purpose |
| - | - | - |
| `storage` | off | A key-value adapter (`getItem`, `setItem`, `removeItem`) for sessions that outlive one launch, the crash outbox, and the queue saved when the app leaves the foreground. The outbox replays on the next launch. A synchronous store such as MMKV waits until the next launch to send the crash; with a promise-returning adapter such as AsyncStorage the write may not finish, so the SDK also sends at once and a crash may arrive twice |
| `network.capture` | `true` | Emit network events |
| `network.propagateTraceContext` | `true` | Inject `traceparent` into eligible requests |
| `network.allowlist` | every app request | Requests eligible for header injection. A string is a host (`api.example.com`), a domain with its subdomains (`.example.com`), or an origin with an optional path prefix; it never matches as a substring. RegExps test the full URL. Passing a list replaces the default. This controls propagation, not capture; intake and development traffic stay excluded |
| `network.redactQueryParams` | built-in list | Extra query parameter names whose values become `[redacted]` in `url.full`, on top of the built-in list of tokens, OAuth codes, API keys, and presigned-URL signatures. See [Redacted URLs](https://docs.sazabi.com/reference/react-native-sdk.md#redacted-urls) |
| `network.sampledFlag` | `false` | Mark minted traces sampled (`-01`) for backend tracers |
| `network.requestIdHeaders` | `x-request-id`, `cf-ray`, `x-vercel-id`, `x-amz-cf-id`, `fly-request-id` | Response headers probed in order for a request id. Passing a list replaces the default |
| `pointer.capture`, `pointer.sampleIntervalMs`, `pointer.partialIntervalMs` | `true`, `1000`, `10000` | Touch episodes and their cadence |
| `console.capture` / `console.levels` | `true`, `["error", "warn"]` | Console mirroring |
| `errors.unhandledRejections` | `true` | Track rejections through the Hermes tracker slot (Hermes only); `false` leaves the slot alone and records no `unhandledrejection` rows |
| `errors.previousRejectionTracker` | off | A tracker (`allRejections`, `onUnhandled`, optional `onHandled`) the SDK runs after its own on every rejection, for a crash reporter initialized before `init()`. See [Other crash reporters](https://docs.sazabi.com/reference/react-native-sdk.md#other-crash-reporters) |
| `consent` | off | Hold all capture until it resolves true |
| `flushIntervalMs` | `5000` | Batch flush cadence, in milliseconds |
| `maxBatchBytes` | `57344` | Soft cap on one request body, in bytes |
| `maxQueuedEvents` | `500` | Buffered events, also capped at 1 MiB; the oldest are dropped first |

### Delivery

408, 429, 5xx, network failures, and 30-second timeouts retry with exponential backoff and `Retry-After`. A 413 splits the batch; a single event that still cannot fit is dropped. Other 4xx responses drop the batch with one warning per status per launch. The crash outbox retains retryable reports for a later launch rather than using a backoff timer. Unlike queued uploads, a stored report rejected with 413 is deleted without splitting it.

The queue has a 1 MiB budget as well as `maxQueuedEvents`. The outbox keeps up to 20 batches within 512 K characters (524,288 UTF-16 code units). Both drop the oldest first; a single oversized crash batch can exceed the character budget. The next delivered row after an in-memory queue eviction or refused queued batch reports the loss as `app.sdk.dropped_events`. Retries can produce duplicate deliveries. See [Delivery](https://docs.sazabi.com/reference/react-native-sdk.md#delivery) for timing and [Crash outbox](https://docs.sazabi.com/reference/react-native-sdk.md#crash-outbox) for storage caveats.

### Privacy controls

* **Touch names come from developer-authored props only**: `accessibilityLabel`, `testID`, or a `sazabiName` prop. Child text is never read. Add `sazabiMask` to any ancestor to suppress names under it.
* **The navigation row records param keys, not values.** A deep link's query string stays in the screen path.
* **Request and response bodies are never captured.**
* **Sensitive URL parameters are redacted.** The values of tokens, OAuth codes, API keys, and presigned-URL signatures in a request URL become `[redacted]`; add your own names with `network.redactQueryParams`. Credentials in the URL (`user:pass@`) are never recorded.
* **Consent gating.** Pass `consent: () => boolean | Promise<boolean>` and the SDK installs nothing until it resolves true.
* **Client-asserted identity.** `session.distinct_id` is whatever the app claims. Treat it as a claim, not as proof of identity.
* **Contained failures.** Each SDK hook catches its own errors, so a failure inside the SDK is not thrown into your app code. `shutdown()` removes the network, console, and touch instrumentation; the global error handler and the Hermes rejection tracker cannot be handed back, so they stay installed and do nothing.

## Prerequisites

* A React Native app on 0.76 or later, or an Expo app on SDK 52 or later.
* A Sazabi project.
* Optional: a key-value store for the `storage` adapter, such as `@react-native-async-storage/async-storage`.

## Set up in the dashboard

**Step 1**

### Open the log source

Go to [**Settings > Log streams**](https://app.sazabi.com/settings/log-source-providers), choose **Sazabi React Native SDK**, and click **Add**.

**Step 2**

### Copy your intake URL

Sazabi shows your complete intake URL at the top of the setup screen. That URL is all the SDK needs: pass it as `intakeUrl`, with or without the `/v1/logs` path.

**Step 3**

### Install the SDK

```bash theme={"system"}
npm install @sazabi/react-native @react-native-async-storage/async-storage
# Expo: npx expo install @sazabi/react-native @react-native-async-storage/async-storage
```

AsyncStorage is the optional `storage` adapter in the next step. Leave it
out if your app already has another key-value store.

**Step 4**

### Initialize in your entry file

Import the register entrypoint as the **first import** of your entry
module, the file your `package.json` `main` field points to: `index.js`
in a React Native CLI app, `index.ts` in an Expo app. Under expo-router,
`main` is `expo-router/entry`, so add an `index.js` that imports the SDK
first and `expo-router/entry` last, and set `"main": "index.js"`. Then
call `init()`:

```ts theme={"system"}
// Import first, because a module that runs earlier can capture the native
// XMLHttpRequest methods before the SDK wraps them.
import "@sazabi/react-native/register";

import AsyncStorage from "@react-native-async-storage/async-storage";
import { init } from "@sazabi/react-native";

init({
  intakeUrl: "<your intake URL>",
  serviceName: "my-mobile-app",
  serviceVersion: "1.4.2",
  environment: "production",
  storage: AsyncStorage, // optional
});
```

React Native's `fetch` is built on `XMLHttpRequest`, so one patch sees
`fetch`, `axios`, and direct XHR callers. The `storage` adapter is
optional: any key-value store with `getItem`, `setItem`, and `removeItem`
(AsyncStorage, an MMKV wrapper, `expo-sqlite/kv-store`) keeps the session
across launches and makes the crash outbox durable. Without it the
session lasts one launch.

In a React Native CLI app, put the intake URL in a constant or read it
with `react-native-config`; `process.env` is not available at runtime.
In an Expo app you can read it from an `EXPO_PUBLIC_*` variable.

A promise-returning adapter such as AsyncStorage may not finish the crash
outbox write before the process dies, so the SDK also sends the batch at
once, and a crash may arrive twice (both copies carry the same
`app.outbox.id`). A synchronous store such as MMKV writes the outbox
before the process ends and waits until the next launch to send the crash. Upload retries can still produce duplicate deliveries.

`init()` is idempotent and catches its own errors. Under
react-native-web it installs nothing and logs a warning that the
[browser SDK](https://docs.sazabi.com/catalogs/log-sources/send-to-an-endpoint/sazabi-browser-sdk.md)
owns that host, so a universal app can initialize both.

**Step 5**

### Capture touches and screens

Wrap your app in `<SazabiRoot>` and pass your navigation container ref to
`trackNavigation()`. expo-router exposes the same ref through its
`useNavigationContainerRef`. Make `<SazabiRoot>` the outermost component,
outside your own error boundaries and providers: it also records render
errors, and only sees what renders inside it.

```tsx theme={"system"}
import { useEffect } from "react";
import {
  NavigationContainer,
  useNavigationContainerRef,
} from "@react-navigation/native";
import { SazabiRoot, trackNavigation } from "@sazabi/react-native";

export default function App() {
  const navigationRef = useNavigationContainerRef();
  useEffect(() => trackNavigation(navigationRef), [navigationRef]);
  return (
    <SazabiRoot>
      <NavigationContainer ref={navigationRef}>{/* screens */}</NavigationContainer>
    </SazabiRoot>
  );
}
```

Under expo-router, do the same in `app/_layout.tsx`:

```tsx theme={"system"}
import { useEffect } from "react";
import { Stack, useNavigationContainerRef } from "expo-router";
import { SazabiRoot, trackNavigation } from "@sazabi/react-native";

export default function RootLayout() {
  const navigationRef = useNavigationContainerRef();
  useEffect(() => trackNavigation(navigationRef), [navigationRef]);
  return (
    <SazabiRoot>
      <Stack />
    </SazabiRoot>
  );
}
```

`<SazabiRoot>` is one full-size `View` that observes touches without
claiming them, so gestures reach your components untouched. It is also an
error boundary: a render error is recorded as `fatal` and then rethrown,
so the app crashes (or shows its red box) as before. The `sazabiName` and
`sazabiMask` props are typed on `View`, `Pressable`, `Text`, and the
`Touchable*` components once you import the SDK. See
[React Native SDK](https://docs.sazabi.com/reference/react-native-sdk.md#name-and-mask-touches).

**Step 6**

### Correlate with your backend

Every request the app makes carries a `traceparent` header by default: a
native app has no browser origin or CORS preflight. Intake uploads are
excluded; in development, Metro dev-server traffic and URLs containing
`/symbolicate` are excluded from capture and propagation too. Use
`network.allowlist` to limit propagation to your own APIs:

```ts theme={"system"}
init({
  // …
  network: {
    allowlist: ["https://api.example.com"],
  },
});
```

A string entry is a host (`api.example.com`), a domain with every
subdomain (`.example.com`), or an origin with an optional path prefix. It
matches by origin or host, never as a substring, so a lookalike host gets
no header. A RegExp is tested against the full URL.

Your backend has to run each request inside the incoming trace context
for its log lines to share the same `trace_id`. If it runs OpenTelemetry,
that extraction already happens by default; set
`network: { sampledFlag: true }` if you also want your backend tracer to
*record* those traces. The log join works either way.

**Step 7**

### Attribute sessions to users (optional)

```ts theme={"system"}
import { addEvent, identify, log, reset } from "@sazabi/react-native";

identify("user_123", { plan: "pro" }); // on sign-in, an internal id, not an email
addEvent("checkout_started", { cartValue: 42 }); // custom mark
log("ERROR", "checkout failed", { orderId }); // app log line
reset(); // on sign-out
```

`reset()` rotates the session and launch ids, so later activity on the
same device never threads into the previous user's timeline.

**Step 8**

### Hand off the change

Commit these changes on a new branch, push it, and open a pull request. Keep the intake URL in your app's configuration or environment, and commit only configuration that references it.

## Set up with the CLI

You can also register the Sazabi React Native SDK log source with the [Sazabi CLI](https://docs.sazabi.com/reference/cli.md) (installed and authenticated, see [CLI reference](https://docs.sazabi.com/reference/cli/log-sources.md)).

Registering the source mints the same intake URL the dashboard shows above, the only value the SDK needs:

```bash theme={"system"}
sazabi log-sources create sazabi_react_native_sdk --mode manual
```

## Verify

1. **Use your app**: launch it and tap through a few screens. The SDK flushes every 5 seconds by default, and again when the app goes to the background, so events land within about 10 seconds.

2. **Check the app**: in the Metro console or the device's network inspector, POSTs to your intake host's `/v1/logs` should return 200.

3. **Confirm arrival**: open the [Intake page](https://docs.sazabi.com/guides/instrument/intake.md) in the Sazabi dashboard and confirm records from the **Sazabi React Native SDK** source appear, or ask the agent in a thread: "Show me recent mobile sessions."

## Troubleshooting

**No requests to intake at all**: confirm `init()` runs in your entry file. Check the Metro console for the `[@sazabi/react-native]` configuration warning, which means `intakeUrl` or `serviceName` was empty, or the app is running in a browser. If you pass a `consent` callback, confirm it resolves `true`.

**Requests return 401**: the intake URL's embedded key was deactivated, or the URL belongs to another project. Check the log stream in [**Settings > Log streams**](https://app.sazabi.com/settings/log-source-providers) and copy the current intake URL.

**Errors and requests arrive, but no touches or screens**: the root is not wrapped in `<SazabiRoot>`, or `trackNavigation()` was never called with the container ref.

**Some requests are missing from the stream**: another library captured `XMLHttpRequest.prototype.send` before the SDK did. Make `import "@sazabi/react-native/register";` the first import of the module your `package.json` `main` field points to. Under expo-router that is your own `index.js`, not `app/_layout.tsx`.

**A fatal error's events never arrived**: without a `storage` adapter there is no crash outbox. With a promise-returning adapter such as AsyncStorage the write may not finish before the process dies and the immediate send may fail; a synchronous store such as MMKV closes that gap. A render error is recorded only when it is thrown inside `<SazabiRoot>`. The outbox is posted on the next launch. Retryable failures stay for a later launch; permanent 4xx failures are deleted with a warning, so correct the intake URL before generating more events.

**Unhandled rejections are missing**: rejection tracking needs the Hermes engine, so an app on JavaScriptCore records none. On Hermes, another SDK took the engine's single rejection-tracker slot after this one; the slot cannot be read or wrapped, so neither side can tell. Initialize `@sazabi/react-native` last and hand the other SDK's capture call to it as `errors.previousRejectionTracker`, or pass `errors: { unhandledRejections: false }` and let the other SDK report them. A development build prints one `[@sazabi/react-native]` note when the slot is taken with neither option set. See [Other crash reporters](https://docs.sazabi.com/reference/react-native-sdk.md#other-crash-reporters).

**A warning about a stored crash report that is not valid JSON**: the SDK discards that entry instead of posting it and removes it from storage on the same launch, so the warning does not come back on the next one. Nothing else is lost.

**Jest fails to load the SDK**: the SDK ships ES modules. Add `@sazabi/react-native` to your `transformIgnorePatterns` exception; see [Jest](https://docs.sazabi.com/reference/react-native-sdk.md#jest). Metro resolves `@sazabi/react-native/register` through the package's `react-native` field even without package exports.

**Two copies of the SDK in one bundle**: keep one copy and import `init` and `SazabiRoot` from it. Initialization is shared, but components from another copy can miss touches or render errors.

**Stream shows Inactive**: not an error. It means no telemetry arrived in the last hour. If you expected telemetry, check that the build calling `init()` has shipped and someone has launched it since, and that a `consent` gate is not resolving false.

### Log stream states

Each log stream for this source has a status visible in [**Settings > Log streams**](https://app.sazabi.com/settings/log-source-providers), derived from recent telemetry activity on the log stream's key:

| Status | Meaning |
| - | - |
| **Active** | Telemetry has been received from this log stream in the last hour. |
| **Inactive** | The stream is live, but no telemetry has been received in the last hour. Expected before your first telemetry arrives, or whenever the source is idle. |
| **Paused** | You paused the log stream, so it no longer accepts telemetry. Historical telemetry already received remains visible. Resume it from the log stream's action menu; the same intake URL starts accepting again, with no reconfiguration. |

## Further reading

> [card] [Intake](https://docs.sazabi.com/guides/instrument/intake.md)
>
> See per-source ingestion volume and health in the Intake page.

> [card] [React Native SDK reference](https://docs.sazabi.com/reference/react-native-sdk.md)
>
> Every init option, capture type, and API of @sazabi/react-native.

> [card] [Instrument your mobile app](https://docs.sazabi.com/guides/instrument/react-native.md)
>
> The task-oriented walkthrough of the same setup.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.