1
6import { describe, expect, it } from "vitest";
7import { context, trace } from "@opentelemetry/api";
8import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
9import {
10 captureTraceparent,
11 resumeTraceparent,
12 parseTraceparent,
13 buildTraceparent,
14 metadataTraceparent,
15 registerTraceparentPropagator,
16 traceparentPropagator
17} from "../traceparent.js";
18
19registerTraceparentPropagator();
20
21
22
23
24context.setGlobalContextManager(new AsyncLocalStorageContextManager());
25
26const TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736";
27const SPAN_ID = "00f067aa0ba902b7";
28
29describe("parseTraceparent", () => {
30 it("parses a valid v00 header", () => {
31 expect(parseTraceparent(`00-${TRACE_ID}-${SPAN_ID}-01`)).toEqual({ traceId: TRACE_ID, spanId: SPAN_ID, flags: 1 });
32 });
33 it("rejects bad versions, short ids, and all-zero ids", () => {
34 expect(parseTraceparent(`01-${TRACE_ID}-${SPAN_ID}-01`)).toBeNull();
35 expect(parseTraceparent(`00-${TRACE_ID}-abc-01`)).toBeNull();
36 expect(parseTraceparent(`00-00000000000000000000000000000000-${SPAN_ID}-01`)).toBeNull();
37 expect(parseTraceparent("not-a-header")).toBeNull();
38 });
39});
40
41describe("captureTraceparent / resumeTraceparent round-trip", () => {
42 it("captures the active span context under an ambient span", () => {
43 const tracer = trace.getTracer("test");
44 const captured: string | undefined = tracer
45 .startActiveSpan("api-request", (span) => {
46 const tp = captureTraceparent();
47 span.end();
48 return tp;
49 });
50
51
52 expect(captured).toBeUndefined();
53 });
54
55 it("resume re-extracts the SAME trace+span ids the header carried", () => {
56 const tp = buildTraceparent(TRACE_ID, SPAN_ID, true);
57 let seen: { traceId?: string | undefined; spanId?: string | undefined } = {};
58 resumeTraceparent(tp, () => {
59 const sc = trace.getSpanContext(context.active());
60 seen = { traceId: sc?.traceId, spanId: sc?.spanId };
61 });
62 expect(seen.traceId).toBe(TRACE_ID);
63 expect(seen.spanId).toBe(SPAN_ID);
64 });
65
66 it("resume with an invalid/absent header runs ambient (no-op contract)", () => {
67 let ran = 0;
68 resumeTraceparent(undefined, () => {
69 ran++;
70 expect(trace.getSpanContext(context.active())).toBeUndefined();
71 });
72 resumeTraceparent("garbage", () => {
73 ran++;
74 });
75 expect(ran).toBe(2);
76 });
77
78 it("a resumed parent propagates INTO a new span's context (child-parent link)", () => {
79 const tp = buildTraceparent(TRACE_ID, SPAN_ID, true);
80 let childTraceId: string | undefined;
81 resumeTraceparent(tp, () => {
82 childTraceId = trace
83 .getTracer("test")
84 .startActiveSpan("worker-job", (span) => {
85 const sc = trace.getSpanContext(context.active());
86 span.end();
87 return sc?.traceId;
88 });
89 });
90 expect(childTraceId).toBe(TRACE_ID);
91 });
92});
93
94describe("metadataTraceparent (the BullMQ job-metadata carrier)", () => {
95 it("extracts a valid traceparent from a job metadata map", () => {
96 const tp = buildTraceparent(TRACE_ID, SPAN_ID);
97 expect(metadataTraceparent({ traceparent: tp, other: 1 })).toBe(tp);
98 });
99 it("rejects missing/null/invalid carriers", () => {
100 expect(metadataTraceparent(null)).toBeUndefined();
101 expect(metadataTraceparent({})).toBeUndefined();
102 expect(metadataTraceparent({ traceparent: "nope" })).toBeUndefined();
103 expect(metadataTraceparent({ traceparent: 42 })).toBeUndefined();
104 });
105});
106
107describe("the W3C propagator itself", () => {
108 it("inject→extract is a lossless round-trip", () => {
109 const carrierIn: Record<string, string> = {};
110
111
112 const original = traceparentPropagator.extract(
113 context.active(),
114 { traceparent: buildTraceparent(TRACE_ID, SPAN_ID, true) }
115 );
116 traceparentPropagator.inject(original, carrierIn);
117 expect(carrierIn["traceparent"]).toBe(`00-${TRACE_ID}-${SPAN_ID}-01`);
118 const again = traceparentPropagator.extract(context.active(), { traceparent: carrierIn["traceparent"] ?? "" });
119 const sc = trace.getSpanContext(again);
120 expect(sc?.traceId).toBe(TRACE_ID);
121 expect(sc?.spanId).toBe(SPAN_ID);
122 expect(sc?.isRemote).toBe(true);
123 });
124});
125