autotel-vitest 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -0
- package/dist/index.cjs +134 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/reporter.cjs +87 -0
- package/dist/reporter.cjs.map +1 -0
- package/dist/reporter.d.cts +36 -0
- package/dist/reporter.d.ts +36 -0
- package/dist/reporter.js +82 -0
- package/dist/reporter.js.map +1 -0
- package/package.json +53 -0
- package/src/index.test.ts +230 -0
- package/src/index.ts +105 -0
- package/src/reporter.test.ts +193 -0
- package/src/reporter.ts +119 -0
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# autotel-vitest
|
|
2
|
+
|
|
3
|
+
`autotel-vitest` gives each Vitest test a parent OpenTelemetry span, so all `autotel` instrumented calls made during that test become child spans.
|
|
4
|
+
|
|
5
|
+
## How test and server appear as one trace
|
|
6
|
+
|
|
7
|
+
When you test an API over HTTP, the test runs in the **Vitest worker process** and the server runs in another process. Use `injectTraceContext()` from `autotel/http` so the request carries a `traceparent` header. Your server uses `extractTraceContext()` and creates its spans in that context, so **the same trace ID** is used on both sides. In your OTLP backend, look up the test’s trace ID to see the full trace: test span and all server spans (HTTP, handlers, `db.userId`, etc.). When you test in-process (no HTTP), all code runs in the same process and the fixture’s context automatically links test and handler spans in one trace.
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
sequenceDiagram
|
|
11
|
+
participant Test as Test process
|
|
12
|
+
participant Server as Server process
|
|
13
|
+
participant OTLP as OTLP backend
|
|
14
|
+
|
|
15
|
+
Note over Test: One span per test (trace ID = X)
|
|
16
|
+
Test->>Server: fetch() with traceparent (injectTraceContext)
|
|
17
|
+
Note over Server: extractTraceContext()<br/>HTTP span + child spans
|
|
18
|
+
Test->>OTLP: Export spans (trace X)
|
|
19
|
+
Server->>OTLP: Export spans (trace X)
|
|
20
|
+
Note over OTLP: One trace = test + API
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Install `autotel-vitest`, `autotel`, and `vitest`:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# pnpm
|
|
29
|
+
pnpm add -D vitest
|
|
30
|
+
pnpm add autotel autotel-vitest
|
|
31
|
+
|
|
32
|
+
# npm
|
|
33
|
+
npm install --save-dev vitest
|
|
34
|
+
npm install autotel autotel-vitest
|
|
35
|
+
|
|
36
|
+
# yarn
|
|
37
|
+
yarn add --dev vitest
|
|
38
|
+
yarn add autotel autotel-vitest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
### 1. Initialize autotel in `globalSetup`
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// globalSetup.ts
|
|
47
|
+
import { init } from 'autotel';
|
|
48
|
+
|
|
49
|
+
export default function globalSetup() {
|
|
50
|
+
init({ service: 'unit-tests' });
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Register `globalSetup` in Vitest config
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// vitest.config.ts
|
|
58
|
+
import { defineConfig } from 'vitest/config';
|
|
59
|
+
|
|
60
|
+
export default defineConfig({
|
|
61
|
+
test: {
|
|
62
|
+
globalSetup: './globalSetup.ts',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Import `test` from `autotel-vitest`
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { test, expect } from 'autotel-vitest';
|
|
71
|
+
|
|
72
|
+
test('creates user', async () => {
|
|
73
|
+
await userService.createUser({ email: 'test@example.com' });
|
|
74
|
+
expect(true).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The fixture is `auto: true`, so every test gets a parent span automatically.
|
|
79
|
+
|
|
80
|
+
## What You Get
|
|
81
|
+
|
|
82
|
+
- One span per test named `test:${task.name}`
|
|
83
|
+
- Span attributes: `test.name`, `test.file`, `test.suite`
|
|
84
|
+
- If a test throws, the span is marked as error and records the exception
|
|
85
|
+
- Any `trace()` / `span()` calls in the test flow become children of the active test span
|
|
86
|
+
|
|
87
|
+
## Optional: Reporter (runner-side spans)
|
|
88
|
+
|
|
89
|
+
Use the reporter when you also want runner-process spans for test/suite timing:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// vitest.config.ts
|
|
93
|
+
import { defineConfig } from 'vitest/config';
|
|
94
|
+
|
|
95
|
+
export default defineConfig({
|
|
96
|
+
test: {
|
|
97
|
+
globalSetup: './globalSetup.ts',
|
|
98
|
+
reporters: ['default', 'autotel-vitest/reporter'],
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Reporter spans:
|
|
104
|
+
|
|
105
|
+
- test span: `test:${name}`
|
|
106
|
+
- suite span: `suite:${name}`
|
|
107
|
+
|
|
108
|
+
## Testing Utilities
|
|
109
|
+
|
|
110
|
+
`autotel-vitest` re-exports helpers from `autotel/testing`:
|
|
111
|
+
|
|
112
|
+
- `createTraceCollector`
|
|
113
|
+
- `assertTraceCreated`
|
|
114
|
+
- `assertTraceSucceeded`
|
|
115
|
+
- `assertTraceFailed`
|
|
116
|
+
- `assertNoErrors`
|
|
117
|
+
- `assertTraceDuration`
|
|
118
|
+
- `waitForTrace`
|
|
119
|
+
- `getTraceDuration`
|
|
120
|
+
- `createMockLogger`
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { test, createTraceCollector, assertTraceCreated } from 'autotel-vitest';
|
|
126
|
+
|
|
127
|
+
test('traces user creation', async () => {
|
|
128
|
+
const collector = createTraceCollector();
|
|
129
|
+
await userService.createUser({ email: 'test@example.com' });
|
|
130
|
+
assertTraceCreated(collector, 'user.createUser');
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Troubleshooting
|
|
135
|
+
|
|
136
|
+
- No spans exported: verify `init()` runs in `globalSetup` before tests start.
|
|
137
|
+
- No child spans under test span: ensure your test imports `test` from `autotel-vitest`, not `vitest`.
|
|
138
|
+
- Reporter not running: ensure `reporters` includes `'autotel-vitest/reporter'`.
|
|
139
|
+
|
|
140
|
+
## API
|
|
141
|
+
|
|
142
|
+
- `test`: extended Vitest `test` with auto per-test span fixture
|
|
143
|
+
- `expect`, `describe`, `beforeEach`, `afterEach`, `beforeAll`, `afterAll`: re-exported from `vitest`
|
|
144
|
+
- `autotel/testing` helpers listed above: re-exported from this package
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var vitest = require('vitest');
|
|
4
|
+
var autotel = require('autotel');
|
|
5
|
+
var testSpanCollector = require('autotel/test-span-collector');
|
|
6
|
+
var processors = require('autotel/processors');
|
|
7
|
+
var testing = require('autotel/testing');
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
var TRACER_NAME = "vitest-tests";
|
|
11
|
+
var TRACER_VERSION = "0.1.0";
|
|
12
|
+
var collector = null;
|
|
13
|
+
function ensureCollector() {
|
|
14
|
+
if (!collector) {
|
|
15
|
+
collector = new testSpanCollector.TestSpanCollector();
|
|
16
|
+
const provider = autotel.getAutotelTracerProvider();
|
|
17
|
+
if ("addSpanProcessor" in provider) {
|
|
18
|
+
provider.addSpanProcessor(new processors.SimpleSpanProcessor(collector));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return collector;
|
|
22
|
+
}
|
|
23
|
+
var test = vitest.test.extend({
|
|
24
|
+
_otelTestSpan: [
|
|
25
|
+
async ({ task }, use) => {
|
|
26
|
+
ensureCollector();
|
|
27
|
+
const tracer = autotel.getTracer(TRACER_NAME, TRACER_VERSION);
|
|
28
|
+
const span = tracer.startSpan(`test:${task.name}`, {
|
|
29
|
+
attributes: {
|
|
30
|
+
"test.name": task.name,
|
|
31
|
+
"test.file": task.file?.name ?? "",
|
|
32
|
+
"test.suite": task.suite?.name ?? ""
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const ctx = autotel.otelTrace.setSpan(autotel.context.active(), span);
|
|
36
|
+
try {
|
|
37
|
+
await autotel.context.with(ctx, () => use(span));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
span.setStatus({ code: autotel.SpanStatusCode.ERROR });
|
|
40
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
41
|
+
throw error;
|
|
42
|
+
} finally {
|
|
43
|
+
span.end();
|
|
44
|
+
const traceId = span.spanContext().traceId;
|
|
45
|
+
const rootSpanId = span.spanContext().spanId;
|
|
46
|
+
const spans = collector.drainTrace(traceId, rootSpanId);
|
|
47
|
+
if (spans.length > 0) {
|
|
48
|
+
task.meta.otelSpans = spans;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{ auto: true }
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
Object.defineProperty(exports, "afterAll", {
|
|
57
|
+
enumerable: true,
|
|
58
|
+
get: function () { return vitest.afterAll; }
|
|
59
|
+
});
|
|
60
|
+
Object.defineProperty(exports, "afterEach", {
|
|
61
|
+
enumerable: true,
|
|
62
|
+
get: function () { return vitest.afterEach; }
|
|
63
|
+
});
|
|
64
|
+
Object.defineProperty(exports, "beforeAll", {
|
|
65
|
+
enumerable: true,
|
|
66
|
+
get: function () { return vitest.beforeAll; }
|
|
67
|
+
});
|
|
68
|
+
Object.defineProperty(exports, "beforeEach", {
|
|
69
|
+
enumerable: true,
|
|
70
|
+
get: function () { return vitest.beforeEach; }
|
|
71
|
+
});
|
|
72
|
+
Object.defineProperty(exports, "describe", {
|
|
73
|
+
enumerable: true,
|
|
74
|
+
get: function () { return vitest.describe; }
|
|
75
|
+
});
|
|
76
|
+
Object.defineProperty(exports, "expect", {
|
|
77
|
+
enumerable: true,
|
|
78
|
+
get: function () { return vitest.expect; }
|
|
79
|
+
});
|
|
80
|
+
Object.defineProperty(exports, "enrichWithTraceContext", {
|
|
81
|
+
enumerable: true,
|
|
82
|
+
get: function () { return autotel.enrichWithTraceContext; }
|
|
83
|
+
});
|
|
84
|
+
Object.defineProperty(exports, "getTraceContext", {
|
|
85
|
+
enumerable: true,
|
|
86
|
+
get: function () { return autotel.getTraceContext; }
|
|
87
|
+
});
|
|
88
|
+
Object.defineProperty(exports, "isTracing", {
|
|
89
|
+
enumerable: true,
|
|
90
|
+
get: function () { return autotel.isTracing; }
|
|
91
|
+
});
|
|
92
|
+
Object.defineProperty(exports, "resolveTraceUrl", {
|
|
93
|
+
enumerable: true,
|
|
94
|
+
get: function () { return autotel.resolveTraceUrl; }
|
|
95
|
+
});
|
|
96
|
+
Object.defineProperty(exports, "assertNoErrors", {
|
|
97
|
+
enumerable: true,
|
|
98
|
+
get: function () { return testing.assertNoErrors; }
|
|
99
|
+
});
|
|
100
|
+
Object.defineProperty(exports, "assertTraceCreated", {
|
|
101
|
+
enumerable: true,
|
|
102
|
+
get: function () { return testing.assertTraceCreated; }
|
|
103
|
+
});
|
|
104
|
+
Object.defineProperty(exports, "assertTraceDuration", {
|
|
105
|
+
enumerable: true,
|
|
106
|
+
get: function () { return testing.assertTraceDuration; }
|
|
107
|
+
});
|
|
108
|
+
Object.defineProperty(exports, "assertTraceFailed", {
|
|
109
|
+
enumerable: true,
|
|
110
|
+
get: function () { return testing.assertTraceFailed; }
|
|
111
|
+
});
|
|
112
|
+
Object.defineProperty(exports, "assertTraceSucceeded", {
|
|
113
|
+
enumerable: true,
|
|
114
|
+
get: function () { return testing.assertTraceSucceeded; }
|
|
115
|
+
});
|
|
116
|
+
Object.defineProperty(exports, "createMockLogger", {
|
|
117
|
+
enumerable: true,
|
|
118
|
+
get: function () { return testing.createMockLogger; }
|
|
119
|
+
});
|
|
120
|
+
Object.defineProperty(exports, "createTraceCollector", {
|
|
121
|
+
enumerable: true,
|
|
122
|
+
get: function () { return testing.createTraceCollector; }
|
|
123
|
+
});
|
|
124
|
+
Object.defineProperty(exports, "getTraceDuration", {
|
|
125
|
+
enumerable: true,
|
|
126
|
+
get: function () { return testing.getTraceDuration; }
|
|
127
|
+
});
|
|
128
|
+
Object.defineProperty(exports, "waitForTrace", {
|
|
129
|
+
enumerable: true,
|
|
130
|
+
get: function () { return testing.waitForTrace; }
|
|
131
|
+
});
|
|
132
|
+
exports.test = test;
|
|
133
|
+
//# sourceMappingURL=index.cjs.map
|
|
134
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["TestSpanCollector","getAutotelTracerProvider","SimpleSpanProcessor","base","getTracer","otelTrace","otelContext","SpanStatusCode"],"mappings":";;;;;;;;;AA4BA,IAAM,WAAA,GAAc,cAAA;AACpB,IAAM,cAAA,GAAiB,OAAA;AAEvB,IAAI,SAAA,GAAsC,IAAA;AAE1C,SAAS,eAAA,GAAqC;AAC5C,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,SAAA,GAAY,IAAIA,mCAAA,EAAkB;AAClC,IAAA,MAAM,WAAWC,gCAAA,EAAyB;AAC1C,IAAA,IAAI,sBAAsB,QAAA,EAAU;AAClC,MAAC,QAAA,CAAiB,gBAAA,CAAiB,IAAIC,8BAAA,CAAoB,SAAS,CAAC,CAAA;AAAA,IACvE;AAAA,EACF;AACA,EAAA,OAAO,SAAA;AACT;AAEO,IAAM,IAAA,GAAOC,YAAK,MAAA,CAAO;AAAA,EAC9B,aAAA,EAAe;AAAA,IACb,OAAO,EAAE,IAAA,EAAK,EAAG,GAAA,KAAQ;AACvB,MAAA,eAAA,EAAgB;AAChB,MAAA,MAAM,MAAA,GAASC,iBAAA,CAAU,WAAA,EAAa,cAAc,CAAA;AACpD,MAAA,MAAM,OAAO,MAAA,CAAO,SAAA,CAAU,CAAA,KAAA,EAAQ,IAAA,CAAK,IAAI,CAAA,CAAA,EAAI;AAAA,QACjD,UAAA,EAAY;AAAA,UACV,aAAa,IAAA,CAAK,IAAA;AAAA,UAClB,WAAA,EAAa,IAAA,CAAK,IAAA,EAAM,IAAA,IAAQ,EAAA;AAAA,UAChC,YAAA,EAAc,IAAA,CAAK,KAAA,EAAO,IAAA,IAAQ;AAAA;AACpC,OACD,CAAA;AACD,MAAA,MAAM,MAAMC,iBAAA,CAAU,OAAA,CAAQC,eAAA,CAAY,MAAA,IAAU,IAAI,CAAA;AACxD,MAAA,IAAI;AACF,QAAA,MAAMA,gBAAY,IAAA,CAAK,GAAA,EAAK,MAAM,GAAA,CAAI,IAAI,CAAC,CAAA;AAAA,MAC7C,SAAS,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAMC,sBAAA,CAAe,OAAO,CAAA;AAC7C,QAAA,IAAA,CAAK,eAAA,CAAgB,iBAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,CAAC,CAAA;AAC9E,QAAA,MAAM,KAAA;AAAA,MACR,CAAA,SAAE;AACA,QAAA,IAAA,CAAK,GAAA,EAAI;AACT,QAAA,MAAM,OAAA,GAAU,IAAA,CAAK,WAAA,EAAY,CAAE,OAAA;AACnC,QAAA,MAAM,UAAA,GAAa,IAAA,CAAK,WAAA,EAAY,CAAE,MAAA;AACtC,QAAA,MAAM,KAAA,GAAQ,SAAA,CAAW,UAAA,CAAW,OAAA,EAAS,UAAU,CAAA;AACvD,QAAA,IAAI,KAAA,CAAM,SAAS,CAAA,EAAG;AACpB,UAAC,IAAA,CAAK,KAAiC,SAAA,GAAY,KAAA;AAAA,QACrD;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IACA,EAAE,MAAM,IAAA;AAAK;AAEjB,CAAC","file":"index.cjs","sourcesContent":["/**\n * autotel-vitest\n *\n * Vitest fixture that creates one OTel span per test so all autotel-instrumented\n * code executed during a test automatically creates child spans under it;\n * making every test run filterable in your OTLP backend.\n *\n * @example\n * // vitest.config.ts: globalSetup calls init({ service: 'unit-tests' })\n * // In spec:\n * import { test, expect } from 'autotel-vitest';\n * test('creates user', async () => {\n * await userService.createUser({ email: 'test@example.com' });\n * // All trace()/span() calls become children of the test span\n * });\n */\n\nimport { test as base } from 'vitest';\nimport {\n getTracer,\n getAutotelTracerProvider,\n context as otelContext,\n otelTrace,\n SpanStatusCode,\n} from 'autotel';\nimport { TestSpanCollector } from 'autotel/test-span-collector';\nimport { SimpleSpanProcessor } from 'autotel/processors';\n\nconst TRACER_NAME = 'vitest-tests';\nconst TRACER_VERSION = '0.1.0';\n\nlet collector: TestSpanCollector | null = null;\n\nfunction ensureCollector(): TestSpanCollector {\n if (!collector) {\n collector = new TestSpanCollector();\n const provider = getAutotelTracerProvider();\n if ('addSpanProcessor' in provider) {\n (provider as any).addSpanProcessor(new SimpleSpanProcessor(collector));\n }\n }\n return collector;\n}\n\nexport const test = base.extend({\n _otelTestSpan: [\n async ({ task }, use) => {\n ensureCollector();\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`test:${task.name}`, {\n attributes: {\n 'test.name': task.name,\n 'test.file': task.file?.name ?? '',\n 'test.suite': task.suite?.name ?? '',\n },\n });\n const ctx = otelTrace.setSpan(otelContext.active(), span);\n try {\n await otelContext.with(ctx, () => use(span));\n } catch (error) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n const traceId = span.spanContext().traceId;\n const rootSpanId = span.spanContext().spanId;\n const spans = collector!.drainTrace(traceId, rootSpanId);\n if (spans.length > 0) {\n (task.meta as Record<string, unknown>).otelSpans = spans;\n }\n }\n },\n { auto: true },\n ],\n});\n\nexport { expect, describe, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';\n\n// Re-export all autotel/testing utilities\nexport {\n createTraceCollector,\n assertTraceCreated,\n assertTraceSucceeded,\n assertTraceFailed,\n assertNoErrors,\n assertTraceDuration,\n waitForTrace,\n getTraceDuration,\n createMockLogger,\n type TraceCollector,\n type TestSpan,\n type LogCollector,\n type LogEntry,\n} from 'autotel/testing';\n\n// Re-export trace context helpers for DX convenience\nexport {\n getTraceContext,\n resolveTraceUrl,\n isTracing,\n enrichWithTraceContext,\n} from 'autotel';\n\nexport type { OtelTraceContext } from 'autotel';\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as vitest from 'vitest';
|
|
2
|
+
export { afterAll, afterEach, beforeAll, beforeEach, describe, expect } from 'vitest';
|
|
3
|
+
export { LogCollector, LogEntry, TestSpan, TraceCollector, assertNoErrors, assertTraceCreated, assertTraceDuration, assertTraceFailed, assertTraceSucceeded, createMockLogger, createTraceCollector, getTraceDuration, waitForTrace } from 'autotel/testing';
|
|
4
|
+
export { OtelTraceContext, enrichWithTraceContext, getTraceContext, isTracing, resolveTraceUrl } from 'autotel';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* autotel-vitest
|
|
8
|
+
*
|
|
9
|
+
* Vitest fixture that creates one OTel span per test so all autotel-instrumented
|
|
10
|
+
* code executed during a test automatically creates child spans under it;
|
|
11
|
+
* making every test run filterable in your OTLP backend.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // vitest.config.ts: globalSetup calls init({ service: 'unit-tests' })
|
|
15
|
+
* // In spec:
|
|
16
|
+
* import { test, expect } from 'autotel-vitest';
|
|
17
|
+
* test('creates user', async () => {
|
|
18
|
+
* await userService.createUser({ email: 'test@example.com' });
|
|
19
|
+
* // All trace()/span() calls become children of the test span
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
declare const test: vitest.TestAPI<{
|
|
23
|
+
_otelTestSpan: unknown;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
export { test };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as vitest from 'vitest';
|
|
2
|
+
export { afterAll, afterEach, beforeAll, beforeEach, describe, expect } from 'vitest';
|
|
3
|
+
export { LogCollector, LogEntry, TestSpan, TraceCollector, assertNoErrors, assertTraceCreated, assertTraceDuration, assertTraceFailed, assertTraceSucceeded, createMockLogger, createTraceCollector, getTraceDuration, waitForTrace } from 'autotel/testing';
|
|
4
|
+
export { OtelTraceContext, enrichWithTraceContext, getTraceContext, isTracing, resolveTraceUrl } from 'autotel';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* autotel-vitest
|
|
8
|
+
*
|
|
9
|
+
* Vitest fixture that creates one OTel span per test so all autotel-instrumented
|
|
10
|
+
* code executed during a test automatically creates child spans under it;
|
|
11
|
+
* making every test run filterable in your OTLP backend.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // vitest.config.ts: globalSetup calls init({ service: 'unit-tests' })
|
|
15
|
+
* // In spec:
|
|
16
|
+
* import { test, expect } from 'autotel-vitest';
|
|
17
|
+
* test('creates user', async () => {
|
|
18
|
+
* await userService.createUser({ email: 'test@example.com' });
|
|
19
|
+
* // All trace()/span() calls become children of the test span
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
declare const test: vitest.TestAPI<{
|
|
23
|
+
_otelTestSpan: unknown;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
export { test };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { test as test$1 } from 'vitest';
|
|
2
|
+
export { afterAll, afterEach, beforeAll, beforeEach, describe, expect } from 'vitest';
|
|
3
|
+
import { getTracer, otelTrace, context, SpanStatusCode, getAutotelTracerProvider } from 'autotel';
|
|
4
|
+
export { enrichWithTraceContext, getTraceContext, isTracing, resolveTraceUrl } from 'autotel';
|
|
5
|
+
import { TestSpanCollector } from 'autotel/test-span-collector';
|
|
6
|
+
import { SimpleSpanProcessor } from 'autotel/processors';
|
|
7
|
+
export { assertNoErrors, assertTraceCreated, assertTraceDuration, assertTraceFailed, assertTraceSucceeded, createMockLogger, createTraceCollector, getTraceDuration, waitForTrace } from 'autotel/testing';
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
var TRACER_NAME = "vitest-tests";
|
|
11
|
+
var TRACER_VERSION = "0.1.0";
|
|
12
|
+
var collector = null;
|
|
13
|
+
function ensureCollector() {
|
|
14
|
+
if (!collector) {
|
|
15
|
+
collector = new TestSpanCollector();
|
|
16
|
+
const provider = getAutotelTracerProvider();
|
|
17
|
+
if ("addSpanProcessor" in provider) {
|
|
18
|
+
provider.addSpanProcessor(new SimpleSpanProcessor(collector));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return collector;
|
|
22
|
+
}
|
|
23
|
+
var test = test$1.extend({
|
|
24
|
+
_otelTestSpan: [
|
|
25
|
+
async ({ task }, use) => {
|
|
26
|
+
ensureCollector();
|
|
27
|
+
const tracer = getTracer(TRACER_NAME, TRACER_VERSION);
|
|
28
|
+
const span = tracer.startSpan(`test:${task.name}`, {
|
|
29
|
+
attributes: {
|
|
30
|
+
"test.name": task.name,
|
|
31
|
+
"test.file": task.file?.name ?? "",
|
|
32
|
+
"test.suite": task.suite?.name ?? ""
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const ctx = otelTrace.setSpan(context.active(), span);
|
|
36
|
+
try {
|
|
37
|
+
await context.with(ctx, () => use(span));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
40
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
41
|
+
throw error;
|
|
42
|
+
} finally {
|
|
43
|
+
span.end();
|
|
44
|
+
const traceId = span.spanContext().traceId;
|
|
45
|
+
const rootSpanId = span.spanContext().spanId;
|
|
46
|
+
const spans = collector.drainTrace(traceId, rootSpanId);
|
|
47
|
+
if (spans.length > 0) {
|
|
48
|
+
task.meta.otelSpans = spans;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{ auto: true }
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export { test };
|
|
57
|
+
//# sourceMappingURL=index.js.map
|
|
58
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["base","otelContext"],"mappings":";;;;;;;;;AA4BA,IAAM,WAAA,GAAc,cAAA;AACpB,IAAM,cAAA,GAAiB,OAAA;AAEvB,IAAI,SAAA,GAAsC,IAAA;AAE1C,SAAS,eAAA,GAAqC;AAC5C,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,SAAA,GAAY,IAAI,iBAAA,EAAkB;AAClC,IAAA,MAAM,WAAW,wBAAA,EAAyB;AAC1C,IAAA,IAAI,sBAAsB,QAAA,EAAU;AAClC,MAAC,QAAA,CAAiB,gBAAA,CAAiB,IAAI,mBAAA,CAAoB,SAAS,CAAC,CAAA;AAAA,IACvE;AAAA,EACF;AACA,EAAA,OAAO,SAAA;AACT;AAEO,IAAM,IAAA,GAAOA,OAAK,MAAA,CAAO;AAAA,EAC9B,aAAA,EAAe;AAAA,IACb,OAAO,EAAE,IAAA,EAAK,EAAG,GAAA,KAAQ;AACvB,MAAA,eAAA,EAAgB;AAChB,MAAA,MAAM,MAAA,GAAS,SAAA,CAAU,WAAA,EAAa,cAAc,CAAA;AACpD,MAAA,MAAM,OAAO,MAAA,CAAO,SAAA,CAAU,CAAA,KAAA,EAAQ,IAAA,CAAK,IAAI,CAAA,CAAA,EAAI;AAAA,QACjD,UAAA,EAAY;AAAA,UACV,aAAa,IAAA,CAAK,IAAA;AAAA,UAClB,WAAA,EAAa,IAAA,CAAK,IAAA,EAAM,IAAA,IAAQ,EAAA;AAAA,UAChC,YAAA,EAAc,IAAA,CAAK,KAAA,EAAO,IAAA,IAAQ;AAAA;AACpC,OACD,CAAA;AACD,MAAA,MAAM,MAAM,SAAA,CAAU,OAAA,CAAQC,OAAA,CAAY,MAAA,IAAU,IAAI,CAAA;AACxD,MAAA,IAAI;AACF,QAAA,MAAMA,QAAY,IAAA,CAAK,GAAA,EAAK,MAAM,GAAA,CAAI,IAAI,CAAC,CAAA;AAAA,MAC7C,SAAS,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAM,cAAA,CAAe,OAAO,CAAA;AAC7C,QAAA,IAAA,CAAK,eAAA,CAAgB,iBAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,CAAC,CAAA;AAC9E,QAAA,MAAM,KAAA;AAAA,MACR,CAAA,SAAE;AACA,QAAA,IAAA,CAAK,GAAA,EAAI;AACT,QAAA,MAAM,OAAA,GAAU,IAAA,CAAK,WAAA,EAAY,CAAE,OAAA;AACnC,QAAA,MAAM,UAAA,GAAa,IAAA,CAAK,WAAA,EAAY,CAAE,MAAA;AACtC,QAAA,MAAM,KAAA,GAAQ,SAAA,CAAW,UAAA,CAAW,OAAA,EAAS,UAAU,CAAA;AACvD,QAAA,IAAI,KAAA,CAAM,SAAS,CAAA,EAAG;AACpB,UAAC,IAAA,CAAK,KAAiC,SAAA,GAAY,KAAA;AAAA,QACrD;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IACA,EAAE,MAAM,IAAA;AAAK;AAEjB,CAAC","file":"index.js","sourcesContent":["/**\n * autotel-vitest\n *\n * Vitest fixture that creates one OTel span per test so all autotel-instrumented\n * code executed during a test automatically creates child spans under it;\n * making every test run filterable in your OTLP backend.\n *\n * @example\n * // vitest.config.ts: globalSetup calls init({ service: 'unit-tests' })\n * // In spec:\n * import { test, expect } from 'autotel-vitest';\n * test('creates user', async () => {\n * await userService.createUser({ email: 'test@example.com' });\n * // All trace()/span() calls become children of the test span\n * });\n */\n\nimport { test as base } from 'vitest';\nimport {\n getTracer,\n getAutotelTracerProvider,\n context as otelContext,\n otelTrace,\n SpanStatusCode,\n} from 'autotel';\nimport { TestSpanCollector } from 'autotel/test-span-collector';\nimport { SimpleSpanProcessor } from 'autotel/processors';\n\nconst TRACER_NAME = 'vitest-tests';\nconst TRACER_VERSION = '0.1.0';\n\nlet collector: TestSpanCollector | null = null;\n\nfunction ensureCollector(): TestSpanCollector {\n if (!collector) {\n collector = new TestSpanCollector();\n const provider = getAutotelTracerProvider();\n if ('addSpanProcessor' in provider) {\n (provider as any).addSpanProcessor(new SimpleSpanProcessor(collector));\n }\n }\n return collector;\n}\n\nexport const test = base.extend({\n _otelTestSpan: [\n async ({ task }, use) => {\n ensureCollector();\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`test:${task.name}`, {\n attributes: {\n 'test.name': task.name,\n 'test.file': task.file?.name ?? '',\n 'test.suite': task.suite?.name ?? '',\n },\n });\n const ctx = otelTrace.setSpan(otelContext.active(), span);\n try {\n await otelContext.with(ctx, () => use(span));\n } catch (error) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n const traceId = span.spanContext().traceId;\n const rootSpanId = span.spanContext().spanId;\n const spans = collector!.drainTrace(traceId, rootSpanId);\n if (spans.length > 0) {\n (task.meta as Record<string, unknown>).otelSpans = spans;\n }\n }\n },\n { auto: true },\n ],\n});\n\nexport { expect, describe, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';\n\n// Re-export all autotel/testing utilities\nexport {\n createTraceCollector,\n assertTraceCreated,\n assertTraceSucceeded,\n assertTraceFailed,\n assertNoErrors,\n assertTraceDuration,\n waitForTrace,\n getTraceDuration,\n createMockLogger,\n type TraceCollector,\n type TestSpan,\n type LogCollector,\n type LogEntry,\n} from 'autotel/testing';\n\n// Re-export trace context helpers for DX convenience\nexport {\n getTraceContext,\n resolveTraceUrl,\n isTracing,\n enrichWithTraceContext,\n} from 'autotel';\n\nexport type { OtelTraceContext } from 'autotel';\n"]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var autotel = require('autotel');
|
|
6
|
+
|
|
7
|
+
// src/reporter.ts
|
|
8
|
+
var TRACER_NAME = "vitest-reporter";
|
|
9
|
+
var TRACER_VERSION = "0.1.0";
|
|
10
|
+
function toError(testError) {
|
|
11
|
+
const err = new Error(testError.message ?? "Unknown error");
|
|
12
|
+
if (testError.stack) err.stack = testError.stack;
|
|
13
|
+
return err;
|
|
14
|
+
}
|
|
15
|
+
var OtelReporter = class {
|
|
16
|
+
testSpans = /* @__PURE__ */ new Map();
|
|
17
|
+
suiteSpans = /* @__PURE__ */ new Map();
|
|
18
|
+
onTestCaseReady(testCase) {
|
|
19
|
+
const tracer = autotel.getTracer(TRACER_NAME, TRACER_VERSION);
|
|
20
|
+
const moduleId = testCase.module.moduleId ?? "";
|
|
21
|
+
const span = tracer.startSpan(`test:${testCase.name}`, {
|
|
22
|
+
attributes: {
|
|
23
|
+
"test.name": testCase.name,
|
|
24
|
+
"test.fullName": testCase.fullName,
|
|
25
|
+
"test.file": moduleId
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
this.testSpans.set(testCase.id, { span, moduleId });
|
|
29
|
+
}
|
|
30
|
+
onTestCaseResult(testCase) {
|
|
31
|
+
const entry = this.testSpans.get(testCase.id);
|
|
32
|
+
if (!entry) return;
|
|
33
|
+
const result = testCase.result();
|
|
34
|
+
if (result.state === "failed") {
|
|
35
|
+
entry.span.setStatus({ code: autotel.SpanStatusCode.ERROR });
|
|
36
|
+
if (result.errors && result.errors.length > 0) {
|
|
37
|
+
for (const error of result.errors) {
|
|
38
|
+
entry.span.recordException(toError(error));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
entry.span.end();
|
|
43
|
+
this.testSpans.delete(testCase.id);
|
|
44
|
+
}
|
|
45
|
+
onTestSuiteReady(testSuite) {
|
|
46
|
+
const tracer = autotel.getTracer(TRACER_NAME, TRACER_VERSION);
|
|
47
|
+
const moduleId = testSuite.module.moduleId ?? "";
|
|
48
|
+
const span = tracer.startSpan(`suite:${testSuite.name}`, {
|
|
49
|
+
attributes: {
|
|
50
|
+
"suite.name": testSuite.name,
|
|
51
|
+
"suite.file": moduleId
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
this.suiteSpans.set(testSuite.id, { span, moduleId });
|
|
55
|
+
}
|
|
56
|
+
onTestSuiteResult(testSuite) {
|
|
57
|
+
const entry = this.suiteSpans.get(testSuite.id);
|
|
58
|
+
if (!entry) return;
|
|
59
|
+
const state = testSuite.state();
|
|
60
|
+
if (state === "failed") {
|
|
61
|
+
entry.span.setStatus({ code: autotel.SpanStatusCode.ERROR });
|
|
62
|
+
}
|
|
63
|
+
entry.span.end();
|
|
64
|
+
this.suiteSpans.delete(testSuite.id);
|
|
65
|
+
}
|
|
66
|
+
onTestModuleEnd(testModule) {
|
|
67
|
+
const moduleId = testModule.moduleId;
|
|
68
|
+
for (const [key, entry] of this.testSpans) {
|
|
69
|
+
if (entry.moduleId === moduleId) {
|
|
70
|
+
entry.span.end();
|
|
71
|
+
this.testSpans.delete(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const [key, entry] of this.suiteSpans) {
|
|
75
|
+
if (entry.moduleId === moduleId) {
|
|
76
|
+
entry.span.end();
|
|
77
|
+
this.suiteSpans.delete(key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var reporter_default = OtelReporter;
|
|
83
|
+
|
|
84
|
+
exports.OtelReporter = OtelReporter;
|
|
85
|
+
exports.default = reporter_default;
|
|
86
|
+
//# sourceMappingURL=reporter.cjs.map
|
|
87
|
+
//# sourceMappingURL=reporter.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/reporter.ts"],"names":["getTracer","SpanStatusCode"],"mappings":"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","file":"reporter.cjs","sourcesContent":["/**\n * Optional Vitest reporter that creates OTel spans for each test and suite.\n * Runs in the runner process; ensure autotel.init() is called in globalSetup so spans are exported.\n *\n * Use when you want test/suite timing and hierarchy in OTLP from the runner side.\n * For \"test → instrumented code\" in one trace (worker side), use the test fixture.\n *\n * @example\n * // vitest.config.ts\n * import { defineConfig } from 'vitest/config';\n *\n * export default defineConfig({\n * test: {\n * reporters: ['default', 'autotel-vitest/reporter'],\n * globalSetup: './globalSetup.ts', // must call init()\n * },\n * });\n */\n\nimport type { Reporter, TestCase, TestModule, TestSuite } from 'vitest/node';\nimport { getTracer, SpanStatusCode } from 'autotel';\n\nconst TRACER_NAME = 'vitest-reporter';\nconst TRACER_VERSION = '0.1.0';\n\ntype SpanEntry = {\n span: ReturnType<ReturnType<typeof getTracer>['startSpan']>;\n moduleId: string;\n};\n\n/** Convert a vitest TestError-like object to a standard Error for OTel. */\nfunction toError(testError: { message?: string; stack?: string }): Error {\n const err = new Error(testError.message ?? 'Unknown error');\n if (testError.stack) err.stack = testError.stack;\n return err;\n}\n\n/**\n * Vitest Reporter that creates one span per test and one per suite (as parents).\n * Requires autotel.init() in globalSetup so spans are exported.\n */\nclass OtelReporter implements Reporter {\n private testSpans = new Map<string, SpanEntry>();\n private suiteSpans = new Map<string, SpanEntry>();\n\n onTestCaseReady(testCase: TestCase): void {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const moduleId = testCase.module.moduleId ?? '';\n const span = tracer.startSpan(`test:${testCase.name}`, {\n attributes: {\n 'test.name': testCase.name,\n 'test.fullName': testCase.fullName,\n 'test.file': moduleId,\n },\n });\n this.testSpans.set(testCase.id, { span, moduleId });\n }\n\n onTestCaseResult(testCase: TestCase): void {\n const entry = this.testSpans.get(testCase.id);\n if (!entry) return;\n\n const result = testCase.result();\n if (result.state === 'failed') {\n entry.span.setStatus({ code: SpanStatusCode.ERROR });\n if (result.errors && result.errors.length > 0) {\n for (const error of result.errors) {\n entry.span.recordException(toError(error));\n }\n }\n }\n entry.span.end();\n this.testSpans.delete(testCase.id);\n }\n\n onTestSuiteReady(testSuite: TestSuite): void {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const moduleId = testSuite.module.moduleId ?? '';\n const span = tracer.startSpan(`suite:${testSuite.name}`, {\n attributes: {\n 'suite.name': testSuite.name,\n 'suite.file': moduleId,\n },\n });\n this.suiteSpans.set(testSuite.id, { span, moduleId });\n }\n\n onTestSuiteResult(testSuite: TestSuite): void {\n const entry = this.suiteSpans.get(testSuite.id);\n if (!entry) return;\n\n const state = testSuite.state();\n if (state === 'failed') {\n entry.span.setStatus({ code: SpanStatusCode.ERROR });\n }\n entry.span.end();\n this.suiteSpans.delete(testSuite.id);\n }\n\n onTestModuleEnd(testModule: TestModule): void {\n const moduleId = testModule.moduleId;\n // Clean up any remaining spans for this specific module\n for (const [key, entry] of this.testSpans) {\n if (entry.moduleId === moduleId) {\n entry.span.end();\n this.testSpans.delete(key);\n }\n }\n for (const [key, entry] of this.suiteSpans) {\n if (entry.moduleId === moduleId) {\n entry.span.end();\n this.suiteSpans.delete(key);\n }\n }\n }\n}\n\nexport { OtelReporter };\nexport default OtelReporter;\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Reporter, TestCase, TestSuite, TestModule } from 'vitest/node';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional Vitest reporter that creates OTel spans for each test and suite.
|
|
5
|
+
* Runs in the runner process; ensure autotel.init() is called in globalSetup so spans are exported.
|
|
6
|
+
*
|
|
7
|
+
* Use when you want test/suite timing and hierarchy in OTLP from the runner side.
|
|
8
|
+
* For "test → instrumented code" in one trace (worker side), use the test fixture.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // vitest.config.ts
|
|
12
|
+
* import { defineConfig } from 'vitest/config';
|
|
13
|
+
*
|
|
14
|
+
* export default defineConfig({
|
|
15
|
+
* test: {
|
|
16
|
+
* reporters: ['default', 'autotel-vitest/reporter'],
|
|
17
|
+
* globalSetup: './globalSetup.ts', // must call init()
|
|
18
|
+
* },
|
|
19
|
+
* });
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Vitest Reporter that creates one span per test and one per suite (as parents).
|
|
24
|
+
* Requires autotel.init() in globalSetup so spans are exported.
|
|
25
|
+
*/
|
|
26
|
+
declare class OtelReporter implements Reporter {
|
|
27
|
+
private testSpans;
|
|
28
|
+
private suiteSpans;
|
|
29
|
+
onTestCaseReady(testCase: TestCase): void;
|
|
30
|
+
onTestCaseResult(testCase: TestCase): void;
|
|
31
|
+
onTestSuiteReady(testSuite: TestSuite): void;
|
|
32
|
+
onTestSuiteResult(testSuite: TestSuite): void;
|
|
33
|
+
onTestModuleEnd(testModule: TestModule): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { OtelReporter, OtelReporter as default };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Reporter, TestCase, TestSuite, TestModule } from 'vitest/node';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional Vitest reporter that creates OTel spans for each test and suite.
|
|
5
|
+
* Runs in the runner process; ensure autotel.init() is called in globalSetup so spans are exported.
|
|
6
|
+
*
|
|
7
|
+
* Use when you want test/suite timing and hierarchy in OTLP from the runner side.
|
|
8
|
+
* For "test → instrumented code" in one trace (worker side), use the test fixture.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // vitest.config.ts
|
|
12
|
+
* import { defineConfig } from 'vitest/config';
|
|
13
|
+
*
|
|
14
|
+
* export default defineConfig({
|
|
15
|
+
* test: {
|
|
16
|
+
* reporters: ['default', 'autotel-vitest/reporter'],
|
|
17
|
+
* globalSetup: './globalSetup.ts', // must call init()
|
|
18
|
+
* },
|
|
19
|
+
* });
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Vitest Reporter that creates one span per test and one per suite (as parents).
|
|
24
|
+
* Requires autotel.init() in globalSetup so spans are exported.
|
|
25
|
+
*/
|
|
26
|
+
declare class OtelReporter implements Reporter {
|
|
27
|
+
private testSpans;
|
|
28
|
+
private suiteSpans;
|
|
29
|
+
onTestCaseReady(testCase: TestCase): void;
|
|
30
|
+
onTestCaseResult(testCase: TestCase): void;
|
|
31
|
+
onTestSuiteReady(testSuite: TestSuite): void;
|
|
32
|
+
onTestSuiteResult(testSuite: TestSuite): void;
|
|
33
|
+
onTestModuleEnd(testModule: TestModule): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { OtelReporter, OtelReporter as default };
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { getTracer, SpanStatusCode } from 'autotel';
|
|
2
|
+
|
|
3
|
+
// src/reporter.ts
|
|
4
|
+
var TRACER_NAME = "vitest-reporter";
|
|
5
|
+
var TRACER_VERSION = "0.1.0";
|
|
6
|
+
function toError(testError) {
|
|
7
|
+
const err = new Error(testError.message ?? "Unknown error");
|
|
8
|
+
if (testError.stack) err.stack = testError.stack;
|
|
9
|
+
return err;
|
|
10
|
+
}
|
|
11
|
+
var OtelReporter = class {
|
|
12
|
+
testSpans = /* @__PURE__ */ new Map();
|
|
13
|
+
suiteSpans = /* @__PURE__ */ new Map();
|
|
14
|
+
onTestCaseReady(testCase) {
|
|
15
|
+
const tracer = getTracer(TRACER_NAME, TRACER_VERSION);
|
|
16
|
+
const moduleId = testCase.module.moduleId ?? "";
|
|
17
|
+
const span = tracer.startSpan(`test:${testCase.name}`, {
|
|
18
|
+
attributes: {
|
|
19
|
+
"test.name": testCase.name,
|
|
20
|
+
"test.fullName": testCase.fullName,
|
|
21
|
+
"test.file": moduleId
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
this.testSpans.set(testCase.id, { span, moduleId });
|
|
25
|
+
}
|
|
26
|
+
onTestCaseResult(testCase) {
|
|
27
|
+
const entry = this.testSpans.get(testCase.id);
|
|
28
|
+
if (!entry) return;
|
|
29
|
+
const result = testCase.result();
|
|
30
|
+
if (result.state === "failed") {
|
|
31
|
+
entry.span.setStatus({ code: SpanStatusCode.ERROR });
|
|
32
|
+
if (result.errors && result.errors.length > 0) {
|
|
33
|
+
for (const error of result.errors) {
|
|
34
|
+
entry.span.recordException(toError(error));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
entry.span.end();
|
|
39
|
+
this.testSpans.delete(testCase.id);
|
|
40
|
+
}
|
|
41
|
+
onTestSuiteReady(testSuite) {
|
|
42
|
+
const tracer = getTracer(TRACER_NAME, TRACER_VERSION);
|
|
43
|
+
const moduleId = testSuite.module.moduleId ?? "";
|
|
44
|
+
const span = tracer.startSpan(`suite:${testSuite.name}`, {
|
|
45
|
+
attributes: {
|
|
46
|
+
"suite.name": testSuite.name,
|
|
47
|
+
"suite.file": moduleId
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
this.suiteSpans.set(testSuite.id, { span, moduleId });
|
|
51
|
+
}
|
|
52
|
+
onTestSuiteResult(testSuite) {
|
|
53
|
+
const entry = this.suiteSpans.get(testSuite.id);
|
|
54
|
+
if (!entry) return;
|
|
55
|
+
const state = testSuite.state();
|
|
56
|
+
if (state === "failed") {
|
|
57
|
+
entry.span.setStatus({ code: SpanStatusCode.ERROR });
|
|
58
|
+
}
|
|
59
|
+
entry.span.end();
|
|
60
|
+
this.suiteSpans.delete(testSuite.id);
|
|
61
|
+
}
|
|
62
|
+
onTestModuleEnd(testModule) {
|
|
63
|
+
const moduleId = testModule.moduleId;
|
|
64
|
+
for (const [key, entry] of this.testSpans) {
|
|
65
|
+
if (entry.moduleId === moduleId) {
|
|
66
|
+
entry.span.end();
|
|
67
|
+
this.testSpans.delete(key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const [key, entry] of this.suiteSpans) {
|
|
71
|
+
if (entry.moduleId === moduleId) {
|
|
72
|
+
entry.span.end();
|
|
73
|
+
this.suiteSpans.delete(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var reporter_default = OtelReporter;
|
|
79
|
+
|
|
80
|
+
export { OtelReporter, reporter_default as default };
|
|
81
|
+
//# sourceMappingURL=reporter.js.map
|
|
82
|
+
//# sourceMappingURL=reporter.js.map
|