npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lumin-monitor/react-native

v0.5.1

Published

React Native SDK for Lumin: screen / track / identify with batched ingest.

Readme

@lumin-monitor/react-native

React Native SDK for Lumin. Drop screen / track / identify into your mobile app; per-session timelines join with your server-side logs and your web events via session_id.

Mirror of the @lumin-monitor/browser SDK, adapted for the React Native runtime. Same wire format, same lmn_pub_* API key kind, same /v1/events endpoint — so a single Lumin project can hold events from your web app, your mobile app, and (after identify) tie them to the same user.

Install

pnpm add @lumin-monitor/react-native
# plus, if you want persistent ids across app launches (recommended):
pnpm add @react-native-async-storage/async-storage
# plus, if you want device-model tracking in the sessions UI:
pnpm add react-native-device-info

@react-native-async-storage/async-storage is no longer auto-detected (it broke Metro bundling — see CHANGELOG 0.3.0). Install it and pass it to init({ storage }) to persist anonymous_id across app launches and let the 30-minute idle session window survive a cold start. Omit storage (or pass null) to use in-memory ids that reset on every cold start — fine for prototypes.

react-native-device-info is also opt-in (same reason). Without it the SDK still reports os + os_version via the X-Lumin-Client header — see Device classification.

React Navigation is an optional peer dep, only needed if you import @lumin-monitor/react-native/react-navigation.

Quick start

import AsyncStorage from "@react-native-async-storage/async-storage";
import { init } from "@lumin-monitor/react-native";

export const lumin = init({
  apiKey: process.env.EXPO_PUBLIC_LUMIN_BROWSER_API_KEY!,
  storage: AsyncStorage,
});

// Bound methods, safe to destructure:
export const { screen, track, identify, flush, close } = lumin;

apiKey is a lmn_pub_* key minted in Settings → API keys with kind Browser SDK (the same key kind covers all client-side SDKs). The server enforces "pub keys can only post to /v1/events" — even if an attacker extracts the key from a decompiled IPA / APK they cannot post logs or metrics, query any data, or pivot to a different endpoint. Rotate from Settings → API keys if it leaks.

If you accidentally paste a lmn_priv_* (server / agent kind) key here the server will return 403 with a hint pointing you back at the mint flow — go pick Browser SDK instead.

endpoint defaults to https://api.getlumin.dev. The Lumin API allows cross-origin requests, so your bundle identifier does not need to match any particular host.

API

init(options): LuminClient

init is synchronous: it returns a client immediately and hydrates the persistent ids from AsyncStorage in the background. You can call screen / track / identify right away; the first flush waits for hydration to complete before sending.

| Option | Default | Notes | | ----------------- | ----------------------------- | --------------------------------------------------------------------------- | | apiKey | — | Required. lmn_pub_… from Settings → API keys (kind: Browser SDK). | | endpoint | https://api.getlumin.dev | Override only for local dev or same-origin proxy. See below. | | batchSize | 50 | Max events buffered before a forced flush. | | flushIntervalMs | 500 | Max ms between flushes. | | sessionIdleMs | 1800000 (30 min) | A new session id is minted on the next event after this much inactivity. | | onError | console.warn | Called as (err, droppedCount) when a batch fails. | | fetch | global fetch | Override for tests. | | storage | null (in-memory ids) | Pass AsyncStorage for persistence across launches. See Install above. | | appState | auto-detect react-native | Pass null to disable the background-flush listener. | | captureUnhandledErrors | true | Install an ErrorUtils.setGlobalHandler chain. See Error capture below. | | errorUtils | auto-detect global ErrorUtils | Override the ErrorUtils-like object the SDK installs into. | | deviceInfo | null (no device model) | Pass DeviceInfo from react-native-device-info to include the device model in the X-Lumin-Client header. See Device classification below. |

screen(name?, properties?)

Fire on navigation. Most apps use the React Navigation helper below; you can also call screen() manually from a screen component's useEffect.

screen();                                  // current screen, no name
screen("Settings");                        // named view
screen("Settings", { tab: "billing" });    // with properties

On the wire screen emits type: "page", matching the browser SDK and the existing server-side schema. The mobile-idiom method name is purely cosmetic.

track(name, properties?)

Custom events. Names are free-form; the Sessions UI lets you filter by name, so keep them stable (signup_completed good, not signup_completed_v2_2026_05).

track("signup_completed", { plan: "indie", source: "appstore" });
track("checkout_clicked");

identify(userId, traits?)

Bind the current anonymous session to a known user — typically called right after login. Every prior event in the session is retroactively associated with userId when the session timeline is rendered.

identify("user_abc123");
identify("user_abc123", { plan: "indie", signedUpAt: "2026-05-01" });

Re-call on every app launch while the user is signed in. It is cheap and ensures a cold start still binds the session.

captureError(err, properties?)

Capture an error. The auto-installed global handler catches uncaught JS throws (and rejected promises that bubble up to RN); call this directly from try/catch blocks where you'd otherwise swallow the failure.

try {
  await applyDiscount(code);
} catch (err) {
  captureError(err, { code, step: "checkout" });
  showToast("Discount didn't apply");
}

Accepts a real Error (preferred — preserves stack and constructor name as error_type), or any value that will be stringified for the message. null / undefined are silently ignored. The same Error object captured twice in the same tick is deduped.

Auto error capture

When captureUnhandledErrors is true (the default), the SDK installs a handler via ErrorUtils.setGlobalHandler that chains to the previous handler, so RN's red-box, the LogBox, and any other error tool already installed still fire. The SDK does not swallow the throw.

Fatal errors (isFatal: true) trigger an immediate flush() so the error row has a chance to leave the device before the RN runtime tears down. Best-effort: the OS may suspend the JS thread before the request lands, but the standard ~30 s background grace window covers the common case.

What is NOT captured:

  • Native iOS / Android crashes. RN's ErrorUtils is JS-only. Native crashes need a native module (Crashlytics, Sentry-native, Bugsnag's native bridge) — out of scope for this SDK.
  • Source-mapped stacks. Stacks ship as the minified RN bundle frames ship them. Symbolication against the bundle's source maps is a v2 concern.

Opt out with captureUnhandledErrors: false if another tool already owns the global handler; manual captureError(err) still works.

flush(): Promise<void>

Force a flush of any buffered events. The SDK auto-flushes on AppState background / inactive; call this manually only when you need to guarantee delivery before kicking off something that may suspend the runtime (a deep link out to a payment SDK, an OAuth handoff, etc.).

close(): Promise<void>

Flush, then tear the SDK down. Subsequent screen/track/identify calls become no-ops. Use only when you genuinely want to stop emitting events for the rest of the app's lifetime — uncommon.

getSessionId() / getAnonymousId(): Promise<string>

Resolve the current ids. Both await AsyncStorage hydration. Use them to attach a X-Lumin-Session header to your API calls so server-side logs join with the session timeline (see "Linking events to server logs" below).

React Navigation integration

import { NavigationContainer, createNavigationContainerRef } from "@react-navigation/native";
import { useLuminScreenviews } from "@lumin-monitor/react-native/react-navigation";

const navigationRef = createNavigationContainerRef();

export default function App() {
  useLuminScreenviews(lumin, navigationRef);
  return (
    <NavigationContainer ref={navigationRef}>
      <RootStack />
    </NavigationContainer>
  );
}

Fires screen() once per route name change. Deliberately ignores param changes — those are usually filter state, not real screen views, and double-counting them inflates funnel metrics. If you want per-param tracking, call screen() manually from the route's effect.

Peer deps: react >= 18, @react-navigation/native >= 6. Both are optional — the subpath only loads them when imported.

Device classification

Browser SDKs send a meaningful User-Agent; the Lumin server parses it into ua_browser / ua_os / ua_device_type columns that the sessions UI groups by. React Native's fetch sends okhttp/… or CFNetwork/…, which carries no device info, so the SDK ships an X-Lumin-Client header on every batch instead:

X-Lumin-Client: sdk=rn/0.4.0; os=ios; os_version=17.4

os and os_version come from React Native's built-in Platform constants — no peer dep required. The server folds the header into the same ua_os / ua_device_type columns the browser SDK has always used, so mobile sessions show up in the UI alongside web sessions.

For the device model (iPhone15,3, Pixel 7), install react-native-device-info and pass it through:

import DeviceInfo from "react-native-device-info";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { init } from "@lumin-monitor/react-native";

export const lumin = init({
  apiKey: process.env.EXPO_PUBLIC_LUMIN_BROWSER_API_KEY!,
  storage: AsyncStorage,
  deviceInfo: DeviceInfo, // populates X-Lumin-Client: device=<model>
});

The SDK never require()s react-native-device-info — same opt-in policy as storage, to keep Metro bundling clean (see CHANGELOG 0.3.0). If the installed version of react-native-device-info ever changes its shape, ship a tiny adapter that satisfies DeviceInfoLike ({ getModel(): string }).

Session semantics

Sessions are idle-based, not tied to a single foreground period:

  • A session_id is minted on the first event after install / clear.
  • It persists across app launches in AsyncStorage.
  • It is replaced on the next event if more than sessionIdleMs (default 30 minutes) has elapsed since the last event.
  • AppState background / foreground does not by itself rotate the session — closing the app for 5 minutes and reopening keeps the same session, which matches what every other mobile analytics tool does.

This matches the convention used by GA, Mixpanel, and friends, and lines up with the browser SDK's sessionStorage-scoped tab session: both represent "the user's current burst of activity".

Linking events to server logs

The whole point of the SDK is the join with your server-side logs. To make "tap a 500 log row → jump to the session" work, your server has to know the mobile client's session_id.

The pattern: attach X-Lumin-Session to every API call, and have your server-side logger include that value as session_id on every log row it emits during that request. Lumin joins on the field automatically.

const sessionId = await lumin.getSessionId();
fetch("https://api.example.com/orders", {
  headers: { "X-Lumin-Session": sessionId, ... },
});

This SDK exposes the ids but does not inject the header for you — pick where in your networking layer to set it.

Delivery semantics

  • Batched. Up to batchSize events or flushIntervalMs, whichever fires first.
  • Auto-flushes on AppState transition to background or inactive. RN keeps the JS runtime alive for ~30 s after backgrounding, which is enough for one batched POST to land. There is no keepalive: true equivalent on RN fetch — the OS-level grace window is the contract.
  • Drops, never throws, on network failure. The onError callback is the only signal — wire it to your own observability if you care about drop rates.
  • No retries today. A failed batch is gone.
  • No on-disk spool. If the app is force-killed before a flush completes, the buffered batch is lost (≤500 ms worth at default settings).

These trade-offs match the SDK's purpose: capture user behavior, not deliver every event under adverse network conditions. If you need guaranteed delivery, write it as a server-side log instead.

Security

The API key is write-only and kind-restricted. Browser SDK keys (lmn_pub_*) can post to /v1/events for one specific project. They cannot read events, cannot touch any other project, cannot post logs or metrics, and cannot reach the app UI. Treat the key like a public token — anyone who decompiles your app can extract it. The real control is rotation, not concealment.

The SDK only sends what you pass it. It does not introspect view hierarchies, scrape form fields, or capture network traffic. The data sent to Lumin is exactly what you put in properties plus session_id, anonymous_id, and (after identify) user_id. Don't put secrets in properties.

Endpoint override. The endpoint option exists for local dev and same-origin proxies. The SDK validates the shape (requires https:// for non-local hosts, rejects paths/queries/fragments, rejects non-http(s) schemes) and throws synchronously on a bad value. This catches typos and accidental misconfiguration, not a determined attacker — code that controls the SDK config already has stronger primitives (direct fetch, etc.).

TypeScript

Ships its own .d.ts files for both ESM and CJS. InitOptions, LuminClient, AsyncStorageLike, AppStateLike, EventType, and WireEvent are exported for code that needs to reference them directly.

License

Apache-2.0. See LICENSE.