Browser SDK (@lumin-monitor/browser)
@lumin-monitor/browser is the web half of the Lumin instrumentation story; @lumin-monitor/react-native is the mobile half. Everything else — logs, metrics, container observation — flows through the agent. A tab is one of the two places an SDK is unavoidable, because there is no agent inside it.
The SDK does four things: page for view tracking, track for custom events, identify for binding the current anonymous session to a known user, and automatic capture of unhandled errors. They all end up in the same event stream and share a session_id with your server-side logs (if you propagate it).
Install
Section titled “Install”pnpm add @lumin-monitor/browser# or: npm install @lumin-monitor/browser / yarn add @lumin-monitor/browserInitialise once at app boot
Section titled “Initialise once at app boot”import { init } from "@lumin-monitor/browser";
const lumin = init({ apiKey: import.meta.env.VITE_LUMIN_BROWSER_API_KEY, endpoint: "https://api.getlumin.dev",});
// Bound methods, safe to destructure:export const { page, track, identify, flush, close } = lumin;The apiKey is a lmn_pub_* key minted in Settings → API keys with Kind = Browser SDK. It will be visible to anyone who opens devtools on your site — that’s expected; the server enforces “pub keys can only post /v1/events”, so a leaked browser key cannot read data, post logs/metrics, or reach any other project. If it leaks, rotate it from the same screen.
A lmn_priv_* key (the server / agent kind) sent here returns 403 — go back to the mint screen and pick “Browser SDK” instead.
Set endpoint to your Lumin ingest host. For SaaS that’s https://api.getlumin.dev.
Options
Section titled “Options”| Option | Default | Notes |
|---|---|---|
apiKey |
— | Required. lmn_pub_… from Settings → API keys (kind: Browser SDK). |
endpoint |
— | Required. Lumin ingest base URL. |
batchSize |
50 |
Max events buffered before a forced flush. |
flushIntervalMs |
500 |
Max ms between flushes. |
captureUnhandledErrors |
true |
Auto-capture unhandled errors. See below. |
visitorPing |
false |
Fire one anonymous visitor heartbeat at init. |
onError |
console.warn |
Called with the failed batch + dropped count. |
fetch |
global fetch |
Override for tests or for environments without a global fetch. |
Page views
Section titled “Page views”Fire page() on route change. For React Router or Next.js apps, hook this into your router’s location-change event.
page(); // current URL, no namepage("Pricing"); // named viewpage("Pricing", { plan: "solo" }); // with propertiesIf your router gives you a stable route template (/projects/:id vs the expanded path), pass it as the name. That keeps cardinality bounded the same way templated routes do server-side.
Custom events
Section titled “Custom events”track("signup_completed", { plan: "solo", referrer: "hn" });track("checkout_clicked");Event names are free-form. The Sessions page lets you filter by name, so keep them stable: signup_completed is good, signup_completed_v2_2026_05 is not.
Identify a user
Section titled “Identify a user”Call identify() once you know who the user is — typically after login. The SDK already has an anonymous session ID; identify binds that session to the userId you pass, and every prior event in the session is retroactively associated with that user when the session timeline is rendered.
identify("user_abc123");identify("user_abc123", { plan: "solo", signedUpAt: "2026-05-01" });Re-call identify with the same userId on every page load while the user is signed in — it’s cheap and makes sure refreshes still bind the session.
React Router integration
Section titled “React Router integration”@lumin-monitor/browser ships a React Router helper that hooks page tracking onto navigation. Import from the /react-router subpath:
import { useLuminPageviews } from "@lumin-monitor/browser/react-router";Mount the hook once in your root component; it fires page() on every location change.
Unhandled errors
Section titled “Unhandled errors”Auto-capture is on by default. The SDK subscribes to error and unhandledrejection with addEventListener rather than assigning window.onerror, so it coexists with any other handler your app installs.
init({ apiKey, endpoint, captureUnhandledErrors: false }); // e.g. Sentry owns thisYou can also report a caught error yourself:
captureError(err, { component: "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 pages 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.
Flushing on unload
Section titled “Flushing on unload”The SDK auto-flushes on visibilitychange and on pagehide. If you need to force a flush manually (e.g. before navigating to a different origin):
await flush();close() does the same and also tears down the SDK; only use it if you genuinely want to stop emitting events for the rest of the page lifetime (uncommon).
Linking browser events to server logs
Section titled “Linking browser events to server logs”You can tag your server-side log rows with the browser’s session_id and then filter the Logs page by it. Three steps, and none of them is automatic.
1. Send the id from the browser
Section titled “1. Send the id from the browser”The SDK does not inject headers for you, and it does not expose the session id on the client object either. Read it from sessionStorage, where the SDK keeps it under lumin.session_id:
export function luminSessionId(): string | null { try { return sessionStorage.getItem("lumin.session_id"); } catch { return null; // Safari private mode, sandboxed iframe }}
// Attach it to your own API calls:const sid = luminSessionId();await fetch("/api/projects", { headers: sid ? { "X-Lumin-Session": sid } : {},});The key is written when you call init(). Read it after that, and expect null on the very first paint if you read it earlier. X-Lumin-Session is a name this guide picked, not a protocol — use any header you like, as long as your server reads the same one.
2. Put it on your log rows
Section titled “2. Put it on your log rows”Bind the header value into your request logger so every line emitted during that request carries it. Each server guide has the snippet: Gin, Spring Boot, Express, FastAPI.
3. Filter the Logs page by it
Section titled “3. Filter the Logs page by it”The field arrives inside the row’s attributes, so query it with the attr. prefix:
attr.session_id:0f8c2a41-...That works on historical queries and on live tail. You can also expand any log row that carries the field and click session_id in the detail panel — the UI composes the same term for you.
What this does not do
Section titled “What this does not do”There is no clickable pivot from a browser session to its logs, and none from a log row back to a session. The Sessions page and the Logs page are separate views joined by a value you copy between them; the session id is the last segment of the session detail URL. There is also no session_id column — the field lives in attributes like any other, which is why the filter needs the attr. prefix.
Session replay does not exist either. The session detail page shows the event history for a session, not a recording of the page.