fix(diagnostics): harden trace context parsing (#70955)

This commit is contained in:
Vincent Koc
2026-04-23 22:47:54 -07:00
committed by GitHub
parent bae7b54a85
commit 350d322180
2 changed files with 53 additions and 5 deletions

View File

@@ -45,12 +45,27 @@ describe("diagnostic-trace-context", () => {
it("rejects malformed traceparent values", () => {
expect(parseDiagnosticTraceparent(undefined)).toBeUndefined();
expect(parseDiagnosticTraceparent(`00-${TRACE_ID}-${SPAN_ID}-01-extra`)).toBeUndefined();
expect(parseDiagnosticTraceparent(`ff-${TRACE_ID}-${SPAN_ID}-01`)).toBeUndefined();
expect(parseDiagnosticTraceparent(`00-${"0".repeat(32)}-${SPAN_ID}-01`)).toBeUndefined();
expect(parseDiagnosticTraceparent(`00-${TRACE_ID}-${"0".repeat(16)}-01`)).toBeUndefined();
expect(parseDiagnosticTraceparent(`00-${TRACE_ID}-${SPAN_ID}-xyz`)).toBeUndefined();
});
it("rejects oversized traceparent values before parsing", () => {
expect(
parseDiagnosticTraceparent(`00-${TRACE_ID}-${SPAN_ID}-01-${"a".repeat(128)}`),
).toBeUndefined();
});
it("continues future-version traceparents from the first four fields", () => {
expect(parseDiagnosticTraceparent(`01-${TRACE_ID}-${SPAN_ID}-01-extra`)).toEqual({
traceId: TRACE_ID,
spanId: SPAN_ID,
traceFlags: "01",
});
});
it("creates a normalized context from explicit fields or traceparent", () => {
expect(
createDiagnosticTraceContext({
@@ -71,6 +86,14 @@ describe("diagnostic-trace-context", () => {
});
});
it("generates valid non-zero ids for fallback contexts", () => {
const context = createDiagnosticTraceContext();
expect(isValidDiagnosticTraceId(context.traceId)).toBe(true);
expect(isValidDiagnosticSpanId(context.spanId)).toBe(true);
expect(formatDiagnosticTraceparent(context)).toBeDefined();
});
it("creates child contexts without retaining parent references or self-parenting", () => {
const parent = createDiagnosticTraceContext({
traceId: TRACE_ID,

View File

@@ -2,9 +2,11 @@ import { randomBytes } from "node:crypto";
const TRACEPARENT_VERSION = "00";
const DEFAULT_TRACE_FLAGS = "01";
const MAX_TRACEPARENT_LENGTH = 128;
const TRACE_ID_RE = /^[0-9a-f]{32}$/;
const SPAN_ID_RE = /^[0-9a-f]{16}$/;
const TRACE_FLAGS_RE = /^[0-9a-f]{2}$/;
const TRACEPARENT_VERSION_RE = /^[0-9a-f]{2}$/;
export type DiagnosticTraceContext = {
/** W3C trace id, 32 lowercase hex chars. */
@@ -29,6 +31,22 @@ function isNonZeroHex(value: string): boolean {
return !/^0+$/.test(value);
}
function randomTraceId(): string {
let traceId = randomHex(16);
while (!isNonZeroHex(traceId)) {
traceId = randomHex(16);
}
return traceId;
}
function randomSpanId(): string {
let spanId = randomHex(8);
while (!isNonZeroHex(spanId)) {
spanId = randomHex(8);
}
return spanId;
}
export function isValidDiagnosticTraceId(value: unknown): value is string {
return typeof value === "string" && TRACE_ID_RE.test(value) && isNonZeroHex(value);
}
@@ -68,12 +86,19 @@ function normalizeTraceFlags(value: unknown): string | undefined {
export function parseDiagnosticTraceparent(
traceparent: string | undefined,
): DiagnosticTraceContext | undefined {
const parts = traceparent?.trim().toLowerCase().split("-");
if (!parts || parts.length !== 4) {
if (typeof traceparent !== "string" || traceparent.length > MAX_TRACEPARENT_LENGTH) {
return undefined;
}
const parts = traceparent.trim().toLowerCase().split("-");
if (!parts || parts.length < 4) {
return undefined;
}
const [version, traceId, spanId, traceFlags] = parts;
if (version !== TRACEPARENT_VERSION) {
if (
!TRACEPARENT_VERSION_RE.test(version) ||
version === "ff" ||
(version === TRACEPARENT_VERSION && parts.length !== 4)
) {
return undefined;
}
const normalizedTraceId = normalizeTraceId(traceId);
@@ -108,8 +133,8 @@ export function createDiagnosticTraceContext(
input: DiagnosticTraceContextInput = {},
): DiagnosticTraceContext {
const parsed = parseDiagnosticTraceparent(input.traceparent);
const traceId = normalizeTraceId(input.traceId) ?? parsed?.traceId ?? randomHex(16);
const spanId = normalizeSpanId(input.spanId) ?? parsed?.spanId ?? randomHex(8);
const traceId = normalizeTraceId(input.traceId) ?? parsed?.traceId ?? randomTraceId();
const spanId = normalizeSpanId(input.spanId) ?? parsed?.spanId ?? randomSpanId();
const parentSpanId = normalizeSpanId(input.parentSpanId);
return {
traceId,