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

@digiform-by-gs/observability-browser

v0.1.0

Published

Real user monitoring for the browser — page-load traces, browser→backend trace joining, web vitals, and JS errors over OTLP.

Readme

@digiform-by-gs/observability-browser

Real user monitoring for the browser. Page-load traces, browser→backend trace joining, Core Web Vitals, and JS errors — all over OTLP to the same collector your services already use.

The browser counterpart of @digiform-by-gs/observability. It answers the one question server-side instrumentation structurally cannot: what did the user actually experience?

Install

npm install @digiform-by-gs/[email protected]

Compatibility

| Requirement | Version | Notes | |---|---|---| | Browsers | Any with PerformanceObserver | Chrome/Edge 64+, Firefox 58+, Safari 14.1+. Degrades quietly on older ones. | | Module system | ESM only | No CommonJS build. Bundlers (Vite, webpack 5, Next.js, Rollup) handle this natively. | | OpenTelemetry JS | SDK ^2.10.0, experimental ^0.221.0 | A compatible set, pinned together. | | @opentelemetry/api | ^1.9.1 | sdk-trace-web peers >=1.0.0 <1.10.0, so 1.10 will need a coordinated bump. | | web-vitals | ^6.2.1 | |

This package is on a newer OpenTelemetry line than the Node wrapper (2.10/0.221 vs 2.7/0.215). That is forced, not sloppy: @opentelemetry/instrumentation-fetch pins sdk-trace-web to an exact version, so the browser instrumentations decide the line. The two packages target different runtimes and are never installed in the same bundle, so they do not need to agree.

Do not install @opentelemetry/api yourself — it is already a dependency here, and two copies at different majors break context propagation silently.

Platform requirement

The collector needs a browser OTLP receiver with CORS (port 4319 on this platform). A standard OTLP endpoint has no CORS headers, so every export dies at the preflight with nothing in your logs to explain it. Ask your platform operator to add your origin to the allowlist — this is a deliberate, per-origin operation.

Quickstart

import { initBrowserObservability } from '@digiform-by-gs/observability-browser';

initBrowserObservability({
  serviceName: 'shop-browser',
  endpoint: import.meta.env.VITE_OTLP_BROWSER_ENDPOINT, // e.g. http://collector.internal:4319
  environment: import.meta.env.MODE,
});

Call it once, as early as possible — document-load timings are captured from the Performance API, so a late call still gets them, but fetches made before initialization are not traced.

In Next.js, call it from pages/_app (pages router) or a 'use client' component mounted in the root layout (app router). It complements @vercel/otel, which covers the server side, rather than replacing it.

Options

| Option | Type | Default | Notes | |---|---|---|---| | serviceName | string | — | Required. Use a name distinct from the server-side service. | | endpoint | string | — | Required. No default — see below. | | propagateTo | (string \| RegExp)[] | [] | URLs that receive traceparent. Read the warning below before setting. | | environment | string | production | | | serviceVersion | string | 0.0.0 | | | route | () => string | () => 'unknown' | Returns the current route template. | | captureErrors | boolean | true | | | captureWebVitals | boolean | true | | | resourceAttributes | Record<string,string> | {} | | | metricExportIntervalMs | number | 60000 | |

There is no environment-variable layer. process.env does not exist in a browser, and a bundler replaces it at build time — an "env fallback" would look like a runtime read while actually being a literal frozen when the bundle was compiled. Pass your build-time variables in explicitly.

Four things that will bite you

1. propagateTo can break your application

Adding a traceparent header makes a cross-origin request preflighted. If your backend does not list traceparent in Access-Control-Allow-Headers, the preflight fails and the real request never happens. Enabling this against an unprepared backend does not merely lose correlation — it takes the app down.

Sequence it: backend CORS first, propagateTo second. This is why the default is empty.

Same-origin requests are never preflighted, so if your API is served from the same host as your app (https://app.example.com and https://app.example.com/api/), none of this applies — propagation is free and safe, and the backend needs no CORS change at all. Check the origin before assuming you need one.

Access-Control-Allow-Headers: traceparent, tracestate, content-type

Also add Timing-Allow-Origin on the backend, or cross-origin spans lose their timing detail.

2. endpoint has no default, on purpose

The Node wrapper defaults to http://localhost:4318, which is correct there — the process runs beside the collector. In a browser, localhost is the visitor's own machine. That default would work flawlessly on your laptop and report nothing from a single real user. This package refuses to start instead.

The endpoint ships inside your JS bundle and is readable by anyone. Treat it as public; it is why the collector uses an origin allowlist.

3. If your app is served over HTTPS, proxy through your own origin

This is the common case, and going straight at the collector does not work:

  • Mixed content. A page on https:// cannot POST to an http:// endpoint. Browsers block it before the request leaves, and no CORS configuration changes that.
  • Routability. A collector on a private address is not reachable from a user's phone or home network at all.

Rather than exposing the collector publicly with TLS, forward a path from the app's own origin:

// next.config.js
async rewrites() {
  return [{ source: '/otel/:path*', destination: 'http://collector.internal:4319/:path*' }];
}
initBrowserObservability({ serviceName: 'shop-browser', endpoint: '/otel' });

endpoint accepts a path as well as a URL. The browser then posts same-origin, so there is no mixed content, no preflight, and no CORS involved at all — the collector stays private and the plain-HTTP hop happens server-side. Your app server must be able to reach the collector.

The equivalent for other stacks is the same idea: a vite.config proxy in development, or an nginx/ingress location block in production.

4. route must return a template, never a URL

route: () => '/orders/:id'   // correct
route: () => location.pathname // WRONG: /orders/42, /orders/43, ...

Web vitals are labelled with route. A concrete path makes every distinct URL its own metric series, which fills the platform's metric store — and on a shared platform that rejects writes for every service, not just yours. When in doubt, return a constant.

What it collects

| Signal | Source | |---|---| | Page load traces | document-load — DNS, TCP, TTFB, DOM processing, resource fetches | | HTTP spans | fetch and XMLHttpRequest, with optional trace propagation | | Web vitals | browser.web_vital.{lcp,inp,cls,ttfb,fcp} histograms, labelled route + rating only | | Errors | error and unhandledrejection as OTLP logs, correlated to the active trace |

user-interaction instrumentation is deliberately not included: it names spans after event type plus DOM target, which is unbounded and would inflate span-metrics cardinality.

Custom instrumentation

import { getTracer, getMeter, getLogger } from '@digiform-by-gs/observability-browser';

const span = getTracer('checkout').startSpan('apply-coupon');
// ...
span.end();

These return the API's no-op implementations before initialization rather than throwing. Module evaluation order in a bundle is not fully under your control, and a component recording a metric at import time must not white-screen the page.

Troubleshooting

| Symptom | Cause | |---|---| | Nothing arrives; console shows a CORS error | Your origin is not on the collector's allowlist, or you are posting to the plain OTLP port instead of the browser one. | | Nothing arrives; no console error at all | An ad blocker or privacy extension is dropping the request. Common ones block URLs containing telemetry, analytics, or collect. | | API calls start failing right after enabling propagateTo | The backend rejects the traceparent preflight. See trap 1 — revert propagateTo, fix the backend's CORS, then re-enable. | | endpoint is required thrown at startup | Your build-time variable is undefined. Bundlers silently substitute undefined for a missing VITE_* / NEXT_PUBLIC_*. | | Browser and backend spans are in separate traces | propagateTo does not match the API URL, or the backend strips the header. | | Page-load spans but no fetch spans | Requests are being made before initBrowserObservability() runs. | | Vitals appear with route="unknown" | No route option supplied. Harmless, but you cannot break results down by page. |

Server-side rendering

initBrowserObservability() returns a no-op handle when window is undefined, so importing it from code that also renders on the server is safe. A frontend must not break because its telemetry could not start.