Span Builder Viz

An interactive OpenTelemetry trace-waterfall builder. Spans appear in the order they execute, with bars positioned by startMs and sized by durationMs. Three built-in scenarios — a simple RAG trace, an agent with tools, and a retry-on-429 failure case — model the shapes most LLM stacks emit. Click any revealed span to open the attribute inspector and read the OTel-flavoured key/value bag.

The teaching insight: per-span attributes (gen_ai.usage.input_tokens, top_score, tool.args, http.status_code, retry.delay_ms) are what makes a production trace diagnosable. The bar chart shows where time went; the attribute panel shows why.

Customize
Initial scenario
Simple RAG

Installation

npx shadcn@latest add https://craftbits.dev/r/span-builder-viz.json

Usage

import { SpanBuilderViz } from "@craft-bits/viz/span-builder-viz";
 
<SpanBuilderViz />

Start on a specific scenario:

<SpanBuilderViz defaultScenarioId="agent-tools" />

Provide your own trace tree:

<SpanBuilderViz
  scenarios={[
    {
      id: "my-trace",
      label: "My RAG",
      totalMs: 900,
      spans: [
        {
          id: "root",
          name: "agent.invoke",
          kind: "root",
          depth: 0,
          startMs: 0,
          durationMs: 900,
          attrs: { "trace.id": "t-001", "service.name": "rag" },
        },
        {
          id: "search",
          name: "retrieval.search",
          kind: "retrieval",
          depth: 1,
          startMs: 20,
          durationMs: 80,
          attrs: { index: "docs", "results.count": 5, top_score: 0.81 },
        },
        {
          id: "llm",
          name: "gen_ai.chat",
          kind: "llm",
          depth: 1,
          startMs: 110,
          durationMs: 760,
          attrs: {
            "gen_ai.usage.input_tokens": 1800,
            "gen_ai.usage.output_tokens": 220,
            "gen_ai.response.finish_reason": "stop",
          },
        },
      ],
    },
  ]}
/>

Subscribe to playback and selection:

<SpanBuilderViz
  onComplete={({ scenarioId, spanCount, totalMs }) => {
    /* trace finished playing */
  }}
  onSpanSelect={(span) => {
    /* user clicked into a span (or null on deselect) */
  }}
/>

Understanding the component

  1. Scenario chips. When more than one scenario is supplied, the chip strip at the top toggles between them. Clicking a chip resets the waterfall and replays from the first span.
  2. Tick row. The header row above the bars draws milestone ticks (every 200ms / 500ms / 1s depending on the trace's total duration) so durations are readable in wall-clock time.
  3. Span rows. Each row has a label column (kind-coloured, indented by depth) and a bar column whose left position maps to startMs / totalMs and whose width maps to durationMs / totalMs. Bars under 8% of the trace get their duration label outside the bar; wider bars get it inside.
  4. Error styling. Spans flagged error swap their solid bar for a candy-stripe pattern, gain an uppercase ERR badge, and tint the attribute inspector's error rows.
  5. Attribute inspector. Selecting a span opens a side panel with kind, duration, start, end summary rows followed by every key in the span's attrs bag. error.* keys are highlighted in the error colour.
  6. Phases. State derives from playback + selection and exposes via data-phase on the root: idleplayingreadyinspecting. The active scenario id is exposed via data-scenario.
  7. Reduced motion. Under prefers-reduced-motion: reduce, every entrance collapses to a snap, bars skip their scale-in, and playback jumps straight to the fully-revealed state.

Props

PropTypeDefaultDescription
scenariosreadonly SpanBuilderVizScenario[]three-scenario default (Simple RAG, Agent with tools, Retry)Scenarios available via the chip strip. The first scenario is auto-played on mount.
defaultScenarioIdstringfirst scenario idInitial scenario id to play.
transitionTransitionSPRINGS.smoothOverride the entrance spring for the waterfall bars and side panel.
onComplete(summary) => voidFires once every span in the active scenario is revealed.
onSpanSelect(span | null) => voidFires when the viewer selects (or deselects) a span.
classNamestringMerged onto the root via cn().

Accessibility

  • The root is role="figure" with an aria-label summarising the active scenario, span count, and total duration so screen-reader users get the headline.
  • An sr-only polite live region announces playback (Playing trace: 4 of 8 spans revealed), selection (Selected: gen_ai.chat, 980ms), and completion (Trace complete. 8 spans. Use arrow keys to inspect.).
  • Every revealed span is a focusable button with aria-pressed reflecting selection and an aria-label that includes name, duration, and error state.
  • Scenario chips are buttons with aria-pressed reflecting the active scenario, disable while a different scenario is mid-playback, and live inside a role="group" wrapper.
  • Keyboard model: / move selection between revealed spans, Esc deselects, R replays the active scenario, Enter / Space toggles selection on the focused span. The handler chains to user-provided onKeyDown and skips default-prevented events.
  • Colour is never the only signal — every span row pairs its bar colour with the kind-coloured label, every error span carries an uppercase ERR badge, and the attribute inspector renders every attrs key/value as text.
  • Motion respects prefers-reduced-motion: reduce — entrances collapse to a snap and bars skip their grow-in.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/systems/SpanBuilderViz.tsx). The source was a lesson primitive wrapping a Widget shell with three modes (Explore/Predict/Challenge), undo history, embedded PREDICT_ROUNDS / CHALLENGE_ROUNDS quiz banks, narration banner, FeedbackBadge/ScoreDots/ChallengeBtn lesson chrome, and CA palette tokens (--color-ink-* / --color-accent-400 / --color-fail-400 / --color-success-400 / --color-warn-400 / --color-surface-raised / ca-narration). The extract strips every lesson-chrome layer and the quiz banks down to the focused viz primitive (waterfall + attribute inspector + scenario chips), lifts the hard-coded simple-rag / agent-tools / retry scenarios into the scenarios prop (defaults preserved as DEFAULT_SCENARIOS), and re-architects to forwardRef + cn() + ...props spread + chained onKeyDown. Per-kind colours remap to semantic cb-* tokens (var(--cb-info) retrieval, var(--cb-accent) LLM, var(--cb-warning) tool, var(--cb-error) error, var(--cb-fg-muted) root). Inline SPRINGS.snappy / SPRINGS.gentle / MICRO.tap / STAGGER.tight / TIMING.* re-key to canonical SPRINGS.snap / SPRINGS.smooth from @craft-bits/core/motion and the canonical STAGGER scalar.