Trace waterfall Bars positioned by start time, not stacked. A span that starts late was waiting; a span that is wide was slow. Those are different problems and only the offset separates them. Tokens this needs: --foreground, --card, --muted-foreground, --border, --status-warn, --status-critical, --chart-1 The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // TraceWaterfall.tsx ──────────────────────────────────────────────────────────────────────── /** * The OpenTelemetry shape, reduced to what the drawing needs: a name, when it * started and ended, and who called it. A root is the span with no parent. */ export type Span = { id: string; parentId?: string; name: string; /** Milliseconds from the trace's start. */ start: number; end: number; status?: "ok" | "error" | "unset"; }; /** Empty space to a bar's left longer than this is shaded. A bar that starts * late was waiting, and the wait is the finding, not the bar. */ const GAP_MS = 50; const ROW = 20; const LABEL_W = 180; const ms = (v: number) => (v ? `${Math.round(v)}ms` : "0"); /** Depth-first, so a child always follows its parent, siblings by start time. */ function order(spans: Span[]) { const kids = (id?: string) => spans.filter((s) => s.parentId === id).sort((a, b) => a.start - b.start); const out: { span: Span; depth: number; gap: number }[] = []; const walk = (parent: Span | undefined, depth: number) => { let latest = parent?.start ?? 0; for (const s of kids(parent?.id)) { out.push({ span: s, depth, gap: s.start - latest }); latest = Math.max(latest, s.end); walk(s, depth + 1); } }; walk(undefined, 0); return out; } /** How much of a parent no child covers. Longer than its children means * time went somewhere nobody instrumented, which no summary metric says. */ function uncovered(parent: Span, spans: Span[]) { const kids = spans.filter((s) => s.parentId === parent.id).sort((a, b) => a.start - b.start); let covered = 0, cursor = parent.start; for (const k of kids) { covered += Math.max(0, k.end - Math.max(k.start, cursor)); cursor = Math.max(cursor, k.end); } return parent.end - parent.start - covered; } export function TraceWaterfall({ spans, onSelect, width = 640 }: { spans: Span[]; /** Attributes belong beside the waterfall, not on another page. */ onSelect?: (span: Span) => void; width?: number; }) { const rows = order(spans); const root = rows[0].span; const total = root.end - root.start; const x = (t: number) => LABEL_W + ((t - root.start) / total) * (width - LABEL_W - 20); const axisY = rows.length * ROW + 12; const missing = uncovered(root, spans); return (
{root.name} · {ms(total)} · {rows.length} spans · {ms(missing)} in the root not covered by any child