import { context, propagation, trace } from "@opentelemetry/api"; import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"; import { MeterProvider, InMemoryMetricExporter, PeriodicExportingMetricReader, AggregationTemporality, } from "@opentelemetry/sdk-metrics"; export function createInMemoryTracing() { // 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 `provider.register()` calls trace/context/propagation // `setGlobal*` — and `setGlobal` no-ops (logs an error, returns false) // when a global is already set. Two patterns hit that path: // 1. `~/v3/tracer.server.ts` runs `provider.register()` via its // `singleton("opentelemetry", setupTelemetry)` — first test in the // shard to import that path sets the globals to webapp's tracer. // 2. A subsequent test calls `createInMemoryTracing()` to swap in its // own in-memory provider. Without disabling first, register() is // a no-op — the test's provider receives spans via its // SimpleSpanProcessor (provider-scoped), but `trace.getActiveSpan()` // (used by code under test, e.g. sentryTraceContext.server.ts) // reads from the stale global context manager from step 1. // // Disable first, then register, so the test's provider always wins // for both span recording and the global API. After the test, the // next caller's createInMemoryTracing rotates again — no leakage. trace.disable(); context.disable(); propagation.disable(); const exporter = new InMemorySpanExporter(); const provider = new NodeTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)], }); provider.register(); // Use the provider's tracer so spans hit this exporter even when a global // NodeTracerProvider was already registered (e.g. via tracer.server import chain). const tracer = provider.getTracer("test-tracer"); return { exporter, tracer, }; } export function createInMemoryMetrics() { // Initialize the metric exporter with cumulative temporality for easier testing const metricExporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE); // Create a metric reader that exports frequently for testing const metricReader = new PeriodicExportingMetricReader({ exporter: metricExporter, exportIntervalMillis: 100, // Export frequently for tests }); // Initialize the meter provider const meterProvider = new MeterProvider({ readers: [metricReader], }); // Retrieve a meter const meter = meterProvider.getMeter("test-meter"); return { metricExporter, metricReader, meterProvider, meter, // Helper to force collection and get metrics async getMetrics() { await metricReader.forceFlush(); return metricExporter.getMetrics(); }, // Helper to reset metrics between tests reset() { metricExporter.reset(); }, // Helper to shutdown the meter provider async shutdown() { await meterProvider.shutdown(); }, }; }