@townco/debugger 0.1.23 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -8
- package/src/App.tsx +13 -0
- package/src/comparison-db.test.ts +113 -0
- package/src/comparison-db.ts +332 -0
- package/src/components/DebuggerHeader.tsx +62 -2
- package/src/components/SessionTimelineView.tsx +173 -0
- package/src/components/SpanTimeline.tsx +6 -4
- package/src/components/UnifiedTimeline.tsx +691 -0
- package/src/db.ts +71 -0
- package/src/index.ts +2 -0
- package/src/lib/metrics.test.ts +51 -0
- package/src/lib/metrics.ts +136 -0
- package/src/lib/pricing.ts +23 -0
- package/src/lib/turnExtractor.ts +64 -23
- package/src/pages/ComparisonView.tsx +685 -0
- package/src/pages/SessionList.tsx +77 -56
- package/src/pages/SessionView.tsx +3 -64
- package/src/pages/TownHall.tsx +406 -0
- package/src/schemas.ts +15 -0
- package/src/server.ts +345 -12
- package/src/types.ts +87 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
3
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
4
|
+
import type { ConversationTrace, Log, Span } from "../types";
|
|
5
|
+
import { LogList } from "./LogList";
|
|
6
|
+
import { UnifiedTimeline } from "./UnifiedTimeline";
|
|
7
|
+
|
|
8
|
+
interface SessionTimelineViewProps {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TraceData {
|
|
13
|
+
trace: ConversationTrace;
|
|
14
|
+
spans: Span[];
|
|
15
|
+
logs: Log[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SessionTimelineView({ sessionId }: SessionTimelineViewProps) {
|
|
19
|
+
const [traces, setTraces] = useState<ConversationTrace[]>([]);
|
|
20
|
+
const [traceData, setTraceData] = useState<Map<string, TraceData>>(new Map());
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
// Fetch all traces for the session
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const params = new URLSearchParams();
|
|
27
|
+
params.set("sessionId", sessionId);
|
|
28
|
+
fetch(`/api/session-conversation?${params}`)
|
|
29
|
+
.then((res) => {
|
|
30
|
+
if (!res.ok) throw new Error("Failed to fetch conversation");
|
|
31
|
+
return res.json();
|
|
32
|
+
})
|
|
33
|
+
.then((data: ConversationTrace[]) => {
|
|
34
|
+
setTraces(data);
|
|
35
|
+
setLoading(false);
|
|
36
|
+
})
|
|
37
|
+
.catch((err) => {
|
|
38
|
+
setError(err.message);
|
|
39
|
+
setLoading(false);
|
|
40
|
+
});
|
|
41
|
+
}, [sessionId]);
|
|
42
|
+
|
|
43
|
+
// Fetch spans and logs for each trace
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (traces.length === 0) return;
|
|
46
|
+
|
|
47
|
+
const fetchAllTraceData = async () => {
|
|
48
|
+
const dataMap = new Map<string, TraceData>();
|
|
49
|
+
|
|
50
|
+
await Promise.all(
|
|
51
|
+
traces.map(async (trace) => {
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(`/api/traces/${trace.trace_id}`);
|
|
54
|
+
if (!res.ok) return;
|
|
55
|
+
const data = await res.json();
|
|
56
|
+
// Use the detailed trace data with updated messages
|
|
57
|
+
const updatedTrace: ConversationTrace = {
|
|
58
|
+
trace_id: trace.trace_id,
|
|
59
|
+
start_time_unix_nano: trace.start_time_unix_nano,
|
|
60
|
+
userInput: data.messages?.userInput ?? trace.userInput,
|
|
61
|
+
llmOutput: data.messages?.llmOutput ?? trace.llmOutput,
|
|
62
|
+
agentMessages:
|
|
63
|
+
data.messages?.agentMessages ?? trace.agentMessages,
|
|
64
|
+
};
|
|
65
|
+
dataMap.set(trace.trace_id, {
|
|
66
|
+
trace: updatedTrace,
|
|
67
|
+
spans: data.spans || [],
|
|
68
|
+
logs: data.logs || [],
|
|
69
|
+
});
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error(`Failed to fetch trace ${trace.trace_id}:`, err);
|
|
72
|
+
}
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
setTraceData(dataMap);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
fetchAllTraceData();
|
|
80
|
+
}, [traces]);
|
|
81
|
+
|
|
82
|
+
// Combine all spans and logs
|
|
83
|
+
const allSpans = useMemo(() => {
|
|
84
|
+
const spans: Array<Span & { traceId: string; turnIndex: number }> = [];
|
|
85
|
+
traces.forEach((trace, index) => {
|
|
86
|
+
const data = traceData.get(trace.trace_id);
|
|
87
|
+
if (data) {
|
|
88
|
+
data.spans.forEach((span) => {
|
|
89
|
+
spans.push({
|
|
90
|
+
...span,
|
|
91
|
+
traceId: trace.trace_id,
|
|
92
|
+
turnIndex: index,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return spans;
|
|
98
|
+
}, [traces, traceData]);
|
|
99
|
+
|
|
100
|
+
const allLogs = useMemo(() => {
|
|
101
|
+
const logs: Log[] = [];
|
|
102
|
+
traces.forEach((trace) => {
|
|
103
|
+
const data = traceData.get(trace.trace_id);
|
|
104
|
+
if (data) {
|
|
105
|
+
logs.push(...data.logs);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return logs;
|
|
109
|
+
}, [traces, traceData]);
|
|
110
|
+
|
|
111
|
+
// Calculate global timeline bounds
|
|
112
|
+
const timelineBounds = useMemo(() => {
|
|
113
|
+
if (allSpans.length === 0) return { start: 0, end: 0 };
|
|
114
|
+
|
|
115
|
+
const start = Math.min(...allSpans.map((s) => s.start_time_unix_nano));
|
|
116
|
+
const end = Math.max(...allSpans.map((s) => s.end_time_unix_nano));
|
|
117
|
+
|
|
118
|
+
return { start, end };
|
|
119
|
+
}, [allSpans]);
|
|
120
|
+
|
|
121
|
+
if (loading) {
|
|
122
|
+
return (
|
|
123
|
+
<div className="p-6">
|
|
124
|
+
<div className="text-muted-foreground">Loading session...</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (error) {
|
|
130
|
+
return (
|
|
131
|
+
<div className="p-6">
|
|
132
|
+
<div className="text-red-500">Error: {error}</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (traces.length === 0) {
|
|
138
|
+
return (
|
|
139
|
+
<div className="p-6">
|
|
140
|
+
<div className="text-muted-foreground">No turns in this session</div>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="p-6 space-y-6 bg-background min-h-screen">
|
|
147
|
+
<Tabs defaultValue="timeline">
|
|
148
|
+
<TabsList>
|
|
149
|
+
<TabsTrigger value="timeline">Timeline</TabsTrigger>
|
|
150
|
+
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
151
|
+
</TabsList>
|
|
152
|
+
|
|
153
|
+
<TabsContent value="timeline" className="mt-6">
|
|
154
|
+
<UnifiedTimeline
|
|
155
|
+
spans={allSpans}
|
|
156
|
+
traces={traces}
|
|
157
|
+
traceData={traceData}
|
|
158
|
+
timelineStart={timelineBounds.start}
|
|
159
|
+
timelineEnd={timelineBounds.end}
|
|
160
|
+
/>
|
|
161
|
+
</TabsContent>
|
|
162
|
+
|
|
163
|
+
<TabsContent value="logs" className="mt-6">
|
|
164
|
+
<Card>
|
|
165
|
+
<CardContent className="p-4">
|
|
166
|
+
<LogList logs={allLogs} />
|
|
167
|
+
</CardContent>
|
|
168
|
+
</Card>
|
|
169
|
+
</TabsContent>
|
|
170
|
+
</Tabs>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -373,7 +373,7 @@ function RenderSpanTreeRow({
|
|
|
373
373
|
return (
|
|
374
374
|
<>
|
|
375
375
|
<div
|
|
376
|
-
className="h-[38px] px-4 py-2 border-b border-border flex items-center gap-2 hover:bg-muted/50 cursor-pointer"
|
|
376
|
+
className="h-[38px] px-4 py-2 border-b border-border flex items-center gap-2 hover:bg-muted/50 cursor-pointer transition-colors"
|
|
377
377
|
style={{ paddingLeft: `${span.depth * 20 + 16}px` }}
|
|
378
378
|
onClick={() => onSpanClick(span)}
|
|
379
379
|
>
|
|
@@ -422,14 +422,16 @@ function RenderSpanTreeTimeline({
|
|
|
422
422
|
|
|
423
423
|
return (
|
|
424
424
|
<>
|
|
425
|
-
<div
|
|
425
|
+
<div
|
|
426
|
+
className="h-[38px] border-b border-border relative flex items-center px-4 hover:bg-muted/50 cursor-pointer transition-colors"
|
|
427
|
+
onClick={() => onSpanClick(span)}
|
|
428
|
+
>
|
|
426
429
|
<div
|
|
427
|
-
className={`absolute h-[18px] rounded ${barColor} flex items-center px-1`}
|
|
430
|
+
className={`absolute h-[18px] rounded ${barColor} flex items-center px-1 pointer-events-none`}
|
|
428
431
|
style={{
|
|
429
432
|
left: `calc(${timeline.leftPercent}% + 16px)`,
|
|
430
433
|
width: `calc(${Math.max(timeline.widthPercent, 0.5)}% - 32px)`,
|
|
431
434
|
}}
|
|
432
|
-
onClick={() => onSpanClick(span)}
|
|
433
435
|
>
|
|
434
436
|
<span className="text-xs text-white whitespace-nowrap">
|
|
435
437
|
{span.durationMs.toFixed(2)}ms
|