executable-stories-vitest 8.8.5 → 8.9.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/dist/index.cjs +16 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/otel.cjs +85 -0
- package/dist/otel.cjs.map +1 -0
- package/dist/otel.d.cts +64 -0
- package/dist/otel.d.ts +64 -0
- package/dist/otel.js +59 -0
- package/dist/otel.js.map +1 -0
- package/package.json +9 -3
package/dist/otel.d.cts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect the spans Vitest's OpenTelemetry instrumentation produces, so a
|
|
3
|
+
* story can claim the ones that belong to it.
|
|
4
|
+
*
|
|
5
|
+
* Vitest wraps every test in a span, which means `story.init()` already
|
|
6
|
+
* captures a trace id with no extra wiring. This is the other half: a
|
|
7
|
+
* SpanProcessor that keeps finished spans grouped by trace id, so the story
|
|
8
|
+
* can take its own at the end of the test.
|
|
9
|
+
*/
|
|
10
|
+
/** A finished span, in the shape an OTel SDK hands a SpanProcessor. */
|
|
11
|
+
interface ReadableSpanLike {
|
|
12
|
+
name: string;
|
|
13
|
+
spanContext(): {
|
|
14
|
+
traceId: string;
|
|
15
|
+
spanId: string;
|
|
16
|
+
};
|
|
17
|
+
/** Current OTel SDKs. */
|
|
18
|
+
parentSpanContext?: {
|
|
19
|
+
spanId: string;
|
|
20
|
+
};
|
|
21
|
+
/** Older OTel SDKs carried the parent as a bare id. */
|
|
22
|
+
parentSpanId?: string;
|
|
23
|
+
attributes?: Record<string, unknown>;
|
|
24
|
+
status?: {
|
|
25
|
+
code: number;
|
|
26
|
+
message?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** A span in the shape the story report carries. */
|
|
30
|
+
interface CollectedSpan {
|
|
31
|
+
spanId: string;
|
|
32
|
+
parentSpanId?: string;
|
|
33
|
+
name: string;
|
|
34
|
+
attributes?: Record<string, unknown>;
|
|
35
|
+
status: "ok" | "error" | "unset";
|
|
36
|
+
statusMessage?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Structurally an OpenTelemetry `SpanProcessor`, plus the drain the story API
|
|
40
|
+
* uses. Declared here rather than imported so the published types carry no
|
|
41
|
+
* OTel dependency; `otel-span-processor.test.ts` holds it to the real
|
|
42
|
+
* interface by handing one to a real tracer provider.
|
|
43
|
+
*
|
|
44
|
+
* `onStart`, `forceFlush` and `shutdown` are not optional. A provider wraps
|
|
45
|
+
* every processor in a `MultiSpanProcessor`, which calls all three without
|
|
46
|
+
* checking they exist — only `onEnding` is guarded — so a collector missing
|
|
47
|
+
* one throws on the first span of the run.
|
|
48
|
+
*/
|
|
49
|
+
interface StorySpanCollector {
|
|
50
|
+
onStart(): void;
|
|
51
|
+
onEnd(span: ReadableSpanLike): void;
|
|
52
|
+
forceFlush(): Promise<void>;
|
|
53
|
+
shutdown(): Promise<void>;
|
|
54
|
+
takeSpansForTrace(traceId: string): CollectedSpan[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The spans that ended during `traceId`, or none when no collector is
|
|
58
|
+
* installed. Called by `story.init()` at test end; a suite that never
|
|
59
|
+
* configured OpenTelemetry gets an empty list and no behaviour change.
|
|
60
|
+
*/
|
|
61
|
+
declare function takeCollectedSpans(traceId: string): CollectedSpan[];
|
|
62
|
+
declare function storySpanCollector(): StorySpanCollector;
|
|
63
|
+
|
|
64
|
+
export { type CollectedSpan, type ReadableSpanLike, type StorySpanCollector, storySpanCollector, takeCollectedSpans };
|
package/dist/otel.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect the spans Vitest's OpenTelemetry instrumentation produces, so a
|
|
3
|
+
* story can claim the ones that belong to it.
|
|
4
|
+
*
|
|
5
|
+
* Vitest wraps every test in a span, which means `story.init()` already
|
|
6
|
+
* captures a trace id with no extra wiring. This is the other half: a
|
|
7
|
+
* SpanProcessor that keeps finished spans grouped by trace id, so the story
|
|
8
|
+
* can take its own at the end of the test.
|
|
9
|
+
*/
|
|
10
|
+
/** A finished span, in the shape an OTel SDK hands a SpanProcessor. */
|
|
11
|
+
interface ReadableSpanLike {
|
|
12
|
+
name: string;
|
|
13
|
+
spanContext(): {
|
|
14
|
+
traceId: string;
|
|
15
|
+
spanId: string;
|
|
16
|
+
};
|
|
17
|
+
/** Current OTel SDKs. */
|
|
18
|
+
parentSpanContext?: {
|
|
19
|
+
spanId: string;
|
|
20
|
+
};
|
|
21
|
+
/** Older OTel SDKs carried the parent as a bare id. */
|
|
22
|
+
parentSpanId?: string;
|
|
23
|
+
attributes?: Record<string, unknown>;
|
|
24
|
+
status?: {
|
|
25
|
+
code: number;
|
|
26
|
+
message?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** A span in the shape the story report carries. */
|
|
30
|
+
interface CollectedSpan {
|
|
31
|
+
spanId: string;
|
|
32
|
+
parentSpanId?: string;
|
|
33
|
+
name: string;
|
|
34
|
+
attributes?: Record<string, unknown>;
|
|
35
|
+
status: "ok" | "error" | "unset";
|
|
36
|
+
statusMessage?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Structurally an OpenTelemetry `SpanProcessor`, plus the drain the story API
|
|
40
|
+
* uses. Declared here rather than imported so the published types carry no
|
|
41
|
+
* OTel dependency; `otel-span-processor.test.ts` holds it to the real
|
|
42
|
+
* interface by handing one to a real tracer provider.
|
|
43
|
+
*
|
|
44
|
+
* `onStart`, `forceFlush` and `shutdown` are not optional. A provider wraps
|
|
45
|
+
* every processor in a `MultiSpanProcessor`, which calls all three without
|
|
46
|
+
* checking they exist — only `onEnding` is guarded — so a collector missing
|
|
47
|
+
* one throws on the first span of the run.
|
|
48
|
+
*/
|
|
49
|
+
interface StorySpanCollector {
|
|
50
|
+
onStart(): void;
|
|
51
|
+
onEnd(span: ReadableSpanLike): void;
|
|
52
|
+
forceFlush(): Promise<void>;
|
|
53
|
+
shutdown(): Promise<void>;
|
|
54
|
+
takeSpansForTrace(traceId: string): CollectedSpan[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The spans that ended during `traceId`, or none when no collector is
|
|
58
|
+
* installed. Called by `story.init()` at test end; a suite that never
|
|
59
|
+
* configured OpenTelemetry gets an empty list and no behaviour change.
|
|
60
|
+
*/
|
|
61
|
+
declare function takeCollectedSpans(traceId: string): CollectedSpan[];
|
|
62
|
+
declare function storySpanCollector(): StorySpanCollector;
|
|
63
|
+
|
|
64
|
+
export { type CollectedSpan, type ReadableSpanLike, type StorySpanCollector, storySpanCollector, takeCollectedSpans };
|
package/dist/otel.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/otel.ts
|
|
2
|
+
var MAX_SPANS_PER_TRACE = 500;
|
|
3
|
+
function statusOf(code) {
|
|
4
|
+
if (code === 2) return "error";
|
|
5
|
+
if (code === 1) return "ok";
|
|
6
|
+
return "unset";
|
|
7
|
+
}
|
|
8
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("executable-stories.span-collector");
|
|
9
|
+
function registry() {
|
|
10
|
+
return globalThis;
|
|
11
|
+
}
|
|
12
|
+
function takeCollectedSpans(traceId) {
|
|
13
|
+
return registry()[REGISTRY]?.takeSpansForTrace(traceId) ?? [];
|
|
14
|
+
}
|
|
15
|
+
function storySpanCollector() {
|
|
16
|
+
const byTrace = /* @__PURE__ */ new Map();
|
|
17
|
+
const collector = {
|
|
18
|
+
// Nothing to do at span start: a span is only useful once it has ended and
|
|
19
|
+
// carries its duration, status and attributes. Present because the SDK
|
|
20
|
+
// calls it, not because it has work.
|
|
21
|
+
onStart() {
|
|
22
|
+
},
|
|
23
|
+
onEnd(span) {
|
|
24
|
+
const { traceId, spanId } = span.spanContext();
|
|
25
|
+
const spans = byTrace.get(traceId) ?? [];
|
|
26
|
+
if (spans.length >= MAX_SPANS_PER_TRACE) return;
|
|
27
|
+
const parentSpanId = span.parentSpanContext?.spanId ?? span.parentSpanId;
|
|
28
|
+
spans.push({
|
|
29
|
+
spanId,
|
|
30
|
+
...parentSpanId ? { parentSpanId } : {},
|
|
31
|
+
name: span.name,
|
|
32
|
+
...span.attributes && Object.keys(span.attributes).length > 0 ? { attributes: span.attributes } : {},
|
|
33
|
+
status: statusOf(span.status?.code),
|
|
34
|
+
...span.status?.message ? { statusMessage: span.status.message } : {}
|
|
35
|
+
});
|
|
36
|
+
byTrace.set(traceId, spans);
|
|
37
|
+
},
|
|
38
|
+
// Nothing is buffered for export and nothing outlives the worker, so both
|
|
39
|
+
// lifecycle calls have nothing to do. Spans are held in memory until the
|
|
40
|
+
// story that owns them takes them, and a shutdown must not discard them:
|
|
41
|
+
// an SDK shut down before the last story drains would otherwise lose it.
|
|
42
|
+
async forceFlush() {
|
|
43
|
+
},
|
|
44
|
+
async shutdown() {
|
|
45
|
+
},
|
|
46
|
+
takeSpansForTrace(traceId) {
|
|
47
|
+
const spans = byTrace.get(traceId) ?? [];
|
|
48
|
+
byTrace.delete(traceId);
|
|
49
|
+
return spans;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
registry()[REGISTRY] = collector;
|
|
53
|
+
return collector;
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
storySpanCollector,
|
|
57
|
+
takeCollectedSpans
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=otel.js.map
|
package/dist/otel.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/otel.ts"],"sourcesContent":["/**\n * Collect the spans Vitest's OpenTelemetry instrumentation produces, so a\n * story can claim the ones that belong to it.\n *\n * Vitest wraps every test in a span, which means `story.init()` already\n * captures a trace id with no extra wiring. This is the other half: a\n * SpanProcessor that keeps finished spans grouped by trace id, so the story\n * can take its own at the end of the test.\n */\n\n/** A finished span, in the shape an OTel SDK hands a SpanProcessor. */\nexport interface ReadableSpanLike {\n name: string;\n spanContext(): { traceId: string; spanId: string };\n /** Current OTel SDKs. */\n parentSpanContext?: { spanId: string };\n /** Older OTel SDKs carried the parent as a bare id. */\n parentSpanId?: string;\n attributes?: Record<string, unknown>;\n status?: { code: number; message?: string };\n}\n\n/** A span in the shape the story report carries. */\nexport interface CollectedSpan {\n spanId: string;\n parentSpanId?: string;\n name: string;\n attributes?: Record<string, unknown>;\n status: \"ok\" | \"error\" | \"unset\";\n statusMessage?: string;\n}\n\n/**\n * Structurally an OpenTelemetry `SpanProcessor`, plus the drain the story API\n * uses. Declared here rather than imported so the published types carry no\n * OTel dependency; `otel-span-processor.test.ts` holds it to the real\n * interface by handing one to a real tracer provider.\n *\n * `onStart`, `forceFlush` and `shutdown` are not optional. A provider wraps\n * every processor in a `MultiSpanProcessor`, which calls all three without\n * checking they exist — only `onEnding` is guarded — so a collector missing\n * one throws on the first span of the run.\n */\nexport interface StorySpanCollector {\n onStart(): void;\n onEnd(span: ReadableSpanLike): void;\n forceFlush(): Promise<void>;\n shutdown(): Promise<void>;\n takeSpansForTrace(traceId: string): CollectedSpan[];\n}\n\n/**\n * Spans kept per trace. Auto-instrumentation on a loop of queries produces\n * hundreds of near-identical leaf spans, and the architecture is described by\n * the first few of them. The head is kept because a trace arrives roughly\n * root-first, so keeping it keeps the shape and drops the repetition.\n */\nconst MAX_SPANS_PER_TRACE = 500;\n\n/** OTel SpanStatusCode: 0 UNSET, 1 OK, 2 ERROR. */\nfunction statusOf(code: number | undefined): \"ok\" | \"error\" | \"unset\" {\n if (code === 2) return \"error\";\n if (code === 1) return \"ok\";\n return \"unset\";\n}\n\n/**\n * The collector the story API drains from, parked on `globalThis`.\n *\n * A module-level variable would be wrong here. The collector is constructed in\n * the OTel SDK module Vitest loads, and the story API is imported by the test\n * file; those are not guaranteed to be the same module instance, and a test\n * that mocks anything in the story API's import chain gets a second module\n * graph where a module-level `installed` reads as undefined. The realm is the\n * only thing both sides reliably share.\n *\n * Still per-worker: Vitest gives each worker its own realm, so two workers\n * never see each other's spans.\n */\nconst REGISTRY = Symbol.for(\"executable-stories.span-collector\");\n\ntype Registry = { [REGISTRY]?: StorySpanCollector };\n\nfunction registry(): Registry {\n return globalThis as unknown as Registry;\n}\n\n/**\n * The spans that ended during `traceId`, or none when no collector is\n * installed. Called by `story.init()` at test end; a suite that never\n * configured OpenTelemetry gets an empty list and no behaviour change.\n */\nexport function takeCollectedSpans(traceId: string): CollectedSpan[] {\n return registry()[REGISTRY]?.takeSpansForTrace(traceId) ?? [];\n}\n\nexport function storySpanCollector(): StorySpanCollector {\n const byTrace = new Map<string, CollectedSpan[]>();\n\n const collector: StorySpanCollector = {\n // Nothing to do at span start: a span is only useful once it has ended and\n // carries its duration, status and attributes. Present because the SDK\n // calls it, not because it has work.\n onStart() {},\n onEnd(span) {\n const { traceId, spanId } = span.spanContext();\n const spans = byTrace.get(traceId) ?? [];\n if (spans.length >= MAX_SPANS_PER_TRACE) return;\n // The parent moved from a bare id to a span context across OTel SDK\n // versions, and this package pins neither.\n const parentSpanId = span.parentSpanContext?.spanId ?? span.parentSpanId;\n spans.push({\n spanId,\n ...(parentSpanId ? { parentSpanId } : {}),\n name: span.name,\n ...(span.attributes && Object.keys(span.attributes).length > 0\n ? { attributes: span.attributes }\n : {}),\n status: statusOf(span.status?.code),\n ...(span.status?.message ? { statusMessage: span.status.message } : {}),\n });\n byTrace.set(traceId, spans);\n },\n // Nothing is buffered for export and nothing outlives the worker, so both\n // lifecycle calls have nothing to do. Spans are held in memory until the\n // story that owns them takes them, and a shutdown must not discard them:\n // an SDK shut down before the last story drains would otherwise lose it.\n async forceFlush() {},\n async shutdown() {},\n takeSpansForTrace(traceId) {\n const spans = byTrace.get(traceId) ?? [];\n byTrace.delete(traceId);\n return spans;\n },\n };\n\n registry()[REGISTRY] = collector;\n return collector;\n}\n"],"mappings":";AAyDA,IAAM,sBAAsB;AAG5B,SAAS,SAAS,MAAoD;AACpE,MAAI,SAAS,EAAG,QAAO;AACvB,MAAI,SAAS,EAAG,QAAO;AACvB,SAAO;AACT;AAeA,IAAM,WAAW,uBAAO,IAAI,mCAAmC;AAI/D,SAAS,WAAqB;AAC5B,SAAO;AACT;AAOO,SAAS,mBAAmB,SAAkC;AACnE,SAAO,SAAS,EAAE,QAAQ,GAAG,kBAAkB,OAAO,KAAK,CAAC;AAC9D;AAEO,SAAS,qBAAyC;AACvD,QAAM,UAAU,oBAAI,IAA6B;AAEjD,QAAM,YAAgC;AAAA;AAAA;AAAA;AAAA,IAIpC,UAAU;AAAA,IAAC;AAAA,IACX,MAAM,MAAM;AACV,YAAM,EAAE,SAAS,OAAO,IAAI,KAAK,YAAY;AAC7C,YAAM,QAAQ,QAAQ,IAAI,OAAO,KAAK,CAAC;AACvC,UAAI,MAAM,UAAU,oBAAqB;AAGzC,YAAM,eAAe,KAAK,mBAAmB,UAAU,KAAK;AAC5D,YAAM,KAAK;AAAA,QACT;AAAA,QACA,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,QACvC,MAAM,KAAK;AAAA,QACX,GAAI,KAAK,cAAc,OAAO,KAAK,KAAK,UAAU,EAAE,SAAS,IACzD,EAAE,YAAY,KAAK,WAAW,IAC9B,CAAC;AAAA,QACL,QAAQ,SAAS,KAAK,QAAQ,IAAI;AAAA,QAClC,GAAI,KAAK,QAAQ,UAAU,EAAE,eAAe,KAAK,OAAO,QAAQ,IAAI,CAAC;AAAA,MACvE,CAAC;AACD,cAAQ,IAAI,SAAS,KAAK;AAAA,IAC5B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,MAAM,aAAa;AAAA,IAAC;AAAA,IACpB,MAAM,WAAW;AAAA,IAAC;AAAA,IAClB,kBAAkB,SAAS;AACzB,YAAM,QAAQ,QAAQ,IAAI,OAAO,KAAK,CAAC;AACvC,cAAQ,OAAO,OAAO;AACtB,aAAO;AAAA,IACT;AAAA,EACF;AAEA,WAAS,EAAE,QAAQ,IAAI;AACvB,SAAO;AACT;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "executable-stories-vitest",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.9.0",
|
|
4
4
|
"description": "TS-first story/given/when/then helpers for Vitest with Markdown user-story doc generation.",
|
|
5
5
|
"author": "Jag Reehal <jag@jagreehal.com>",
|
|
6
6
|
"homepage": "https://github.com/jagreehal/executable-stories#readme",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"types": "./dist/reporter.d.ts",
|
|
29
29
|
"import": "./dist/reporter.js",
|
|
30
30
|
"require": "./dist/reporter.cjs"
|
|
31
|
+
},
|
|
32
|
+
"./otel": {
|
|
33
|
+
"types": "./dist/otel.d.ts",
|
|
34
|
+
"import": "./dist/otel.js",
|
|
35
|
+
"require": "./dist/otel.cjs"
|
|
31
36
|
}
|
|
32
37
|
},
|
|
33
38
|
"files": [
|
|
@@ -41,11 +46,12 @@
|
|
|
41
46
|
"dependencies": {
|
|
42
47
|
"fast-glob": "^3.3.3",
|
|
43
48
|
"picomatch": "^4.0.5",
|
|
44
|
-
"executable-stories-core": "0.
|
|
45
|
-
"executable-stories-formatters": "1.
|
|
49
|
+
"executable-stories-core": "0.25.0",
|
|
50
|
+
"executable-stories-formatters": "1.17.0"
|
|
46
51
|
},
|
|
47
52
|
"devDependencies": {
|
|
48
53
|
"@opentelemetry/api": "^1.9.1",
|
|
54
|
+
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
49
55
|
"@types/node": "^26.2.0",
|
|
50
56
|
"@types/picomatch": "^4.0.3",
|
|
51
57
|
"tsup": "^8.5.1",
|