React Native SDK observability reference

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.

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.

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:

ResourceLocation
SDK API documentationObservability plugin API docs
GitHub repository (observability)@launchdarkly/observability-react-native
Published module (observability)npm
GitHub repository (session replay)@launchdarkly/session-replay-react-native
Published module (session replay)npm

Prerequisites and dependencies

This reference guide assumes you are familiar with the LaunchDarkly React Native SDK.

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

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:

FrameworkSupported versions
React Native0.74, 0.76, 0.79, 0.81, 0.83
Expo51, 52, 53, 54

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.

Get started

Follow these steps to get started:

Install the plugin

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

The first step is to make both the SDK and the observability plugin available as dependencies.

Here’s how:

$ npm install @launchdarkly/react-native-client-sdk
$ npm install @launchdarkly/observability-react-native

Then, import the plugin into your code:

Import, React Native SDK v10.10+
1import { ReactNativeLDClient } from '@launchdarkly/react-native-client-sdk';
2import { Observability, LDObserve } from '@launchdarkly/observability-react-native';

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 in the React Native SDK reference guide.

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.

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.

Here’s how to initialize the SDK and plugin:

Initialize, React Native SDK v10.10+
1const client = new ReactNativeLDClient(
2 'example-mobile-key',
3 AutoEnvAttributes.Enabled,
4 {
5 plugins: [
6 new Observability()
7 ],
8 }
9);

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:

Plugin options, React Native SDK v10.10+
1const client = new ReactNativeLDClient(
2 'example-mobile-key',
3 AutoEnvAttributes.Enabled,
4 {
5 plugins: [
6 new Observability({
7 serviceName: 'example-service',
8 // we recommend setting service_version to the latest deployed git SHA
9 serviceVersion: 'example-sha'
10 })
11 ],
12 }
13);

For more information on plugin options, read Configuration for client-side observability.

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.

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

Imports
1import { Platform } from 'react-native'
2import { LDObserve } from '@launchdarkly/observability-react-native'
3import { context, propagation, SpanStatusCode, trace } from '@opentelemetry/api'

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.

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.

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:

Nested spans with withSpan
1const nestedSpans = async () => {
2 // `withSpan` ends each span automatically, and the `child` method on each
3 // SpanScope (`load`, `fetchScope`) nests in the captured context. The
4 // LoadProducts > FetchFromApi > DeserializeJson / RenderUI hierarchy survives
5 // the `await` calls without threading the context by hand. (React Native's
6 // StackContextManager only tracks the active span synchronously.)
7 const count = await LDObserve.withSpan('LoadProducts', async (load) => {
8 const items = await load.child('FetchFromApi', async (fetchScope) => {
9 const response = await fetch('https://api.example.com/products')
10 fetchScope.span.setAttribute('http.status_code', response.status)
11 const json = await response.text()
12 // Nests under FetchFromApi even though we are past two awaits.
13 return fetchScope.child('DeserializeJson', (parseScope) => {
14 const result = JSON.parse(json) as unknown[]
15 parseScope.span.setAttribute('product_count', result.length)
16 return result
17 })
18 })
19 // Nests under LoadProducts (not FetchFromApi) — uses the captured context.
20 load.child('RenderUI', (renderScope) => {
21 renderScope.span.setAttribute('product_count', items.length)
22 })
23 return items.length
24 })
25}

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:

Start a root span
1const rootSpan = () => {
2 LDObserve.startActiveSpan(
3 'app-cold-start',
4 (span) => {
5 span.setAttribute('launch_type', 'cold')
6 span.setAttribute('device_os', Platform.OS)
7 span.addEvent('splash_rendered')
8 span.addEvent('home_screen_ready')
9 span.end()
10 },
11 { root: true },
12 )
13}

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:

Automatically instrumented child span
1async function syncOrders() {
2 await LDObserve.startActiveSpan('SyncOrders', async (span) => {
3 span.setAttribute('sync.direction', 'pull')
4
5 // The HTTP span for this fetch is auto-created as a child of "SyncOrders"
6 const response = await fetch('https://api.example.com/orders?_limit=5')
7 span.setAttribute('http.status_code', response.status)
8
9 const orders = (await response.json()) as unknown[]
10 span.setAttribute('order_count', orders.length)
11 span.end()
12 })
13}

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.

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:

Configure tracing origins
1new Observability({
2 serviceName: 'my-react-native-app',
3 // Attach trace headers to requests where the URL matches any of these entries.
4 tracingOrigins: ['api.example.com', /\.internal\.example\.com$/],
5})

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

Mobile-to-backend trace
1const backendDistributedTrace = async () => {
2 await LDObserve.withSpan('Checkout', async ({ span }) => {
3 span.setAttribute('cart.id', 'cart-7')
4 // The SDK adds the W3C `traceparent` HTTP header to this request automatically
5 // because the host is a tracing origin, so a backend span joins this trace.
6 const response = await fetch('https://api.example.com/checkout', {
7 method: 'POST',
8 headers: { 'Content-Type': 'application/json' },
9 body: JSON.stringify({ cartId: 'cart-7' }),
10 })
11 span.setAttribute('http.status_code', response.status)
12 })
13}

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.

Product analytics events

The React Native observability 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.

Record a custom event

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

Record a track event
1// Track an event with properties and an optional metric value
2LDObserve.track(
3 'purchase_completed',
4 {
5 product_id: 'SKU-123',
6 price: 29.99,
7 },
8 29.99,
9)
10
11// Track an event with no properties
12LDObserve.track('button_tapped')

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 attaches each property as a span attribute.
  • metricValue: an optional numeric value used for LaunchDarkly custom numeric metrics. The plugin records it as the span’s value attribute.

Configure session replay

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.

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:

$npm install @launchdarkly/session-replay-react-native

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

iOS
$cd ios && pod install

Then, import the plugin into your code:

Import
1import { createSessionReplayPlugin } from '@launchdarkly/session-replay-react-native';

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.

Initialize with session replay
1import {
2 ReactNativeLDClient,
3 AutoEnvAttributes,
4} from '@launchdarkly/react-native-client-sdk';
5import { Observability } from '@launchdarkly/observability-react-native';
6import { createSessionReplayPlugin } from '@launchdarkly/session-replay-react-native';
7
8const sessionReplay = createSessionReplayPlugin({
9 isEnabled: true,
10 maskTextInputs: true,
11 maskWebViews: true,
12 maskLabels: true,
13 maskImages: true,
14 maskTestIDs: ['password', 'ssn'],
15});
16
17const client = new ReactNativeLDClient(
18 'example-mobile-key',
19 AutoEnvAttributes.Enabled,
20 {
21 plugins: [
22 new Observability({ serviceName: 'example-service' }),
23 sessionReplay,
24 ],
25 }
26);

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.

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.

1import { createSessionReplayPlugin, startSessionReplay, stopSessionReplay } from '@launchdarkly/session-replay-react-native';
2
3const sessionReplay = createSessionReplayPlugin({
4 isEnabled: false, // don't start recording automatically
5 maskTextInputs: true,
6});
7
8const client = new ReactNativeLDClient(
9 'example-mobile-key',
10 AutoEnvAttributes.Enabled,
11 { plugins: [sessionReplay] }
12);
13
14// Later, after user consent or a feature flag check:
15await startSessionReplay();
16
17// To stop recording:
18await stopSessionReplay();

This approach allows you to:

  • 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

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.

Global privacy toggles
1const sessionReplay = createSessionReplayPlugin({
2 isEnabled: true,
3 maskTextInputs: true, // default — masks every <TextInput>
4 maskLabels: false, // when true, masks every <Text>
5 maskImages: false, // when true, masks every <Image>
6 maskWebViews: false, // when true, masks every <WebView>
7});

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.

Mask or unmask by testID
1const sessionReplay = createSessionReplayPlugin({
2 maskTestIDs: ['password', 'ssn'],
3 unmaskTestIDs: ['greeting'],
4});

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.

Wrapper components
1import { LDMask, LDUnmask } from '@launchdarkly/session-replay-react-native';
2
3<LDMask>
4 <Text>account balance: $1,234</Text>
5</LDMask>;
6
7<LDUnmask>
8 <Text>display even when maskLabels is on</Text>
9</LDUnmask>;

Session replay configuration options

The SessionReplayOptions object supports the following parameters:

  • isEnabled: Controls whether recording starts automatically when the plugin registers. Defaults to true. Set to false to start recording manually with startSessionReplay().
  • 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.

For more information on session replay configuration, read Configuration for session replay.

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:

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.