Skip to content

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

Terminal window
pnpm dlx shadcn@latest add https://spotlightjs.com/r/trace-item.json

This installs:

  • trace-item.tsx - Main trace row component
  • time-since.tsx - Relative time display component
  • trace-badge.tsx - Status/method/environment badges
  • types.ts - TypeScript type definitions
  • duration.ts - Duration formatting utilities
  • utils.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

PropTypeDefaultDescription
traceTraceDatarequiredThe trace data to display
isSelectedbooleanfalseWhether this trace is currently selected
onSelect(id: string, trace: TraceData) => void-Callback when trace is clicked
classNamestring-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 icon
const 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" badge

Examples

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,
};
}