Skip to content
Last9
Book demo

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.8.0 (resolved from CDN Maven — the consumer app must declare the repo; see Installation)
    • iOS: Last9RUM 1.8.0 (resolved from CDN podspec)

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.

  1. Open Last9 → Settings → Ingestion Tokens
  2. 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.
  3. Create a second Client token for iOS with allowed origin ios://com.yourcompany.yourapp (your app’s exact bundle ID). Copy the token.
  4. Copy the OTLP endpoint URL (the same URL is used for both platforms)

CDN artifacts

ArtifactStable URLVersioned URL
Tarballhttps://cdn.last9.io/rum-sdk/react-native/builds/stable/v1/last9-rum-react-native.tgzhttps://cdn.last9.io/rum-sdk/react-native/builds/1.8.0/last9-rum-react-native-1.8.0.tgz
Checksumhttps://cdn.last9.io/rum-sdk/react-native/builds/stable/v1/last9-rum-react-native.tgz.sha256https://cdn.last9.io/rum-sdk/react-native/builds/1.8.0/last9-rum-react-native-1.8.0.tgz.sha256

The major-pinned stable/v1 channel currently serves React Native RUM SDK 1.8.0. The latest versioned release is 1.8.0; staging builds use the -alpha.<run_number> suffix and explicit versioned URLs.

Installing or upgrading to 1.8.0

The stable/v1 tarball currently serves 1.8.0. To pin an explicit version in your lockfile, use the versioned 1.8.0 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:

# npm
npm uninstall @last9/rum-react-native
npm cache clean --force
npm install https://cdn.last9.io/rum-sdk/react-native/builds/1.8.0/last9-rum-react-native-1.8.0.tgz
# yarn: remove the @last9/rum-react-native entry from yarn.lock, then
yarn cache clean
yarn add https://cdn.last9.io/rum-sdk/react-native/builds/1.8.0/last9-rum-react-native-1.8.0.tgz

To 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

  1. Install the package

    npm install https://cdn.last9.io/rum-sdk/react-native/builds/1.8.0/last9-rum-react-native-1.8.0.tgz
  2. 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-android resolves on the app’s classpath.

    In android/settings.gradle (or android/build.gradle):

    dependencyResolutionManagement {
    repositories {
    google()
    mavenCentral()
    maven { url uri("https://cdn.last9.io/rum-sdk/android/maven/") }
    }
    }
  3. iOS — add the Last9RUM podspec

    In ios/Podfile:

    pod 'Last9RUM', :podspec => 'https://cdn.last9.io/rum-sdk/ios/builds/1.8.0/Last9RUM.podspec'

    Then install pods:

    cd ios && pod install
  4. 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.

Expo (Continuous Native Generation)

From SDK 1.2.0, an Expo config plugin re-applies the native install steps on every expo prebuild. Add the plugin to app.json (or app.config.js):

{
"expo": {
"plugins": ["@last9/rum-react-native"]
}
}

Then run a prebuild or dev build:

npx expo prebuild --clean
# or: npx expo run:ios / npx expo run:android

The plugin injects the Last9 CDN Maven repository on Android and the Last9RUM podspec source on iOS into the generated native projects. It is idempotent across repeated prebuilds and requires a development build (EAS Build or expo run:*) — it does not run in Expo Go, which cannot load native modules. Bare React Native apps can continue using the manual install steps above.

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+).

ArchitectureSupport
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).

What’s captured automatically

SignalDetails
SessionsStarted by initialize(). Home or background keeps the session, and returning within 30 minutes resumes the same session.id (v1.4.1+)
Screen viewsNative Activity and ViewController auto-tracking, controlled by autoViewTrackingEnabled (v1.5.0+)
Network requestsNative OkHttp (Android) and URLSession (iOS) interception — latency, status code, URL
Network phasesdns, tcp_connect, tls_handshake, and ttfb child spans on each request
GraphQLOperation name and type on the span, and error.type when an HTTP 200 carries a non-empty errors[]
ErrorsUnhandled JavaScript errors and promise rejections
Resource metricsMemory and CPU sampled every resourceSamplingIntervalMs
View vitalsPer-view refresh rate, slow/frozen frames, memory, plus a JS-thread refresh rate on each View span (v1.7.0+)
ANR detectionMain thread blocks beyond anrThresholdMs — Android only

Everything else needs app code. React Navigation screen names, user identity, custom events, manual views, and WebView correlation are all in the API reference below.

Per-view performance vitals (v1.7.0+)

When resourceMonitoringEnabled is on (the default), each View span carries per-view frame and memory vitals so you can find which screens janked or ran hot without extra instrumentation. React Native adds a JS-thread refresh rate on top of the native UI-frame vitals.

AttributeTypeDescription
view.refresh_rate_averagefloatAverage native UI refresh rate over the view, normalized to 0–60 Hz across 60 Hz and 120 Hz devices
view.refresh_rate_minfloatLowest native UI refresh rate seen during the view
view.frame.slowintSlow native frames — inter-tick interval above 1.5× the display refresh period
view.frame.frozenintFrozen native frames — longer than 700 ms
view.memory_averageintAverage resident memory bytes over the view
view.memory_maxintPeak resident memory bytes during the view
process.cpu.usagefloatProcess CPU usage averaged over the view
view.js_refresh_rate.averagefloatAverage JS-thread refresh rate over the view
view.js_refresh_rate.minfloatLowest JS-thread refresh rate over the view
view.js_refresh_rate.maxfloatHighest JS-thread refresh rate over the view
  • The view.refresh_rate_*, view.frame.*, view.memory_*, and process.cpu.usage vitals come from the native SDKs. The view.js_refresh_rate.* vitals are measured on the JS thread, so a busy JS thread shows up even when native UI frames look healthy.
  • JS-thread vitals are flushed onto each View before startView and on shutdown; the JS monitor stops cleanly when monitoring is disabled or on shutdown.
  • view.js_refresh_rate.* is clamped to a 60 Hz ceiling because React Native exposes no reliable display refresh rate, so on 90/120 Hz devices it under-reports relative to the native view.refresh_rate_*. Frame gaps longer than 1 s (for example while backgrounded) are discarded rather than recorded as a near-zero sample.
  • No wrapper API change: the vitals ride on the existing resourceMonitoringEnabled flag. Set it to false to turn off resource sampling and these vitals together.

Where to see it: open a session in the Last9 RUM UI, select a view on the timeline, and check the Attributes tab. Views that carry these attributes show a MOBILE VITALS section, with rows such as Refresh Rate (avg), Slow Frames, Frozen Frames, Memory (avg), and CPU Usage. JS Refresh Rate (avg/min/max) appears when present, and all rows are formatted with units. Web views are unaffected.

MOBILE VITALS section in the view Attributes tab

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.
// Fractional values are rounded to the nearest integer before
// the native bridge, so iOS and Android sample at the same percentage.
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,
// How long native batch processors wait before flushing queued spans/logs (ms).
// Omit: 5000 in production, 1000 when debugLogs is true (`v1.4.0+`).
scheduleDelayMs: undefined,
// When false, native Activity/ViewController auto-tracking no longer opens
// or closes view spans — the app owns view names (`v1.5.0+`).
autoViewTrackingEnabled: true,
// 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, and traceparent handling for the
// requests they drop. See "Network ignore patterns" below for the shape
// of these two options.
ignorePatterns: undefined,
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,
},
});

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.

Country and city

The mobile SDKs do not automatically infer location from IP addresses or collect GPS location. To populate country and city in Sessions, add the location your app already knows to resourceAttributes in your existing L9Rum.initialize() configuration:

const geoResourceAttributes = {
"enduser.geo.country": "India",
"enduser.geo.country_code": "IN",
"enduser.geo.city": "Bengaluru",
};

Set resourceAttributes: geoResourceAttributes, or merge these keys with your other resource attributes. Values must be strings; use a two-letter country code. Replace the example values with known location data and omit unknown fields.

Use coarse location already available from your app or backend, including a cached result. Do not delay SDK initialization for a network lookup. Resource attributes are set at initialization; calling initialize() again does not update them. identify() and spanAttributes() do not populate the standard session location fields.

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 spanWhat it measures
dnsDNS lookup duration
tcp_connectTCP connection establishment
tls_handshakeTLS negotiation
ttfbTime 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.

When nativeNetworkInterception is enabled, React Native 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. 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.

From 1.3.0, REST network span names fold fully-numeric and UUID path segments to ? (for example GET /workspaces/1 becomes GET /workspaces/?). GraphQL span names are unaffected. The raw URL is retained on the native span.

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 attributeExampleNotes
Span nameGraphQL: "GetUserPreferences" queryRenamed to a descriptive name
graphql.operation.nameGetUserPreferencesquery, mutation, or subscription name
graphql.operation.typequeryOperation type
error.typeGraphQLErrorSet when the response contains a non-empty errors[]
graphql.error.countnumberNumber 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.

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

Every method below is on the L9Rum object, except the two named exports in the last two rows.

MethodWhat it does
await initialize(config)Starts the SDK and resolves with { sessionId }. Rejects when native init fails
await isActive()true between a successful initialize and shutdown
await getSessionId()The current session.id. Never an empty string from 1.1.9
await shutdown()Flushes and tears down. A later initialize re-arms the SDK. See Embedded per-flow lifecycle
flush()Sends queued spans and logs without tearing down
identify(user)Attaches id, name, email, and roles to the session. See Identify a user
clearUser()Drops the identified user on sign-out
captureError(error, attributes?)Records a handled error. See Capture errors
startView(name)Opens a view span. Ends the previous view immediately
setViewName(name)Renames the active view in place
addEvent(name, attributes?)Emits a span event on the active view and an OTLP log. See Custom events
spanAttributes(attributes)Sets global attributes on every later span. Pass null to clear
instrumentWebView(target)Shares the native session with Browser RUM inside a WebView. See WebView correlation
L9ReactNavigationInstrumentation.onStateChangePass to NavigationContainer for automatic screen names
l9GqlLink(options)Apollo Link for richer GraphQL telemetry. See Layer 2

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" });
}

The SDK normalizes non-Error JavaScript throws and promise rejections, including strings, primitives, plain objects, and bridged native fallback errors, so exception.type stays populated.

Track views manually

L9Rum.startView("ProductDetailsScreen");
L9Rum.setViewName("Product #42");

From 1.5.0, startView never leaves a duplicate native view alongside Activity/ViewController auto-tracking, and setViewName renames the active view in place (including auto-tracked child view controllers). Set autoViewTrackingEnabled: false when the app owns all view names via navigation instrumentation or manual startView.

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, and session.id/user attributes. When a view is active the log is correlated to it via trace.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.

From 1.5.2, an addEvent fired while a screen is being torn down attaches to that screen (a 1000 ms grace window after the view ends), not the next one. After the window, and whenever no view is current, the OTLP log still correlates to the last view for the rest of the session. A manual startView(...) ends the previous view immediately. Prefer firing screen summaries before teardown (for example on blur / a screen’s unmount effect).

Global span attributes

L9Rum.spanAttributes({
experiment: "checkout_v2",
feature_flag: "new_cart",
});
// Clear
L9Rum.spanAttributes(null);

Session ID / init status

const { sessionId: readySessionId } = await L9Rum.initialize(config);
const sessionId = await L9Rum.getSessionId(); // same id once active
const running = await L9Rum.isActive(); // true between successful initialize and shutdown

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

From 1.4.1, backgrounding the app (Home button or a system file picker) no longer ends the RUM session. Returning within 30 minutes resumes the same session.id. A force-quit or crash backfills Session End on the next cold start (session.end_reason=process_death) and starts a fresh session.

Every span carries session.start_time (epoch ms) alongside session.id, so the backend can read the real start time of a session from any span. When a session rolls over on inactivity timeout or max duration, Session End and session.time_spent use the last recorded activity, so idle and background time is not counted, and the new session keeps session.previous_id. No wrapper API change.

Browser RUM spans inside an instrumented WebView adopt the native session.id, but do not carry session.start_time: the native-context bridge does not send a start time.

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 { WebView } from 'react-native-webview';
import { L9Rum } from '@last9/rum-react-native';
<WebView
source={{ uri: 'https://app.example.com' }}
onLoadStart={(e) => L9Rum.instrumentWebView(e.nativeEvent.target)}
/>

Pass the load event’s nativeEvent.target (the native reactTag). This form works across all react-native-webview versions and both RN architectures. instrumentWebView also accepts a numeric tag, a ref object, or a component instance, but react-native-webview >= 13 exposes a methods-only imperative handle on .current that cannot be resolved — so prefer the event.

The SDK resolves the underlying native WKWebView (iOS) or android.webkit.WebView (Android) and re-injects session context on every navigation automatically.

From 1.6.0, the host native view is auto-named from the current main-frame URL: app.screen.name becomes the folded pathname, and view.url carries the full URL. Android updates the name on full loads and in-WebView history changes; iOS updates on navigation commit (SPA route changes are not observable). An explicit L9Rum.startView / setViewName still wins.

Name the native host screen with setViewName() — do not call native startView() on an auto-tracked Activity/ViewController. In-WebView SPA routes stay on Browser RUM startView(). See the WebView Session Correlation guide for the full pattern, auto-load options, and verification steps.

Verification

Confirm the SDK is reporting before you move on:

  1. Set debugLogs: true and rebuild. The console prints SDK initialization output.
  2. Await initialize() and log the returned sessionId, or call await L9Rum.isActive(). A rejection or false means native init failed.
  3. Watch the device’s outgoing requests for calls to your baseUrl. A 403 means the origin and clientToken pair does not match the token you created.
  4. Open Discover > Applications and confirm the session appears within 2-3 minutes.

Next steps

Once data is flowing, explore it in Discover > Applications — performance, errors, and sessions.

For the version history of this SDK, see the RUM changelog.


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 versioned: 1.8.0), 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.2.1 or later (stable/v1 currently serves 1.8.0; latest versioned is 1.8.0), reinstall from stable/v1 or the versioned 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.

Please get in touch with us on Discord or Email if you have any questions.