@townco/debugger 0.1.1

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/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # bun-react-tailwind-shadcn-template
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To start a development server:
10
+
11
+ ```bash
12
+ bun dev
13
+ ```
14
+
15
+ To run for production:
16
+
17
+ ```bash
18
+ bun start
19
+ ```
20
+
21
+ This project was created using `bun init` in bun v1.3.2. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@townco/debugger",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "engines": {
6
+ "bun": ">=1.3.0"
7
+ },
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "scripts": {
12
+ "dev": "bun --hot src/index.ts",
13
+ "start": "NODE_ENV=production bun src/index.ts",
14
+ "check": "tsc --noEmit"
15
+ },
16
+ "dependencies": {
17
+ "@townco/otlp-server": "0.1.1",
18
+ "@radix-ui/react-label": "^2.1.7",
19
+ "@radix-ui/react-select": "^2.2.6",
20
+ "@radix-ui/react-slot": "^1.2.3",
21
+ "bun-plugin-tailwind": "^0.1.2",
22
+ "class-variance-authority": "^0.7.1",
23
+ "clsx": "^2.1.1",
24
+ "lucide-react": "^0.545.0",
25
+ "react": "^19",
26
+ "react-dom": "^19",
27
+ "tailwind-merge": "^3.3.1"
28
+ },
29
+ "devDependencies": {
30
+ "@townco/tsconfig": "0.1.43",
31
+ "@types/bun": "latest",
32
+ "@types/react": "^19",
33
+ "@types/react-dom": "^19",
34
+ "tailwindcss": "^4.1.11",
35
+ "tw-animate-css": "^1.4.0",
36
+ "typescript": "^5.9.3"
37
+ }
38
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,103 @@
1
+ import { Component, type ReactNode } from "react";
2
+ import "./index.css";
3
+ import { SessionView } from "./pages/SessionView";
4
+ import { TraceDetail } from "./pages/TraceDetail";
5
+ import { TraceList } from "./pages/TraceList";
6
+
7
+ interface ErrorBoundaryState {
8
+ hasError: boolean;
9
+ error: Error | null;
10
+ }
11
+
12
+ class ErrorBoundary extends Component<
13
+ { children: ReactNode },
14
+ ErrorBoundaryState
15
+ > {
16
+ constructor(props: { children: ReactNode }) {
17
+ super(props);
18
+ this.state = { hasError: false, error: null };
19
+ }
20
+
21
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
22
+ return { hasError: true, error };
23
+ }
24
+
25
+ override render() {
26
+ if (this.state.hasError) {
27
+ return (
28
+ <div
29
+ style={{
30
+ padding: "2rem",
31
+ maxWidth: "600px",
32
+ margin: "4rem auto",
33
+ fontFamily: "system-ui, sans-serif",
34
+ }}
35
+ >
36
+ <h1 style={{ color: "#dc2626", marginBottom: "1rem" }}>
37
+ Something went wrong
38
+ </h1>
39
+ <p style={{ color: "#6b7280", marginBottom: "1rem" }}>
40
+ The debugger encountered an unexpected error.
41
+ </p>
42
+ <pre
43
+ style={{
44
+ background: "#f3f4f6",
45
+ padding: "1rem",
46
+ borderRadius: "0.5rem",
47
+ overflow: "auto",
48
+ fontSize: "0.875rem",
49
+ color: "#374151",
50
+ }}
51
+ >
52
+ {this.state.error?.message}
53
+ </pre>
54
+ <button
55
+ onClick={() => window.location.reload()}
56
+ style={{
57
+ marginTop: "1rem",
58
+ padding: "0.5rem 1rem",
59
+ background: "#3b82f6",
60
+ color: "white",
61
+ border: "none",
62
+ borderRadius: "0.375rem",
63
+ cursor: "pointer",
64
+ }}
65
+ >
66
+ Reload
67
+ </button>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ return this.props.children;
73
+ }
74
+ }
75
+
76
+ function AppContent() {
77
+ const pathname = window.location.pathname;
78
+
79
+ // Route: /sessions/:sessionId
80
+ const sessionMatch = pathname.match(/^\/sessions\/(.+)$/);
81
+ if (sessionMatch?.[1]) {
82
+ return <SessionView sessionId={sessionMatch[1]} />;
83
+ }
84
+
85
+ // Route: /trace/:traceId
86
+ const traceMatch = pathname.match(/^\/trace\/(.+)$/);
87
+ if (traceMatch?.[1]) {
88
+ return <TraceDetail traceId={traceMatch[1]} />;
89
+ }
90
+
91
+ // Default: Trace list
92
+ return <TraceList />;
93
+ }
94
+
95
+ export function App() {
96
+ return (
97
+ <ErrorBoundary>
98
+ <AppContent />
99
+ </ErrorBoundary>
100
+ );
101
+ }
102
+
103
+ export default App;
@@ -0,0 +1,26 @@
1
+ interface AttributeViewerProps {
2
+ label: string;
3
+ data: string | null;
4
+ }
5
+
6
+ export function AttributeViewer({ label, data }: AttributeViewerProps) {
7
+ if (!data || data === "{}" || data === "[]") return null;
8
+
9
+ let parsed: unknown;
10
+ try {
11
+ parsed = JSON.parse(data);
12
+ } catch {
13
+ parsed = data;
14
+ }
15
+
16
+ return (
17
+ <div className="mb-2">
18
+ <div className="text-xs text-muted-foreground uppercase font-medium">
19
+ {label}
20
+ </div>
21
+ <pre className="text-xs bg-muted p-2 rounded overflow-x-auto whitespace-pre-wrap">
22
+ {typeof parsed === "string" ? parsed : JSON.stringify(parsed, null, 2)}
23
+ </pre>
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,72 @@
1
+ import { useState } from "react";
2
+ import type { Log } from "../types";
3
+ import { AttributeViewer } from "./AttributeViewer";
4
+
5
+ function getSeverityColor(severityNumber: number): string {
6
+ if (severityNumber >= 17) return "text-red-500"; // ERROR/FATAL
7
+ if (severityNumber >= 13) return "text-yellow-500"; // WARN
8
+ if (severityNumber >= 9) return "text-blue-500"; // INFO
9
+ return "text-muted-foreground"; // DEBUG/TRACE
10
+ }
11
+
12
+ function formatTimestamp(nanoseconds: number): string {
13
+ return new Date(nanoseconds / 1_000_000).toLocaleTimeString();
14
+ }
15
+
16
+ function LogRow({ log }: { log: Log }) {
17
+ const [expanded, setExpanded] = useState(false);
18
+
19
+ const hasDetails =
20
+ (log.attributes && log.attributes !== "{}") ||
21
+ (log.resource_attributes && log.resource_attributes !== "{}");
22
+
23
+ return (
24
+ <div>
25
+ <div
26
+ className={`flex items-start gap-2 py-1 px-2 hover:bg-muted rounded ${hasDetails ? "cursor-pointer" : ""}`}
27
+ onClick={() => hasDetails && setExpanded(!expanded)}
28
+ >
29
+ <span
30
+ className={`${getSeverityColor(log.severity_number)} font-medium text-xs w-12 shrink-0`}
31
+ >
32
+ {log.severity_text || "LOG"}
33
+ </span>
34
+ <span className="flex-1 text-sm truncate">{log.body}</span>
35
+ <span className="text-muted-foreground text-xs shrink-0">
36
+ {formatTimestamp(log.timestamp_unix_nano)}
37
+ </span>
38
+ </div>
39
+
40
+ {expanded && (
41
+ <div className="py-2 px-4 bg-muted/50 rounded mb-1 ml-14">
42
+ <AttributeViewer label="Body" data={log.body} />
43
+ <AttributeViewer label="Attributes" data={log.attributes} />
44
+ <AttributeViewer label="Resource" data={log.resource_attributes} />
45
+ {log.span_id && (
46
+ <div className="text-xs text-muted-foreground">
47
+ Span ID: {log.span_id}
48
+ </div>
49
+ )}
50
+ </div>
51
+ )}
52
+ </div>
53
+ );
54
+ }
55
+
56
+ interface LogListProps {
57
+ logs: Log[];
58
+ }
59
+
60
+ export function LogList({ logs }: LogListProps) {
61
+ if (logs.length === 0) {
62
+ return <div className="text-muted-foreground text-sm">No logs</div>;
63
+ }
64
+
65
+ return (
66
+ <div className="font-mono text-sm space-y-0.5">
67
+ {logs.map((log) => (
68
+ <LogRow key={log.id} log={log} />
69
+ ))}
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,116 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import type { Trace } from "../types";
4
+
5
+ function formatRelativeTime(nanoseconds: number): string {
6
+ const ms = nanoseconds / 1_000_000;
7
+ const seconds = Math.floor((Date.now() - ms) / 1000);
8
+
9
+ if (seconds < 0) return "just now";
10
+ if (seconds < 60) return `${seconds}s ago`;
11
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
12
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
13
+ return new Date(ms).toLocaleDateString();
14
+ }
15
+
16
+ function formatDuration(startNano: number, endNano: number): string {
17
+ const ms = (endNano - startNano) / 1_000_000;
18
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
19
+ return `${(ms / 1000).toFixed(1)}s`;
20
+ }
21
+
22
+ interface SessionTraceListProps {
23
+ sessionId: string;
24
+ selectedTraceId: string | null;
25
+ onSelectTrace: (traceId: string) => void;
26
+ onTracesLoaded?: (traces: Trace[]) => void;
27
+ }
28
+
29
+ export function SessionTraceList({
30
+ sessionId,
31
+ selectedTraceId,
32
+ onSelectTrace,
33
+ onTracesLoaded,
34
+ }: SessionTraceListProps) {
35
+ const [traces, setTraces] = useState<Trace[]>([]);
36
+ const [loading, setLoading] = useState(true);
37
+ const [error, setError] = useState<string | null>(null);
38
+
39
+ const fetchTraces = useCallback(() => {
40
+ setLoading(true);
41
+ const params = new URLSearchParams();
42
+ params.set("sessionId", sessionId);
43
+ fetch(`/api/traces?${params}`)
44
+ .then((res) => {
45
+ if (!res.ok) throw new Error("Failed to fetch traces");
46
+ return res.json();
47
+ })
48
+ .then((data: Trace[]) => {
49
+ setTraces(data);
50
+ setLoading(false);
51
+ onTracesLoaded?.(data);
52
+ })
53
+ .catch((err) => {
54
+ setError(err.message);
55
+ setLoading(false);
56
+ });
57
+ }, [sessionId, onTracesLoaded]);
58
+
59
+ useEffect(() => {
60
+ fetchTraces();
61
+ }, [fetchTraces]);
62
+
63
+ if (loading) {
64
+ return (
65
+ <div className="p-4 text-muted-foreground text-sm">Loading traces...</div>
66
+ );
67
+ }
68
+
69
+ if (error) {
70
+ return <div className="p-4 text-red-500 text-sm">Error: {error}</div>;
71
+ }
72
+
73
+ if (traces.length === 0) {
74
+ return (
75
+ <div className="p-4 text-muted-foreground text-sm">
76
+ No traces found for this session
77
+ </div>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <div className="divide-y">
83
+ {traces.map((trace) => {
84
+ const isSelected = trace.trace_id === selectedTraceId;
85
+ return (
86
+ <div
87
+ key={trace.trace_id}
88
+ className={cn(
89
+ "p-3 cursor-pointer hover:bg-muted/50 transition-colors",
90
+ isSelected && "bg-muted border-l-2 border-l-primary",
91
+ )}
92
+ onClick={() => onSelectTrace(trace.trace_id)}
93
+ >
94
+ <div className="flex items-center justify-between gap-2">
95
+ <span className="font-medium text-sm truncate flex-1">
96
+ {trace.first_span_name || "Unknown"}
97
+ </span>
98
+ <span className="w-2 h-2 rounded-full bg-green-500 shrink-0" />
99
+ </div>
100
+ <div className="text-xs text-muted-foreground mt-1">
101
+ {formatRelativeTime(trace.start_time_unix_nano)}
102
+ </div>
103
+ <div className="text-xs text-muted-foreground">
104
+ {formatDuration(
105
+ trace.start_time_unix_nano,
106
+ trace.end_time_unix_nano,
107
+ )}
108
+ {" · "}
109
+ {trace.span_count} spans
110
+ </div>
111
+ </div>
112
+ );
113
+ })}
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,205 @@
1
+ import { useState } from "react";
2
+ import type { Span, SpanNode } from "../types";
3
+ import { AttributeViewer } from "./AttributeViewer";
4
+
5
+ function parseAttributes(attrs: string | null): Record<string, unknown> {
6
+ if (!attrs) return {};
7
+ try {
8
+ return JSON.parse(attrs);
9
+ } catch {
10
+ return {};
11
+ }
12
+ }
13
+
14
+ export function buildSpanTree(spans: Span[]): SpanNode[] {
15
+ const spanMap = new Map<string, SpanNode>();
16
+ const roots: SpanNode[] = [];
17
+
18
+ // First pass: create nodes
19
+ for (const span of spans) {
20
+ spanMap.set(span.span_id, {
21
+ ...span,
22
+ children: [],
23
+ depth: 0,
24
+ durationMs:
25
+ (span.end_time_unix_nano - span.start_time_unix_nano) / 1_000_000,
26
+ });
27
+ }
28
+
29
+ // Second pass: build tree
30
+ for (const span of spans) {
31
+ const node = spanMap.get(span.span_id);
32
+ if (!node) continue;
33
+
34
+ if (span.parent_span_id && spanMap.has(span.parent_span_id)) {
35
+ const parent = spanMap.get(span.parent_span_id);
36
+ if (parent) {
37
+ node.depth = parent.depth + 1;
38
+ parent.children.push(node);
39
+ }
40
+ } else {
41
+ roots.push(node);
42
+ }
43
+ }
44
+
45
+ return roots;
46
+ }
47
+
48
+ function SpanRow({ span }: { span: SpanNode }) {
49
+ const [expanded, setExpanded] = useState(false);
50
+ const [showRaw, setShowRaw] = useState(false);
51
+
52
+ const attrs = parseAttributes(span.attributes);
53
+
54
+ // Determine span type
55
+ const isToolCall = span.name === "agent.tool_call";
56
+ const isChatSpan =
57
+ span.name.startsWith("chat") && "gen_ai.input.messages" in attrs;
58
+ const isSpecialSpan = isToolCall || isChatSpan;
59
+
60
+ // Get display name based on span type
61
+ const displayName = isToolCall
62
+ ? (attrs["tool.name"] as string) || span.name
63
+ : isChatSpan
64
+ ? (attrs["gen_ai.request.model"] as string) || span.name
65
+ : span.name;
66
+
67
+ const statusColor =
68
+ span.status_code === 2
69
+ ? "text-red-500"
70
+ : span.status_code === 1
71
+ ? "text-green-500"
72
+ : "text-gray-400";
73
+
74
+ const hasDetails =
75
+ (span.attributes && span.attributes !== "{}") ||
76
+ (span.events && span.events !== "[]") ||
77
+ (span.resource_attributes && span.resource_attributes !== "{}");
78
+
79
+ return (
80
+ <div>
81
+ <div
82
+ className={`flex items-center py-1 px-2 hover:bg-muted rounded ${hasDetails ? "cursor-pointer" : ""}`}
83
+ style={{ paddingLeft: span.depth * 20 + 8 }}
84
+ onClick={() => hasDetails && setExpanded(!expanded)}
85
+ >
86
+ <span className={`${statusColor} mr-2`}>●</span>
87
+ <span className="font-medium flex-1 truncate">{displayName}</span>
88
+ <span className="text-muted-foreground text-sm ml-2">
89
+ {span.durationMs.toFixed(2)}ms
90
+ </span>
91
+ </div>
92
+
93
+ {expanded && (
94
+ <div
95
+ className="py-2 px-4 bg-muted/50 rounded mb-1"
96
+ style={{ marginLeft: span.depth * 20 + 28 }}
97
+ >
98
+ <div className="text-xs text-muted-foreground mb-2">
99
+ {span.span_id}
100
+ </div>
101
+
102
+ {/* Tool call span: show input/output */}
103
+ {isToolCall && (
104
+ <>
105
+ <AttributeViewer
106
+ label="Input"
107
+ data={attrs["tool.input"] as string}
108
+ />
109
+ <AttributeViewer
110
+ label="Output"
111
+ data={attrs["tool.output"] as string}
112
+ />
113
+ </>
114
+ )}
115
+
116
+ {/* Chat span: show system instructions, input/output messages */}
117
+ {isChatSpan && (
118
+ <>
119
+ <AttributeViewer
120
+ label="System Instructions"
121
+ data={attrs["gen_ai.system_instructions"] as string}
122
+ />
123
+ <AttributeViewer
124
+ label="Input Messages"
125
+ data={attrs["gen_ai.input.messages"] as string}
126
+ />
127
+ <AttributeViewer
128
+ label="Output Messages"
129
+ data={attrs["gen_ai.output.messages"] as string}
130
+ />
131
+ </>
132
+ )}
133
+
134
+ {/* For special spans, show raw attributes collapsed; for default, show them directly */}
135
+ {isSpecialSpan ? (
136
+ <div className="mt-2">
137
+ <button
138
+ type="button"
139
+ className="text-xs text-muted-foreground hover:text-foreground"
140
+ onClick={(e) => {
141
+ e.stopPropagation();
142
+ setShowRaw(!showRaw);
143
+ }}
144
+ >
145
+ {showRaw ? "▼ Hide" : "▶ Show"} raw attributes
146
+ </button>
147
+ {showRaw && (
148
+ <div className="mt-2">
149
+ <AttributeViewer label="Attributes" data={span.attributes} />
150
+ <AttributeViewer label="Events" data={span.events} />
151
+ <AttributeViewer
152
+ label="Resource"
153
+ data={span.resource_attributes}
154
+ />
155
+ </div>
156
+ )}
157
+ </div>
158
+ ) : (
159
+ <>
160
+ <AttributeViewer label="Attributes" data={span.attributes} />
161
+ <AttributeViewer label="Events" data={span.events} />
162
+ <AttributeViewer
163
+ label="Resource"
164
+ data={span.resource_attributes}
165
+ />
166
+ </>
167
+ )}
168
+
169
+ {span.status_message && (
170
+ <div className="text-xs">
171
+ <span className="text-muted-foreground uppercase font-medium">
172
+ Status Message
173
+ </span>
174
+ <div className="text-red-500">{span.status_message}</div>
175
+ </div>
176
+ )}
177
+ </div>
178
+ )}
179
+
180
+ {span.children.map((child) => (
181
+ <SpanRow key={child.span_id} span={child} />
182
+ ))}
183
+ </div>
184
+ );
185
+ }
186
+
187
+ interface SpanTreeProps {
188
+ spans: Span[];
189
+ }
190
+
191
+ export function SpanTree({ spans }: SpanTreeProps) {
192
+ const tree = buildSpanTree(spans);
193
+
194
+ if (tree.length === 0) {
195
+ return <div className="text-muted-foreground text-sm">No spans</div>;
196
+ }
197
+
198
+ return (
199
+ <div className="font-mono text-sm">
200
+ {tree.map((span) => (
201
+ <SpanRow key={span.span_id} span={span} />
202
+ ))}
203
+ </div>
204
+ );
205
+ }