Skip to main content
The Lucent React Native SDK records mobile sessions with native screenshot-mode replay. Use it for React Native apps, Expo development builds, EAS builds, and Expo projects that have generated native iOS or Android projects.
React Native replay is screenshot based. It does not use rrweb DOM or wireframe recording because React Native has no browser DOM.

Setup difficulty

App typeDifficultyWhat to expect
Bare React NativeEasyInstall the package, run pods for iOS, and rebuild the app.
Expo with dev-client or EASEasyInstall the package, generate or rebuild the native app, and run a development or production build.
Expo Go-onlyMediumExpo Go cannot load Lucent’s custom native modules. Move to a development build, EAS build, or prebuilt native project for full replay.

Install

npm install @lucenthq/react-native
npx expo install expo-dev-client
npx expo prebuild
npx expo run:ios
Use npx expo run:android for Android, or build with EAS:
eas build --profile development --platform ios
eas build --profile development --platform android
See Expo setup for the full Expo flow.

Requirements

  • React 18 or newer.
  • React Native 0.71 or newer.
  • A Lucent public key from Settings -> SDK keys in the dashboard.
  • A native build environment for the platforms you ship.
  • Expo development builds, EAS builds, or prebuilt native projects for Expo apps.
Expo Go can import the JavaScript package without crashing, but it cannot record full Lucent replay. Native screenshot replay, native crash capture, native network capture, and native log capture are disabled in Expo Go.

Quickstart

Wrap your app with LucentProvider near the root of the React tree.
import { Button } from "react-native";
import {
  LucentIdentify,
  LucentProvider,
  useLucent,
} from "@lucenthq/react-native";

export function App() {
  return (
    <LucentProvider
      publicKey="luc_pk_..."
      autocapture={{ captureTouches: true }}
      options={{
        ingestBaseUrl: "https://ingest-api.lucenthq.com",
      }}
    >
      <LucentIdentify
        id={user?.id}
        email={user?.email}
        name={user?.name}
        properties={{ plan: user?.plan }}
      />
      <RootNavigator />
    </LucentProvider>
  );
}

function CheckoutButton() {
  const lucent = useLucent();

  return (
    <Button
      title="Pay"
      onPress={() => {
        void lucent.track("checkout_tapped", { plan: "pro" });
      }}
    />
  );
}
The provider starts recording automatically by default. Pass options={{ autoStart: false }} if you need to wait for consent, then call lucent.start() when recording can begin.

Replay model

Every React Native replay batch is sent as mobile screenshot replay:
{
  "snapshotSource": "mobile",
  "snapshotLibrary": "lucent-react-native",
  "snapshotMode": "screenshot"
}
snapshotMode is not configurable in the React Native package. Browser sessions use rrweb DOM events; React Native sessions use native iOS and Android modules to capture screenshots, touch events, screen names, logs, network telemetry, and crashes when enabled.

API

The main API mirrors the browser SDK where the platform allows it.

Provider and identity

import {
  LucentIdentify,
  LucentProvider,
  useLucent,
} from "@lucenthq/react-native";
LucentProvider
component
Creates one Lucent client for your app. Pass publicKey, optional options, optional autocapture, and your app tree as children.
LucentIdentify
component
Syncs identity with Lucent. React Native uses id, email, name, and properties.
useLucent
hook
Returns the Lucent client from the nearest provider. It must run inside LucentProvider.

Shared tracking methods

await lucent.start();
await lucent.track("checkout_tapped", { plan: "pro" });
await lucent.setSessionProperties({ cartValue: 129 });
await lucent.setUserProperties({ plan: "pro" });
await lucent.identify({ id: "user_123", email: "[email protected]" });
await lucent.resetIdentity();
await lucent.captureException(error, { flow: "checkout" });
await lucent.addExceptionStep("Payment API returned 402");
await lucent.flush();
await lucent.stop();

React Native methods

await lucent.screen("Checkout", { step: "payment" });
await lucent.startRecording();
await lucent.stopRecording();

const session = await lucent.getSessionInfo();
lucent.screen(name, properties?)
Promise<void>
Sets the native replay screen name and emits a $screen event.
lucent.startRecording(resumeCurrent?)
Promise<void>
Resumes screenshot replay capture. resumeCurrent defaults to true.
lucent.stopRecording()
Promise<void>
Pauses screenshot replay capture without clearing identity.
lucent.getSessionInfo()
Promise<LucentSessionInfo | null>
Returns the current sessionId, windowId, and recordingEnabled state. In Expo Go fallback mode this returns null.

Screen tracking

Track screen changes directly from a screen component:
import { useLucentScreen } from "@lucenthq/react-native";

export function CheckoutScreen() {
  useLucentScreen("/checkout/review", { step: "payment" });

  return <CheckoutForm />;
}
For React Navigation-style refs, use useLucentNavigationTracking. Lucent does not add a navigation peer dependency.
import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native";
import { useLucentNavigationTracking } from "@lucenthq/react-native";

function LucentNavigationTracker({ navigationRef }) {
  useLucentNavigationTracking(navigationRef, {
    properties: (route) => ({
      routeKey: route.key,
      path: route.path,
    }),
  });

  return null;
}

export function RootNavigator() {
  const navigationRef = useNavigationContainerRef();

  return (
    <NavigationContainer ref={navigationRef}>
      <LucentNavigationTracker navigationRef={navigationRef} />
      {/* screens */}
    </NavigationContainer>
  );
}

Processing controls

Processing controls decide which mobile sessions are recorded, processed, and analyzed. Filters and sampling usually control whether a session is analyzed, not whether the app can send events at all. React Native replay batches can still be accepted by Lucent even when a session later does not match your processing rules.

Required URL and screen filters

Lucent Required URL filters are fail-closed. If your source has Required URL patterns, Lucent evaluates them before full AI analysis:
  • Sessions with matching URL or screen evidence are processed.
  • Sessions without matching evidence are skipped from analysis as required_url_missing.
  • Skipped sessions may still exist as ingested SDK replay data, depending on the product surface and session status.
For React Native and Expo sessions, Lucent builds URL evidence from screen and route metadata. The SDK records path-style mobile URLs with the lucent-react-native scheme so URL filters can match mobile screens the same way they match web paths. Use path-style screen names when you want mobile sessions to match path-style filters. For example, a Required URL filter of /onboarding works reliably when the app records /onboarding as screen evidence.
Lucent filterRecommended SDK callNotes
Required URL /onboardinguseLucentScreen("/onboarding")Best for a screen component.
Required URL /checkout/reviewlucent.screen("/checkout/review", { step: "payment" })Best from a router listener or custom navigation handler.
Required URL /onboardingAvoid relying on lucent.screen("Onboarding")Onboarding is a human screen label, not a URL path. It may not match /onboarding.
import { useLucentScreen } from "@lucenthq/react-native";

export function OnboardingScreen() {
  useLucentScreen("/onboarding");

  return <OnboardingFlow />;
}
await lucent.screen("/checkout/review", {
  step: "payment",
});
Human labels such as "Onboarding" are fine for analytics, but do not rely on them when the Lucent filter is path-like, such as /onboarding. For React Navigation, matching is strongest when the route evidence is path-like. Prefer route paths such as /onboarding or /checkout/review over display names such as Onboarding.
function LucentNavigationTracker({ navigationRef }) {
  useLucentNavigationTracking(navigationRef, {
    properties: (route) => ({
      path: route.path,
    }),
  });

  return null;
}

Excluded URL and domain filters

Excluded URL and domain filters skip sessions only when Lucent observes screen or URL evidence that matches the excluded patterns. The same React Native path caveat applies: use path-style screen names if you expect path-style filters to match mobile sessions.
// Matches an excluded URL filter such as /internal-tools.
useLucentScreen("/internal-tools");

User, email, and anonymous-user controls

User, email, and domain filters depend on identity metadata. Identify users as soon as the app knows who they are, ideally before the important flow begins. Anonymous sessions may not match user, email, or email-domain filters.
<LucentIdentify
  id={user.id}
  email={user.email}
  name={user.name}
  properties={{ plan: user.plan }}
/>
If your organization enables Ignore Anonymous Users, anonymous SDK sessions may be skipped from processing. Call identify as soon as the user is known so their mobile session is eligible for user-aware filters and processing rules.

Property filters

User and session properties are filterable metadata where configured. Set them before important flows so the properties are present when Lucent evaluates the session.
await lucent.setUserProperties({
  plan: "pro",
  role: "admin",
});

await lucent.setSessionProperties({
  checkoutVariant: "new-flow",
  entryPoint: "push-notification",
});

Sampling, event triggers, and minimum duration

You should not expect every mobile app launch to produce an analyzed session. Local SDK sampling and remote-config sampling can prevent a session from being recorded or processed.
<LucentProvider
  publicKey="luc_pk_..."
  options={{
    sampling: { sessionReplay: 0.25 },
    remoteConfig: true,
  }}
>
  <RootNavigator />
</LucentProvider>
If replay is configured to start only after certain events, activity before the trigger may not have replay data. This is useful when you only want replay after an important flow begins.
<LucentProvider
  publicKey="luc_pk_..."
  options={{
    replay: {
      eventTriggers: ["checkout_started"],
    },
  }}
>
  <RootNavigator />
</LucentProvider>
await lucent.track("checkout_started");
Very short sessions may also be dropped or skipped depending on replay.minimumDurationMs. If you are testing with tiny sessions, keep the app open long enough to exceed your minimum duration and allow a flush.

Privacy masking

Lucent masks sensitive mobile content before screenshots leave the device. Input fields, images, and sandboxed system views are masked by default.
<LucentProvider
  publicKey="luc_pk_..."
  options={{
    privacy: {
      maskAllInputs: true,
      maskAllImages: true,
      maskSandboxedViews: true,
    },
  }}
>
  <RootNavigator />
</LucentProvider>

Capture toggles

Lucent can capture console logs, network telemetry, app lifecycle events, JavaScript errors, unhandled promise rejections, and native crashes. Disable individual capture types when they do not fit your privacy or telemetry policy.
<LucentProvider
  publicKey="luc_pk_..."
  options={{
    capture: {
      console: true,
      network: true,
      appLifecycle: true,
      errors: true,
      unhandledRejections: true,
      nativeCrashes: true,
    },
  }}
>
  <RootNavigator />
</LucentProvider>
Expo Go cannot load Lucent’s native module, so native crash, network, and log capture do not behave like they do in a development client, EAS build, or prebuilt native app.

Offline and batching behavior

Mobile sessions are batched and flushed periodically. If the app is offline or backgrounded, events may be queued and flushed later. Do not expect every mobile session to appear instantly in the dashboard.
<LucentProvider
  publicKey="luc_pk_..."
  options={{
    batching: {
      flushIntervalMs: 10000,
      maxEventsPerBatch: 50,
      maxBatchBytes: 1048576,
    },
  }}
>
  <RootNavigator />
</LucentProvider>

Source maps and release metadata

Set release metadata and upload source maps for readable React Native JavaScript stack traces. For Expo and EAS builds, configure the optional Expo plugin, Metro helper, and build environment variables described in Expo source-map tooling.
<LucentProvider
  publicKey="luc_pk_..."
  options={{
    metadata: {
      app: "Acme Mobile",
      namespace: "com.acme.mobile",
      version: "1.2.3",
      build: "123",
      release: "1.2.3+123",
    },
  }}
>
  <RootNavigator />
</LucentProvider>

Autocapture and errors

Enable touch autocapture by passing autocapture. Lucent wraps your app in a responder-capture view and emits $touch events alongside replay screenshots.
<LucentProvider publicKey="luc_pk_..." autocapture={{ captureTouches: true }}>
  <RootNavigator />
</LucentProvider>
Unhandled JavaScript errors, unhandled promise rejections, and native uncaught exceptions are captured by default when the SDK starts. Native crashes are stored synchronously and emitted as $native_crash on the next launch.
import { LucentErrorBoundary } from "@lucenthq/react-native";

<LucentProvider publicKey="luc_pk_...">
  <LucentErrorBoundary>
    <RootNavigator />
  </LucentErrorBoundary>
</LucentProvider>
Disable capture hooks when needed:
<LucentProvider
  publicKey="luc_pk_..."
  options={{
    capture: {
      console: false,
      network: false,
      errors: false,
      unhandledRejections: false,
      nativeCrashes: false,
    },
  }}
>
  <RootNavigator />
</LucentProvider>

Configuration

<LucentProvider
  publicKey="luc_pk_..."
  options={{
    ingestBaseUrl: "https://ingest-api.lucenthq.com",
    sampling: { sessionReplay: 1.0 },
    privacy: {
      maskAllInputs: true,
      maskAllImages: true,
      maskSandboxedViews: true,
    },
    capture: {
      console: true,
      network: true,
      appLifecycle: true,
      errors: true,
      unhandledRejections: true,
      nativeCrashes: true,
    },
    remoteConfig: true,
    replay: {
      screenshotThrottleMs: 1000,
      minimumDurationMs: 3000,
      screenshotBackgroundCapture: false,
      eventTriggers: ["checkout_tapped"],
    },
    metadata: {
      app: "Acme Mobile",
      namespace: "com.acme.mobile",
      version: "1.2.3",
      build: "123",
      release: "1.2.3+123",
      properties: {
        channel: "production",
      },
    },
  }}
>
  <RootNavigator />
</LucentProvider>
Remote config is enabled by default. When it is enabled, native replay waits for Lucent’s init response before auto-starting recording and falls back to local config if the request fails.

Optional Expo and Metro tooling

No Expo config plugin is required for replay capture. The native module autolinks during React Native linking or Expo prebuild/dev-client/EAS native project generation. Use the optional Expo config plugin and Metro helper only when you want Lucent production source-map metadata and upload hooks.
{
  "expo": {
    "plugins": [["@lucenthq/react-native/expo", { "skipOnConflict": true }]]
  }
}
const { getDefaultConfig } = require("expo/metro-config");
const { withLucentMetroConfig } = require("@lucenthq/react-native/metro");

const config = getDefaultConfig(__dirname);

module.exports = withLucentMetroConfig(config, {
  release: process.env.LUCENT_RELEASE,
});
See Expo setup for the complete Expo path.

Troubleshooting

Expo Go cannot load Lucent’s native module. Use an Expo development build, EAS build, or prebuilt native project via npx expo run:ios or npx expo run:android. In Expo Go, provider/hooks/API calls are safe, but getSessionInfo() returns null and no native replay batches are sent.
Rebuild the native app after installing the package. For Expo, run npx expo prebuild and then npx expo run:ios, npx expo run:android, or an EAS build. For bare React Native, run npx pod-install for iOS and rebuild both native targets.
Run npx pod-install from the app root. If pods are stale, delete ios/Pods and ios/Podfile.lock, then run npx pod-install again. Make sure Xcode command-line tools and CocoaPods are installed before rebuilding.
Install Android Studio command-line tools, a JDK supported by your React Native version, and Gradle or the app’s android/gradlew wrapper. Set ANDROID_HOME or ANDROID_SDK_ROOT if your SDK is not in the default location.
Confirm you are running a native build, not Expo Go. Check that the public key starts with luc_pk_, ingestBaseUrl is reachable, and sampling.sessionReplay is greater than 0. If you configured replay.eventTriggers, replay waits until one of those events is tracked. If you configured replay.minimumDurationMs, very short sessions may be dropped.
Check whether your source has Required URL filters. Required URL filtering applies to processing eligibility, not ingestion. If the filter is /onboarding, use path-style screen evidence such as useLucentScreen("/onboarding") instead of relying on a display label such as Onboarding.
Add a temporary button that calls lucent.track("lucent_mobile_test"), interact with the app in a native build, then open Sessions in the Lucent dashboard. Call await lucent.getSessionInfo() from inside the app to confirm the SDK has an active session.

Next steps

Expo setup

Configure Lucent in Expo development builds, EAS builds, and prebuilt apps.

Session replay integrations

See how Lucent handles web, mobile, screenshot, and wireframe replay data.