Skip to content

React Native SDK (@lumin-monitor/react-native)

@lumin-monitor/react-native is the mobile half of the Lumin instrumentation story. Everything server-side — logs, metrics, container observation — flows through the agent. An SDK is unavoidable inside a user’s app, because there is no agent there.

It mirrors @lumin-monitor/browser: same wire format, same lmn_pub_* key kind, same /v1/events endpoint. One Lumin project can therefore hold events from your web app and your mobile app, tied to the same person once you call identify.

The SDK does four things: screen for view tracking, track for custom events, identify for binding the session to a known user, and automatic capture of unhandled JS errors.

Terminal window
pnpm add @lumin-monitor/react-native

Two optional peers, both worth adding:

Terminal window
# persistent ids across app launches (recommended)
pnpm add @react-native-async-storage/async-storage
# device model in the sessions UI
pnpm add react-native-device-info

Neither is auto-detected. The SDK never require()s them, which keeps Metro bundling clean — you pass them to init() yourself.

import { init } from "@lumin-monitor/react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
export const lumin = init({
apiKey: process.env.EXPO_PUBLIC_LUMIN_KEY!,
storage: AsyncStorage,
});
export const { screen, track, identify, captureError, flush } = lumin;

init is synchronous. The client buffers events while it reads storage for the persistent ids in the background, so you can call screen() and track() immediately — the first flush waits for hydration.

Calling init more than once produces independent clients. Pin one at module level if you want a single shared instance.

The apiKey is a lmn_pub_* key minted in Settings → API keys with Kind = Browser SDK. It ships inside your app bundle and is extractable — that is expected. The server enforces that pub keys can only post /v1/events, so a leaked key cannot read data, post logs or metrics, or reach another project. A lmn_priv_* key sent here returns 403.

Option Default Notes
apiKey — Required. lmn_pub_… from Settings → API keys.
endpoint https://api.getlumin.dev Must be a valid URL with no path beyond /. https:// is required except for localhost, 127.0.0.1, and .localhost subdomains. Invalid shapes throw from init(), so a misconfiguration surfaces at boot.
storage null Pass AsyncStorage. Omit for in-memory ids that reset on every cold start.
batchSize 50 Max events buffered before a forced flush.
flushIntervalMs 500 Max ms between flushes.
sessionIdleMs 30 min A new session_id is minted on the next event after this much idle time.
captureUnhandledErrors true See below.
deviceInfo null Pass DeviceInfo to add the device model to the sessions UI.
visitorPing false Fire one anonymous visitor heartbeat at init.
onError console.warn Called with the failed batch and dropped count.
screen("Pricing");
screen("Pricing", { plan: "solo" });

Import the hook from the /react-navigation subpath and mount it once, beside your NavigationContainer:

import { useLuminScreenviews } from "@lumin-monitor/react-native/react-navigation";
useLuminScreenviews(lumin, navigationRef);

It fires screen() on every route change, and once on mount if navigation has already hydrated. Repeat navigations to the same route name do not re-emit — params are usually filter state rather than a real screen view, and double-counting them inflates funnel numbers. Call screen() yourself from the route’s effect if you want per-param tracking.

Pass null for the client and the hook becomes a no-op, which is the clean way to disable the SDK in development.

track("checkout_started", { plan: "solo" });

Keep names stable. The Sessions page filters by name, so signup_completed is good and signup_completed_v2 is not.

identify("user_abc123", { plan: "solo" });

Call it after login. The SDK already holds an anonymous session; identify binds it to your user id, and the whole session reads back against that user. Because the browser SDK writes into the same project with the same call, a person who uses your web app and your mobile app resolves to one user.

Auto-capture is on by default. The SDK installs a global handler through ErrorUtils.setGlobalHandler and chains to any handler already installed, so React Native’s red box and any other reporter still fire.

init({ apiKey, captureUnhandledErrors: false }); // e.g. Sentry owns the handler

You can also report a caught error yourself:

captureError(err, { screen: "Checkout" });

Pass a real Error where you can — the SDK keeps the stack and uses the constructor name as error_type. Any other value is stringified into the message.

Errors land in the session timeline in order with the screens before them, so you see what the user did on the way to the failure. There is no issue list, no grouping, and no dedupe; this is not an error tracker.

Native crashes are not captured. ErrorUtils sees the JS thread only. An iOS or Android crash needs native modules that are outside this SDK’s scope.

Without storage, the anonymous_id and session_id live in memory and reset on every cold start. Every launch then looks like a new person. Pass AsyncStorage unless you are prototyping.

The SDK flushes when the app goes to the background, via AppState. Force one before something you expect to lose:

await flush();

visitorPing: true POSTs one empty request to /v1/visit at init, so the project’s visitor gauge counts the launch before any screen or track call. It carries no payload, no identifiers, and no cookies — the server derives a daily-rotating, non-personal hash from the connection and the X-Lumin-Client header. It is safe to enable without consent. screen, track, and identify stay behind whatever consent layer you wrap the SDK in.