Trace Waterfall Viz

An interactive OpenTelemetry-style trace waterfall for a single AI request. Spans nest by depth and are laid out on a shared 0-3500ms time axis. A pulsing LOW badge on the retrieval span surfaces the bug — the top-3 relevance scores are all below the 0.75 threshold, so the model is answering from irrelevant context. Toggle "Fix retrieval" to swap in a second trace where scores climb above the threshold, input tokens drop from 4.8K → 3.0K, cost drops from $0.026 → $0.017, and the answer flips from Incorrect to Correct.

Trace WaterfallIncorrect
3.2s total
0500ms1.0s1.5s2.0s2.5s3.0s3.5s
request
3.2s
context-assembly
180ms
system-prompt
15ms
history-fetch
45ms
retrievalLOW
120ms
embed-query
30ms
vector-search
60ms
rerank
30ms
model-call
2.8s · 4.8K in · $0.026
safety-check
80ms
delivery
40ms
↑↓ navigate spans · Enter expand · F toggle fix · Esc deselect11 spans
Trace waterfall ready. Use arrow keys to navigate spans.

This is an OpenTelemetry-style trace for a single AI request. Each horizontal bar is a span — a unit of work with a name, start time, and duration. Click any span to inspect its details. Look for the warning badge.

Customize
Trace
broken
3500 ms

Installation

npx shadcn@latest add https://craftbits.dev/r/trace-waterfall-viz.json

Usage

import { TraceWaterfallViz } from "@craft-bits/viz/trace-waterfall-viz";
 
<TraceWaterfallViz />

Start in the fixed state:

<TraceWaterfallViz defaultFixed />

Bring your own traces:

<TraceWaterfallViz
  brokenTrace={{
    totalMs: 2000,
    answerCorrect: false,
    answerLabel: "Incorrect",
    spans: [
      {
        id: "request",
        name: "request",
        category: "request",
        depth: 0,
        startMs: 0,
        durationMs: 2000,
        details: { traceId: "t-001", method: "POST" },
      },
      {
        id: "retrieval",
        name: "retrieval",
        category: "retrieval",
        depth: 1,
        startMs: 10,
        durationMs: 80,
        details: { docCount: 4, topRelevance: [0.4, 0.3], threshold: 0.75 },
        warning: "Low relevance — model is hallucinating.",
      },
    ],
  }}
  fixedTrace={{
    /* ...same shape, higher scores... */
  }}
/>

Subscribe to fix-toggle and selection events:

<TraceWaterfallViz
  onFixChange={(fixed) => {
    /* lift the toggle state into analytics */
  }}
  onSpanSelect={(span) => {
    /* fires on every selection / deselection */
  }}
/>

Understanding the component

  1. Header. The mono "Trace Waterfall" wordmark sits beside a semantic answer badge (var(--cb-success) when correct, var(--cb-error) when not), the total wall-clock duration in tabular-nums, and the right-aligned "Fix retrieval" toggle. The toggle is a m.button with aria-pressed, a sliding pip animated by SPRINGS.snap, and a (F) keyboard hint.
  2. Time axis. Tick labels every 500ms across the maxMs window, fixed at 3500ms by default so the broken and fixed traces share a scale. The label column is reserved at labelColumnWidth (160px default) so bar left% and width% stay readable.
  3. Span rows. Each row is a role="listitem" button with a label column (depth-indented, kind-coloured when selected) and a bar column. Selection lifts the row's background to a color-mix of the kind colour at 8% alpha and adds a soft outer glow via inset+outer box-shadow. Bars animate from scaleX: 0 to scaleX: 1 on entry, staggered by STAGGER.
  4. Warning badge. Spans with a warning string surface a pulsing LOW chip in their label, swap the solid bar for a linear-gradient from the kind colour to var(--cb-error), and gate the colour transition on the bug-found phase.
  5. Detail panel. Selecting a span opens the side panel (full-width on mobile, 280px on lg). Top section shows kind dot + name; middle shows duration/start/end summary; the third section walks selected.details with special renderers for topRelevance (per-score above/below threshold badges), categories (chip strip), and sources (numbered list). Pure key/value entries fall through to the generic DetailRow.
  6. Comparison summary. When fixed flips on, a four-stat row slides in beneath the waterfall (SPRINGS.smooth) showing strike-through before → highlighted after for total time, input tokens, cost, and top relevance — wholly driven by comparisonStats.
  7. Phases. State derives from selection + fix and exposes via data-phase on the root: explorebug-found (a warning span is selected) → fixed (toggle is on). The narration banner's background tints from accent → error → success on phase change.
  8. Reduced motion. Under prefers-reduced-motion: reduce, every entrance collapses to a snap, the LOW badge stops pulsing, the toggle pip jumps, bars skip their grow-in, and the comparison stats appear instantly.

Props

PropTypeDefaultDescription
brokenTraceTraceWaterfallVizTracebundled 11-span RAG traceTrace shown when the fix toggle is off.
fixedTraceTraceWaterfallVizTracebundled 11-span fixed RAG traceTrace shown when the fix toggle is on.
comparisonStatsreadonly TraceWaterfallVizComparisonStat[]4 statsBefore/after stats shown in the comparison summary.
defaultFixedbooleanfalseWhether the viz starts in the fixed state.
maxMsnumber3500Upper bound of the time axis in milliseconds.
labelColumnWidthnumber160Width of the label column in pixels.
maxHeightnumber440Cap on the visible waterfall height; scrolls past it.
transitionTransitionSPRINGS.smoothOverride the entrance spring for span rows and bars.
onFixChange(fixed: boolean) => voidFires when the fix toggle flips.
onSpanSelect(span | null) => voidFires when the selected span changes.
classNamestringMerged onto the root via cn().

Accessibility

  • The root is role="figure" with an aria-label summarising span count, total duration, and the keyboard model.
  • A polite live region (aria-live="polite") announces selection, warnings, and the fix-applied state.
  • Span rows are role="listitem" buttons with aria-pressed reflecting selection and an aria-label including name, duration, and warning state. The depth connector glyph is aria-hidden.
  • The "Fix retrieval" toggle is a button with aria-pressed, an explicit aria-label, and a visible (F) keyboard hint.
  • Keyboard model: / move selection between spans, Enter toggles the detail panel, F toggles the retrieval fix, Esc deselects. The handler chains to user-provided onKeyDown and skips default-prevented events.
  • Colour is never the only signal — every warning span pairs the gradient bar with a LOW chip and a side-panel callout; every relevance score badge spells "all above" / "all below" the threshold in text.
  • Motion respects prefers-reduced-motion: reduce — entrances collapse to { duration: 0 }, the LOW chip stops pulsing, the toggle pip jumps, and the comparison summary appears instantly.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/systems/TraceWaterfallViz.tsx). The source was a lesson primitive with hard-coded broken / fixed TRACE constants, CA palette tokens (--color-accent-400 / --color-ink-* / --color-surface-raised, ca-narration), inline oklch(0.76 0.16 155) success / oklch(0.70 0.20 25) warning constants, and CA-specific spring aliases (SPRINGS.snappy / SPRINGS.gentle, STAGGER.tight). The craft-bits extract lifts the broken / fixed traces and the comparison summary into the brokenTrace / fixedTrace / comparisonStats props with the originals exposed as TRACE_WATERFALL_VIZ_DEFAULT_BROKEN / TRACE_WATERFALL_VIZ_DEFAULT_FIXED / TRACE_WATERFALL_VIZ_DEFAULT_COMPARISON. The six-slot category palette re-keys to canonical cb-* tokens; inline OKLCH success / warning constants become var(--cb-success) / var(--cb-error). Inline SPRINGS.snappy / SPRINGS.gentle re-key to canonical SPRINGS.snap / SPRINGS.smooth from @craft-bits/core/motion; STAGGER.tight collapses to the canonical STAGGER scalar. forwardRef + cn() + ...props spread + chained onKeyDown + data-phase / data-fixed + onFixChange / onSpanSelect callbacks were added. SvgLabel / ChallengeBtn / lessonId lesson chrome was stripped (none was present in source). Keyboard model extended with Escape to deselect.