fix: complete traces on idle, improve dashboard span/event/analytics views
This commit is contained in:
@@ -12,6 +12,7 @@ interface TraceData {
|
||||
metadata: Record<string, unknown>;
|
||||
costUsd: number | null;
|
||||
totalCost: number | null;
|
||||
totalTokens: number | null;
|
||||
decisionPoints: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -96,6 +97,8 @@ export default async function TraceDetailPage({ params }: TraceDetailPageProps)
|
||||
tags: trace.tags,
|
||||
metadata: trace.metadata,
|
||||
costUsd: trace.costUsd ?? trace.totalCost,
|
||||
totalTokens: trace.totalTokens ?? null,
|
||||
totalCost: trace.totalCost ?? null,
|
||||
}}
|
||||
decisionPoints={trace.decisionPoints}
|
||||
spans={trace.spans}
|
||||
|
||||
@@ -49,6 +49,8 @@ interface Trace {
|
||||
tags: string[];
|
||||
metadata: Record<string, unknown>;
|
||||
costUsd: number | null;
|
||||
totalTokens: number | null;
|
||||
totalCost: number | null;
|
||||
}
|
||||
|
||||
interface TraceAnalyticsProps {
|
||||
@@ -112,9 +114,15 @@ function ExecutionTimeline({ trace, spans }: { trace: Trace; spans: Span[] }) {
|
||||
}
|
||||
|
||||
const traceStartTime = new Date(trace.startedAt).getTime();
|
||||
const lastSpanEnd = spans.reduce((max, s) => {
|
||||
const end = s.endedAt ? new Date(s.endedAt).getTime() : 0;
|
||||
return end > max ? end : max;
|
||||
}, 0);
|
||||
const traceEndTime = trace.endedAt
|
||||
? new Date(trace.endedAt).getTime()
|
||||
: Date.now();
|
||||
: trace.status === "COMPLETED" || trace.status === "ERROR"
|
||||
? lastSpanEnd || traceStartTime
|
||||
: Date.now();
|
||||
const totalDuration = traceEndTime - traceStartTime;
|
||||
|
||||
// Build span hierarchy for nesting
|
||||
@@ -310,7 +318,7 @@ function CostBreakdown({
|
||||
(sum, d) => sum + (d.cost || 0),
|
||||
0
|
||||
);
|
||||
const totalCostValue = trace.costUsd ?? totalSpanCost + totalDecisionCost;
|
||||
const totalCostValue = trace.costUsd ?? trace.totalCost ?? totalSpanCost + totalDecisionCost;
|
||||
const hasData =
|
||||
totalCostValue > 0 || spanCostsData.length > 0 || decisionCostsData.length > 0;
|
||||
|
||||
@@ -462,6 +470,7 @@ function CostBreakdown({
|
||||
function TokenUsageGauge({ trace }: { trace: Trace }) {
|
||||
const tokenData = useMemo(() => {
|
||||
const totalTokens =
|
||||
trace.totalTokens ??
|
||||
(trace.metadata?.totalTokens as number | null | undefined) ??
|
||||
(trace.metadata?.tokenCount as number | null | undefined) ??
|
||||
null;
|
||||
|
||||
@@ -77,6 +77,8 @@ interface Trace {
|
||||
tags: string[];
|
||||
metadata: Record<string, unknown>;
|
||||
costUsd: number | null;
|
||||
totalTokens: number | null;
|
||||
totalCost: number | null;
|
||||
}
|
||||
|
||||
interface TraceDetailProps {
|
||||
@@ -530,19 +532,77 @@ function SpanItem({ span, maxDuration }: { span: Span; maxDuration: number }) {
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-neutral-500 mb-2">Output</h5>
|
||||
<pre className="p-3 bg-neutral-950 rounded-lg overflow-x-auto text-xs text-neutral-300">
|
||||
{JSON.stringify(span.output, null, 2)}
|
||||
</pre>
|
||||
{span.output === null || span.output === undefined || span.output === "" ? (
|
||||
<p className="text-sm text-neutral-500 italic">No output recorded</p>
|
||||
) : (
|
||||
<pre className="p-3 bg-neutral-950 rounded-lg overflow-x-auto text-xs text-neutral-300">
|
||||
{JSON.stringify(span.output, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{Object.keys(span.metadata).length > 0 && (
|
||||
{Object.entries(span.metadata).some(([, v]) => v !== null && v !== undefined) && (
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-neutral-500 mb-2">Metadata</h5>
|
||||
<pre className="p-3 bg-neutral-950 rounded-lg overflow-x-auto text-xs text-neutral-300">
|
||||
{JSON.stringify(span.metadata, null, 2)}
|
||||
{JSON.stringify(
|
||||
Object.fromEntries(Object.entries(span.metadata).filter(([, v]) => v !== null && v !== undefined)),
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
<details className="group">
|
||||
<summary className="flex items-center gap-2 text-sm text-neutral-400 hover:text-neutral-200 cursor-pointer transition-colors">
|
||||
<FileJson className="w-4 h-4" />
|
||||
<span>Raw JSON</span>
|
||||
<ChevronRight className="w-4 h-4 group-open:rotate-90 transition-transform" />
|
||||
</summary>
|
||||
<pre className="mt-3 p-4 bg-neutral-950 rounded-lg overflow-x-auto text-xs text-neutral-300 max-h-96 overflow-y-auto">
|
||||
{JSON.stringify(span, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EventItem({ event }: { event: Event }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { icon: Icon, color } = eventTypeColors[event.type] || eventTypeColors.DEFAULT;
|
||||
const hasMetadata = event.metadata && Object.entries(event.metadata).some(([, v]) => v !== null && v !== undefined);
|
||||
|
||||
return (
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-xl hover:border-neutral-700 transition-colors">
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="w-full flex items-center gap-4 p-4"
|
||||
>
|
||||
<div className={cn("p-2 rounded-lg bg-neutral-800", color)}>
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-medium text-neutral-100">{event.name}</h4>
|
||||
<p className="text-xs text-neutral-500">{event.type}</p>
|
||||
</div>
|
||||
<span className="text-sm text-neutral-400">
|
||||
{formatRelativeTime(event.timestamp)}
|
||||
</span>
|
||||
{hasMetadata && (
|
||||
expanded ? (
|
||||
<ChevronDown className="w-5 h-5 text-neutral-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5 text-neutral-500" />
|
||||
)
|
||||
)}
|
||||
</button>
|
||||
{expanded && hasMetadata && (
|
||||
<div className="px-4 pb-4 pt-0 border-t border-neutral-800">
|
||||
<pre className="mt-3 p-3 bg-neutral-950 rounded-lg overflow-x-auto text-xs text-neutral-300 max-h-64 overflow-y-auto">
|
||||
{JSON.stringify(event.metadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -560,26 +620,9 @@ function EventsTab({ events }: { events: Event[] }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{events.map((event) => {
|
||||
const { icon: Icon, color } = eventTypeColors[event.type] || eventTypeColors.DEFAULT;
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className="flex items-center gap-4 p-4 bg-neutral-900 border border-neutral-800 rounded-xl hover:border-neutral-700 transition-colors"
|
||||
>
|
||||
<div className={cn("p-2 rounded-lg bg-neutral-800", color)}>
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-neutral-100">{event.name}</h4>
|
||||
<p className="text-xs text-neutral-500">{event.type}</p>
|
||||
</div>
|
||||
<span className="text-sm text-neutral-400">
|
||||
{formatRelativeTime(event.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{events.map((event) => (
|
||||
<EventItem key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user