> ## 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.

# React Native SDK

> Reference for the Sazabi React Native SDK (@sazabi/react-native), covering init, SazabiRoot, trackNavigation, identify, custom events, logs, and automatic capture.

**Early preview**

The 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.

The React Native SDK (`@sazabi/react-native`) instruments a React Native or Expo app and sends telemetry to Sazabi. It captures JavaScript errors, screen changes, touches, network calls, console output, and app lifecycle, and lets you emit custom events and logs. It authenticates with a **public key**, which is write-only and safe to ship in your app bundle.

The package is pure JavaScript: no native module, no config plugin, and it runs in Expo Go. It needs React Native 0.76 or later (Expo SDK 52 or later). For a web build of the same app use the [browser SDK](https://docs.sazabi.com/reference/browser-sdk.md); this SDK installs nothing under react-native-web.

For server-side reads and writes against the API, use the [TypeScript SDK](https://docs.sazabi.com/reference/sdk.md) with a secret key instead.

## Install

```bash theme={"system"}
npm install @sazabi/react-native
```

The examples below also pass AsyncStorage as the optional `storage` adapter. Install it if your app does not have it yet:

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

The package ships ES modules and a `react-native` manifest field. Metro resolves `@sazabi/react-native/register` with or without package exports, including React Native 0.76 to 0.78 and Expo SDK 52, where package exports are off by default.

### Jest

React Native's Jest preset does not transform this SDK's ES modules in `node_modules` by default. Include the SDK in your `transformIgnorePatterns` exception:

```js title="jest.config.js" theme={"system"}
module.exports = {
  preset: "react-native",
  transformIgnorePatterns: [
    "node_modules/(?!((jest-)?react-native|@react-native(-community)?|@sazabi/react-native)/)",
  ],
};
```

If you already have an exception for other dependencies, add `@sazabi/react-native` to that expression.

## Initialize

Import the `register` entrypoint as the first import of your app's entry module, the file your `package.json` `main` field points to. It installs the SDK's `XMLHttpRequest` patch before any other module can capture the prototype methods. Then call `init`. Every import in the file runs before the first statement, so `init` still runs before your first screen renders.

### React Native CLI

The entry module is `index.js`, the file that calls `AppRegistry.registerComponent`. A bare React Native app has no `process.env` at runtime, so put the intake URL in a constant or read it with [`react-native-config`](https://github.com/lugg/react-native-config):

```js title="index.js" theme={"system"}
import "@sazabi/react-native/register";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { init } from "@sazabi/react-native";
import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";

// Or `Config.SAZABI_INTAKE_URL` with react-native-config.
const SAZABI_INTAKE_URL = "https://<your-intake-host>.intake.sazabi.com";

init({
  intakeUrl: SAZABI_INTAKE_URL,
  serviceName: "my-mobile-app",
  storage: AsyncStorage,
});

AppRegistry.registerComponent(appName, () => App);
```

### Expo

Without expo-router, the entry module is `index.ts`, the file that calls `registerRootComponent`. Expo inlines `EXPO_PUBLIC_*` variables at build time:

```ts title="index.ts" theme={"system"}
import "@sazabi/react-native/register";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { init } from "@sazabi/react-native";
import { registerRootComponent } from "expo";
import App from "./App";

init({
  intakeUrl: process.env.EXPO_PUBLIC_SAZABI_INTAKE_URL,
  serviceName: "my-mobile-app",
  storage: AsyncStorage,
});

registerRootComponent(App);
```

### Expo with expo-router

expo-router sets `main` to `expo-router/entry`, so `app/_layout.tsx` loads after expo-router and every screen module. Add your own entry file that imports the SDK first and then expo-router, and point `main` at it:

```json title="package.json" theme={"system"}
{
  "main": "index.js"
}
```

```js title="index.js" theme={"system"}
import "@sazabi/react-native/register";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { init } from "@sazabi/react-native";
import "expo-router/entry";

init({
  intakeUrl: process.env.EXPO_PUBLIC_SAZABI_INTAKE_URL,
  serviceName: "my-mobile-app",
  storage: AsyncStorage,
});
```

### Options

`init` is idempotent across module evaluations: a second call finds the running instance and is ignored. Keep one copy of the SDK in your bundle and import `init`, `SazabiRoot`, and the other APIs from that copy. Two copies do not install a second transport or global error handler, but components from a different copy can miss touches or render errors because their capture hooks belong to that copy. When `intakeUrl` or `serviceName` is missing it logs a `[@sazabi/react-native]` warning and installs nothing. `init` options:

| Option | Required | Default | Purpose |
| - | - | - | - |
| `intakeUrl` | Yes | | Your log source's intake URL, from [**Settings > Log streams**](https://app.sazabi.com/settings/log-source-providers) (or `sazabi log-sources create sazabi_react_native_sdk`). It embeds a write-only public key, so it is safe to ship in the bundle. `/v1/logs` may be included or left off. |
| `publicKey` | No | From `intakeUrl` | Only needed when `intakeUrl` points at a proxy you run that carries no key. |
| `serviceName` | Yes | | Logical name of the app. Stamped on every event. |
| `serviceVersion` | No | | App version (for example the marketing version or a git SHA). |
| `environment` | No | | Deployment environment, such as `production`. |
| `storage` | No | Off | A key-value adapter with `getItem`, `setItem`, and `removeItem`, synchronous or promise-returning, plus an optional `getAllKeys` that lets a launch recover a crash report whose index entry never landed. Keeps the session across launches, turns on the crash outbox, and saves the queue when the app leaves the foreground. See [Crash outbox](#crash-outbox) and [Storage keys](#storage-keys). |
| `network.capture` | No | `true` | Emit network events, excluding intake uploads and development traffic described below. |
| `network.propagateTraceContext` | No | `true` | Inject a W3C `traceparent` header into eligible requests. |
| `network.allowlist` | No | Every app request | Requests eligible for `traceparent` injection. A string is a host (`api.example.com`, with the port when it is not the default), a domain with every subdomain (`.example.com`), or an origin with an optional path prefix (`https://api.example.com`, `https://example.com/api`); a RegExp is tested against the full URL. A string never matches a URL that merely contains it. Passing a list replaces the default, so only matching requests get the header. Intake uploads and development traffic remain excluded. This controls propagation, not capture. See [Correlate with your backend](#correlate-with-your-backend). |
| `network.redactQueryParams` | No | The built-in list | Extra query and fragment parameter names whose values are replaced with `[redacted]` in `url.full`, in addition to the built-in list. A string matches a name exactly (case-insensitively); a RegExp is tested against the name. See [Redacted URLs](#redacted-urls). |
| `network.sampledFlag` | No | `false` | Mark the traces the SDK starts as sampled (`-01`), so backend tracers that follow the parent decision record them. |
| `network.requestIdHeaders` | No | `x-request-id`, `cf-ray`, `x-vercel-id`, `x-amz-cf-id`, `fly-request-id` | Response headers probed in order for a platform request id, attached as `app.network.request_id`. Passing a list replaces the default. |
| `pointer.capture` | No | `true` | Record touch episodes. |
| `pointer.sampleIntervalMs` | No | `1000` | How often a touch's path is sampled. |
| `pointer.partialIntervalMs` | No | `10000` | How often a long touch emits a partial row. |
| `console.capture` | No | `true` | Mirror console output into the stream. |
| `console.levels` | No | `["error", "warn"]` | Console levels to mirror. |
| `errors.unhandledRejections` | No | `true` | Track unhandled promise rejections through the Hermes rejection tracker. Set `false` to leave its single slot alone; the SDK then records no `unhandledrejection` events. See [Other crash reporters](#other-crash-reporters). |
| `errors.previousRejectionTracker` | No | | A rejection tracker (`allRejections`, `onUnhandled`, optional `onHandled`) the SDK runs after its own on every rejection, for a library that installed one before `init`. See [Other crash reporters](#other-crash-reporters). |
| `consent` | No | | A function returning a boolean or promise; capture stays dormant until it resolves `true`. |
| `flushIntervalMs` | No | `5000` | Batch flush cadence, in milliseconds. |
| `maxBatchBytes` | No | `57344` | Soft cap on one request body, in bytes. A larger queue is sent in several requests. |
| `maxQueuedEvents` | No | `500` | Maximum buffered events while uploads cannot complete. A separate 1 MiB cap also applies; the oldest events are dropped first. |

## Capture touches and screens

Wrap your app in `<SazabiRoot>` and pass your navigation container ref to `trackNavigation`. `<SazabiRoot>` must be the outermost component, outside your own error boundaries and providers: it observes touches and records render errors, and it only sees what renders inside it.

### React Navigation

```tsx title="App.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>
  );
}
```

### expo-router

expo-router owns the navigation container and exposes its ref through `useNavigationContainerRef`:

```tsx title="app/_layout.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` renders one full-size `View` that observes touches without claiming them. It is also an error boundary: an error thrown while React renders your components is recorded as a `fatal` error and written to the [crash outbox](#crash-outbox), then rethrown, so the app crashes (or a development build shows its red box) exactly as it would without the SDK. An error boundary of yours above `SazabiRoot` that recovers from the error leaves a `fatal` event for a crash that did not happen, which is another reason to keep `SazabiRoot` outermost. `trackNavigation(ref)` subscribes to the container ref and records each screen change; it returns an unsubscribe function.

### Name and mask touches

Touches are named from the nearest `accessibilityLabel` or `testID`. A `sazabiName` prop overrides the name, and `sazabiMask` on an ancestor suppresses names under it:

```tsx theme={"system"}
<Pressable sazabiName="checkout-pay" onPress={pay}>
  <Text>Pay {total}</Text>
</Pressable>

<View sazabiMask>
  {/* touches in here are recorded without a name */}
</View>
```

Both props are typed on `View`, `Pressable`, `Text`, and the `Touchable*` components as soon as you import the SDK, so no declaration file is needed.

## Identify and reset

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

// After sign-in:
identify("user_123", { plan: "pro" });

// On sign-out:
reset();
```

`identify` attaches a client-asserted user identity (and optional traits) to subsequent events. `reset` clears the identity and rotates the session so a shared device does not thread activity into the previous user's timeline.

### Session lifetime

With a `storage` adapter, a session survives an app restart while it is within the 30-minute inactivity and 24-hour age limits. A stored session also rotates if its last activity is more than 60 seconds in the future after the device clock moves backward, or its stored format cannot be read. Upgrading the app alone does not rotate a valid session.

## Custom events and logs

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

addEvent("checkout_started", { cartValue: 42 });
log("WARN", "cache lookup failed", { requestId: "req_9" });
```

`addEvent(name, attributes?)` emits a custom event into the session stream. `log(severity, message, metadata?)` emits an application log line with session context; `severity` is one of `DEBUG`, `INFO`, `WARN`, or `ERROR`.

## Flush and shutdown

```ts theme={"system"}
import { flush, shutdown } from "@sazabi/react-native";

await flush(); // Send queued events unless retry backoff is active.
await shutdown(); // Flush, then stop capture.
```

The SDK flushes every 5 seconds by default (`flushIntervalMs`) and again when the app goes to the background.

`shutdown` removes the network, console, and touch instrumentation and stops emitting. Two hooks cannot be handed back: the Hermes rejection tracker has one slot with no way to restore its previous occupant, and another library may have wrapped the SDK's global error handler. Both stay installed but do nothing after `shutdown`; the previous global error handler and a tracker passed as `errors.previousRejectionTracker` still run.

### Delivery

The intake response determines what happens to each batch:

| Intake response | SDK behavior |
| - | - |
| 2xx | Marks the batch delivered. |
| 408, 429, 5xx, network error, or no response within 30 seconds | Keeps the batch and retries with exponential backoff, starting at 1 second and doubling to a 60-second cap, with jitter. A longer `Retry-After` delays the retry, up to 5 minutes. `flush()` does not bypass backoff. |
| 413 | Splits the batch in half and sends each half. A single event that is still too large is dropped. |
| Other 4xx, including 400, 401, 403, and 404 | Drops the batch without retrying and logs one warning per status per launch. |

The queue holds at most `maxQueuedEvents` events (500 by default) and has a 1 MiB byte budget. If either limit is reached, the oldest events are dropped first. A full queue logs one `[@sazabi/react-native]` warning per launch. The next delivered event after queue evictions or refused queued batches carries `app.sdk.dropped_events` with the number of events lost. This counter covers the in-memory queue; reports evicted or refused from the stored crash outbox are not included. A single oversized event can exceed the queue's byte budget.

Uploads may be delivered more than once: a timed-out request may have been accepted before the SDK retries it. Keep this in mind when counting events.

### Saved queue

The OS can kill a backgrounded app without warning, and an offline queue would die with it. With a `storage` adapter, the SDK saves the queue under `sazabi.outbox.queue` each time the app leaves the foreground, before it flushes, within the same 20-batch and 512 K-character budget as the crash outbox. The copy is removed once everything queued has been delivered; otherwise the next launch posts it. Events captured after the app went to the background are saved only the next time it does. If the live upload reached the intake but the app was killed before the answer came back, the next launch posts the copy too; both deliveries carry the same `app.outbox.id`.

## Crash outbox

A fatal JavaScript error, including a render error caught by `SazabiRoot`, ends the process shortly after React Native's error handler returns, so a normal upload cannot finish. With a `storage` adapter, the SDK writes the pending events, the error included, to an outbox in storage before the app terminates. On the next launch, `init` posts the outbox. Retryable failures stay for a later attempt; permanent failures are removed according to the delivery policy below.

How often a crash is delivered depends on the adapter:

* **Synchronous store (an MMKV wrapper):** the write is on disk before the process ends, so the SDK waits until the next launch to send the crash. If the app is still running two seconds after the error (a development build, or an error handler that does not terminate), the SDK posts the outbox itself.
* **Promise-returning store (AsyncStorage):** the write may not finish before the process dies, so the SDK also sends the batch at once. If the write is lost and the send fails, that crash's events are lost. If both land, the next launch posts the batch a second time.

Every event in one crash batch carries the same `app.outbox.id`. Group or deduplicate on it to drop the second copy an AsyncStorage app can deliver.

Delivery is at least once. A crash whose acknowledgement the app never saw, because the process died or the response never arrived, is posted again on the next launch under the same `app.outbox.id`, and the [saved queue](#saved-queue) can be posted in the same launch as a crash batch. The batch key and the index are two writes, and the process can die between them, leaving a report under its key that no index lists. When the adapter provides `getAllKeys` (AsyncStorage and MMKV both do), the next launch lists the store's keys once and treats every `sazabi.outbox.<id>` key the index does not name as a stored report, posting and removing it like any other. A custom adapter without `getAllKeys` loses only a crash that died in that window. A delivered report's key is removed before the index stops listing it, so a cleanup interrupted between the two leaves a listed id with no key, which the next launch drops rather than posting again.

The outbox retains reports after 408, 429, 5xx, network errors, and timeouts for a later launch. Unlike queued uploads, stored reports are not retried on a backoff timer and are not split on 413: a 413 or any other 4xx except 408 and 429 deletes the stored report with one warning per status per launch. The outbox retains the newest batches within a budget of 20 batches and 512 K characters (524,288 UTF-16 code units), dropping the oldest first. A single oversized batch can exceed that character budget.

Stored values are checked when they are read. A stored entry that is not valid JSON is discarded with one warning, never posted, and the first launch that discards it also removes it from storage, so the warning does not come back on every launch.

Without a `storage` adapter there is no outbox; the SDK only attempts an immediate send.

### Storage keys

The SDK writes these keys through the `storage` adapter. Do not read or edit them from your app.

| Key | Contents |
| - | - |
| `sazabi.session`, `sazabi.distinctId` | The current session and the identity set with `identify`. |
| `sazabi.outbox.<id>` | One crash batch, written under its own key so a crash writes a few kilobytes instead of rewriting every stored report. |
| `sazabi.outbox.index` | The list of stored crash batch ids. A crash writes its batch key first and lists it here second; delivery removes the batch key first and delists it second. A batch key this list does not name is recovered on the next launch when the adapter provides `getAllKeys`. |
| `sazabi.outbox.queue` | The queue saved when the app leaves the foreground. See [Saved queue](#saved-queue). |

## Other crash reporters

Hermes has one slot for unhandled promise rejection tracking, and the last library to claim it silences the others. Hermes gives no way to read or wrap that slot (`HermesInternal` is frozen and its global cannot be reassigned), so `init` replaces whatever holds it, and a library initialized after `init` replaces the SDK's tracker in turn; neither side can tell. Your options:

* Hand the SDK a tracker to run after its own on every rejection with `errors.previousRejectionTracker`. Most crash reporters (Sentry, Bugsnag) install theirs inline and do not expose it, so write the hand-off yourself against their capture API:

  ```ts theme={"system"}
  init({
    // …
    errors: {
      previousRejectionTracker: {
        allRejections: true,
        onUnhandled: (id, rejection) => Sentry.captureException(rejection),
      },
    },
  });
  ```

* Leave the slot to the other SDK with `errors: { unhandledRejections: false }`. The SDK then records no `unhandledrejection` events.

* Initialize the other SDK after `init` and accept that the SDK records no `unhandledrejection` events.

In a development build the SDK prints one `[@sazabi/react-native]` console note when it takes the slot with neither option set. Global error handlers (`ErrorUtils`) always chain, in any order, so a crash reporter's global handler keeps running whether it was installed before or after `init`.

## What the SDK captures automatically

After `init`, the SDK captures, with session context attached:

* **Errors**: errors that reach React Native's global error handler, errors thrown while React renders inside `SazabiRoot`, and unhandled promise rejections, with stack traces. `app.error.kind` is `uncaught`, `fatal`, or `unhandledrejection`. `fatal` and `uncaught` are React Native's own `isFatal` flag passed through: a development build reports an uncaught error as fatal even though the red box keeps the app alive, and `ErrorUtils.reportError` reports one as not fatal. A render error caught by `SazabiRoot` is always `fatal`. Each error is one row: the `console.error` line React Native prints for it is not recorded again as a `log` event. Rejection tracking needs the Hermes engine; an app on JavaScriptCore gets no rejection events. In a development build without `errors.previousRejectionTracker`, the SDK prints the same "Possible unhandled promise rejection" warning React Native's own tracker would, which LogBox shows. See [Other crash reporters](#other-crash-reporters) for the Hermes tracker slot.
* **Screens**: route changes from the navigation container you pass to `trackNavigation`, with the route name, path, and param keys.
* **Touches**: one event per touch from down to up, with coordinates, distance, duration, pressure, concurrent touches, and a once-per-second path sample. Taps and rapid repeat taps are derived from them. When a scroll view takes over a touch, the event ends at that point with `app.pointer.ended_by: cancel`.
* **Network calls**: `fetch` and `XMLHttpRequest` calls with method, status, and duration, with sensitive query parameters redacted in `url.full` (see [Redacted URLs](#redacted-urls)). Intake uploads are excluded, matched by the intake's own origin, so a lookalike host is still captured. In development, requests to the Metro dev server origin and any URL containing `/symbolicate` are excluded from both capture and trace propagation. Other requests receive a W3C `traceparent` header by default; `network.allowlist` narrows propagation without disabling capture (see [Correlate with your backend](#correlate-with-your-backend)). Set `network.propagateTraceContext: false` to disable injection while keeping capture.
* **Console**: mirrors `console.error` and `console.warn` by default (the original console output is untouched).
* **Lifecycle**: the app going to the background and coming back.

The SDK never captures request or response bodies, typed text, or navigation param values. The screen path (`url.path` and `app.screen.url`) is recorded as your router reports it, so a deep link's query string, values included, is kept, as the browser SDK keeps the page URL. The SDK does not capture native crashes, ANRs, app-start or frame metrics, or session replay.

Each SDK hook catches its own errors, so a failure inside the SDK is not thrown into your app code. Error capture accepts strings, plain or circular objects, and errors with throwing property getters. Error messages and stacks are truncated to 4,096 characters.

## Correlate with your backend

By default the SDK injects a W3C `traceparent` header into every request your app makes. A native app has no origin and no CORS preflight, so there is no same-origin default to fall back on; the browser SDK defaults to same-origin requests. Requests to the Sazabi intake are never injected, and in development neither are requests to the Metro dev server. Use `network.allowlist` to choose which hosts get the header, so third-party APIs never see one:

```ts theme={"system"}
init({
  // …
  network: {
    allowlist: [
      "api.example.com",
      ".internal.example.com",
      /^https:\/\/edge-\d+\.example\.net\//,
    ],
  },
});
```

A string entry is one of:

* A host: `api.example.com`. Add the port when it is not the default (`api.example.com:8443`).
* A domain with every subdomain: `.example.com` matches `example.com` and `api.example.com`.
* An origin with an optional path prefix: `https://api.example.com`, or `https://example.com/api` to match only requests under that path.

A string matches by parsed origin or host, never as a substring, so `https://evil.test/?next=api.example.com` and `api.example.com.evil.test` get no header. A RegExp is tested against the full URL.

Backend log lines that run inside the extracted trace context then share a `trace_id` with the originating request. If your backend 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.

## Redacted URLs

Network events keep the query string in `url.full`, but replace the value of every sensitive parameter with `[redacted]`. The built-in list is `token`, `access_token`, `refresh_token`, `id_token`, `api_key`, `apikey`, `key`, `secret`, `password`, `signature`, `sig`, `x-amz-signature`, `x-amz-credential`, `x-goog-signature`, `sentry_key`, `authorization`, `auth`, `session`, `sid`, and `code`. Names match case-insensitively, in the query and in the fragment, so a hash router's `#/reset?token=…` is redacted too. Credentials in the URL (`user:pass@`) are never recorded.

Add your own parameter names with `network.redactQueryParams`. A string matches a name exactly; a RegExp is tested against the name, from the start each time, so a `g` flag is safe:

```ts theme={"system"}
init({
  // …
  network: { redactQueryParams: ["tenant", /^x-myco-/i] },
});
```

The option extends the built-in list; it cannot remove a built-in name. The same rule covers every recorded URL, not only requests: the screen URL stamped on every event (`app.screen.url`) and a deep link's route path on `navigation` events (`url.path`). The browser SDK takes the same option and applies it to the page URL on every event, and to a navigation's `url.full` and `app.navigation.from`, the referrer included.

## Attributes

Events carry the attributes below, plus attributes specific to its `app.event_type`:

| Attribute | On | Meaning |
| - | - | - |
| `app.event_type` | Every event | The kind of event: `navigation`, `pointer`, `click`, `rage_click`, `error`, `network`, `log`, `custom`, or `lifecycle`. |
| `session.id` | Every event | The session, kept across launches when you pass a `storage` adapter. |
| `session.window_id` | Every event | One app launch. |
| `session.distinct_id` | Events after `identify` | The user id passed to `identify`, until `reset`. |
| `app.screen.name` / `app.screen.url` | Every event | The current screen, from `trackNavigation`. |
| `app.error.kind` | `error` events | `uncaught`, `fatal`, or `unhandledrejection`. |
| `exception.type`, `exception.message`, `exception.stacktrace` | `error` events | The error. |
| `url.full` | `network` events | The request URL, with sensitive query parameter values replaced by `[redacted]`. See [Redacted URLs](#redacted-urls). |
| `app.sdk.dropped_events` | Next delivered event after a queue eviction or refused queued batch | Number of events lost from the in-memory queue since the previous drop count was delivered. |
| `app.outbox.id` | Events delivered from the crash outbox | One id shared by every event of a crash batch. A re-sent copy carries the same id, so deduplicate on it. |

The resource on every request carries `service.name`, `service.version`, `deployment.environment`, `telemetry.sdk.name` (`@sazabi/react-native`), `telemetry.sdk.version`, `telemetry.sdk.language` (`javascript`), `os.name` (`ios` or `android`), `os.version`, and device attributes. Android supplies `device.manufacturer` and `device.model.identifier` from `Platform.constants`. iOS supplies `device.manufacturer: "Apple"` and `device.type` from its interface idiom (`phone`, `pad`, `tv`, `mac`, `vision`, or `carplay`). iOS does not expose a device model to JavaScript without a native module, so `device.model.identifier` is absent there.

## Further reading

> [card] [Instrument your mobile app](https://docs.sazabi.com/guides/instrument/react-native.md)
>
> The guide to instrumenting a mobile app.

> [card] [Browser SDK](https://docs.sazabi.com/reference/browser-sdk.md)
>
> The same event stream for web apps.


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