TraceItem Component
The TraceItem component renders a summary row for a single distributed trace. It displays the trace ID, timing information, status, and transaction details in a compact, clickable format.
Installation
pnpm dlx shadcn@latest add https://spotlightjs.com/r/trace-item.jsonThis installs:
trace-item.tsx- Main trace row componenttime-since.tsx- Relative time display componenttrace-badge.tsx- Status/method/environment badgestypes.ts- TypeScript type definitionsduration.ts- Duration formatting utilitiesutils.ts- Utility functions
Basic Usage
import { TraceItem } from "@/components/trace-item";
function TraceList({ traces }) { const [selectedId, setSelectedId] = useState<string>();
return ( <div className="divide-y"> {traces.map((trace) => ( <TraceItem key={trace.trace_id} trace={trace} isSelected={selectedId === trace.trace_id} onSelect={(id) => setSelectedId(id)} /> ))} </div> );}Props
TraceItem Props
| Prop | Type | Default | Description |
|---|---|---|---|
trace | TraceData | required | The trace data to display |
isSelected | boolean | false | Whether this trace is currently selected |
onSelect | (id: string, trace: TraceData) => void | - | Callback when trace is clicked |
className | string | - | Additional CSS classes |
TraceData Interface
interface TraceData { trace_id: string; start_timestamp: number; timestamp: number; status?: "ok" | "error" | string; spans: Map<string, SpanData>; spanTree: SpanData[]; rootTransactionName: string; rootTransactionMethod?: string; transactionCount?: number; spanCount?: number; platform?: string; environment?: string;}Features
Status Indication
The component shows different icons based on trace status:
- Activity icon - For successful traces (
status: "ok"or undefined) - AlertCircle icon - For error traces (any other status)
// Error trace will show red alert iconconst errorTrace = { ...trace, status: "error"};
<TraceItem trace={errorTrace} />Transaction Display
Shows the HTTP method (if available) and transaction name:
const trace = { trace_id: "abc123", rootTransactionName: "/api/users/:id", rootTransactionMethod: "GET", // ...};
// Renders: [GET] /api/users/:id<TraceItem trace={trace} />Environment Badge
If the trace has an environment, it’s displayed as a badge:
const trace = { trace_id: "abc123", environment: "production", // ...};
// Shows environment badge next to trace ID<TraceItem trace={trace} />Relative Time
The timestamp is displayed as relative time (e.g., “2 minutes ago”) and updates automatically every 5 seconds.
Styling
The component uses shadcn design tokens:
/* Default state */.trace-item { background: transparent;}
/* Hover state */.trace-item:hover { background: hsl(var(--muted) / 0.5);}
/* Selected state */.trace-item.selected { background: hsl(var(--muted));}
/* Error status */.trace-item .status-error { color: hsl(var(--destructive));}Custom Styling
<TraceItem trace={trace} className="border-b last:border-b-0"/>Sub-Components
TimeSince
Displays relative time that auto-updates:
import { TimeSince } from "@/components/time-since";
<TimeSince date={timestamp} refreshInterval={1000} // Update every second className="text-sm text-muted-foreground"/>TraceBadge
Status, method, and environment badges:
import { StatusBadge, MethodBadge, EnvironmentBadge } from "@/components/trace-badge";
<StatusBadge status="ok" /> // Green "OK" badge<StatusBadge status="error" /> // Red "ERROR" badge<MethodBadge method="POST" /> // Neutral "POST" badge<EnvironmentBadge environment="dev" /> // Secondary "dev" badgeExamples
With Trace Details
function TraceViewer({ traces }) { const [selectedTrace, setSelectedTrace] = useState<TraceData>();
return ( <div className="flex h-screen"> {/* Trace list */} <div className="w-1/3 overflow-auto border-r"> {traces.map((trace) => ( <TraceItem key={trace.trace_id} trace={trace} isSelected={selectedTrace?.trace_id === trace.trace_id} onSelect={(_, trace) => setSelectedTrace(trace)} /> ))} </div>
{/* Trace details */} <div className="flex-1 overflow-auto"> {selectedTrace ? ( <SpanTree spans={selectedTrace.spanTree} traceStartTimestamp={selectedTrace.start_timestamp} traceDuration={selectedTrace.timestamp - selectedTrace.start_timestamp} /> ) : ( <div className="flex h-full items-center justify-center text-muted-foreground"> Select a trace to view details </div> )} </div> </div> );}Filtered List
function FilteredTraceList({ traces, statusFilter }) { const filteredTraces = useMemo(() => { if (!statusFilter) return traces; return traces.filter(t => statusFilter === "error" ? t.status && t.status !== "ok" : t.status === "ok" || !t.status ); }, [traces, statusFilter]);
return ( <div className="divide-y"> {filteredTraces.length === 0 ? ( <div className="p-4 text-center text-muted-foreground"> No traces match the filter </div> ) : ( filteredTraces.map((trace) => ( <TraceItem key={trace.trace_id} trace={trace} /> )) )} </div> );}With Virtualization
For large lists, combine with a virtualization library:
import { useVirtualizer } from "@tanstack/react-virtual";
function VirtualizedTraceList({ traces }) { const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({ count: traces.length, getScrollElement: () => parentRef.current, estimateSize: () => 60, // Approximate row height });
return ( <div ref={parentRef} className="h-full overflow-auto"> <div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative", }} > {virtualizer.getVirtualItems().map((virtualItem) => ( <div key={virtualItem.key} style={{ position: "absolute", top: 0, transform: `translateY(${virtualItem.start}px)`, width: "100%", }} > <TraceItem trace={traces[virtualItem.index]} /> </div> ))} </div> </div> );}Custom Actions
function TraceListWithActions({ traces }) { return ( <div className="divide-y"> {traces.map((trace) => ( <div key={trace.trace_id} className="flex items-center"> <div className="flex-1"> <TraceItem trace={trace} onSelect={(id) => console.log("Selected:", id)} /> </div> <div className="px-4"> <Button variant="ghost" size="sm" onClick={() => copyToClipboard(trace.trace_id)} > Copy ID </Button> </div> </div> ))} </div> );}Building TraceData
Convert your trace data to the expected format:
function buildTraceData(rawTrace: RawTrace): TraceData { const spans = new Map<string, SpanData>();
for (const span of rawTrace.spans) { spans.set(span.span_id, { span_id: span.span_id, trace_id: rawTrace.trace_id, parent_span_id: span.parent_span_id, op: span.name, description: span.attributes?.description, start_timestamp: span.start_time, timestamp: span.end_time, status: span.status, data: span.attributes, }); }
return { trace_id: rawTrace.trace_id, start_timestamp: Math.min(...rawTrace.spans.map(s => s.start_time)), timestamp: Math.max(...rawTrace.spans.map(s => s.end_time)), status: rawTrace.status, spans, spanTree: buildSpanTree(Array.from(spans.values())), rootTransactionName: rawTrace.name || "Unknown", rootTransactionMethod: rawTrace.method, environment: rawTrace.environment, };}