diff --git a/src/index.ts b/src/index.ts index 3814614..7e2878f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,40 +39,74 @@ const EMIT_IMAGE_MEDIA = (() => { return raw ? !["false", "0"].includes(raw) : true; })(); +const ENV_TRACEPARENT = "LANGFUSE_PI_TRACEPARENT"; const ENV_PARENT_TRACE_ID = "LANGFUSE_PI_PARENT_TRACE_ID"; const ENV_PARENT_SPAN_ID = "LANGFUSE_PI_PARENT_SPAN_ID"; const ENV_PARENT_SESSION_ID = "LANGFUSE_PI_PARENT_SESSION_ID"; const ENV_PARENT_DEPTH = "LANGFUSE_PI_PARENT_DEPTH"; +const ENV_PARENT_EXTERNAL_TRACE = "LANGFUSE_PI_PARENT_EXTERNAL_TRACE"; const HEX_TRACE_ID = /^[0-9a-f]{32}$/; const HEX_SPAN_ID = /^[0-9a-f]{16}$/; +const isUsableTraceId = (v: string | undefined): v is string => + !!v && HEX_TRACE_ID.test(v) && !/^0+$/.test(v); +const isUsableSpanId = (v: string | undefined): v is string => + !!v && HEX_SPAN_ID.test(v) && !/^0+$/.test(v); + +const TRACEPARENT = /^(?!ff)([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.*)?$/; + +export function parseTraceparent(value: string): { traceId: string; spanId: string } | undefined { + const match = TRACEPARENT.exec(value.trim().toLowerCase()); + if (!match) return undefined; + const [, version, traceId, spanId, , trailing] = match; + if (version === "00" && trailing) return undefined; + if (!isUsableTraceId(traceId) || !isUsableSpanId(spanId)) return undefined; + return { traceId, spanId }; +} + export interface InheritedParent { spanContext: SpanContext; sessionId?: string; depth: number; + source: "subagent" | "attached"; + externalTrace: boolean; } export function readInheritedParent(env: NodeJS.ProcessEnv = process.env): InheritedParent | undefined { - const traceId = env[ENV_PARENT_TRACE_ID]?.trim().toLowerCase(); - const spanId = env[ENV_PARENT_SPAN_ID]?.trim().toLowerCase(); - if (!traceId || !spanId) return undefined; - if (!HEX_TRACE_ID.test(traceId) || !HEX_SPAN_ID.test(spanId)) return undefined; - // OTel defines the all-zero ids as invalid. - if (/^0+$/.test(traceId) || /^0+$/.test(spanId)) return undefined; + const parent = readParentIds(env); + if (!parent) return undefined; const depth = Number(env[ENV_PARENT_DEPTH] ?? "0"); return { spanContext: { - traceId, - spanId, + traceId: parent.traceId, + spanId: parent.spanId, traceFlags: TraceFlags.SAMPLED, isRemote: true, }, sessionId: env[ENV_PARENT_SESSION_ID]?.trim() || undefined, depth: Number.isFinite(depth) && depth > 0 ? depth : 0, + source: parent.source, + externalTrace: parent.source === "attached" || env[ENV_PARENT_EXTERNAL_TRACE] === "1", }; } +type ParentIds = { traceId: string; spanId: string; source: "subagent" | "attached" }; + +function readParentIds(env: NodeJS.ProcessEnv): ParentIds | undefined { + const raw = env[ENV_TRACEPARENT]?.trim(); + if (raw) { + const parsed = parseTraceparent(raw); + if (parsed) return { ...parsed, source: "attached" }; + console.error(`[pi-langfuse] Ignoring malformed ${ENV_TRACEPARENT}: ${JSON.stringify(raw)}`); + } + const traceId = env[ENV_PARENT_TRACE_ID]?.trim().toLowerCase(); + const spanId = env[ENV_PARENT_SPAN_ID]?.trim().toLowerCase(); + if (!traceId && !spanId) return undefined; + if (!isUsableTraceId(traceId) || !isUsableSpanId(spanId)) return undefined; + return { traceId, spanId, source: "subagent" }; +} + // --------------------------------------------------------------------------- // Config // --------------------------------------------------------------------------- @@ -661,11 +695,15 @@ export default function (pi: ExtensionAPI) { let lastContextHistory: ChatMlMessage[] | undefined; let compactionStartedAt: Date | undefined; const inheritedParent = readInheritedParent(); + const isAttached = inheritedParent?.source === "attached"; + const externalTrace = inheritedParent?.externalTrace ?? false; const inheritedParentEnv: Record = { + [ENV_TRACEPARENT]: process.env[ENV_TRACEPARENT], [ENV_PARENT_TRACE_ID]: process.env[ENV_PARENT_TRACE_ID], [ENV_PARENT_SPAN_ID]: process.env[ENV_PARENT_SPAN_ID], [ENV_PARENT_SESSION_ID]: process.env[ENV_PARENT_SESSION_ID], [ENV_PARENT_DEPTH]: process.env[ENV_PARENT_DEPTH], + [ENV_PARENT_EXTERNAL_TRACE]: process.env[ENV_PARENT_EXTERNAL_TRACE], }; const ensureRuntime = (): Runtime => { @@ -718,10 +756,13 @@ export default function (pi: ExtensionAPI) { // Do not publish a root that the sampler dropped. Child spans would point // to a trace with no exported root. if (!(ctx.traceFlags & TraceFlags.SAMPLED)) return; + delete process.env[ENV_TRACEPARENT]; process.env[ENV_PARENT_TRACE_ID] = ctx.traceId; process.env[ENV_PARENT_SPAN_ID] = ctx.spanId; process.env[ENV_PARENT_SESSION_ID] = sessionId; process.env[ENV_PARENT_DEPTH] = String((inheritedParent?.depth ?? 0) + 1); + if (externalTrace) process.env[ENV_PARENT_EXTERNAL_TRACE] = "1"; + else delete process.env[ENV_PARENT_EXTERNAL_TRACE]; }; // A child must not attach to a turn that has ended. The inherited values @@ -805,15 +846,17 @@ export default function (pi: ExtensionAPI) { const userText = [event.prompt, ...promptImages.map(describeImage)].filter(Boolean).join("\n"); lastPromptText = userText; lastContextHistory = undefined; - const isSubagent = Boolean(inheritedParent); - traceAttributes = { - ...(isSubagent ? {} : { [LangfuseOtelSpanAttributes.TRACE_NAME]: TRACE_NAME }), - [LangfuseOtelSpanAttributes.TRACE_SESSION_ID]: isSubagent - ? (inheritedParent!.sessionId ?? sessionId) - : sessionId, - [LangfuseOtelSpanAttributes.TRACE_TAGS]: BASE_TAGS, - ...(config.userId ? { [LangfuseOtelSpanAttributes.TRACE_USER_ID]: config.userId } : {}), - }; + const isSubagent = inheritedParent?.source === "subagent"; + traceAttributes = externalTrace + ? {} + : { + ...(isSubagent ? {} : { [LangfuseOtelSpanAttributes.TRACE_NAME]: TRACE_NAME }), + [LangfuseOtelSpanAttributes.TRACE_SESSION_ID]: isSubagent + ? (inheritedParent!.sessionId ?? sessionId) + : sessionId, + [LangfuseOtelSpanAttributes.TRACE_TAGS]: BASE_TAGS, + ...(config.userId ? { [LangfuseOtelSpanAttributes.TRACE_USER_ID]: config.userId } : {}), + }; const root = startObservation( isSubagent ? SUBAGENT_ROOT_OBSERVATION_NAME : ROOT_OBSERVATION_NAME, @@ -831,6 +874,7 @@ export default function (pi: ExtensionAPI) { ...(isSubagent ? { pi_subagent: true, subagent_depth: inheritedParent!.depth, parent_session_id: inheritedParent!.sessionId } : {}), + ...(isAttached ? { attached_to_external_parent: true } : {}), }, }, { asType: "span", ...(inheritedParent ? { parentSpanContext: inheritedParent.spanContext } : {}) }, @@ -1081,12 +1125,14 @@ export default function (pi: ExtensionAPI) { // /compact in a fresh process would hit the unset tracer provider. ensureRuntime(); const sessionId = ctx.sessionManager.getSessionId(); - traceAttributes = { - ...(inheritedParent ? {} : { [LangfuseOtelSpanAttributes.TRACE_NAME]: `Pi ${name}` }), - [LangfuseOtelSpanAttributes.TRACE_SESSION_ID]: inheritedParent?.sessionId ?? sessionId, - [LangfuseOtelSpanAttributes.TRACE_TAGS]: BASE_TAGS, - ...(config.userId ? { [LangfuseOtelSpanAttributes.TRACE_USER_ID]: config.userId } : {}), - }; + traceAttributes = externalTrace + ? {} + : { + ...(inheritedParent ? {} : { [LangfuseOtelSpanAttributes.TRACE_NAME]: `Pi ${name}` }), + [LangfuseOtelSpanAttributes.TRACE_SESSION_ID]: inheritedParent?.sessionId ?? sessionId, + [LangfuseOtelSpanAttributes.TRACE_TAGS]: BASE_TAGS, + ...(config.userId ? { [LangfuseOtelSpanAttributes.TRACE_USER_ID]: config.userId } : {}), + }; const obs = startObservation( name, { diff --git a/test/fixtures/env-probe.ts b/test/fixtures/env-probe.ts index 3d4fd6b..f990578 100644 --- a/test/fixtures/env-probe.ts +++ b/test/fixtures/env-probe.ts @@ -7,8 +7,11 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; export default function (pi: ExtensionAPI) { pi.on("before_provider_request", async () => { console.error(`PROBE turn ${process.env.LANGFUSE_PI_PARENT_TRACE_ID ?? ""}`); + console.error(`PROBE turn-tp ${process.env.LANGFUSE_PI_TRACEPARENT ?? ""}`); + console.error(`PROBE turn-ext ${process.env.LANGFUSE_PI_PARENT_EXTERNAL_TRACE ?? ""}`); }); pi.on("agent_settled", async () => { console.error(`PROBE settled ${process.env.LANGFUSE_PI_PARENT_TRACE_ID ?? ""}`); + console.error(`PROBE settled-tp ${process.env.LANGFUSE_PI_TRACEPARENT ?? ""}`); }); } diff --git a/test/helpers.ts b/test/helpers.ts index 4b8a298..5814035 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -223,7 +223,6 @@ export interface Capture { port: number; requests: unknown[]; spans: () => CapturedSpan[]; - /** Attributes of the OTLP resource the exported spans were sent under. */ resourceAttrs: () => Record; close: () => void; } @@ -318,7 +317,6 @@ export interface Sandbox { export interface SandboxOptions { contextWindow?: number; keepRecentTokens?: number; - /** Pads README.md so reading it grows the context past keepRecentTokens. */ readmeFillerLines?: number; } @@ -378,7 +376,6 @@ export function runPi( opts: { continue?: boolean; env?: Record; - /** More extensions to load with ours, for example the subagent fixture. */ extensions?: string[]; } = {}, ): Promise<{ status: number | null; stdout: string; stderr: string }> { @@ -399,7 +396,6 @@ export function runPi( return new Promise((resolvePromise) => { const child = spawn(PI_BIN, args, { cwd: sandbox.workspace, - // stdin must be closed — pi's print mode waits for EOF on piped stdin. stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, @@ -414,15 +410,15 @@ export function runPi( LANGFUSE_TRACING_ENVIRONMENT: undefined, LANGFUSE_RELEASE: undefined, LANGFUSE_TRACING_ENABLED: undefined, - // These feed the exported OTel resource, so the runner's own - // environment must not reach the span payload either. OTEL_SERVICE_NAME: undefined, OTEL_RESOURCE_ATTRIBUTES: undefined, // A test run must not get a parent trace from this test process. + LANGFUSE_PI_TRACEPARENT: undefined, LANGFUSE_PI_PARENT_TRACE_ID: undefined, LANGFUSE_PI_PARENT_SPAN_ID: undefined, LANGFUSE_PI_PARENT_SESSION_ID: undefined, LANGFUSE_PI_PARENT_DEPTH: undefined, + LANGFUSE_PI_PARENT_EXTERNAL_TRACE: undefined, // The subagent fixture uses these values. TEST_PI_BIN: PI_BIN, TEST_LANGFUSE_EXTENSION: EXTENSION, @@ -448,7 +444,6 @@ export function runPi( }); } -/** Poll until the capture holds at least `n` export requests (flushes are async). */ export async function waitForRequests(capture: Capture, n: number, timeoutMs = 5000): Promise { const start = Date.now(); while (capture.requests.length < n && Date.now() - start < timeoutMs) { diff --git a/test/integration.test.ts b/test/integration.test.ts index 2cc5365..310a87a 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -326,7 +326,10 @@ describe("integration: pi -> extension -> Langfuse export", () => { const sandbox = createSandbox(mock.port); const result = await runPi(sandbox, "Delegate the repo inspection, then summarize", { env: buildLangfuseEnv(capture), - extensions: [join(REPO_ROOT, "test", "fixtures", "subagent-tool.ts")], + extensions: [ + join(REPO_ROOT, "test", "fixtures", "subagent-tool.ts"), + join(REPO_ROOT, "test", "fixtures", "env-probe.ts"), + ], }); assert.equal(result.status, 0, `pi failed: ${result.stderr}`); // The parent and the child export separately. Wait for the two exports. @@ -430,6 +433,158 @@ describe("integration: pi -> extension -> Langfuse export", () => { // when it prints at agent_settled. assert.match(result.stderr, /PROBE turn [0-9a-f]{32}/, "parent ids must be published during the turn"); assert.match(result.stderr, /PROBE settled /, "parent ids must be withdrawn after the turn"); + assert.match( + result.stderr, + /PROBE turn-ext /, + "a standalone run owns its trace, so nothing marks it as externally owned", + ); + } finally { + capture.close(); + } + }); + + it("attaches every turn under an external traceparent", async () => { + const capture = await startCaptureServer(); + const appTraceId = "4bf92f3577b34da6a3ce929d0e0e4736"; + const appSpanId = "00f067aa0ba902b7"; + try { + const sandbox = createSandbox(mock.port); + const result = await runPi(sandbox, "Explore this project and summarize it", { + env: { + ...buildLangfuseEnv(capture), + LANGFUSE_PI_TRACEPARENT: `00-${appTraceId}-${appSpanId}-01`, + }, + }); + assert.equal(result.status, 0, `pi failed: ${result.stderr}`); + + const spans = capture.spans(); + const root = findSpansByName(spans, "Conversational Turn")[0]; + assert.ok(root, "expected a turn root span"); + assert.equal(root!.traceId, appTraceId, "the turn must join the application's trace"); + assert.equal(root!.parentSpanId, appSpanId, "the turn must nest under the application's span"); + + assert.equal( + findSpansByName(spans, "Subagent Turn").length, + 0, + "an attached run must not be labelled a subagent", + ); + assert.equal( + root!.attrs["langfuse.observation.metadata.pi_subagent"], + undefined, + "an attached run must not carry subagent metadata", + ); + assert.equal( + String(root!.attrs["langfuse.observation.metadata.attached_to_external_parent"]), + "true", + ); + + for (const [attr, why] of [ + ["langfuse.trace.name", "must not overwrite the application's trace name"], + ["session.id", "must not claim the application's session"], + ["langfuse.trace.tags", "must not add pi's tags to the application's trace"], + ["user.id", "must not set the user on the application's trace"], + ] as const) { + for (const span of spans) { + assert.equal(span.attrs[attr], undefined, `${span.name} ${why}`); + } + } + assert.ok( + root!.attrs["langfuse.observation.metadata.session_id"], + "pi's own session id stays readable in the observation metadata", + ); + + for (const span of spans) { + assert.equal(span.traceId, appTraceId, `${span.name} must be in the application's trace`); + } + assert.ok( + spans.some((s) => s.name === "LLM Call"), + "the generations must still be traced in attached mode", + ); + } finally { + capture.close(); + } + }); + + it("falls back to its own trace when the traceparent is unusable", async () => { + const capture = await startCaptureServer(); + try { + const sandbox = createSandbox(mock.port); + const result = await runPi(sandbox, "Explore this project and summarize it", { + env: { + ...buildLangfuseEnv(capture), + LANGFUSE_PI_TRACEPARENT: `00-${"0".repeat(32)}-${"0".repeat(16)}-01`, + }, + }); + assert.equal(result.status, 0, `pi failed: ${result.stderr}`); + + const root = findSpansByName(capture.spans(), "Conversational Turn")[0]; + assert.ok(root, "expected a turn root span"); + assert.equal(root!.parentSpanId, undefined, "the turn must be its own root"); + assert.notEqual(root!.traceId, "0".repeat(32), "the all-zero trace id must never be adopted"); + assert.ok(root!.attrs["langfuse.trace.name"], "a standalone turn still owns its trace name"); + assert.match(result.stderr, /Ignoring malformed LANGFUSE_PI_TRACEPARENT/); + } finally { + capture.close(); + } + }); + + it("nests a subagent of an attached run under the turn and off the application's trace fields", async () => { + const capture = await startCaptureServer(); + const appTraceId = "4bf92f3577b34da6a3ce929d0e0e4736"; + const appSpanId = "00f067aa0ba902b7"; + try { + const sandbox = createSandbox(mock.port); + const result = await runPi(sandbox, "Delegate the repo inspection, then summarize", { + env: { + ...buildLangfuseEnv(capture), + LANGFUSE_PI_TRACEPARENT: `00-${appTraceId}-${appSpanId}-01`, + }, + extensions: [ + join(REPO_ROOT, "test", "fixtures", "subagent-tool.ts"), + join(REPO_ROOT, "test", "fixtures", "env-probe.ts"), + ], + }); + assert.equal(result.status, 0, `pi failed: ${result.stderr}`); + await waitForRequests(capture, 2, 15_000); + const spans = capture.spans(); + + assert.match( + result.stderr, + /PROBE turn-tp /, + "the inherited traceparent must be out of the child's way while a turn is published", + ); + assert.match( + result.stderr, + new RegExp(`PROBE settled-tp 00-${appTraceId}-${appSpanId}-01`), + "and must be restored afterwards, since the application's span is still valid", + ); + assert.match( + result.stderr, + /PROBE turn-ext 1/, + "external ownership must be published for the child to inherit", + ); + + const parentRoot = findSpansByName(spans, "Conversational Turn")[0]; + const subagentRoot = findSpansByName(spans, "Subagent Turn")[0]; + assert.ok(parentRoot, "parent turn must be traced"); + assert.ok(subagentRoot, "the subagent must still be a Subagent Turn, not a second top-level turn"); + assert.equal(subagentRoot!.parentSpanId, parentRoot!.spanId, "the subagent belongs under the turn"); + assert.equal( + String(subagentRoot!.attrs["langfuse.observation.metadata.pi_subagent"]), + "true", + "and keeps its subagent metadata", + ); + + for (const span of spans) { + assert.equal(span.traceId, appTraceId, `${span.name} must be in the application's trace`); + for (const attr of ["langfuse.trace.name", "session.id", "langfuse.trace.tags", "user.id"] as const) { + assert.equal( + span.attrs[attr], + undefined, + `${span.name} must not set ${attr} on a trace the application owns`, + ); + } + } } finally { capture.close(); } diff --git a/test/unit.test.ts b/test/unit.test.ts index c3ec0fd..07734bf 100644 --- a/test/unit.test.ts +++ b/test/unit.test.ts @@ -3,6 +3,7 @@ import { describe, it } from "node:test"; import { extractText, readInheritedParent, + parseTraceparent, buildCostDetails, describeImage, extractImages, @@ -417,6 +418,119 @@ describe("readInheritedParent", () => { assert.equal(readInheritedParent(base)?.depth, 0); assert.equal(readInheritedParent({ ...base, LANGFUSE_PI_PARENT_DEPTH: "abc" })?.depth, 0); }); + + it("reads a W3C traceparent, which is the form a launcher's OTel SDK hands it", () => { + const parent = readInheritedParent({ + LANGFUSE_PI_TRACEPARENT: `00-${traceId}-${spanId}-01`, + LANGFUSE_PI_PARENT_SESSION_ID: "sess-app", + }); + assert.equal(parent?.spanContext.traceId, traceId); + assert.equal(parent?.spanContext.spanId, spanId); + assert.equal(parent?.spanContext.isRemote, true); + assert.equal(parent?.sessionId, "sess-app"); + }); + + it("resolves parent source and trace ownership per input shape", () => { + const pair = { LANGFUSE_PI_PARENT_TRACE_ID: traceId, LANGFUSE_PI_PARENT_SPAN_ID: spanId }; + const tp = { LANGFUSE_PI_TRACEPARENT: `00-${traceId}-${spanId}-01` }; + const cases: Array<[string, NodeJS.ProcessEnv, "subagent" | "attached", boolean]> = [ + ["the id pair is what publishParentContext writes", pair, "subagent", false], + ["a traceparent can only come from outside pi", tp, "attached", true], + ["a malformed traceparent still falls back to the pair", { ...pair, LANGFUSE_PI_TRACEPARENT: "junk" }, "subagent", false], + ["a subagent inherits external ownership without becoming attached", { ...pair, LANGFUSE_PI_PARENT_EXTERNAL_TRACE: "1" }, "subagent", true], + ["and an explicit 0 does not claim it", { ...pair, LANGFUSE_PI_PARENT_EXTERNAL_TRACE: "0" }, "subagent", false], + ]; + for (const [why, env, source, externalTrace] of cases) { + const parent = readInheritedParent(env); + assert.equal(parent?.source, source, why); + assert.equal(parent?.externalTrace, externalTrace, why); + } + }); + + it("lets the traceparent win over the explicit pair", () => { + const other = "1111111111111111111111111111111a"; + const parent = readInheritedParent({ + LANGFUSE_PI_TRACEPARENT: `00-${traceId}-${spanId}-01`, + LANGFUSE_PI_PARENT_TRACE_ID: other, + LANGFUSE_PI_PARENT_SPAN_ID: "cccccccccccccccc", + }); + assert.equal(parent?.spanContext.traceId, traceId); + assert.equal(parent?.spanContext.spanId, spanId); + }); + + it("falls back to the pair when the traceparent is malformed", () => { + const parent = readInheritedParent({ + LANGFUSE_PI_TRACEPARENT: "not-a-traceparent", + LANGFUSE_PI_PARENT_TRACE_ID: traceId, + LANGFUSE_PI_PARENT_SPAN_ID: spanId, + }); + assert.equal(parent?.spanContext.traceId, traceId); + assert.equal(parent?.spanContext.spanId, spanId); + }); + + it("ignores a bare TRACEPARENT, which an instrumented parent exports at us", () => { + assert.equal(readInheritedParent({ TRACEPARENT: `00-${traceId}-${spanId}-01` }), undefined); + assert.equal(readInheritedParent({ traceparent: `00-${traceId}-${spanId}-01` }), undefined); + }); +}); + +describe("parseTraceparent", () => { + const traceId = "0af7651916cd43dd8448eb211c80319c"; + const spanId = "b7ad6b7169203331"; + + it("parses the version-00 form, trimmed and case-folded", () => { + assert.deepEqual(parseTraceparent(`00-${traceId}-${spanId}-01`), { traceId, spanId }); + assert.deepEqual(parseTraceparent(` 00-${traceId.toUpperCase()}-${spanId.toUpperCase()}-01 `), { + traceId, + spanId, + }); + }); + + it("is a tolerant reader on the flags, so an unsampled launcher span still nests", () => { + assert.deepEqual(parseTraceparent(`00-${traceId}-${spanId}-00`), { traceId, spanId }); + }); + + it("accepts a future version and ignores the fields it appends", () => { + for (const header of [ + `01-${traceId}-${spanId}-01`, + `cc-${traceId}-${spanId}-01`, + `01-${traceId}-${spanId}-01-somethingnew`, + `02-${traceId}-${spanId}-01-a-b-c`, + ]) { + assert.deepEqual(parseTraceparent(header), { traceId, spanId }, header); + } + }); + + it("still rejects trailing fields at version 00, which defines exactly four", () => { + assert.equal(parseTraceparent(`00-${traceId}-${spanId}-01-extra`), undefined); + }); + + it("refuses version ff, which the spec forbids", () => { + assert.equal(parseTraceparent(`ff-${traceId}-${spanId}-01`), undefined); + assert.equal(parseTraceparent(`FF-${traceId}-${spanId}-01`), undefined); + }); + + + it("rejects the all-zero ids an idle OTel launcher emits", () => { + assert.equal(parseTraceparent(`00-${"0".repeat(32)}-${spanId}-01`), undefined); + assert.equal(parseTraceparent(`00-${traceId}-${"0".repeat(16)}-01`), undefined); + }); + + it("rejects junk without throwing", () => { + for (const bad of [ + "", + "-", + "not-a-traceparent", + `00-${traceId}-${spanId}`, + `00-${traceId}-${spanId}-01-extra`, + `00-xyz-${spanId}-01`, + `00-${traceId}-short-01`, + `00-${traceId.slice(0, 31)}-${spanId}-01`, + `0-${traceId}-${spanId}-01`, + ]) { + assert.equal(parseTraceparent(bad), undefined, `must reject ${JSON.stringify(bad)}`); + } + }); }); describe("markDataUris", () => {