React Native RUM SDK
Install and configure the Last9 React Native RUM SDK. TypeScript API, CDN-hosted npm tarball, TurboModule over native Android and iOS SDKs. Auto-instruments fetch/XHR, React Navigation, errors, ANRs. New Architecture (bridgeless) supported.
Real User Monitoring for React Native apps. Automatic instrumentation for sessions, views, network requests (fetch/XHR), errors, and resource metrics via OpenTelemetry. ANR detection is available on Android only.
The React Native SDK wraps the Android and iOS native SDKs. Each platform’s CDN repo must be configured so the native dependencies resolve.
Prerequisites
- React Native >= 0.74 (New Architecture / bridgeless is the default from 0.76+; supported from SDK
1.1.6) - iOS 15.1+
- Android minSdk 21 (Android 5.0+)
- Native dependencies:
- Android:
io.last9:rum-android:1.1.10(resolved from CDN Maven — the consumer app must declare the repo; see Installation) - iOS:
Last9RUM 1.1.10(resolved from CDN podspec)
- Android:
Create Client Monitoring Tokens
Create one Client token per platform — do not combine Android and iOS origins on a single token. Each build uses the native SDK for that platform, so separate tokens keep origin scoping, rotation, and access control aligned with Android and iOS setup.
- Open Last9 → Settings → Ingestion Tokens
- Click Create Token → choose type Client → create an Android token with allowed origin
android://com.yourcompany.yourapp(your app’s exact package name). Copy the token. - Create a second Client token for iOS with allowed origin
ios://com.yourcompany.yourapp(your app’s exact bundle ID). Copy the token. - Copy the OTLP endpoint URL (the same URL is used for both platforms)
CDN artifacts
| Artifact | Stable URL | Versioned URL |
|---|---|---|
| Tarball | https://cdn.last9.io/rum-sdk/react-native/builds/stable/v1/last9-rum-react-native.tgz | https://cdn.last9.io/rum-sdk/react-native/builds/1.1.10/last9-rum-react-native-1.1.10.tgz |
| Checksum | https://cdn.last9.io/rum-sdk/react-native/builds/stable/v1/last9-rum-react-native.tgz.sha256 | https://cdn.last9.io/rum-sdk/react-native/builds/1.1.10/last9-rum-react-native-1.1.10.tgz.sha256 |
The major-pinned stable/v1 channel currently serves React Native RUM SDK
1.1.10. The latest versioned release is 1.1.10; staging builds use the
-alpha.<run_number> suffix and explicit versioned URLs.
Installing or upgrading to 1.1.10
The stable/v1 tarball currently serves 1.1.10. To pin an explicit version in
your lockfile, use the versioned 1.1.10 URL. npm and yarn pin URL dependencies in the
lockfile with an integrity hash from the first download, so using an explicit
version also avoids stable-channel cache and EINTEGRITY surprises:
# npmnpm uninstall @last9/rum-react-nativenpm cache clean --forcenpm install https://cdn.last9.io/rum-sdk/react-native/builds/1.1.10/last9-rum-react-native-1.1.10.tgz
# yarn: remove the @last9/rum-react-native entry from yarn.lock, thenyarn cache cleanyarn add https://cdn.last9.io/rum-sdk/react-native/builds/1.1.10/last9-rum-react-native-1.1.10.tgzTo check which version the stable channel currently serves:
curl -sL https://cdn.last9.io/rum-sdk/react-native/builds/stable/v1/last9-rum-react-native.tgz | tar -xzO package/package.json | grep '"version"'Installation
-
Install the package
npm install https://cdn.last9.io/rum-sdk/react-native/builds/1.1.10/last9-rum-react-native-1.1.10.tgz -
Android — add the CDN Maven repository
Gradle repositories are not transitive — the React Native package cannot inject this repo into your app. Your consumer app must declare it so
io.last9:rum-androidresolves on the app’s classpath.In
android/settings.gradle(orandroid/build.gradle):dependencyResolutionManagement {repositories {google()mavenCentral()maven { url uri("https://cdn.last9.io/rum-sdk/android/maven/") }}} -
iOS — add the Last9RUM podspec
In
ios/Podfile:pod 'Last9RUM', :podspec => 'https://cdn.last9.io/rum-sdk/ios/builds/1.1.10/Last9RUM.podspec'Then install pods:
cd ios && pod install -
Initialize the SDK
At app entry (before any screens render):
import { Platform } from "react-native";import { L9Rum } from "@last9/rum-react-native";const isIos = Platform.OS === "ios";try {const { sessionId } = await L9Rum.initialize({baseUrl: "https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>",// Use the platform-matching token and origin pair — see// "Create Client Monitoring Tokens" above.origin: isIos? "ios://com.yourcompany.yourapp": "android://com.yourcompany.yourapp",clientToken: isIos? "your-ios-client-token": "your-android-client-token",serviceName: "my-rn-app",serviceVersion: "1.0.0",deploymentEnvironment: "production",});// Prefer awaiting initialize (or L9Rum.isActive()) over treating a null// getSessionId() after a fire-and-forget call as the only failure signal.console.log("RUM ready", sessionId);} catch (error) {console.error("RUM failed to initialize", error);}
There is no postinstall script or codegen stub to run after npm install — the package ships a real React Native Codegen spec (RNL9RumSpec) and Codegen runs as part of the normal native build.
New Architecture (bridgeless)
From SDK 1.1.6, the native module is a real TurboModule (RNL9RumSpec). JS↔native calls dispatch over JSI directly, so promises settle reliably on the New Architecture (bridgeless mode, the default in React Native 0.76+).
| Architecture | Support |
|---|---|
| New Architecture (bridgeless) | Full support from 1.1.6. Use await L9Rum.initialize(...) as shown above. |
| Old Architecture (Paper) | Still supported. TurboModuleRegistry.getEnforcing falls back to the legacy native module path when bridgeless is off. |
The public JS API (L9Rum, config types) is unchanged — no app-code changes are required beyond upgrading the package and rebuilding native projects (pod install, Gradle sync).
Configuration
L9Rum.initialize({ // --- Required ---------------------------------------------------------
// OTLP collector endpoint baseUrl: "https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>",
// Authentication token from Last9 — use the Android or iOS Client token // for the platform this build is running on. clientToken: Platform.OS === "ios" ? "your-ios-client-token" : "your-android-client-token",
// Application identifier (maps to service.name) serviceName: "my-rn-app",
// App version string (maps to service.version) serviceVersion: "1.0.0",
// Environment name deploymentEnvironment: "production",
// --- Optional ---------------------------------------------------------
// Origin sent as X-LAST9-ORIGIN header. Required for client_monitoring // tokens. Must match the origin on the platform's Client token — // android://<package> on Android, ios://<bundle-id> on iOS. Pass the // matching clientToken for the same platform. Needs `import { Platform } // from "react-native"` as shown in the Installation section above. origin: Platform.OS === "ios" ? "ios://com.yourcompany.yourapp" : "android://com.yourcompany.yourapp",
// Specific build identifier (maps to app.build_id) appBuildId: "1.0.0-build-42",
// Optional override for the app.installation.id resource attribute. // The Client-ID header always uses the native SDK-generated per-install UUID. appInstallationId: undefined,
// Session sampling rate: 0-100 (percentage). 100 = sample everything. sampleRate: 100,
// Print debug logs to console debugLogs: false,
// Automatically instrument network requests through native hooks networkInstrumentation: true,
// Automatically capture unhandled JS errors errorInstrumentation: true,
// Max spans per export batch maxExportBatchSize: 100,
// Export timeout in milliseconds exportTimeoutMs: 30000,
// ANR detection (Android only) anrDetectionEnabled: true, anrThresholdMs: 5000,
// Periodically sample memory and CPU resourceMonitoringEnabled: true, resourceSamplingIntervalMs: 30000,
// Setting this to true will hide network requests (and their // DNS/TCP/TLS/TTFB phase child spans) from the Last9 dashboard's // Sessions → APIs tab. Each request would get its own traceId // instead of sharing the current view's traceId, and that tab // only fetches spans that share the View's traceId. Keep this // false unless you specifically need per-request trace isolation. isolateTracePerRequest: false,
// Fine-grained network ignore rules. Strings use substring matching; // RegExp uses regex search semantics. Matched URLs are dropped before // span creation. ignorePatterns: { fullUrl: ["https://cdn.example.com", /^https:\/\/.*\.example\.com/i], pathname: [".pdf", ".jpg", /^\/internal\/metrics/], hostname: ["cdn.example.com", /(^|\.)assets\.example\.com$/i], },
// 'preserve' (default): keep traceparent on ignored requests. // 'strip': remove traceparent from ignored requests. propagationMode: "preserve",
// Custom resource attributes added to every span resourceAttributes: { "app.platform": "react-native", },
// W3C Baggage propagation on outgoing requests baggage: { enabled: false, allowedKeys: ["session.id", "user.id"], maxTotalBytes: 8192, warnAtPercentage: 80, },});Stable release notes
React Native 1.1.10 fixes a New Architecture Android clean-build failure. Rebuild native projects after upgrading (cd ios && pod install, then a clean Android build).
1.1.9 bumps native Android and iOS SDK references to 1.1.9. On iOS this fixes await L9Rum.initialize(config) resolving with a null or empty sessionId even on success — native session start is now synchronous and never surfaces an internal placeholder id, so initialize() resolves with a real session id or rejects. getSessionId() is fixed by the same change on both platforms and never returns an empty string. Earlier: 1.1.7 picked up the Android AGP consumer compatibility fix (no forced androidx.core 1.17.0), and from 1.1.6 the bridge is a real TurboModule so initialize() resolves on the New Architecture (bridgeless). The public JS API is unchanged.
appInstallationId only overrides the app.installation.id resource attribute. It does not override the Client-ID header, so the header and the resource attribute can differ when you set appInstallationId manually.
The SDK also normalizes non-Error JavaScript throws and promise rejections, including strings, primitives, plain objects, and bridged native fallback errors, so exception.type stays populated.
When nativeNetworkInterception is enabled, RN Android apps use the native OkHttp interceptor. As of native 1.1.1, telemetry attribute collection is isolated from the request path and telephony reads are permission-gated and guarded, so a missing (or OEM-restricted) READ_PHONE_STATE permission can no longer turn a successful HTTP response into a failed request. iOS is not affected.
Network ignore patterns
Skip noisy URLs before span creation by matching against full URL, pathname, or hostname. Strings use substring matching; RegExp uses regex search semantics.
L9Rum.initialize({ ignorePatterns: { fullUrl: ["https://cdn.example.com", /^https:\/\/.*\.example\.com/i], pathname: [".pdf", ".jpg", /^\/internal\/metrics/], hostname: ["cdn.example.com", /(^|\.)assets\.example\.com$/i], }, // 'preserve' (default): keep traceparent on ignored requests. // 'strip': remove traceparent from ignored requests. propagationMode: "strip",});Network phase child spans
Network instrumentation is native by default so the SDK emits child spans for individual HTTP phases:
| Child span | What it measures |
|---|---|
dns | DNS lookup duration |
tcp_connect | TCP connection establishment |
tls_handshake | TLS negotiation |
ttfb | Time from request sent to first response byte |
No SDK config change is required. Android wires the OkHttp client factory with the Last9 interceptor and EventListener.Factory. iOS uses native URLProtocol/URLSession instrumentation and reads URLSession task metrics.
Reused connections skip DNS, TCP, and TLS work. For those requests the SDK emits zero-duration child spans with l9rum.network.phase.skipped=true so the waterfall shape stays consistent.
GraphQL network observability
GraphQL enrichment is on by default (Layer 1). GraphQL requests get a descriptive span name and operation metadata, and server-side errors returned with an HTTP 200 are flagged:
| Span attribute | Example | Notes |
|---|---|---|
| Span name | GraphQL: "GetUserPreferences" query | Renamed to a descriptive name |
graphql.operation.name | GetUserPreferences | query, mutation, or subscription name |
graphql.operation.type | query | Operation type |
error.type | GraphQLError | Set when the response contains a non-empty errors[] |
graphql.error.count | number | Number of entries in the errors[] array |
With native interception enabled, enrichment comes from the Android/iOS interceptors. When nativeNetworkInterception: false, the JS fetch/XHR interceptor captures request/response bodies (capped) and forwards them through the bridge so the same enrichment applies.
Layer 2 — Apollo Link (opt-in)
For richer GraphQL telemetry with Apollo Client, use the l9GqlLink Apollo Link. It owns the GraphQL span and suppresses the SDK’s JS transport interceptor for that request, so pair it with nativeNetworkInterception: false. It requires the optional @apollo/client peer dependency.
import { ApolloClient, InMemoryCache, HttpLink, from } from "@apollo/client";import { l9GqlLink } from "@last9/rum-react-native";
const client = new ApolloClient({ cache: new InMemoryCache(), link: from([ l9GqlLink({ captureVariables: true, // allow-listed + redacted captureErrorMessages: true, // truncated }), new HttpLink({ uri: "https://api.example.com/graphql" }), ]),});captureVariables— opt in to capture operation variables. Values are allow-listed and redacted.captureErrorMessages— opt in to capture GraphQL error messages. Values are truncated.
API reference
React Navigation integration
For automatic view tracking with React Navigation:
import { L9ReactNavigationInstrumentation } from '@last9/rum-react-native';import { NavigationContainer } from '@react-navigation/native';
function App() { return ( <NavigationContainer onStateChange={L9ReactNavigationInstrumentation.onStateChange} > {/* screens */} </NavigationContainer> );}Identify a user
L9Rum.identify({ id: "user-123", name: "Jane", email: "jane@example.com", fullName: "Jane Doe", roles: ["admin"],});Clear user on sign-out
L9Rum.clearUser();Capture errors
try { // risky operation} catch (error) { L9Rum.captureError(error, { screen: "checkout" });}Track views manually
L9Rum.startView("ProductDetailsScreen");L9Rum.setViewName("Product #42");Custom events
L9Rum.addEvent("purchase_completed", { product_id: "12345", amount: 29.99,});Each call dual-emits (behavior inherited from the native Android and iOS SDKs):
- A span event on the active view span, so the event shows up on the view’s timeline in RUM.
- An OTLP log record carrying
event.type=custom,event.name, your attributes, andsession.id/user attributes. When a view is active the log is correlated to it viatrace.id/span.id/view.id, so you can pivot between the log and the RUM session.
Log emission is unconditional. If no view is active when you call addEvent, the span event is dropped (no view is fabricated to attach it to) but the log is still sent as an orphan, omitting trace.id/span.id/view.id. A custom event never silently vanishes because it fired outside a view.
Global span attributes
L9Rum.spanAttributes({ experiment: "checkout_v2", feature_flag: "new_cart",});
// ClearL9Rum.spanAttributes(null);Session ID / init status
const { sessionId: readySessionId } = await L9Rum.initialize(config);const sessionId = await L9Rum.getSessionId(); // same id once activeconst running = await L9Rum.isActive(); // true between successful initialize and shutdowninitialize rejects when native init fails (missing config or the SDK does not become active). Use that rejection — or isActive() — rather than assuming a null getSessionId() after a void/fire-and-forget call.
From 1.1.9, when initialize() resolves it always returns a real sessionId — never null or an empty string. Earlier iOS builds could resolve successfully but hand back an empty or missing session id because the native session started asynchronously; native session start is now synchronous and the internal placeholder id is never surfaced. getSessionId() never returns an empty string on either platform.
Flush pending data
L9Rum.flush();Embedded per-flow lifecycle
For embedded integrations scoped to a single flow, use shutdown() and isActive() (both bridged to the native SDKs) to control the SDK lifecycle:
// Scope RUM to a single flow, then tear it down so the next flow starts clean.await L9Rum.initialize(config);
// Attributes known only after the flow starts apply to every later span.L9Rum.spanAttributes({ "tenant.id": "acme", "feature.flag": "beta" });
if (await L9Rum.isActive()) { await L9Rum.shutdown(); // flush + full teardown; a later initialize() re-arms RUM}shutdown() returns a Promise<void> that resolves once native teardown completes, so a per-flow caller can await L9Rum.shutdown() before re-initializing the next flow instead of racing teardown. A later initialize() starts a fresh flow.
WebView correlation
Instrument a react-native-webview WebView to share the native session ID with Browser RUM spans running inside it:
import { useRef } from 'react';import { WebView } from 'react-native-webview';import { L9Rum } from '@last9/rum-react-native';
const webViewRef = useRef<WebView>(null);
<WebView ref={webViewRef} source={{ uri: 'https://app.example.com' }} onLoad={() => { if (webViewRef.current) { L9Rum.instrumentWebView(webViewRef); } }}/>The SDK resolves the underlying native WKWebView (iOS) or android.webkit.WebView (Android) and re-injects session context on every navigation automatically.
See the WebView Session Correlation guide for auto-load options and verification steps.
Next steps
Once data is flowing, explore it in Discover > Applications — performance, errors, and sessions.
Troubleshooting
await L9Rum.initialize() never resolves (hangs)
Symptom: The app starts but RUM never becomes active — initialize() never settles and isActive() stays false.
Cause: On the New Architecture (bridgeless), SDK versions before 1.1.6 routed through React Native’s legacy interop layer, where native resolve() did not deliver to the JS promise.
Fix: Upgrade to @last9/rum-react-native 1.1.6 or later (latest: 1.1.10 on stable/v1), rebuild native projects (cd ios && pod install, then a clean Android build), and confirm with await L9Rum.initialize(...).
Android :app:configureCMakeDebug fails on clean build (New Architecture)
Symptom: Clean or parallel Android builds (or Android Studio Gradle sync) fail with add_subdirectory given source ".../codegen/jni/" which is not an existing directory and react_codegen_RNL9RumSpec which is not built by this project.
Cause: Introduced in React Native SDK 1.1.6 (TurboModule/Codegen migration). The app’s CMake configure can run before the library’s Gradle codegen task produces the JNI directory.
Fix: Upgrade to @last9/rum-react-native 1.1.10 or later, reinstall from stable/v1 or the versioned 1.1.10 tarball, then run a clean Android build.
Android build cannot resolve io.last9:rum-android
Gradle repositories are not transitive. Add the Last9 CDN Maven repo to your app’s settings.gradle / build.gradle as shown in Installation — not only in a library module.
iOS pod install fails to find Last9RUM
Add the CDN podspec URL to your Podfile as shown in Installation. The pod is not published to the public CocoaPods trunk.