Skip to the content

OpenTelemetry without zone.js

Keep the active span of OpenTelemetry through await in the browser, with the context manager of the library.

The problem

OpenTelemetry keeps the active span in a context. In the browser, the ZoneContextManager of OpenTelemetry keeps the context with zone.js. zone.js patches the callback APIs, but it does not see a native await. After an await, a span can get the wrong parent, or no parent.

AsyncContextManager keeps the context with the AsyncLocalStorage class of this library. In code that the Vite plugin or the Babel preset transforms, the active span stays correct after each await.

Install

npm install async-browser-context @opentelemetry/api @opentelemetry/sdk-trace-web

Set up the transform. The first guide tells you how.

Register the context manager

// tracing.ts
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
import { AsyncContextManager } from "async-browser-context/opentelemetry";

const provider = new WebTracerProvider();
provider.register({ contextManager : new AsyncContextManager() });

Import async-browser-context before all other modules in the entry file of the application. The library patches the global functions when it loads (refer to Patched APIs).

Make spans

import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("shop");

async function checkout(cart : Cart) : Promise<void> {
    await tracer.startActiveSpan("checkout", async (span) => {
        const prices = await loadPrices(cart);
        // The span of loadPrices() and of save() have the parent "checkout".
        await save(cart, prices);
        span.end();
    });
}

Without the library, the code after the first await can get the root context. Then the spans of save() start a new trace.

What the context manager does

MethodWhat it does
active()Gives the active context, or the root context outside with().
with(context, fn, thisArg, ...args)Starts fn with the context as the active context. The context stays after each await in transformed code, and in the callbacks of the patched APIs.
bind(context, target)Gives a function that starts target in the context. The function keeps the this value, the arguments and the length. Other values stay as they are.
disable()active() gives the root context until the next with().

On Node.js, the node export condition selects an entry with the native AsyncLocalStorage. Thus, code that runs on the server and in the browser can use one import.

Cautionzone.js

Do not use the library together with zone.js or with the ZoneContextManager. The two libraries patch the same functions with different rules.

Tests

The tests in test/api/opentelemetry.test.ts use the context manager through the API of OpenTelemetry. They examine await, timers, then callbacks, two operations at the same time, bind() and the active span. The tests operate on Node.js and in Chromium, Firefox and WebKit.

async-browser-context: AsyncLocalStorage and the TC39 AsyncContext API for browsers. The source code is on GitHub.

To change a page, edit its file in 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.