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.jsonUsage
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
- 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.
- 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.
- Span rows. Each row has a label column (kind-coloured, indented by
depth) and a bar column whose left position maps tostartMs / totalMsand whose width maps todurationMs / totalMs. Bars under 8% of the trace get their duration label outside the bar; wider bars get it inside. - Error styling. Spans flagged
errorswap their solid bar for a candy-stripe pattern, gain an uppercaseERRbadge, and tint the attribute inspector's error rows. - Attribute inspector. Selecting a span opens a side panel with
kind,duration,start,endsummary rows followed by every key in the span'sattrsbag.error.*keys are highlighted in the error colour. - Phases. State derives from playback + selection and exposes via
data-phaseon the root:idle→playing→ready→inspecting. The active scenario id is exposed viadata-scenario. - 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
| Prop | Type | Default | Description |
|---|---|---|---|
scenarios | readonly SpanBuilderVizScenario[] | three-scenario default (Simple RAG, Agent with tools, Retry) | Scenarios available via the chip strip. The first scenario is auto-played on mount. |
defaultScenarioId | string | first scenario id | Initial scenario id to play. |
transition | Transition | SPRINGS.smooth | Override the entrance spring for the waterfall bars and side panel. |
onComplete | (summary) => void | — | Fires once every span in the active scenario is revealed. |
onSpanSelect | (span | null) => void | — | Fires when the viewer selects (or deselects) a span. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The root is
role="figure"with anaria-labelsummarising the active scenario, span count, and total duration so screen-reader users get the headline. - An
sr-onlypolite 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-pressedreflecting selection and anaria-labelthat includes name, duration, and error state. - Scenario chips are buttons with
aria-pressedreflecting the active scenario, disable while a different scenario is mid-playback, and live inside arole="group"wrapper. - Keyboard model:
↑/↓move selection between revealed spans,Escdeselects,Rreplays the active scenario,Enter/Spacetoggles selection on the focused span. The handler chains to user-providedonKeyDownand 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
ERRbadge, and the attribute inspector renders everyattrskey/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, embeddedPREDICT_ROUNDS/CHALLENGE_ROUNDSquiz 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-codedsimple-rag/agent-tools/retryscenarios into thescenariosprop (defaults preserved asDEFAULT_SCENARIOS), and re-architects toforwardRef+cn()+...propsspread + chainedonKeyDown. Per-kind colours remap to semanticcb-*tokens (var(--cb-info)retrieval,var(--cb-accent)LLM,var(--cb-warning)tool,var(--cb-error)error,var(--cb-fg-muted)root). InlineSPRINGS.snappy/SPRINGS.gentle/MICRO.tap/STAGGER.tight/TIMING.*re-key to canonicalSPRINGS.snap/SPRINGS.smoothfrom@craft-bits/core/motionand the canonicalSTAGGERscalar.