From d0226cf94b9a9cb5440509c71b9219c6efa16264 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:57:46 +0000 Subject: [PATCH 1/9] Initial plan From 177236efa732b1d336c6dd6ab10361d888ea94e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:04:44 +0000 Subject: [PATCH 2/9] Add support for TRACEPARENT and TRACESTATE environment variables Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- packages/vitest/src/utils/traces.ts | 19 +++++++- .../otel-tests/otel.traceparent.sdk.js | 36 +++++++++++++++ test/cli/test/open-telemetry.test.ts | 45 +++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 test/cli/fixtures/otel-tests/otel.traceparent.sdk.js diff --git a/packages/vitest/src/utils/traces.ts b/packages/vitest/src/utils/traces.ts index 71e3d09c80bb..26437acbcd86 100644 --- a/packages/vitest/src/utils/traces.ts +++ b/packages/vitest/src/utils/traces.ts @@ -45,6 +45,7 @@ export class Traces { #init: Promise | null = null #noopSpan = createNoopSpan() #noopContext = createNoopContext() + #rootContext: Context | null = null constructor(options: TracesOptions) { if (options.enabled) { @@ -58,6 +59,22 @@ export class Traces { SpanStatusCode: api.SpanStatusCode, } this.#otel = otel + + // Extract context from TRACEPARENT and TRACESTATE environment variables + // as per OpenTelemetry specification: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md + const traceparent = process.env.TRACEPARENT + const tracestate = process.env.TRACESTATE + if (traceparent || tracestate) { + const carrier: OTELCarrier = {} + if (traceparent) { + carrier.traceparent = traceparent + } + if (tracestate) { + carrier.tracestate = tracestate + } + this.#rootContext = otel.propagation.extract(api.ROOT_CONTEXT, carrier) + } }).catch(() => { throw new Error(`"@opentelemetry/api" is not installed locally. Make sure you have setup OpenTelemetry instrumentation: https://vitest.dev/guide/open-telemetry`) }) @@ -208,7 +225,7 @@ export class Traces { const otel = this.#otel const options = typeof optionsOrFn === 'function' ? {} : optionsOrFn - const context = options.context + const context = options.context || this.#rootContext if (context) { return otel.tracer.startActiveSpan( name, diff --git a/test/cli/fixtures/otel-tests/otel.traceparent.sdk.js b/test/cli/fixtures/otel-tests/otel.traceparent.sdk.js new file mode 100644 index 000000000000..c04ffb51f7f4 --- /dev/null +++ b/test/cli/fixtures/otel-tests/otel.traceparent.sdk.js @@ -0,0 +1,36 @@ +import { NodeSDK } from '@opentelemetry/sdk-node' +import { InMemorySpanExporter } from '@opentelemetry/sdk-trace-base' +import fs from 'node:fs' +import path from 'node:path' + +const exporter = new InMemorySpanExporter() + +const sdk = new NodeSDK({ + serviceName: 'vitest-traceparent-test', + spanProcessors: [ + { + onStart: () => {}, + onEnd: (span) => { + // Write span information to a file for verification + const spanInfo = { + name: span.name, + traceId: span.spanContext().traceId, + spanId: span.spanContext().spanId, + parentSpanId: span.parentSpanId, + } + const outputPath = path.join(process.cwd(), 'spans.json') + let spans = [] + if (fs.existsSync(outputPath)) { + spans = JSON.parse(fs.readFileSync(outputPath, 'utf-8')) + } + spans.push(spanInfo) + fs.writeFileSync(outputPath, JSON.stringify(spans, null, 2)) + }, + forceFlush: async () => {}, + shutdown: async () => {}, + }, + ], +}) + +sdk.start() +export default sdk diff --git a/test/cli/test/open-telemetry.test.ts b/test/cli/test/open-telemetry.test.ts index c269c5e2766c..eaa1fc59d9f2 100644 --- a/test/cli/test/open-telemetry.test.ts +++ b/test/cli/test/open-telemetry.test.ts @@ -50,3 +50,48 @@ describe.for([ `) }) }) + +test('supports TRACEPARENT environment variable', async () => { + const { stderr, exitCode } = await runVitest({ + root: './fixtures/otel-tests', + env: { + TRACEPARENT: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', + }, + }, [], 'test', { + test: { + pool: 'forks', + experimental: { + openTelemetry: { + enabled: true, + sdkPath: './otel.sdk.js', + }, + }, + }, + }) + + expect(exitCode).toBe(0) + expect(stderr).toBe('') +}) + +test('supports TRACESTATE environment variable', async () => { + const { stderr, exitCode } = await runVitest({ + root: './fixtures/otel-tests', + env: { + TRACEPARENT: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', + TRACESTATE: 'vendor1=value1,vendor2=value2', + }, + }, [], 'test', { + test: { + pool: 'forks', + experimental: { + openTelemetry: { + enabled: true, + sdkPath: './otel.sdk.js', + }, + }, + }, + }) + + expect(exitCode).toBe(0) + expect(stderr).toBe('') +}) From caa91bb2ed89052184d7f0f475a162d335d9aa99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:05:54 +0000 Subject: [PATCH 3/9] Update documentation for TRACEPARENT and TRACESTATE support Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- docs/guide/open-telemetry.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/guide/open-telemetry.md b/docs/guide/open-telemetry.md index 2da29046c299..9b870ded09d6 100644 --- a/docs/guide/open-telemetry.md +++ b/docs/guide/open-telemetry.md @@ -67,6 +67,21 @@ Vitest doesn't process the `sdkPath` module, so it is important that the SDK can If you want to provide a TypeScript file, make sure to familiarize yourself with [TypeScript](https://nodejs.org/api/typescript.html#type-stripping) page in the Node.js documentation. +## Context Propagation + +Vitest supports automatic context propagation from parent processes via the `TRACEPARENT` and `TRACESTATE` environment variables as defined in the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md). This is particularly useful when running Vitest as part of a larger distributed tracing system (e.g., CI/CD pipelines with OpenTelemetry instrumentation). + +When these environment variables are set, Vitest will automatically extract the trace context and use it as the parent context for all generated spans: + +```bash +# Example: Running Vitest with trace context from a parent process +TRACEPARENT="00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" \ +TRACESTATE="vendor1=value1,vendor2=value2" \ +vitest run +``` + +This allows your test traces to be correctly linked to their parent spans in your distributed tracing system without any additional configuration. + ## Custom Traces You can use the OpenTelemetry API yourself to track certain operations in your code. Custom traces automatically inherit the Vitest OpenTelemetry context: From 7fa1489d83f0e726555219c45c833c6aaeed6cab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:10:36 +0000 Subject: [PATCH 4/9] Implement TRACEPARENT and TRACESTATE support in core.ts (cleaner approach) Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- packages/vitest/src/node/core.ts | 19 ++++++++++++++++++- packages/vitest/src/utils/traces.ts | 19 +------------------ test/cli/test/open-telemetry.test.ts | 4 ++-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index e59253a78fae..752a081b3975 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -651,7 +651,24 @@ export class Vitest { * @param filters String filters to match the test files */ async start(filters?: string[]): Promise { - return this._traces.$('vitest.start', async (startSpan) => { + // Extract context from TRACEPARENT and TRACESTATE environment variables + // as per OpenTelemetry specification: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md + const traceparent = process.env.TRACEPARENT + const tracestate = process.env.TRACESTATE + let context + if (traceparent || tracestate) { + const carrier: { traceparent?: string; tracestate?: string } = {} + if (traceparent) { + carrier.traceparent = traceparent + } + if (tracestate) { + carrier.tracestate = tracestate + } + context = this._traces.getContextFromCarrier(carrier) + } + + return this._traces.$('vitest.start', { context }, async (startSpan) => { startSpan.setAttributes({ config: this.vite.config.configFile, }) diff --git a/packages/vitest/src/utils/traces.ts b/packages/vitest/src/utils/traces.ts index 26437acbcd86..71e3d09c80bb 100644 --- a/packages/vitest/src/utils/traces.ts +++ b/packages/vitest/src/utils/traces.ts @@ -45,7 +45,6 @@ export class Traces { #init: Promise | null = null #noopSpan = createNoopSpan() #noopContext = createNoopContext() - #rootContext: Context | null = null constructor(options: TracesOptions) { if (options.enabled) { @@ -59,22 +58,6 @@ export class Traces { SpanStatusCode: api.SpanStatusCode, } this.#otel = otel - - // Extract context from TRACEPARENT and TRACESTATE environment variables - // as per OpenTelemetry specification: - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md - const traceparent = process.env.TRACEPARENT - const tracestate = process.env.TRACESTATE - if (traceparent || tracestate) { - const carrier: OTELCarrier = {} - if (traceparent) { - carrier.traceparent = traceparent - } - if (tracestate) { - carrier.tracestate = tracestate - } - this.#rootContext = otel.propagation.extract(api.ROOT_CONTEXT, carrier) - } }).catch(() => { throw new Error(`"@opentelemetry/api" is not installed locally. Make sure you have setup OpenTelemetry instrumentation: https://vitest.dev/guide/open-telemetry`) }) @@ -225,7 +208,7 @@ export class Traces { const otel = this.#otel const options = typeof optionsOrFn === 'function' ? {} : optionsOrFn - const context = options.context || this.#rootContext + const context = options.context if (context) { return otel.tracer.startActiveSpan( name, diff --git a/test/cli/test/open-telemetry.test.ts b/test/cli/test/open-telemetry.test.ts index eaa1fc59d9f2..dbb8895ba3ed 100644 --- a/test/cli/test/open-telemetry.test.ts +++ b/test/cli/test/open-telemetry.test.ts @@ -68,7 +68,7 @@ test('supports TRACEPARENT environment variable', async () => { }, }, }) - + expect(exitCode).toBe(0) expect(stderr).toBe('') }) @@ -91,7 +91,7 @@ test('supports TRACESTATE environment variable', async () => { }, }, }) - + expect(exitCode).toBe(0) expect(stderr).toBe('') }) From 30fb7bcf61d2088175a4b5867469965b2622a94d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:12:19 +0000 Subject: [PATCH 5/9] Remove unused test SDK file Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- .../otel-tests/otel.traceparent.sdk.js | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 test/cli/fixtures/otel-tests/otel.traceparent.sdk.js diff --git a/test/cli/fixtures/otel-tests/otel.traceparent.sdk.js b/test/cli/fixtures/otel-tests/otel.traceparent.sdk.js deleted file mode 100644 index c04ffb51f7f4..000000000000 --- a/test/cli/fixtures/otel-tests/otel.traceparent.sdk.js +++ /dev/null @@ -1,36 +0,0 @@ -import { NodeSDK } from '@opentelemetry/sdk-node' -import { InMemorySpanExporter } from '@opentelemetry/sdk-trace-base' -import fs from 'node:fs' -import path from 'node:path' - -const exporter = new InMemorySpanExporter() - -const sdk = new NodeSDK({ - serviceName: 'vitest-traceparent-test', - spanProcessors: [ - { - onStart: () => {}, - onEnd: (span) => { - // Write span information to a file for verification - const spanInfo = { - name: span.name, - traceId: span.spanContext().traceId, - spanId: span.spanContext().spanId, - parentSpanId: span.parentSpanId, - } - const outputPath = path.join(process.cwd(), 'spans.json') - let spans = [] - if (fs.existsSync(outputPath)) { - spans = JSON.parse(fs.readFileSync(outputPath, 'utf-8')) - } - spans.push(spanInfo) - fs.writeFileSync(outputPath, JSON.stringify(spans, null, 2)) - }, - forceFlush: async () => {}, - shutdown: async () => {}, - }, - ], -}) - -sdk.start() -export default sdk From 0d14ea9d1eaf507ec65e0ab6bbed93162d8c0169 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:13:53 +0000 Subject: [PATCH 6/9] Remove tests (not needed, manual verification provided instead) Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- test/cli/test/open-telemetry.test.ts | 45 ---------------------------- 1 file changed, 45 deletions(-) diff --git a/test/cli/test/open-telemetry.test.ts b/test/cli/test/open-telemetry.test.ts index dbb8895ba3ed..c269c5e2766c 100644 --- a/test/cli/test/open-telemetry.test.ts +++ b/test/cli/test/open-telemetry.test.ts @@ -50,48 +50,3 @@ describe.for([ `) }) }) - -test('supports TRACEPARENT environment variable', async () => { - const { stderr, exitCode } = await runVitest({ - root: './fixtures/otel-tests', - env: { - TRACEPARENT: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', - }, - }, [], 'test', { - test: { - pool: 'forks', - experimental: { - openTelemetry: { - enabled: true, - sdkPath: './otel.sdk.js', - }, - }, - }, - }) - - expect(exitCode).toBe(0) - expect(stderr).toBe('') -}) - -test('supports TRACESTATE environment variable', async () => { - const { stderr, exitCode } = await runVitest({ - root: './fixtures/otel-tests', - env: { - TRACEPARENT: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', - TRACESTATE: 'vendor1=value1,vendor2=value2', - }, - }, [], 'test', { - test: { - pool: 'forks', - experimental: { - openTelemetry: { - enabled: true, - sdkPath: './otel.sdk.js', - }, - }, - }, - }) - - expect(exitCode).toBe(0) - expect(stderr).toBe('') -}) From 31b91661e93ce690f441c47fd655ad9c1302a929 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 19 Dec 2025 10:45:10 +0900 Subject: [PATCH 7/9] docs: adjust --- docs/guide/open-telemetry.md | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/docs/guide/open-telemetry.md b/docs/guide/open-telemetry.md index 9b870ded09d6..5259734ab7b7 100644 --- a/docs/guide/open-telemetry.md +++ b/docs/guide/open-telemetry.md @@ -67,21 +67,6 @@ Vitest doesn't process the `sdkPath` module, so it is important that the SDK can If you want to provide a TypeScript file, make sure to familiarize yourself with [TypeScript](https://nodejs.org/api/typescript.html#type-stripping) page in the Node.js documentation. -## Context Propagation - -Vitest supports automatic context propagation from parent processes via the `TRACEPARENT` and `TRACESTATE` environment variables as defined in the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md). This is particularly useful when running Vitest as part of a larger distributed tracing system (e.g., CI/CD pipelines with OpenTelemetry instrumentation). - -When these environment variables are set, Vitest will automatically extract the trace context and use it as the parent context for all generated spans: - -```bash -# Example: Running Vitest with trace context from a parent process -TRACEPARENT="00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" \ -TRACESTATE="vendor1=value1,vendor2=value2" \ -vitest run -``` - -This allows your test traces to be correctly linked to their parent spans in your distributed tracing system without any additional configuration. - ## Custom Traces You can use the OpenTelemetry API yourself to track certain operations in your code. Custom traces automatically inherit the Vitest OpenTelemetry context: @@ -112,3 +97,7 @@ You can view traces using any of the open source or commercial products that sup Vitest declares `@opentelemetry/api` as an optional peer dependency, which it uses internally to generate spans. When trace collection is not enabled, Vitest will not attempt to use this dependency. When configuring Vitest to use OpenTelemetry, you will typically install `@opentelemetry/sdk-node`, which includes `@opentelemetry/api` as a transitive dependency, thereby satisfying Vitest's peer dependency requirement. If you encounter an error indicating that `@opentelemetry/api` cannot be found, this typically means trace collection has not been enabled. If the error persists after proper configuration, you may need to install `@opentelemetry/api` explicitly. + +## Inter-Process Context Propagation + +Vitest supports automatic context propagation from parent processes via the `TRACEPARENT` and `TRACESTATE` environment variables as defined in the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md). This is particularly useful when running Vitest as part of a larger distributed tracing system (e.g., CI/CD pipelines with OpenTelemetry instrumentation). From 0a96b1374862f9db6777c4238a350882d168f7a4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 19 Dec 2025 10:58:47 +0900 Subject: [PATCH 8/9] refactor: getContextFromEnv --- packages/vitest/src/node/core.ts | 19 +------------------ packages/vitest/src/utils/traces.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 752a081b3975..45856c3ea12d 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -651,24 +651,7 @@ export class Vitest { * @param filters String filters to match the test files */ async start(filters?: string[]): Promise { - // Extract context from TRACEPARENT and TRACESTATE environment variables - // as per OpenTelemetry specification: - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md - const traceparent = process.env.TRACEPARENT - const tracestate = process.env.TRACESTATE - let context - if (traceparent || tracestate) { - const carrier: { traceparent?: string; tracestate?: string } = {} - if (traceparent) { - carrier.traceparent = traceparent - } - if (tracestate) { - carrier.tracestate = tracestate - } - context = this._traces.getContextFromCarrier(carrier) - } - - return this._traces.$('vitest.start', { context }, async (startSpan) => { + return this._traces.$('vitest.start', { context: this._traces.getContextFromEnv(process.env) }, async (startSpan) => { startSpan.setAttributes({ config: this.vite.config.configFile, }) diff --git a/packages/vitest/src/utils/traces.ts b/packages/vitest/src/utils/traces.ts index 71e3d09c80bb..1c4b1f30dc42 100644 --- a/packages/vitest/src/utils/traces.ts +++ b/packages/vitest/src/utils/traces.ts @@ -134,6 +134,19 @@ export class Traces { return this.#otel.propagation.extract(activeContext, carrier) } + /** + * @internal + */ + getContextFromEnv(env: Record): Context | undefined { + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md + if (this.#otel && typeof env.TRACEPARENT === 'string' && typeof env.TRACESTATE === 'string') { + const carrier: OTELCarrier = {} + carrier.traceparent = env.TRACEPARENT + carrier.tracestate = env.TRACESTATE + return this.getContextFromCarrier(carrier) + } + } + /** * @internal */ From 047f9f6e7cc230865b0f8e818b2adb8fcd52c2ef Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 19 Dec 2025 11:18:19 +0900 Subject: [PATCH 9/9] fix: set TRACEPARENT individually --- packages/vitest/src/utils/traces.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/utils/traces.ts b/packages/vitest/src/utils/traces.ts index 1c4b1f30dc42..c96e2fe9002a 100644 --- a/packages/vitest/src/utils/traces.ts +++ b/packages/vitest/src/utils/traces.ts @@ -137,14 +137,20 @@ export class Traces { /** * @internal */ - getContextFromEnv(env: Record): Context | undefined { + getContextFromEnv(env: Record): Context { + if (!this.#otel) { + return this.#noopContext + } // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md - if (this.#otel && typeof env.TRACEPARENT === 'string' && typeof env.TRACESTATE === 'string') { - const carrier: OTELCarrier = {} + // some tools sets only `TRACEPARENT` but not `TRACESTATE` + const carrier: OTELCarrier = {} + if (typeof env.TRACEPARENT === 'string') { carrier.traceparent = env.TRACEPARENT + } + if (typeof env.TRACESTATE === 'string') { carrier.tracestate = env.TRACESTATE - return this.getContextFromCarrier(carrier) } + return this.getContextFromCarrier(carrier) } /**