Skip to the content

Quick start

Add the monitors to a browser app, send the main-thread metrics to an OpenTelemetry backend, and stop the monitors.

Note

This page is a draft. The content is not complete and can change.

This page tells you how to add the monitors to a browser app. You need an OpenTelemetry setup for the page. The examples use otel-ts, but each OpenTelemetry Meter and Logger works.

Before you start

You need:

  • an app that a bundler builds, for example Vite. The bundler must support new Worker(new URL(...), { type: "module" }).
  • an OTLP/HTTP endpoint, for example the Grafana stack on your computer.

Install

Install the package from npm:

npm install @mark1russell7/lag

The package is an ES module with TypeScript types. @mark1russell7/lag has the monitors, and @mark1russell7/lag/worker has createLagWorker(). The API reference lists all entry points. The package page on npm has a short example with the OpenTelemetry SDK.

otel-ts is not on npm. To use it as in these examples, install it from GitHub:

npm install github:mark1russell7/otel-ts

NoteThe Vite dev server

The dev server of Vite optimizes the dependencies into one folder. Then it cannot find the file of the worker. Exclude the worker export from the optimization in vite.config.ts: optimizeDeps: { exclude: ["@mark1russell7/lag/worker"] }. vite build needs no setting.

Start the monitors

import { createInstanceId, init } from "@mark1russell7/otel-ts";
import { createBrowserDeps, createOtelEventSink, createOtelLoggerAdapter, setupAllMonitors } from "@mark1russell7/lag";
import { createLagWorker } from "@mark1russell7/lag/worker";

const endpoint = "http://localhost:4318";
const serviceInstanceId = createInstanceId();

const otel = init({
    serviceName : "shop",
    serviceInstanceId,
    endpoint,
    histogramAggregation : "exponential",
});

const worker = createLagWorker();
const monitors = setupAllMonitors(createBrowserDeps(window, {
    meter : otel.getMeter("lag"),
    logger : createOtelLoggerAdapter(otel.getLogger("lag")),
    events : createOtelEventSink(otel.getLogger("lag-events")),
    worker,
    workerHangReport : {
        url : `${endpoint}/v1/logs`,
        resource : { "service.name" : "shop", "service.instance.id" : serviceInstanceId },
    },
    pageContext : () => ({ "session.id" : otel.getSessionId() }),
}));

// Record the pending values of the monitors before each export
otel.onBeforeFlush(() => monitors.flush());

The code does these steps:

  1. init() starts the OpenTelemetry SDK with a new service.instance.id. Histograms use the exponential aggregation (native histograms in Mimir).
  2. createLagWorker() starts the Web Worker of the worker-lag monitor.
  3. createBrowserDeps(window, options) gets the browser APIs. It examines each API, thus each browser gets the monitors that it can support.
  4. setupAllMonitors() starts the monitors and gives their handles.
  5. onBeforeFlush makes sure that the export at page hide contains the final values of the page view.

The worker sends its hang reports without the OpenTelemetry SDK, because the main thread cannot operate during a hang. workerHangReport.resource gives the reports the same service.instance.id as the page. pageContext gives them the session ID.

Warning

Connect monitors.flush() to the exporter. The otel-ts library flushes after the monitors recorded the end of the page, because both use the lifecycle tracker that the page shares. An exporter that listens to pagehide itself can flush before the monitors record their final values in Chromium. Refer to page lifecycle.

What the monitors need

setupAllMonitors() starts a monitor only when its dependencies exist:

DependencyMonitors
Timers, document, window (always necessary)Lifecycle, DriftLag, MacrotaskLag, timer throttle
PerformanceObserverLong animation frames, Event Timing, layout shift, page-view vitals
requestAnimationFrameFrame timing
requestIdleCallback (not in Safari)Idle availability
MessageChannel and queueMicrotaskScheduling fairness
worker and performanceWorker lag
worker and a cross-origin-isolated pageShared-memory liveness
performance and the wall clockClock reliability, clock drift

createBrowserDeps finds each of these in the browser. To change a dependency, give your own object of type AllMonitorDeps to setupAllMonitors().

Send the metrics to a backend

Start the Grafana stack on your computer:

cd node_modules/@mark1russell7/grafana-infra
docker compose up -d

Then open Grafana at http://localhost:3000. The dashboards page tells you what each panel shows.

Stop the monitors

monitors.stop();
worker.terminate();

stop() stops each monitor in the reverse sequence of their start. It releases each timer, listener and observer. The worker is yours: stop it with worker.terminate().

Next steps

lag: Main-thread responsiveness monitoring for browser apps, exported as OpenTelemetry metrics.

To change a page, edit its file in packages/site/content/. The writing style guide tells you how.

An AI model (Claude, from Anthropic) wrote most of the text and the code of this site and of the library, under the direction of the author. The tests and the STE linter examine them. The writing standard gives the reason for this note.