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

@xeonr/telemetry-web

v0.2.0

Published

Browser telemetry for Xeonr consoles: errors, reachability, web vitals and RPC spans as OTLP/HTTP JSON to Lantern's client ingest, and a reader for a client project's own telemetry (./read). Replaces Sentry.

Readme

@xeonr/telemetry-web

Browser telemetry for Xeonr consoles, sent to Lantern's client ingest (https://in.prod.wtf; 0.1.0 defaulted to telemetry.xeonr.io, which still works as an alias — the name moved because telemetry. hosts are on browser block lists and home DNS filters). It replaces Sentry: uncaught errors, failed and timed-out requests, web vitals and one span per RPC, as OTLP/HTTP JSON, with an offline buffer so a console that cannot reach us still reports once it can. No runtime dependencies; the browser entry is ~9 KiB min+gzip.

Every console that mounts @xeonr/console-shell ≥ 0.7 already has it — the shell's ConsoleProvider starts it. Use this package directly on a page without the shell, or for the extra pieces below (your own Connect clients, the Vite plugin).

Design: fleet observability plan §10.2; wire contract: lantern/docs/WAVE4-client-ingest.md §7 (both in platform-docs).

Install

pnpm add @xeonr/telemetry-web

Start it

import { init } from "@xeonr/telemetry-web";

init({
  service: "functions-ui",   // the Lantern client project; default <meta name="lantern-service">
  product: "functions",      // service.namespace
  // release:     default <meta name="lantern-release">, else "dev"
  // endpoint:    default https://in.prod.wtf
  // sampleRate:  0..1 for RPC spans and web vitals; errors and reachability always go
  // enabled:     default on, except on localhost/*.localhost/*.test and under
  //              <meta name="lantern-telemetry" content="off">
});

Once per page: a second init returns the first client. Nothing the SDK does ever throws into the page, and it never reports its own sends.

Authentication is the public-origin model: every request goes to <endpoint>/v1/logs?service=<service> (or /v1/traces), credentials: "omit", as text/plain so no request needs a preflight. The ingest accepts it only from an origin on the project's allow-list.

What it sends

| Record | When | Key attributes | |---|---|---| | exception (ERROR) | window error, unhandled rejection, the React boundary, captureException, a kit error toast | exception.type/message/stacktrace (raw stack — Lantern symbolicates), exception.mechanism, code.filepath/lineno/column, url.path, http.response.header.x-trace-id, app.breadcrumbs | | client.request.failed (WARN) | a request through wrapFetch or the interceptor that got no answer, timed out, or failed server-side | server.address, rpc.method, error.type = timeout\|http_5xx\|cors\|network\|aborted, client.online, client.since_last_success_ms | | web_vital (INFO, sampled) | LCP, INP, CLS (on hide), FCP, TTFB | web_vital.name/value/rating | | page_view, route_change (INFO) | load; trackRoute(path) | url.path, document_load.* | | CLIENT span (sampled) | every RPC / wrapped fetch | named by the procedure; rpc.method, server.address, rpc.response.status_code |

Every record carries session.id (per tab session); the resource carries service.instance.id (per browser install), telemetry.sdk.* and browser.*. Breadcrumbs are the last 20 route changes and request outcomes — procedure and code, never a payload. Messages lose URL query strings, email addresses and bearer-looking secrets before they leave; stack frames are sent untouched.

A browser cannot tell DNS from TLS from a refused connection: fetch rejects with the same TypeError for all of them. Offline is network; a same-origin failure is network; a cross-origin one is cors if a no-cors probe of the same URL gets an answer, else network. dns and tls are never sent from a browser.

Delivery: batches every 5 s or at 50 records. A send that gets no answer, a 408, a 429 or a 5xx moves its records to localStorage (at most 200 records / 256 KiB, oldest non-errors dropped first) and waits for Retry-After or a backoff; the next page load replays them. On pagehide / hidden, whatever is left goes by navigator.sendBeacon.

Errors

import { captureException, traceIdFrom } from "@xeonr/telemetry-web";

try { await save(); } catch (err) {
  captureException(err, { extra: { "app.form": "schedule" } });   // route and X-Trace-Id are filled in
  showError(err, traceIdFrom(err));   // "trace 1a2b3c4d… [copy]"
}

traceIdFrom(errOrResponse) reads X-Trace-Id from a ConnectError's metadata, a fetch Response's headers, or a traceId (and from their cause). The kit's TraceRef renders it.

React

import { TelemetryErrorBoundary } from "@xeonr/telemetry-web/react";

<TelemetryErrorBoundary resetKeys={[pathname]} fallback={({ error, traceId, reset }) => <Oops traceId={traceId} onRetry={reset} />}>
  <App />
</TelemetryErrorBoundary>

Requests

// ConnectRPC: first in the list, so it times what the others do.
import { telemetryInterceptor } from "@xeonr/telemetry-web/connect";
createConnectTransport({ baseUrl, interceptors: [telemetryInterceptor(), ...yours] });

// fetch
import { wrapFetch } from "@xeonr/telemetry-web";
const api = wrapFetch();            // wraps the global fetch; or wrapFetch(myFetch)

A same-origin call carries a W3C traceparent, so the browser span parents the server's trace at the edge. A cross-origin API gets none unless you say so — its CORS policy has to allow the header first:

init({ service: "x-ui", getTraceHeaders: (url, { traceparent }) => url.hostname === "api.x.xeonr.io" ? { traceparent } : undefined });

Releases and source maps (Vite)

// vite.config.ts
import { lanternTelemetry } from "@xeonr/telemetry-web/vite";

export default defineConfig({
  plugins: [react(), lanternTelemetry({ service: "functions-ui" })],
});

The plugin:

  1. writes <meta name="lantern-service"> and <meta name="lantern-release"> into index.html — the release is VERSION, else VITE_APP_VERSION, else dev — so the SDK needs no configuration for either;
  2. forces build.sourcemap = "hidden" for the browser's build (maps are written, no bundle links them). A server environment (SSR, a Worker) writes none unless its own environments.<name>.build.sourcemap asks for them;
  3. after the browser's build, with CI_JOB_TOKEN (or LANTERN_RELEASE_KEY) set, registers the release (POST /v1/releases) and uploads every **/*.map in that build's outDir (gzipped) to /v1/sourcemaps?service=&release=&path= keyed by the .js it belongs to. A map with empty mappings (an empty chunk's) is skipped. LANTERN_INGEST_URL moves the endpoint. A failed upload warns; it never fails the build;
  4. deletes the .map files from that outDir, so they are never served.

The CI job token must come from a GitLab project listed in the Lantern client project's CI projects (LAN-136); a release key needs no binding.

Vite 6+ environment builds (React Router framework mode, SSR, Workers)

Vite calls the plugin once per environment. Only the client environment's build is a browser's, so the upload reads that environment's own outDir (build/client in React Router), never the top-level dist nothing writes, and the server's build is left alone. There is no index.html to put the tags in, so render them from the values the plugin defines:

// root.tsx
<meta name="lantern-service" content={import.meta.env.VITE_LANTERN_SERVICE} />
<meta name="lantern-release" content={import.meta.env.VITE_LANTERN_RELEASE} />

The k8s pipeline already passes CI_JOB_TOKEN and VERSION to every image build; declare them in the builder stage, before vite build:

FROM node:24-slim AS builder
WORKDIR /src
RUN corepack enable
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
ARG CI_JOB_TOKEN
ARG VERSION
RUN pnpm run build          # vite build: meta tags, hidden maps, upload, maps deleted

FROM nginx:alpine
COPY --from=builder /src/dist /usr/share/nginx/html

The ARGs sit after the install so a new version or token does not bust the dependency layer, and they never reach the final stage.

Reading telemetry back (./read)

A product that shows its own customers their devices' logs (an admin "Logs" pane, a TV's session log) reads them from Lantern with a client token: a device token minted from one of the client project's device keys with a read grant, by the product's backend. The grant is the whole authorization; lantern-ingest ANDs its filters onto every read, so the page can narrow and never widen.

import { createReader } from "@xeonr/telemetry-web/read";

const reader = createReader({ token: () => api.mintTelemetryReadToken(scope) });

// History, newest first, cursor-paged.
const { records, nextCursor } = await reader.listLogs(
  { minSeverity: "warn", filters: [{ key: "attr:session_id", values: [sessionId] }] },
  { limit: 200 },
);

// Events, facets and a timeline.
await reader.listEvents({ from });
await reader.facets("logs", ["severity", "service.instance.id"], { from });
await reader.timeline("logs", { from }, "severity");

// Live: records as they land. Reconnects by itself (a stream lasts 30
// minutes, and the token function is called again each time); "refused"
// is a verdict and stops it.
const tail = reader.tail({ onRecords: (rs) => append(rs), onStatus: (s, why) => show(s, why) });
tail.close();

Filter keys are service.instance.id (the device, install or tab), service.version, severity, body, event.name, trace_id, span_id and attr:<key> for a record's own attribute. It is the Connect protocol over JSON with no dependencies, so React Native can use it too, except tail(), which needs a streaming fetch body. Poll listLogs there. The service is lantern.api.v1.ClientReadService (xeonr/protocol modules/lantern-api).

Testing against it

createClient(options, env) builds a client against an injected environment (fetch, storage, timers, events) instead of the page's, without touching the page-wide singleton.