Skip to content

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).

Terminal window
pnpm add @lumin-monitor/browser
# or: npm install @lumin-monitor/browser / yarn add @lumin-monitor/browser
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.

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.

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 name
page("Pricing"); // named view
page("Pricing", { plan: "solo" }); // with properties

If 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.

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.

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.

@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.

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 this

You 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.

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).

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.

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.

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.

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.

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.