Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions scenarios/http/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ other language.
```text
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
Expand Down Expand Up @@ -56,6 +58,24 @@ undici does not go through Node's `http` module, so its client spans come from
`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

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:

```sh
npm exec playwright install chromium
```

## Running one

Use Node 22.19.0 or newer. The lockfile pins `undici@8.10.0`, which requires
Expand All @@ -65,6 +85,7 @@ 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/client
Expand Down
87 changes: 87 additions & 0 deletions scenarios/http/js/fetch/opentelemetry-fetch/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

"use strict";

const {
OTLPTraceExporter,
} = require("@opentelemetry/exporter-trace-otlp-proto");
const { registerInstrumentations } = require("@opentelemetry/instrumentation");
const {
FetchInstrumentation,
} = require("@opentelemetry/instrumentation-fetch");
const {
SimpleSpanProcessor,
WebTracerProvider,
} = require("@opentelemetry/sdk-trace-web");
const { drive } = require("@otel-conformance/fetch-scenarios");

/** Resolves once exactly `expected` Fetch spans have reached the exporter. */
function trackingExporter(exporter, expected) {
let exported = 0;
let resolve;
let reject;
const complete = new Promise((onResolve, onReject) => {
resolve = onResolve;
reject = onReject;
});
return {
export(spans, callback) {
exporter.export(spans, (result) => {
callback(result);
if (result.code !== 0) {
reject(result.error || new Error("OTLP export failed"));
return;
}
exported += spans.length;
if (exported === expected) {
resolve();
} else if (exported > expected) {
reject(new Error(`exported ${exported} spans, expected ${expected}`));
}
});
},
forceFlush() {
return exporter.forceFlush();
},
shutdown() {
return exporter.shutdown();
},
waitForExpectedSpans() {
return complete;
},
};
}

async function run() {
const { baseUrl, exchanges, traceExporterUrl } = window.__otelConformance;
const exporter = trackingExporter(
new OTLPTraceExporter({ url: traceExporterUrl }),
exchanges.filter((exchange) => !exchange.readiness).length,
);
const provider = new WebTracerProvider({
spanProcessors: [new SimpleSpanProcessor(exporter)],
});
provider.register();
registerInstrumentations({
tracerProvider: provider,
instrumentations: [
new FetchInstrumentation({ ignoreUrls: [traceExporterUrl] }),
],
});
await drive(baseUrl, exchanges);
await exporter.waitForExpectedSpans();
await provider.forceFlush();
}

run().then(
() => {
window.__otelConformanceResult = { ok: true };
},
(error) => {
window.__otelConformanceResult = {
ok: false,
error: String(error.stack || error),
};
},
);
17 changes: 17 additions & 0 deletions scenarios/http/js/fetch/opentelemetry-fetch/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

"use strict";

const path = require("node:path");

const { runBrowserScenario } = require("@otel-conformance/browser-runner");
const { scenarioRequest } = require("@otel-conformance/http-test-client");

runBrowserScenario({
entrypoint: path.join(__dirname, "browser.js"),
exchanges: [scenarioRequest()],
}).catch((error) => {
console.error(error);
process.exitCode = 1;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

runner: http-conformance
instrumented_library: fetch
instrumentation_library: "@opentelemetry/instrumentation-fetch"

setup: otel-conformance-js install

server:
run: http-mock-server --port ${PORT}

scenarios:
client:
run: node ../client.js
53 changes: 53 additions & 0 deletions scenarios/http/js/fetch/opentelemetry-fetch/client/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"spans": {
"http.client": [
"error.type",
"http.request.method",
"http.response.status_code",
"server.address",
"server.port",
"url.full"
]
},
"events": {},
"metrics": {},
"entities": {
"service": {
"identity": [
"service.name"
],
"description": []
},
"telemetry.sdk": {
"identity": [
"telemetry.sdk.language",
"telemetry.sdk.name"
],
"description": [
"telemetry.sdk.version"
]
}
},
"findings": [
{
"id": "recommended_attribute_not_present",
"message": "Span 'GET' is missing recommended attribute 'network.protocol.version'",
"signal_type": "span",
"signal_name": "GET",
"context": {
"attribute_key": "network.protocol.version",
"kind": "client"
}
},
{
"id": "recommended_attribute_not_present",
"message": "Span 'POST' is missing recommended attribute 'network.protocol.version'",
"signal_type": "span",
"signal_name": "POST",
"context": {
"attribute_key": "network.protocol.version",
"kind": "client"
}
}
]
}
16 changes: 16 additions & 0 deletions scenarios/http/js/fetch/opentelemetry-fetch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@otel-conformance/fetch-opentelemetry-fetch",
"version": "0.1.0",
"private": true,
"description": "Browser Fetch measured through @opentelemetry/instrumentation-fetch.",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/exporter-trace-otlp-proto": "0.221.0",
"@opentelemetry/instrumentation": "0.221.0",
"@opentelemetry/instrumentation-fetch": "0.221.0",
"@opentelemetry/sdk-trace-web": "2.10.0",
"@otel-conformance/browser-runner": "file:../../../../../tools/js/browser-runner",
"@otel-conformance/fetch-scenarios": "*",
"@otel-conformance/http-test-client": "file:../../../../../tools/http/test-client/js"
}
}
Loading