Display

Stack Trace

A contract-driven V8 stack trace inspector with controlled expansion, copy feedback, frame filtering, and four explicit data states.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { AlertTriangle, Check, ChevronDown, Copy, RotateCcw } from "lucide-react"
import { cn } from "@/lib/utils"
import type { ParsedStackTrace, StackFrame, StackTraceData } from "./stack-trace.contract"

const V8_FRAME_WITH_FUNCTION = /^at\s+(.+?)\s+\((.+):(\d+):(\d+)\)$/
const V8_FRAME_WITHOUT_FUNCTION = /^at\s+(.+):(\d+):(\d+)$/
const ERROR_HEADER = /^([A-Za-z][\w.]*(?:Error|Exception)?):\s*(.*)$/

function isInternalPath(filePath: string | null, raw: string) {
  const value = (filePath ?? raw).replaceAll("\\", "/")
  return value.startsWith("node:") || value.includes("node_modules/") || value.includes("internal/")

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/stack-trace.json

Prompt

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 defaultExpanded keeps 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.

On This Page