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.
Install
Section titled “Install”pnpm add @lumin-monitor/react-nativeTwo optional peers, both worth adding:
# persistent ids across app launches (recommended)pnpm add @react-native-async-storage/async-storage# device model in the sessions UIpnpm add react-native-device-infoNeither is auto-detected. The SDK never require()s them, which keeps Metro bundling clean — you pass them to init() yourself.
Initialise once at app boot
Section titled “Initialise once at app boot”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.
Options
Section titled “Options”| 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 views
Section titled “Screen views”screen("Pricing");screen("Pricing", { plan: "solo" });React Navigation
Section titled “React Navigation”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.
Custom events
Section titled “Custom events”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 a user
Section titled “Identify a user”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.
Unhandled errors
Section titled “Unhandled errors”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 handlerYou 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.
Persistent ids
Section titled “Persistent ids”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.
Flushing
Section titled “Flushing”The SDK flushes when the app goes to the background, via AppState. Force one before something you expect to lose:
await flush();Visitor counts
Section titled “Visitor counts”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.