Skip to content

Commit b9fcb1f

Browse files
matt-aitkenclaude
andcommitted
fix(test/utils/tracing): disable OTel globals before re-registering in createInMemoryTracing
The previous form (commit 133d468) skipped `provider.register()` to avoid "Attempted duplicate registration of API: trace" when another test in the same shard had already loaded `~/v3/tracer.server.ts` (which calls register() via its `singleton("opentelemetry", …)`). That fix went too far: tests like `sentryTraceContext.server.test.ts` exercise OTel's *global* API (`trace.getActiveSpan()` via `getActiveTraceIds`), which requires a registered global context manager. Without register(), `trace.getActiveSpan()` returned undefined — 6/9 assertions in that file flipped red on shard 1 of the latest CI run. Webapp's vitest config uses `pool: "forks"` with `--no-file-parallelism`, so all test files in a shard share one process. globalThis persists across files even though vitest clears the module cache between them. OTel's `setGlobal*` no-ops (logs an error, returns false) when a global is already set — so the second caller's register() is a no-op, leaving the globals pointed at the first caller's provider. Fix: call `trace.disable(); context.disable(); propagation.disable();` *before* `provider.register()`. The disables clear any previously-set globals (whether from `tracer.server.ts`'s singleton or a prior createInMemoryTracing call), so the test's provider always wins for both span recording (via SimpleSpanProcessor) AND the global API. Each createInMemoryTracing call rotates the globals; no leakage to the next test file. Verified locally: `pnpm vitest run test/sentryTraceContext.server.test.ts` 9/9 pass. Running it alongside test/engine/triggerTask.test.ts (which loads tracer.server.ts) reaches the test-setup phase normally — no more module-load OTel collision. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c70d755 commit b9fcb1f

1 file changed

Lines changed: 25 additions & 11 deletions

File tree

apps/webapp/test/utils/tracing.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { context, propagation, trace } from "@opentelemetry/api";
12
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
23
import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
34
import {
@@ -8,22 +9,35 @@ import {
89
} from "@opentelemetry/sdk-metrics";
910

1011
export function createInMemoryTracing() {
11-
// Initialize the tracer provider and exporter — but do NOT call
12-
// `provider.register()`. Calling register() sets the OTel global APIs
13-
// (trace/context/propagation), and webapp's `~/v3/tracer.server.ts`
14-
// also calls register() via its singleton. Webapp's `vitest.config.ts`
15-
// uses `pool: "forks"` with `--no-file-parallelism`, so all test
16-
// files in a shard share one process — globals set by the first test
17-
// to load tracer.server.ts conflict with subsequent createInMemoryTracing
18-
// calls, throwing "Attempted duplicate registration of API: trace".
12+
// Webapp's vitest config uses `pool: "forks"` with `--no-file-parallelism`,
13+
// so all test files in a shard share one process. globalThis persists
14+
// across files even though vitest clears the module cache between them.
1915
//
20-
// The tracer returned from `provider.getTracer(...)` is scoped to
21-
// this provider, so the InMemorySpanExporter still receives the
22-
// spans the consuming test creates — no global registration needed.
16+
// OTel's `provider.register()` calls trace/context/propagation
17+
// `setGlobal*` — and `setGlobal` no-ops (logs an error, returns false)
18+
// when a global is already set. Two patterns hit that path:
19+
// 1. `~/v3/tracer.server.ts` runs `provider.register()` via its
20+
// `singleton("opentelemetry", setupTelemetry)` — first test in the
21+
// shard to import that path sets the globals to webapp's tracer.
22+
// 2. A subsequent test calls `createInMemoryTracing()` to swap in its
23+
// own in-memory provider. Without disabling first, register() is
24+
// a no-op — the test's provider receives spans via its
25+
// SimpleSpanProcessor (provider-scoped), but `trace.getActiveSpan()`
26+
// (used by code under test, e.g. sentryTraceContext.server.ts)
27+
// reads from the stale global context manager from step 1.
28+
//
29+
// Disable first, then register, so the test's provider always wins
30+
// for both span recording and the global API. After the test, the
31+
// next caller's createInMemoryTracing rotates again — no leakage.
32+
trace.disable();
33+
context.disable();
34+
propagation.disable();
35+
2336
const exporter = new InMemorySpanExporter();
2437
const provider = new NodeTracerProvider({
2538
spanProcessors: [new SimpleSpanProcessor(exporter)],
2639
});
40+
provider.register();
2741

2842
const tracer = provider.getTracer("test-tracer");
2943

0 commit comments

Comments
 (0)