Skip to content

Commit 1ec3a8b

Browse files
authored
feat: support openTelemetry for browser mode (#9180)
1 parent b67788c commit 1ec3a8b

File tree

33 files changed

+466
-47
lines changed

33 files changed

+466
-47
lines changed

docs/config/experimental.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,13 @@ Please, leave feedback regarding this feature in a [GitHub Discussion](https://g
117117
interface OpenTelemetryOptions {
118118
enabled: boolean
119119
/**
120-
* A path to a file that exposes an OpenTelemetry SDK.
120+
* A path to a file that exposes an OpenTelemetry SDK for Node.js.
121121
*/
122122
sdkPath?: string
123+
/**
124+
* A path to a file that exposes an OpenTelemetry SDK for the browser.
125+
*/
126+
browserSdkPath?: string
123127
}
124128
```
125129

@@ -133,9 +137,7 @@ OpenTelemetry may significantly impact Vitest performance; enable it only for lo
133137

134138
You can use a [custom service](/guide/open-telemetry) together with Vitest to pinpoint which tests or files are slowing down your test suite.
135139

136-
::: warning BROWSER SUPPORT
137-
At the moment, Vitest does not start any spans when running in [the browser](/guide/browser/).
138-
:::
140+
For browser mode, see the [Browser Mode](/guide/open-telemetry#browser-mode) section of the OpenTelemetry guide.
139141

140142
An `sdkPath` is resolved relative to the [`root`](/config/root) of the project and should point to a module that exposes a started SDK instance as a default export. For example:
141143

docs/guide/open-telemetry.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,59 @@ test('db connects properly', async () => {
8484
})
8585
```
8686

87+
## Browser Mode
88+
89+
When running tests in [browser mode](/guide/browser/), Vitest propagates trace context between Node.js and the browser. Node.js side traces (test orchestration, browser driver communication) are available without additional configuration.
90+
91+
To capture traces from the browser runtime, provide a browser-compatible SDK via `browserSdkPath`:
92+
93+
```shell
94+
npm i @opentelemetry/sdk-trace-web @opentelemetry/exporter-trace-otlp-proto
95+
```
96+
97+
::: code-group
98+
```js [otel-browser.js]
99+
import {
100+
BatchSpanProcessor,
101+
WebTracerProvider,
102+
} from '@opentelemetry/sdk-trace-web'
103+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
104+
105+
const provider = new WebTracerProvider({
106+
spanProcessors: [
107+
new BatchSpanProcessor(new OTLPTraceExporter()),
108+
],
109+
})
110+
111+
provider.register()
112+
export default provider
113+
```
114+
```js [vitest.config.js]
115+
import { defineConfig } from 'vitest/config'
116+
117+
export default defineConfig({
118+
test: {
119+
browser: {
120+
enabled: true,
121+
provider: 'playwright',
122+
instances: [{ browser: 'chromium' }],
123+
},
124+
experimental: {
125+
openTelemetry: {
126+
enabled: true,
127+
sdkPath: './otel.js',
128+
browserSdkPath: './otel-browser.js',
129+
},
130+
},
131+
},
132+
})
133+
```
134+
:::
135+
136+
::: warning ASYNC CONTEXT
137+
Unlike Node.js, browsers do not have automatic async context propagation. Vitest handles this internally for test execution, but custom spans in deeply nested async code may not propagate context automatically.
138+
:::
139+
87140
## View Traces
88141

89142
To generate traces, run Vitest as usual. You can run Vitest in either watch mode or run mode. Vitest will call `sdk.shutdown()` manually after everything is finished to make sure traces are handled properly.

examples/opentelemetry/jaeger-config.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ extensions:
1414
storage:
1515
traces: main_storage
1616

17+
# https://opentelemetry.io/docs/languages/js/exporters/#configure-cors-headers
1718
receivers:
1819
otlp:
1920
protocols:
20-
grpc:
21-
endpoint: 0.0.0.0:4317
2221
http:
2322
endpoint: 0.0.0.0:4318
23+
cors:
24+
allowed_origins:
25+
- http://localhost:*
2426

2527
processors:
2628
batch:
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
2+
import { resourceFromAttributes } from '@opentelemetry/resources'
3+
import { BatchSpanProcessor, WebTracerProvider } from '@opentelemetry/sdk-trace-web'
4+
5+
const provider = new WebTracerProvider({
6+
resource: resourceFromAttributes({
7+
'service.name': 'vitest-browser',
8+
}),
9+
spanProcessors: [
10+
new BatchSpanProcessor(new OTLPTraceExporter()),
11+
// you can add a ConsoleSpanExporter for debugging purposes
12+
// (available in @opentelemetry/sdk-trace-web)
13+
// new SimpleSpanProcessor(new ConsoleSpanExporter()),
14+
],
15+
})
16+
17+
provider.register({
18+
// you can customize contextManager but browser support has limitation
19+
// cf. https://github.com/open-telemetry/opentelemetry-js/discussions/2060
20+
// contextManager: new StackContextManager(), // this is the default (avialable in sdk-trace-web)
21+
// contextManager: new ZoneContextManager(), // doesn't seem to help (avialable in @opentelemetry/context-zone)
22+
})
23+
24+
export default provider

examples/opentelemetry/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
"test": "vitest"
99
},
1010
"devDependencies": {
11+
"@opentelemetry/api": "^1.9.0",
12+
"@opentelemetry/context-zone": "^2.2.0",
13+
"@opentelemetry/exporter-trace-otlp-proto": "^0.208.0",
14+
"@opentelemetry/resources": "^2.2.0",
1115
"@opentelemetry/sdk-node": "^0.208.0",
16+
"@opentelemetry/sdk-trace-web": "^2.2.0",
1217
"@vitest/browser-playwright": "latest",
1318
"vite": "latest",
1419
"vitest": "latest"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { trace } from '@opentelemetry/api'
2+
import { test } from 'vitest'
3+
4+
test('other', async () => {
5+
await new Promise(r => setTimeout(r, 150))
6+
})
7+
8+
test('custom', async () => {
9+
// this starts span synchronously inside test function,
10+
// so trace parent works without async context manager (e.g. on browser mode).
11+
12+
// console.log(context.active())
13+
// > vitest.test.runner.test.callback
14+
// > custom-span
15+
16+
const tracer = trace.getTracer('custom-scope')
17+
await tracer.startActiveSpan('custom-span', async (span) => {
18+
span.setAttribute('custom-attribute', 'hello world')
19+
await new Promise(resolve => setTimeout(resolve, 50))
20+
span.end()
21+
})
22+
23+
// however the context is dropped on browser mode.
24+
// console.log(context.active())
25+
})
26+
27+
test.runIf(typeof document !== 'undefined')('browser test', async () => {
28+
const { page } = await import('vitest/browser')
29+
document.body.innerHTML = `<button>Hello Vitest</button>`
30+
await page.getByRole('button', { name: 'Hello Vitest' }).click()
31+
})

examples/opentelemetry/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default defineConfig({
88
// enable via CLI flag --experimental.openTelemetry.enabled=true
99
enabled: false,
1010
sdkPath: './otel.js',
11+
browserSdkPath: './otel-browser.js',
1112
},
1213
},
1314
browser: {

packages/browser/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"build": "premove dist && pnpm build:node && pnpm build:client",
5757
"build:client": "vite build src/client",
5858
"build:node": "rollup -c",
59-
"dev:client": "vite build src/client --watch",
59+
"dev:client": "node --watch-preserve-output --watch-path src/client scripts/build-client.js",
6060
"dev:node": "rollup -c --watch --watch.include 'src/**'",
6161
"dev": "premove dist && pnpm run --stream '/^dev:/'"
6262
},
@@ -74,6 +74,7 @@
7474
"ws": "catalog:"
7575
},
7676
"devDependencies": {
77+
"@opentelemetry/api": "^1.9.0",
7778
"@testing-library/user-event": "^14.6.1",
7879
"@types/pngjs": "^6.0.5",
7980
"@types/ws": "catalog:",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// wrapper script for `node --watch`.
2+
// this works around some issues with `vite build --watch`.
3+
import { spawn } from 'node:child_process'
4+
5+
spawn('node', ['--run', 'build:client'], {
6+
stdio: 'inherit',
7+
})

packages/browser/src/client/channel.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { CancelReason, FileSpecification } from '@vitest/runner'
2+
import type { OTELCarrier } from 'vitest/internal/browser'
23
import { getBrowserState } from './utils'
34

45
export interface IframeViewportEvent {
@@ -41,6 +42,7 @@ export interface IframePrepareEvent {
4142
event: 'prepare'
4243
iframeId: string
4344
startTime: number
45+
otelCarrier?: OTELCarrier
4446
}
4547

4648
export type GlobalChannelIncomingEvent = GlobalChannelTestRunCanceledEvent

0 commit comments

Comments
 (0)