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

Source: https://last9.io/docs/real-user-monitoring/react-native/

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](/docs/real-user-monitoring/android/) and [iOS](/docs/real-user-monitoring/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.6.0` (resolved from CDN Maven — the consumer app must declare the repo; see Installation)
- iOS: `Last9RUM 1.6.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](/docs/real-user-monitoring/android/) and [iOS](/docs/real-user-monitoring/ios/) setup.

1. Open [Last9 → Settings → Ingestion Tokens](https://app.last9.io/control-plane/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)

:::note
The origin is a scope guard — requests are rejected if the `X-LAST9-ORIGIN` header doesn't match the token's allowed origin. At app startup, pass the **matching pair** of `clientToken` and `origin` for the platform the app is running on.
:::

## 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.6.0/last9-rum-react-native-1.6.0.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.6.0/last9-rum-react-native-1.6.0.tgz.sha256` |

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

### Installing or upgrading to `1.6.0`

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

```bash
# npm
npm uninstall @last9/rum-react-native
npm cache clean --force
npm install https://cdn.last9.io/rum-sdk/react-native/builds/1.6.0/last9-rum-react-native-1.6.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.6.0/last9-rum-react-native-1.6.0.tgz
```

To check which version the stable channel currently serves:

```bash
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"'
```

:::tip
For reproducible builds and lockfile-friendly upgrades, pin the versioned tarball URL in `package.json` (for example `https://cdn.last9.io/rum-sdk/react-native/builds/1.6.0/last9-rum-react-native-1.6.0.tgz`) and bump it explicitly when upgrading.
:::

## Installation

1.  **Install the package**

    ```bash
    npm install https://cdn.last9.io/rum-sdk/react-native/builds/1.6.0/last9-rum-react-native-1.6.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`):

    ```groovy
    dependencyResolutionManagement {
        repositories {
            google()
            mavenCentral()
            maven { url uri("https://cdn.last9.io/rum-sdk/android/maven/") }
        }
    }
    ```

3.  **iOS — add the Last9RUM podspec**

    In `ios/Podfile`:

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

    Then install pods:

    ```bash
    cd ios && pod install
    ```

4.  **Initialize the SDK**

    At app entry (before any screens render):

    ```typescript
    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`):

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

Then run a prebuild or dev build:

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

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

:::caution
On SDK versions **before `1.1.6`**, `await L9Rum.initialize()` could **hang forever** on the New Architecture: native code ran and called `resolve(...)`, but the JavaScript promise never settled because the module was reached through React Native's interop layer ([facebook/react-native#44555](https://github.com/facebook/react-native/issues/44555)). Upgrade to `1.1.6` or later if you see this symptom.
:::

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

```typescript
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,

  // How long native batch processors wait before flushing queued spans/logs (ms).
  // Omit: 5000 in production, 1000 when debugLogs is true (`1.4.0+`).
  scheduleDelayMs: undefined,

  // When false, native Activity/ViewController auto-tracking no longer opens
  // or closes view spans — the app owns view names (`1.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. 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,
  },
});
```

:::caution
Never put secrets or PII in `baggage.allowedKeys`. Baggage travels in plain HTTP headers.
:::

## Stable release notes

React Native `1.6.0` auto-names WebView-hosted views from the current URL (via native `instrumentWebView`): `app.screen.name` becomes the folded pathname and `view.url` carries the full URL. An explicit `startView` / `setViewName` still wins. `1.5.2` attaches `addEvent` during screen teardown to that view (1000 ms grace), not the next one. `1.5.1` is a version-parity release that pins native Android `1.5.1` and iOS `Last9RUM 1.5.1`. `1.5.0` adds `autoViewTrackingEnabled` (default `true`) and carries native fixes so `startView` never leaves a duplicate native view and `setViewName` renames the active view in place. `1.4.1` keeps the session alive across Home / background. `1.4.0` adds `scheduleDelayMs`. `1.3.0` folds dynamic segments in network span names. React Native `1.2.1` hardens `instrumentWebView` so it accepts a WebView load event (`event.nativeEvent.target`), a numeric reactTag, a ref, or a component instance without throwing — recommended: `onLoadStart={(e) => L9Rum.instrumentWebView(e.nativeEvent.target)}`. Native pins bump to `1.2.1`, carrying an iOS fix for a main-thread hang when instrumenting a `WKWebView`. `1.2.0` adds an Expo config plugin for CNG apps. `1.1.10` fixes a New Architecture Android clean-build failure at `:app:configureCMakeDebug`. `1.1.9` bumps native Android and iOS SDK references and fixes `await L9Rum.initialize(config)` resolving with a `null` or empty `sessionId` on iOS — native session start is now synchronous and never surfaces an internal placeholder id. `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). Rebuild native projects after upgrading.

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

```typescript
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.

:::note
If `nativeNetworkInterception` is explicitly disabled, the SDK falls back to JS fetch/XHR instrumentation. That fallback preserves parent HTTP spans but DNS/TCP/TLS phase timings require native interception.
:::

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

```typescript
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:

```typescript
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

```typescript
L9Rum.identify({
  id: "user-123",
  name: "Jane",
  email: "jane@example.com",
  fullName: "Jane Doe",
  roles: ["admin"],
});
```

### Clear user on sign-out

```typescript
L9Rum.clearUser();
```

### Capture errors

```typescript
try {
  // risky operation
} catch (error) {
  L9Rum.captureError(error, { screen: "checkout" });
}
```

### Track views manually

```typescript
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

```typescript
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

```typescript
L9Rum.spanAttributes({
  experiment: "checkout_v2",
  feature_flag: "new_cart",
});

// Clear
L9Rum.spanAttributes(null);
```

### Session ID / init status

```typescript
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.

### Flush pending data

```typescript
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:

```typescript
// 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:

```typescript
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](/docs/discover-applications-webview-session-correlation/) for the full pattern, auto-load options, and verification steps.

## Next steps

Once data is flowing, explore it in [Discover > Applications](/docs/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 versioned: `1.6.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.6.0`; latest versioned is `1.6.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.

### Still stuck?

Get in touch on [Discord](https://discord.com/invite/Q3p2EEucx9) or [Email](mailto:support@last9.io).
