diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index a23504f4..51da7f67 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -90,6 +90,7 @@ export { export { getDetailedTrace, listTransactions, + normalizeTraceSpan, } from "./api/traces.js"; export { diff --git a/src/lib/api/traces.ts b/src/lib/api/traces.ts index 1b2cdfbe..0b8cc895 100644 --- a/src/lib/api/traces.ts +++ b/src/lib/api/traces.ts @@ -48,7 +48,24 @@ export async function getDetailedTrace( }, } ); - return data; + return data.map(normalizeTraceSpan); +} + +/** + * The trace detail API (`/trace/{id}/`) returns each span's unique identifier + * as `event_id` rather than `span_id`. The value is the same 16-hex-char span + * ID that `parent_span_id` references on child spans. We copy it to `span_id` + * so the rest of the codebase can use a single, predictable field name. + */ +export function normalizeTraceSpan(span: TraceSpan): TraceSpan { + const normalized = { ...span }; + if (!normalized.span_id && normalized.event_id) { + normalized.span_id = normalized.event_id; + } + if (normalized.children) { + normalized.children = normalized.children.map(normalizeTraceSpan); + } + return normalized; } /** Fields to request from the transactions API */ diff --git a/src/lib/formatters/human.ts b/src/lib/formatters/human.ts index 765bd6e8..30442411 100644 --- a/src/lib/formatters/human.ts +++ b/src/lib/formatters/human.ts @@ -1122,6 +1122,8 @@ function formatSpanSimple(span: TraceSpan, opts: FormatSpanOptions): void { line += ` ${muted(`(${prettyMs(durationMs)})`)}`; } + line += ` ${muted(span.span_id ?? "")}`; + lines.push(line); if (currentDepth < maxDepth) { diff --git a/test/lib/api-client.normalize-trace-span.test.ts b/test/lib/api-client.normalize-trace-span.test.ts new file mode 100644 index 00000000..41c66e04 --- /dev/null +++ b/test/lib/api-client.normalize-trace-span.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "bun:test"; +import { normalizeTraceSpan } from "../../src/lib/api-client.js"; + +describe("normalizeTraceSpan", () => { + test("copies event_id to span_id when span_id is missing", () => { + const span = { event_id: "abc123", start_timestamp: 0 } as Parameters< + typeof normalizeTraceSpan + >[0]; + const result = normalizeTraceSpan(span); + expect(result.span_id).toBe("abc123"); + }); + + test("preserves existing span_id", () => { + const result = normalizeTraceSpan({ + span_id: "existing", + event_id: "other", + start_timestamp: 0, + }); + expect(result.span_id).toBe("existing"); + }); + + test("recurses into children", () => { + const result = normalizeTraceSpan({ + span_id: "parent", + start_timestamp: 0, + children: [{ event_id: "child1", start_timestamp: 1 } as any], + }); + expect(result.children?.[0]?.span_id).toBe("child1"); + }); +});