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

# React Native SDK observability reference

<View title="Developer" />

<View title="Federal docs" />

<View title="EU docs" />

<Note>
  **This LaunchDarkly observability plugin is available for early access**

  This LaunchDarkly observability plugin is currently available in Early Access, and APIs are subject to change until a 1.x version is released.
</Note>

This topic documents how to get started with the LaunchDarkly observability plugin for the React Native SDK.

The React Native SDK supports the **observability plugin** for error monitoring, logging, and tracing, and the **session replay plugin** for capturing screen recordings of user sessions.

<Note>
  **SDK quick links**

  LaunchDarkly's SDKs are open source. In addition to this reference guide, we provide source, API reference documentation, and a sample application:

  <table>
    <thead>
      <tr>
        <th>Resource</th>
        <th>Location</th>
      </tr>
    </thead>

    <tbody>
      <tr>
        <td>SDK API documentation</td>
        <td>[Observability plugin API docs](https://launchdarkly.github.io/observability-sdk/sdk/@launchdarkly/observability-react-native/)</td>
      </tr>

      <tr>
        <td>GitHub repository (observability)</td>
        <td>[@launchdarkly/observability-react-native](https://github.com/launchdarkly/observability-sdk/tree/main/sdk/%40launchdarkly/observability-react-native)</td>
      </tr>

      <tr>
        <td>Published module (observability)</td>
        <td>[npm](https://www.npmjs.com/package/@launchdarkly/observability-react-native)</td>
      </tr>

      <tr>
        <td>GitHub repository (session replay)</td>
        <td>[@launchdarkly/session-replay-react-native](https://github.com/launchdarkly/observability-sdk/tree/main/sdk/%40launchdarkly/react-native-ld-session-replay)</td>
      </tr>

      <tr>
        <td>Published module (session replay)</td>
        <td>[npm](https://www.npmjs.com/package/@launchdarkly/session-replay-react-native)</td>
      </tr>
    </tbody>
  </table>
</Note>

## Prerequisites and dependencies

This reference guide assumes you are familiar with the LaunchDarkly [React Native SDK](/docs/sdk/client-side/react/react-native).

The observability plugin requires React Native SDK version 10.10.0 or later.

The observability plugin also requires `@react-native-async-storage/async-storage` version 1.17.0 or later as a peer dependency. The plugin uses it to persist and resume a session across a JavaScript reload. To learn more, read [App reload events](#app-reload-events).

The React Native SDK version 10.x is compatible with Expo. Only iOS and Android platforms are supported. Web is not supported.

### Supported React Native and Expo versions

The LaunchDarkly observability and session replay plugins for React Native target the following React Native and Expo versions:

<table>
  <thead>
    <tr>
      <th>Framework</th>
      <th>Supported versions</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>React Native</td>
      <td>0.75+</td>
    </tr>

    <tr>
      <td>Expo</td>
      <td>51+</td>
    </tr>
  </tbody>
</table>

Both plugins support the React Native New Architecture and the Legacy Architecture (bridge / `NativeModules`). The module registers itself on whichever architecture the host app uses, so no per-app configuration is required for the module to load.

Not all React Native and Expo versions have been explicitly tested with the observability and session replay plugins, as both are in Early Access. New React Native and Expo releases are expected to work; if you run into an issue, please [file an issue on GitHub](https://github.com/launchdarkly/observability-sdk/issues).

## Get started

Follow these steps to get started:

* [Install the plugin](#install-the-plugin)
* [Initialize the React Native SDK client](#initialize-the-client)
* [Configure the plugin options](#configure-the-plugin-options)
* [Configure session replay](#configure-session-replay)
* [Explore supported features](#explore-supported-features)
* [Review observability data in LaunchDarkly](#review-observability-data-in-launchdarkly)

## Install the plugin

LaunchDarkly uses a plugin to the React Native SDK to provide observability.

The first step is to make the SDK, the observability plugin, and the `@react-native-async-storage/async-storage` peer dependency available as dependencies.

Here's how:

<CodeGroup>
  ```bash title="npm, React Native SDK v10.10+" lines wrap theme={null}
   npm install @launchdarkly/react-native-client-sdk
   npm install @launchdarkly/observability-react-native
   npm install @react-native-async-storage/async-storage
  ```

  ```bash title="yarn, React Native SDK v10.10+" lines wrap theme={null}
  yarn add @launchdarkly/react-native-client-sdk
  yarn add @launchdarkly/observability-react-native
  yarn add @react-native-async-storage/async-storage
  ```
</CodeGroup>

The observability plugin uses `@react-native-async-storage/async-storage` to persist and resume a session across a JavaScript reload. If your app already depends on AsyncStorage, you can reuse the existing installation. If AsyncStorage is not present, the plugin disables session preservation and each reload starts a new session. To learn more, read [App reload events](#app-reload-events).

Then, import the plugin into your code:

<CodeGroup>
  ```js title="Import, React Native SDK v10.10+" lines wrap theme={null}
  import { ReactNativeLDClient } from '@launchdarkly/react-native-client-sdk';
  import { Observability, LDObserve } from '@launchdarkly/observability-react-native';
  ```
</CodeGroup>

## Initialize the client

Next, initialize the SDK and the plugin.

To initialize, you need your LaunchDarkly environment's mobile key. This authorizes your application to connect to a particular environment within LaunchDarkly. To learn more, read [Initialize the client and identify a context](/docs/sdk/client-side/react/react-native#initialize-the-client-and-identify-a-context) in the React Native SDK reference guide.

<Warning>
  **React Native observability SDK credentials**

  The React Native observability SDK uses a mobile key. Keys are specific to each project and environment. They are available on the **SDK keys** page under **Settings**. To learn more about key types, read [Keys](/docs/sdk/concepts/client-side-server-side#keys-and-credentials).

  Mobile keys are not secret and you can expose them in your client-side code without risk. However, never embed a server-side SDK key into a client-side application.
</Warning>

Here's how to initialize the SDK and plugin:

<CodeGroup>
  ```js title="Initialize, React Native SDK v10.10+" lines wrap theme={null}
  const client = new ReactNativeLDClient(
      'example-mobile-key',
      AutoEnvAttributes.Enabled,
      {
        plugins: [
          new Observability()
        ],
      }
  );
  ```
</CodeGroup>

## Configure the plugin options

You can configure options for the observability plugin when you initialize the SDK. The plugin constructor takes an optional object with the configuration details.

Here is an example:

<CodeGroup>
  ```js title="Plugin options, React Native SDK v10.10+" lines wrap theme={null}
  const client = new ReactNativeLDClient(
      'example-mobile-key',
      AutoEnvAttributes.Enabled,
      {
        plugins: [
          new Observability({
            serviceName: 'example-service',
            // we recommend setting service_version to the latest deployed git SHA
            serviceVersion: 'example-sha'
          })
        ],
      }
  );
  ```
</CodeGroup>

For more information on plugin options, read [Configuration for client-side observability](/docs/sdk/features/observability-config-client-side).

## Report version information in Expo apps

If you build your app with Expo, the observability plugin does not read a version from the Expo runtime automatically. It uses the `serviceVersion` string that you pass, which defaults to `1.0.0` when you leave it unset. To attribute telemetry to a specific build, read the version from Expo and pass it to the plugins.

<Note>
  **Expo Go does not support the plugins**

  The observability and session replay plugins rely on native modules, so they do not run in Expo Go. Use an [Expo development build](https://docs.expo.dev/develop/development-builds/introduction/) or a standalone build instead.
</Note>

### Set the service version

To report the store-visible app version, read it with [`expo-application`](https://docs.expo.dev/versions/latest/sdk/application/) and set it as `serviceVersion` on both plugins:

<CodeGroup>
  ```tsx title="Set serviceVersion from expo-application" expandable lines wrap theme={null}
  import * as Application from 'expo-application';
  import { createSessionReplayPlugin } from '@launchdarkly/session-replay-react-native';
  import { Observability } from '@launchdarkly/observability-react-native';

  const serviceVersion = Application.nativeApplicationVersion ?? '1.0.0';

  const observability = new Observability({
    serviceName: 'my-expo-app',
    serviceVersion,
  });

  const sessionReplay = createSessionReplayPlugin({
    isEnabled: true,
    serviceName: 'my-expo-app',
    serviceVersion,
  });
  ```
</CodeGroup>

`Application.nativeApplicationVersion` maps to `CFBundleShortVersionString` on iOS and `versionName` on Android, which is the same version that users see in the app store listing.

### Identify over-the-air updates

If you ship over-the-air (OTA) updates with [Expo Updates](https://docs.expo.dev/versions/latest/sdk/updates/), the native app version stays fixed across updates. To tell builds apart, pass the update identifiers as `resourceAttributes` on the observability plugin:

<CodeGroup>
  ```tsx title="Report OTA update identifiers" lines wrap theme={null}
  import * as Application from 'expo-application';
  import * as Updates from 'expo-updates';

  const observability = new Observability({
    serviceName: 'my-expo-app',
    serviceVersion: Application.nativeApplicationVersion ?? '1.0.0',
    resourceAttributes: {
      'ota.update_id': Updates.updateId ?? 'embedded',
      'ota.runtime_version': Updates.runtimeVersion ?? undefined,
    },
  });
  ```
</CodeGroup>

`Updates.updateId` identifies the running OTA bundle. It is `null`, shown here as `embedded`, when the app runs the bundle shipped with the binary. `Updates.runtimeVersion` is the native-compatibility gate for OTA delivery.

## Tracing

This topic explains how to use the observability plugin to add custom tracing to your React Native application. A trace represents the path of an operation through your application as a tree of timed spans. The observability plugin automatically instruments network requests and LaunchDarkly SDK operations. You can also create your own custom spans to trace work that's not automatically instrumented.

The SDK returns custom spans as standard OpenTelemetry `Span` objects, so every span operation in this section uses the regular OpenTelemetry API. To learn about advanced tracing patterns beyond the basics covered here, such as correlated logs, error handling, span events, and baggage propagation, read the [React Native tracing guide](https://github.com/launchdarkly/observability-sdk/blob/main/sdk/%40launchdarkly/observability-react-native/guides/tracing.md).

The examples assume that you have already initialized the SDK and added the following imports:

<CodeGroup>
  ```typescript title="Imports" lines wrap theme={null}
  import { Platform } from 'react-native'
  import { LDObserve } from '@launchdarkly/observability-react-native'
  import { context, propagation, SpanStatusCode, trace } from '@opentelemetry/api'
  ```
</CodeGroup>

### About context propagation in React Native

Unlike server-side JavaScript runtimes, React Native has limited support for automatic context propagation. React Native uses OpenTelemetry's `StackContextManager`, which has no `AsyncLocalStorage` equivalent, so the SDK tracks the active span only synchronously. The SDK does not restore the active context after an `await`, `setTimeout`, `Promise` callback, or event handler. This includes `await` calls that occur within the same `startActiveSpan` callback.

In practice, the SDK automatically nests anything that you create in the synchronous part of a callback, before the first `await`. Any span or log that you create after an `await` call begins a new root trace unless you define a parent trace. Because of this limitation, manual tracing in React Native requires more attention to span context than it does in other server-side SDKs.

<Warning>
  **Assign parent spans after asynchronous boundaries**

  Because React Native tracks the active span only synchronously, spans and logs that you create after an `await`, timer, or callback are not connected to their parent unless you pass the parent context yourself. We recommend using `LDObserve.withSpan`, which ends spans automatically and keeps the trace hierarchy intact across asynchronous boundaries.
</Warning>

### Use withSpan for nested, asynchronous work

We recommend `LDObserve.withSpan` for most tracing. `LDObserve.withSpan` starts a span, runs your callback within it, and ends it automatically. It sets the span status to `OK` on success, or to `ERROR` and records the error if the callback fails.

The callback receives a `SpanScope` object that solves the context propagation problem described above. A `SpanScope` provides the following members:

* `span`: the underlying OpenTelemetry span. Use it to set attributes and add events.
* `child(name, fn, options?)`: starts a child span nested in this scope. Because the parent comes from the captured scope rather than the active context, child spans nest correctly even after an `await`, and across concurrent work.
* `active(fn)`: runs `fn` with this span active. Use it to nest instrumented `fetch` and `XMLHttpRequest` spans that start after an `await`.
* `ctx`: this span's context, for cases where you need to pass an explicit parent elsewhere, such as with a `setTimeout` callback.

To trace a nested workflow using `withSpan`:

<CodeGroup>
  ```typescript title="Nested spans with withSpan" expandable lines wrap theme={null}
  const nestedSpans = async () => {
    // `withSpan` ends each span automatically, and the `child` method on each
    // SpanScope (`load`, `fetchScope`) nests in the captured context. The
    // LoadProducts > FetchFromApi > DeserializeJson / RenderUI hierarchy survives
    // the `await` calls without threading the context by hand. (React Native's
    // StackContextManager only tracks the active span synchronously.)
    const count = await LDObserve.withSpan('LoadProducts', async (load) => {
      const items = await load.child('FetchFromApi', async (fetchScope) => {
        const response = await fetch('https://api.example.com/products')
        fetchScope.span.setAttribute('http.status_code', response.status)
        const json = await response.text()
        // Nests under FetchFromApi even though we are past two awaits.
        return fetchScope.child('DeserializeJson', (parseScope) => {
          const result = JSON.parse(json) as unknown[]
          parseScope.span.setAttribute('product_count', result.length)
          return result
        })
      })
      // Nests under LoadProducts (not FetchFromApi) — uses the captured context.
      load.child('RenderUI', (renderScope) => {
        renderScope.span.setAttribute('product_count', items.length)
      })
      return items.length
    })
  }
  ```
</CodeGroup>

The result is a trace with `LoadProducts` at the root, with `FetchFromApi` and `RenderUI` as its children, and `DeserializeJson` nested under `FetchFromApi`. Because each child span nests under its own captured context, `withSpan` also keeps the nesting correct for concurrent work started with `Promise.all`.

### Start a root span

To create an independent span that begins a brand-new trace, pass `{ root: true }`. `startActiveSpan` makes the span active for the duration of the callback, but it does not end the span for you. Call `span.end()` when the work is done, otherwise the span is never exported:

<CodeGroup>
  ```typescript title="Start a root span" lines wrap theme={null}
  const rootSpan = () => {
    LDObserve.startActiveSpan(
      'app-cold-start',
      (span) => {
        span.setAttribute('launch_type', 'cold')
        span.setAttribute('device_os', Platform.OS)
        span.addEvent('splash_rendered')
        span.addEvent('home_screen_ready')
        span.end()
      },
      { root: true },
    )
  }
  ```
</CodeGroup>

To control the span's lifetime manually, for example when it ends in a different function, use `startSpan` and call `span.end()` yourself. Use `{ root: true }` if you want to create a span that starts a new trace regardless of any existing context.

### Trace network requests

The SDK automatically instruments `fetch` and `XMLHttpRequest`, unless you set the `disableTraces` option. Every network request generates its own span. If a custom span is active when the request runs, the automatically generated HTTP span becomes its child:

<CodeGroup>
  ```typescript title="Automatically instrumented child span" lines wrap theme={null}
  async function syncOrders() {
    await LDObserve.startActiveSpan('SyncOrders', async (span) => {
      span.setAttribute('sync.direction', 'pull')

      // The HTTP span for this fetch is auto-created as a child of "SyncOrders"
      const response = await fetch('https://api.example.com/orders?_limit=5')
      span.setAttribute('http.status_code', response.status)

      const orders = (await response.json()) as unknown[]
      span.setAttribute('order_count', orders.length)
      span.end()
    })
  }
  ```
</CodeGroup>

You do not need to create a span for the HTTP call itself because your business logic span provides the parent context. The only requirement is that the request occurs while your span is active, inside a `startActiveSpan` or `withSpan` callback. After an `await`, the active context is gone, so a `fetch` that started later would create a root HTTP span. To re-establish the context for such calls, use `scope.active`, as described in the [React Native tracing guide](https://github.com/launchdarkly/observability-sdk/blob/main/sdk/%40launchdarkly/observability-react-native/guides/tracing.md).

### Connect mobile traces

Distributed tracing links a span on a mobile device to the spans your backend produces for the same request. The SDK does this by injecting a W3C `traceparent` header into outgoing requests, but only for URLs that you configure using the `tracingOrigins` option. This prevents the SDK from leaking trace headers to third-party domains.

To configure tracing origins:

<CodeGroup>
  ```typescript title="Configure tracing origins" lines wrap theme={null}
  new Observability({
    serviceName: 'my-react-native-app',
    // Attach trace headers to requests where the URL matches any of these entries.
    tracingOrigins: ['api.example.com', /\.internal\.example\.com$/],
  })
  ```
</CodeGroup>

With `tracingOrigins` configured, any `fetch` or `XHR` request to a matching host carries a `traceparent` header, so the backend continues the same trace:

<CodeGroup>
  ```typescript title="Mobile-to-backend trace" lines wrap theme={null}
  const backendDistributedTrace = async () => {
    await LDObserve.withSpan('Checkout', async ({ span }) => {
      span.setAttribute('cart.id', 'cart-7')
      // The SDK adds the W3C `traceparent` HTTP header to this request automatically
      // because the host is a tracing origin, so a backend span joins this trace.
      const response = await fetch('https://api.example.com/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ cartId: 'cart-7' }),
      })
      span.setAttribute('http.status_code', response.status)
    })
  }
  ```
</CodeGroup>

The resulting trace links the mobile `Checkout` span, the automatically instrumented HTTP request span, and the backend spans for the same request into a single trace. To suppress propagation for sensitive endpoints, use the `urlBlocklist` option. To learn more, read the [React Native tracing guide](https://github.com/launchdarkly/observability-sdk/blob/main/sdk/%40launchdarkly/observability-react-native/guides/tracing.md).

### Use the OpenTelemetry tracer

The `LDObserve` methods cover most tracing needs, but the SDK also supports the standard OpenTelemetry API. Call `LDObserve.getTracer()` to get an `LDTracer` object. `LDTracer` is an OpenTelemetry `Tracer` with `startSpan` and `startActiveSpan`, plus the async-safe `withSpan` helper methods for React Native. Use it when you want to follow the official OpenTelemetry JavaScript documentation, or when you want to integrate a third-party library that expects an OpenTelemetry `Tracer`.

The returned tracer uses the same exporter and sampler as the rest of the SDK, so it is always safe to call. You can use the tracer before the SDK finishes initializing or even after you set the `disableTraces` option, because `getTracer()` returns a no-op tracer if tracing is disabled. The `disableTraces` option affects only the custom tracing APIs, not the SDK's automatic instrumentation.

Here is an example that uses the standard OpenTelemetry API:

<CodeGroup>
  ```typescript title="Use the standard OpenTelemetry tracer" expandable lines wrap theme={null}
  const withTracer = async () => {
    const tracer = LDObserve.getTracer()

    // Unlike `withSpan`, the standard OpenTelemetry API does not manage the span
    // lifecycle for you. Set the status yourself and call `span.end()` when done.
    await tracer.startActiveSpan('Checkout', async (span) => {
      span.setAttribute('cart.id', 'cart-7')
      try {
        const response = await fetch('https://api.example.com/checkout')
        span.setAttribute('http.status_code', response.status)
        span.setStatus({ code: SpanStatusCode.OK })
      } catch (err) {
        span.recordException(err as Error)
        span.setStatus({ code: SpanStatusCode.ERROR })
      } finally {
        span.end()
      }
    })
  }
  ```
</CodeGroup>

The `LDTracer` also provides `withSpan`, which works the same way as `LDObserve.withSpan`. Use `tracer.withSpan` when you want to use the OpenTelemetry API but still require async-safe span nesting in React Native:

<CodeGroup>
  ```typescript title="Use withSpan from the tracer" expandable lines wrap theme={null}
  const withTracerNested = async () => {
    const tracer = LDObserve.getTracer()

    // `tracer.withSpan` behaves exactly like `LDObserve.withSpan`. It ends each
    // span automatically, and `scope.child` keeps the span hierarchy intact across
    // `await` calls. The following code builds LoadProducts > FetchFromApi > DeserializeJson.
    const count = await tracer.withSpan('LoadProducts', async (load) => {
      const items = await load.child('FetchFromApi', async (fetchScope) => {
        const response = await fetch('https://api.example.com/products')
        fetchScope.span.setAttribute('http.status_code', response.status)
        const json = await response.text()
        return fetchScope.child('DeserializeJson', (parseScope) => {
          const result = JSON.parse(json) as unknown[]
          parseScope.span.setAttribute('product_count', result.length)
          return result
        })
      })
      return items.length
    })
  }
  ```
</CodeGroup>

## Product analytics events

The React Native observability plugin automatically records these product analytics events:

* **Taps**: A `click` span for each user tap, including details about the tapped element and screen location. To learn how to give a tapped element a stable identifier, read [Identify tapped elements](#identify-tapped-elements).
* **App launches**: An `app_launch` span for each native process launch, including the launch type and startup performance.
* **App lifecycle changes**: An `app_foreground` or `app_background` span as the app moves between the foreground and background states.
* **Screen views**: A `screen_view` span when the app shows a screen, including the screen name and optional details.
* **App reloads**: An `app_reload` span when a session continues across a JavaScript reload. To learn more, read [App reload events](#app-reload-events).
* **Custom metric events**: A `track` span for each event your code records with `track()`.

Taps, app launches, app lifecycle changes, and screen views come from the native observability layer that the plugin initializes, so they require no additional wiring. To learn more, read [Product analytics events](/docs/home/observability/product-analytics).

The plugin records custom events as `track` product analytics spans. The plugin records a `track` span in either of these cases:

* Your code calls `LDObserve.track(...)` directly.
* Your code calls the React Native SDK's `LDClient.track(...)` method. The plugin records the matching `track` span automatically.

Each `track` span carries:

* the event key
* an optional numeric value used by LaunchDarkly for custom numeric metrics
* any properties that you pass as additional span attributes.

Spans that the SDK creates from `LDClient.track(...)` also include information about the LaunchDarkly context that generated the event.

Use the generated span events to create custom product analytics charts, such as time series and funnels. To learn more, read [Product analytics events](/docs/home/observability/product-analytics).

### Record a custom event

To record a custom event as a `track` span, call `LDObserve.track`:

<CodeGroup>
  ```typescript title="Record a track event" lines wrap theme={null}
  // Track an event with properties and an optional metric value
  LDObserve.track(
    'purchase_completed',
    {
      product_id: 'SKU-123',
      price: 29.99,
    },
    29.99,
  )

  // Track an event with no properties
  LDObserve.track('button_tapped')
  ```
</CodeGroup>

The `track` method takes the following parameters:

* **key**: the key for the event. The plugin records it as the span's `key` attribute.
* **properties**: optional data associated with the event. The plugin records each property on the span. Property values can be strings, numbers, booleans, `null`, arrays, or nested objects.
* **metricValue**: an optional numeric value used for LaunchDarkly custom numeric metrics. The plugin records it as the span's `value` attribute.

The `properties` object can contain nested objects and arrays, not just flat key-value pairs. Use nested properties to group related data within a single event.

Here is an example:

<CodeGroup>
  ```typescript title="Record an event with nested properties" lines wrap theme={null}
  LDObserve.track('checkout_completed', {
    order_id: 'ORD-1024',
    total: 129.97,
    items: [
      { sku: 'SKU-123', quantity: 2 },
      { sku: 'SKU-456', quantity: 1 },
    ],
    shipping: {
      method: 'express',
      cost: 9.99,
    },
  })
  ```
</CodeGroup>

### Identify tapped elements

The plugin captures taps natively and records them as `click` events. To reliably identify an element in product analytics, regardless of layout, visible text, or A/B copy, give it a stable ID.

Tap capture requires the observability plugin (`@launchdarkly/observability-react-native`). Tagging a subtree with `<LDClick>` also requires the session replay plugin (`@launchdarkly/session-replay-react-native`).

For a single element such as a `Button`, set React Native's built-in `nativeID` prop directly:

<CodeGroup>
  ```tsx title="Tag a single element with nativeID" lines wrap theme={null}
  <Button title="Pay" nativeID="checkout.pay_button" onPress={pay} />
  ```
</CodeGroup>

React Native carries `nativeID` to the native view with no additional setup, and the plugin reads it as the `event.id` attribute on the `click` event.

To tag a whole subtree, an element that React Native might flatten away, or a component that does not forward `nativeID`, wrap it in `<LDClick>` and pass an `id`:

<CodeGroup>
  ```tsx title="Tag a subtree with LDClick" lines wrap theme={null}
  import { LDClick } from '@launchdarkly/session-replay-react-native';

  <LDClick id="checkout.pay_button">
    <Button title="Pay" onPress={pay} />
  </LDClick>;
  ```
</CodeGroup>

`<LDClick>` reports the `id` you provide as `event.id`, the same as `nativeID`. A tap on any descendant resolves to the ID of the nearest enclosing `<LDClick>`, so wrapping a composite control tags the entire control.

The click ID is a dedicated channel. Unlike `testID`, it is not overloaded with end-to-end testing, and session replay privacy masking never strips it. When an element has both a `testID` and a click ID, the click ID from `<LDClick>` or `nativeID` takes precedence for `event.id`.

### App reload events

The plugin automatically records an `app_reload` event when a user's session continues across a JavaScript reload, such as a soft reload during development, an over-the-air (OTA) bundle update, or a quick relaunch. Instead of starting a new session, the plugin resumes the existing session, so activity from before and after the reload stays connected as a single session.

The plugin persists the active session and resumes it on the next load when the app reloads within the session timeout window, which is 15 minutes by default. If more than the session timeout elapses before the next load, the plugin starts a new session instead. You can adjust the window with the `sessionTimeout` option. To learn more, read [Configuration for client-side observability](/docs/sdk/features/observability-config-client-side#react-native).

Session persistence requires the `@react-native-async-storage/async-storage` peer dependency. Without it, the plugin cannot preserve a session across a reload: each reload starts a new session and does not emit an `app_reload` event. To learn how to install AsyncStorage, read [Install the plugin](#install-the-plugin).

The `app_reload` span carries the following attributes:

* `event.elapsed_ms`: the time in milliseconds between the previous session's last recorded activity and the reload.
* `event.reload_count`: the number of times the current session has reloaded.

You do not need to record this event manually. To learn more about product analytics events, read [Product analytics events](/docs/home/observability/product-analytics).

## Configure session replay

<Note>
  **Session replay is in Early Access**

  Session replay for React Native is available in Early Access. APIs are subject to change until a 1.x version is released.
</Note>

Session replay captures screen recordings of user interactions to help you understand how users interact with your application. Session replay is delivered as a separate plugin, `@launchdarkly/session-replay-react-native`, that works alongside the observability plugin.

Session replay for React Native is supported on **iOS** and **Android**.

### Install the session replay plugin

Add the session replay package as a dependency alongside the observability plugin:

<CodeGroup>
  ```bash title="npm" lines wrap theme={null}
  npm install @launchdarkly/session-replay-react-native
  ```

  ```bash title="yarn" lines wrap theme={null}
  yarn add @launchdarkly/session-replay-react-native
  ```
</CodeGroup>

After installing, run the iOS pod install step so that CocoaPods pulls in the native `LaunchDarklyObservability` and `LaunchDarklySessionReplay` frameworks:

<CodeGroup>
  ```bash title="iOS" lines wrap theme={null}
  cd ios && pod install
  ```
</CodeGroup>

Then, import the plugin into your code:

<CodeGroup>
  ```js title="Import" lines wrap theme={null}
  import { createSessionReplayPlugin } from '@launchdarkly/session-replay-react-native';
  ```
</CodeGroup>

### Initialize session replay

To enable session replay, create the session replay plugin and add it to the `plugins` list passed to `ReactNativeLDClient`. You can use session replay on its own, or alongside the observability plugin.

<CodeGroup>
  ```js title="Initialize with session replay" expandable lines wrap theme={null}
  import {
    ReactNativeLDClient,
    AutoEnvAttributes,
  } from '@launchdarkly/react-native-client-sdk';
  import { Observability } from '@launchdarkly/observability-react-native';
  import { createSessionReplayPlugin } from '@launchdarkly/session-replay-react-native';

  const sessionReplay = createSessionReplayPlugin({
    isEnabled: true,
    maskTextInputs: true,
    maskWebViews: true,
    maskLabels: true,
    maskImages: true,
    maskTestIDs: ['password', 'ssn'],
  });

  const client = new ReactNativeLDClient(
    'example-mobile-key',
    AutoEnvAttributes.Enabled,
    {
      plugins: [
        new Observability({ serviceName: 'example-service' }),
        sessionReplay,
      ],
    }
  );
  ```
</CodeGroup>

### Initialize session replay manually

You can initialize the session replay plugin manually, after the SDK client is initialized. This approach is useful for feature-flagged rollouts, or for deferring data collection until after you have received end user consent.

Set `isEnabled` to `false` in the plugin options, then call `startSessionReplay()` when you are ready to begin recording. With `isEnabled` set to `false`, the plugin still initializes native session replay and observability. Signals such as tap events continue to flow, and only recording is off.

Deferred recording requires session replay plugin version 0.24.0 or later. In earlier versions, `startSessionReplay()` applies the configured `isEnabled` value again. As a result, a plugin created with `isEnabled: false` never records.

`startSessionReplay()` starts recording regardless of the configured `isEnabled` value, and you do not need to call `configureSessionReplay()` first. Recording continues until you call `stopSessionReplay()`. Calling `startSessionReplay()` after that resumes recording.

As an alternative to registering session replay as a plugin, you can control it imperatively. You use the lower-level `configureSessionReplay()` and `startSessionReplay()` functions without registering a plugin at all.

Here is an example of each approach:

<CodeGroup>
  ```js title="Deferred start with plugin" expandable lines wrap theme={null}
  import { createSessionReplayPlugin, startSessionReplay, stopSessionReplay } from '@launchdarkly/session-replay-react-native';

  const sessionReplay = createSessionReplayPlugin({
    isEnabled: false, // don't start recording automatically
    maskTextInputs: true,
  });

  const client = new ReactNativeLDClient(
    'example-mobile-key',
    AutoEnvAttributes.Enabled,
    { plugins: [sessionReplay] }
  );

  // Later, after user consent or a feature flag check:
  await startSessionReplay();

  // To stop recording:
  await stopSessionReplay();
  ```

  ```js title="Imperative API" lines wrap theme={null}
  import {
    configureSessionReplay,
    startSessionReplay,
    stopSessionReplay,
  } from '@launchdarkly/session-replay-react-native';

  await configureSessionReplay('example-mobile-key', {
    isEnabled: true,
    maskTextInputs: true,
  });
  await startSessionReplay();

  // Later:
  await stopSessionReplay();
  ```
</CodeGroup>

This approach lets you:

* Feature-flag the rollout of session replay to a subset of end users
* Wait for end user consent before starting data collection
* Dynamically enable session replay based on runtime conditions
* Maintain compliance with privacy regulations

When you start recording manually:

* **`sampleRate` still applies.** The SDK makes the sampling decision once per recording cycle. If sampling excludes a session, calling `startSessionReplay()` again does not record it. Calling `stopSessionReplay()` resets the decision.
* **Recording continues after a JavaScript reload.** The native session replay instance lives in the host app process. An over-the-air update or other JavaScript reload that runs your plugin setup again does not stop a recording that an earlier `startSessionReplay()` call began. To turn recording off, call `stopSessionReplay()`.

### Configure session replay privacy options

The session replay plugin provides several privacy controls that decide whether each view is captured. Pass them to `createSessionReplayPlugin` or `configureSessionReplay`.

#### How the SDK decides what to mask

For each view, the SDK evaluates the following rules in order and stops at the first that applies:

1. **Explicit masking (highest priority)**: The view, or any of its ancestors, is wrapped in `<LDMask>` or has a `testID` matched by `maskTestIDs`. The view is **masked**.
2. **Explicit unmasking**: The view, or any of its ancestors, is wrapped in `<LDUnmask>` or has a `testID` matched by `unmaskTestIDs`. The view is **unmasked**.
3. **Global configuration**: The global privacy options (`maskTextInputs`, `maskLabels`, `maskImages`, `maskWebViews`) apply.

If two rules conflict at the same level, masking takes precedence over unmasking. An ancestor `<LDMask>` overrides any `<LDUnmask>` further down the tree.

#### Global toggles by component type

Each global toggle affects every instance of the corresponding React Native component across your app, on both iOS and Android.

<CodeGroup>
  ```js title="Global privacy toggles" lines wrap theme={null}
  const sessionReplay = createSessionReplayPlugin({
    isEnabled: true,
    maskTextInputs: true, // default — masks every <TextInput>
    maskLabels: false,    // when true, masks every <Text>
    maskImages: false,    // when true, masks every <Image>
    maskWebViews: false,  // when true, masks every <WebView>
  });
  ```
</CodeGroup>

#### Mask or unmask views by `testID`

Use `maskTestIDs` and `unmaskTestIDs` to target specific views by their `testID` property. Matches use exact string equality, so `'password'` matches `<View testID="password" />` but not `<View testID="password_field" />`. Both options work on iOS and Android.

<CodeGroup>
  ```js title="Mask or unmask by testID" lines wrap theme={null}
  const sessionReplay = createSessionReplayPlugin({
    maskTestIDs: ['password', 'ssn'],
    unmaskTestIDs: ['greeting'],
  });
  ```
</CodeGroup>

#### Mask or unmask a subtree with `<LDMask>` and `<LDUnmask>`

Use the `<LDMask>` and `<LDUnmask>` wrapper components to redact a subtree without giving it a `testID`. `<LDMask>` propagates to all descendants. After you wrap a subtree in `<LDMask>`, nothing inside it can opt out of masking.

<CodeGroup>
  ```jsx title="Wrapper components" lines wrap theme={null}
  import { LDMask, LDUnmask } from '@launchdarkly/session-replay-react-native';

  <LDMask>
    <Text>account balance: $1,234</Text>
  </LDMask>;

  <LDUnmask>
    <Text>display even when maskLabels is on</Text>
  </LDUnmask>;
  ```
</CodeGroup>

#### Session replay configuration options

The `SessionReplayOptions` object supports the following parameters:

* **isEnabled**: Controls whether recording starts as soon as session replay initializes. Defaults to `true`. When `false`, the plugin still initializes native session replay and observability. Call `startSessionReplay()` to begin recording later. To learn more, read [Initialize session replay manually](#initialize-session-replay-manually).
* **serviceName**: The service name used for session replay telemetry. Defaults to `"sessionreplay-react-native"`.
* **maskTextInputs**: Masks all `<TextInput>` components. Defaults to `true`.
* **maskWebViews**: Masks the contents of `<WebView>` components. When enabled, web views are rendered as blank rectangles in session replays. Defaults to `false`.
* **maskLabels**: Masks all `<Text>` components. Defaults to `false`.
* **maskImages**: Masks all `<Image>` components. Defaults to `false`.
* **maskTestIDs**: Masks an array of `testID` values. Matches use exact string equality. Applied on iOS and Android.
* **unmaskTestIDs**: Excludes an array of `testID` values from masking. Matches use exact string equality. Applied on iOS and Android.
* **minimumAlpha**: Minimum alpha value for view visibility in recordings. Views with alpha below this threshold are not captured. Defaults to `0.02`. iOS only.
* **sampleRate**: The probability, from `0.0` to `1.0`, that recording starts when it is turned on, either by `isEnabled` or by `startSessionReplay()`. `0.0` never records, and `1.0` always records. The SDK makes the decision once per recording cycle and resets it when you call `stopSessionReplay()`. Defaults to `1.0`.
* **frameRate**: The target capture rate, in frames per second. Defaults to `1.0`.
* **scale**: The resolution multiplier for captured and exported frames. `1.0` is 1x (160 dots per inch), and `2.0` is 2x. The SDK treats values of zero or less as `1.0`. Defaults to `1.0`.
* **imageQuality**: The JPEG encoding quality of exported frames, from `0.0` (lowest quality, smallest payload) to `1.0` (highest quality, largest payload). Defaults to `0.3`.

For more information on session replay configuration, read [Configuration for session replay](/docs/sdk/features/session-replay-config).

## Explore supported features

The observability plugin supports the following features. After the SDK and plugins are initialized, you can access these from within your application:

* [Configuration for client-side observability](/docs/sdk/features/observability-config-client-side)
* [Configuration for session replay](/docs/sdk/features/session-replay-config#react-native)
* [Errors](/docs/sdk/features/observability-errors#react-native)
* [Logs](/docs/sdk/features/observability-logs#react-native)
* [Metrics](/docs/sdk/features/observability-metrics#react-native)
* [Symbolication](/docs/sdk/features/observability-symbolication#react-native)
* [Tracing](/docs/sdk/features/observability-traces#react-native)

## Review observability data in LaunchDarkly

After you initialize the SDK and observability plugin, your application automatically starts sending observability data back to LaunchDarkly, including errors and logs. You can review this information in the LaunchDarkly user interface. To learn how, read [Observability](/docs/home/observability).
