Node's HTTP instrumentations, measured against the same contract as every other language.
express/scenarios/ what the server does, no OTel
express/opentelemetry-express/ server.js, server/
fetch/scenarios/ what the browser Fetch client does, no OTel
fetch/opentelemetry-fetch/ browser.js, client/
http/scenarios/ built-in client and server, no OTel
http/opentelemetry-http/ client.js, server.js, client/, server/
undici/scenarios/ what the client does, no OTel
undici/opentelemetry-undici/ client.js, client/
This directory is the build root: one npm workspace, whose package.json and
package-lock.json are committed so a run measures pinned versions. It pulls
in the shared packages under tools/js and
tools/http/test-client/js by path rather
than restating them.
A <library>/scenarios package is the workload with no OpenTelemetry in it at
all, so a second instrumentation of the same library measures the same
behavior. Each <instrumentation> package is only the launch: it registers what
it is measuring and hands the workload over.
Client and server are separate packages even where one library has both, since coverage reduces everything a package emits and combining them could hide a client span in a server run.
The server declares its routes in Express's own routing API, because that
declaration is what an instrumentation reads http.route from:
app.get("/health", answer);
app.get("/users/:userId", answer);
app.post("/items", answer);
app.get("/status/:code", answer);Answering is a shared lookup by concrete method and path, so no scenario restates a status or a response body.
Two instrumentations are registered rather than one:
@opentelemetry/instrumentation-express produces http.route and a span per
middleware and route handler, but not the server span they hang from — that is
@opentelemetry/instrumentation-http's. The package is named for the one being
measured, and the other is an ordinary dependency of it.
undici does not go through Node's http module, so its client spans come from
@opentelemetry/instrumentation-undici alone. The requests go through
undici.request, which reports every status rather than throwing on 4xx and
5xx: the contract's failing statuses are traffic to be measured like any other.
Fetch is a browser API, so its scenario opens a headless browser through the shared browser runner. The runner serves the bundled browser workload, reserves only its own exporter and script endpoints, and proxies every contract request to the mock server unchanged. Browser spans therefore retain the contract's exact path and query string. Its telemetry bridge accepts the browser's OTLP/HTTP protobuf exports and forwards them to the runner's OTLP/gRPC collector; the exporter endpoint is ignored by Fetch instrumentation.
The browser runner first launches an installed Google Chrome and falls back to Playwright-managed Chromium. Install that fallback once in the build root when Chrome is unavailable:
npm exec playwright install chromiumUse Node 22.19.0 or newer. The lockfile pins undici@8.10.0, which requires
that version.
pip install -e tools/runner -e tools/http/runner -e tools/http/mock-server \
-e tools/http/test-client/python -e tools/js
otel-conformance scenarios/http/js/express/opentelemetry-express/server
otel-conformance scenarios/http/js/fetch/opentelemetry-fetch/client
otel-conformance scenarios/http/js/http/opentelemetry-http/client
otel-conformance scenarios/http/js/http/opentelemetry-http/server
otel-conformance scenarios/http/js/undici/opentelemetry-undici/clientEach package's setup: is otel-conformance-js install, which installs this
whole workspace from the lockfile. Because the shared packages are installed as
copies rather than links, editing one under tools/ takes effect on the next
install — which every run performs, so no run measures a stale copy, but an
npm ci here is what makes an edit visible to anything else.
The helper's own unit tests and the formatter run from here:
npm --prefix scenarios/http/js test
npm --prefix scenarios/http/js run format:check