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/browser

v0.3.1

Published

Browser SDK for Lumin: page / track / identify with batched ingest.

Readme

@lumin-monitor/browser

Browser SDK for Lumin. Drop page / track / identify into your frontend; per-session timelines join with your server-side logs via session_id.

This is the one SDK in the Lumin instrumentation story. Everything else — host metrics, container metrics, container logs, Prometheus /metrics scraping — flows through the lumin-agent container next to your services. The browser is the one place an SDK is unavoidable because there is no agent inside the user's tab.

Install

pnpm add @lumin-monitor/browser
# or: npm install @lumin-monitor/browser / yarn add @lumin-monitor/browser

Zero runtime dependencies. React Router is an optional peer dep (only needed if you import @lumin-monitor/browser/react-router).

Quick start

import { init } from "@lumin-monitor/browser";

const lumin = init({
  apiKey: import.meta.env.VITE_LUMIN_BROWSER_API_KEY,
});

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

apiKey is a lmn_pub_* key minted in Settings → API keys with kind Browser SDK. The server enforces "pub keys can only post to /v1/events" on its side — even if an attacker copies the key out of devtools 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.

That's it for production — endpoint defaults to https://api.getlumin.dev. The Lumin API serves the Access-Control-Allow-* headers needed for cross-origin requests, so your app does not need to be on the same origin as Lumin.

API

init(options): LuminClient

| 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. | | onError | console.warn | Called as (err, droppedCount) when a batch fails. | | fetch | global fetch | Override for tests or environments without a global fetch. | | captureUnhandledErrors | true | Install window.error + unhandledrejection listeners. See Error capture below. |

When to override endpoint

You almost never need to. The two legitimate cases:

  1. Local development. Point at your dev server, e.g. http://localhost:8765 or http://api.localhost:8443. The SDK allows http:// only for localhost, 127.0.0.1, ::1, and any *.localhost host; everything else must be https://.
  2. Same-origin proxy. If you front Lumin through your own domain (https://acme.com/lumin reverse-proxied to api.getlumin.dev) for ad-blocker resistance or CSP simplicity, pass that base URL.

The endpoint must be a base URL with no path — pass https://api.getlumin.dev, not https://api.getlumin.dev/v1/events. The SDK validates this at construction and throws synchronously on a bad shape so misconfigurations surface at boot, not at flush time.

Returns a LuminClient with bound page, track, identify, flush, close methods. Calling init multiple times produces independent clients; apps that want one shared instance should pin it at module scope.

page(name?, properties?)

Fire on route change. For React Router or Next.js apps, hook this into your router's location-change event (or use the included React Router helper below).

page();                                  // current URL, no name
page("Pricing");                         // named view
page("Pricing", { plan: "indie" });      // with properties

If your router gives you a stable route template (/projects/:id vs the expanded path), pass it as the name — keeps cardinality bounded the same way templated routes do server-side.

track(name, properties?)

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

track("signup_completed", { plan: "indie", referrer: "hn" });
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 page load while the user is signed in. It is cheap and ensures a hard refresh still binds the session.

captureError(err, properties?)

Capture an error. Auto-installed handlers catch window.error and unhandledrejection; 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, try again");
}

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 (e.g. once via captureError and once via the auto handler) is deduped, so wrapping a throw with captureError(err); throw err; is safe.

Auto error capture

When captureUnhandledErrors is true (the default), the SDK installs listeners for the two browser events that carry unhandled exceptions:

  • window.addEventListener("error", ...) — uncaught throws
  • window.addEventListener("unhandledrejection", ...) — rejected promises that never had .catch() attached

Both go through the same captureError path. The SDK uses addEventListener (not window.onerror = …), so it coexists with other tools (Sentry, framework dev overlays) that may have installed their own handler.

What is NOT captured:

  • Source-mapped stacks. Minified bundles ship as minified stacks. Symbolication against source maps is a v2 concern.
  • Cross-origin script errors. Browsers scrub these to "Script error." with no Error object — the SDK still ships that minimum but stack and type are unavailable. Set crossorigin="anonymous" on <script> tags and serve scripts with Access-Control-Allow-Origin if you need full visibility into errors from third-party-hosted bundles.

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

flush(): Promise<void>

Force a flush of any buffered events. The SDK already auto-flushes on visibilitychange and pagehide; call this manually only when you need to guarantee delivery before a navigation to a different origin (payment redirect, OAuth handoff, etc.).

close(): Promise<void>

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

React Router integration

import { useLuminPageviews } from "@lumin-monitor/browser/react-router";

const lumin = init({ apiKey: "...", endpoint: "..." });

export default function App() {
  useLuminPageviews(lumin);
  return <Outlet />;
}

Fires page() once per pathname change. Deliberately ignores search and hash — those are usually filter state, not real pageviews, and double-counting them inflates page metrics.

Peer deps: react >= 18, react-router >= 7. Both are optional — the subpath only loads them when imported.

Linking browser events to server logs

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

The pattern: have the browser send session_id as a request header (e.g. X-Lumin-Session) on 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.

This SDK exposes the session ID but does not inject the header for you — pick where in your network layer to set it. See the Lumin stack guides for server-side recipes (Gin, Spring Boot, Express, FastAPI).

Delivery semantics

  • Batched. Up to batchSize events or flushIntervalMs, whichever fires first.
  • Auto-flushes on visibilitychange (hidden) and pagehide. These use navigator.sendBeacon when available so flushes survive tab close.
  • 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.

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 & CSP

A few things worth knowing for a security review.

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. The server returns 403 for any attempt to use a pub key on a non-events endpoint, and returns 403 for any server-kind (lmn_priv_*) key sent to /v1/events. Treat the browser key like a public token — anyone with devtools can see it. The real control is rotation, not concealment.

The SDK only reads what you pass it. It does not introspect the DOM, scrape form fields, or capture network traffic. The data sent to Lumin is exactly what you put in properties plus the auto-captured url, referrer, session_id, anonymous_id, and (after identify) user_id. Don't put secrets in properties.

Content Security Policy. If you ship a strict CSP, allowlist the ingest origin in connect-src:

Content-Security-Policy:
  connect-src 'self' https://api.getlumin.dev;

(Substitute your same-origin proxy URL if you use one.) With CSP in place, even a compromised SDK build cannot exfiltrate events to a different origin — the browser blocks the request before it leaves. This is the strongest defense if you are worried about supply-chain attacks on the SDK.

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 strictly has more powerful primitives available (direct fetch, etc.). CSP is the defense if you need to prevent exfiltration to an unexpected host.

TypeScript

Ships its own .d.ts files. The init, page, track, identify signatures are typed end-to-end. InitOptions, LuminClient, EventType, and WireEvent are exported for code that needs to reference them directly.

License

Apache-2.0. See LICENSE.