Stack Trace
A contract-driven V8 stack trace inspector with controlled expansion, copy feedback, frame filtering, and four explicit data states.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/stack-trace.jsonPrompt
Build a React + TypeScript + Tailwind "StackTrace" component with lucide-react
icons and a zod contract.
Contract
- Make a discriminated zod union the source of truth:
{ status: "loading" } |
{ status: "empty"; emptyMessage?: string } |
{ status: "error"; errorMessage: string } |
{ status: "ready"; trace: string; label?: string }.
- Component props add controlled expansion (`expanded?`,
`defaultExpanded = false`, `onExpandedChange?`), `showInternalFrames = true`,
`onFrameClick?(frame)`, `onRetry?`, `copyResetDelay = 2000`,
`frameRenderLimit = 200` (clamped to 1…2000), and controlled/uncontrolled
`renderAllFrames` / `defaultRenderAllFrames` / `onRenderAllFramesChange`,
plus className and native div attributes.
- Export `parseStackTrace(raw)` and inferred ParsedStackTrace / StackFrame types.
A frame keeps raw, functionName, filePath, lineNumber, columnNumber, and
isInternal so consumers never have to reverse-engineer rendered text.
Behavior
- Render loading, empty, data-load error, and ready as four first-class
branches. The loading skeleton mirrors the header and frame rows; retry only
appears when the consumer supplies onRetry.
- Parse common V8 forms: `at fn (path:line:column)` and
`at path:line:column`. Accept file URLs, Windows-style drive prefixes, and
`node:` paths by matching line/column from the right. Normalize CRLF. Any
non-matching line becomes a raw frame row rather than being dropped; this
keeps "Caused by", Promise index, and vendor-specific evidence visible.
- Keep expansion controllable: read `expanded` when supplied, otherwise own
state initialized by `defaultExpanded`; every toggle calls
onExpandedChange. The summary trigger is a real button with aria-expanded
and aria-controls. Keep the copy button as its sibling so interactive
controls are never nested.
- Copy the original raw trace with navigator.clipboard. Give every async copy
attempt a monotonically increasing token, ignore stale completions, and
invalidate the token plus clear the reset timer on unmount. A permanently
mounted polite live region announces success or visible failure.
- Internal frames are those from node:, internal/, or node_modules/. When
showInternalFrames is false, filter only the rendered list; keep the copied
raw trace complete. File locations become buttons only when onFrameClick is
present.
- After filtering, render only the first frameRenderLimit frames by default,
report the omitted count, and expose Show all / Show first controls. This
bounds the initial DOM without truncating parsing or the raw text copied to
the clipboard.
Rendering & styling
- Use semantic tokens only: bg-card shell, bg-muted/20 frame surface,
text-muted-foreground for secondary/internal frames, text-destructive for
the error identity, and ring-ring for keyboard focus. Merge className with
cn().
- Put expanded rows in overflow-x-auto and give each row w-max/min-w-full plus
whitespace-pre, so long paths scroll instead of wrapping into unreadable
fragments or clipping.
- Let the error summary wrap on narrow screens instead of truncating the
diagnostic message. Render internal frames with the full
text-muted-foreground token rather than reduced-opacity text.
- Respect prefers-reduced-motion: remove chevron/hover transitions and loading
pulse animation under motion-reduce. Mark decorative icons aria-hidden,
expose copy feedback through a persistent status live region, and retain keyboard-native
button behavior.
Customization levers
- Density: change only the header padding, frame leading, and row gap; parser
and accessibility wiring remain unchanged.
- Noise filtering: replace the `isInternal` path predicate or add a
`frameFilter(frame)` prop for product-specific SDK/vendor frames.
- Render budget: lower frameRenderLimit in embedded surfaces, raise it up to
the safety ceiling for desktop consoles, or own renderAllFrames when the host
must persist the user's Show all choice.
- Disclosure: default the panel open in local development and closed in
customer-facing error reports, or persist the controlled `expanded` value
outside the component.
- Navigation: map onFrameClick to an editor deep link, source viewer, or
source-map resolver; leave it absent for a read-only trace.
- Parser coverage: add a separate parser branch for Firefox/Safari formats,
but preserve the raw fallback as the final branch.Concepts
- Lossless parser fallback — unfamiliar stack lines remain visible as raw rows, so a parser that only understands common V8 frames never destroys the evidence it cannot classify.
- Controlled disclosure — the host can own expansion for persistence or coordinated panels, while
defaultExpandedkeeps the component convenient in standalone use. - Filter the view, not the evidence — hiding internal frames changes only the rendered list; copying still returns the complete raw trace.
- Bounded frame rendering — a default DOM budget keeps giant traces responsive, while an omitted count and Show all control make the limit explicit and reversible.
- Race-safe clipboard feedback — latest-attempt and mounted guards prevent a slow copy promise or timer from overwriting a newer result or updating after unmount.
- Scrollable diagnostic lines — file paths retain their exact single-line shape and move into horizontal scrolling, preserving line/column readability on narrow surfaces.
Schema Display
An accessible API operation viewer with parameters, request and response schemas, recursive fields, depth guards, and four fetch states.
Leaderboard
A ranked standings board — podium tiers, rank-change arrows, a highlighted current-user row with a pinned "your rank" bar, and loading/empty/error/ready from one contract.