@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/src/logo.svg ADDED
@@ -0,0 +1 @@
1
+ <svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70"><title>Bun Logo</title><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" data-name="Bottom Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" data-name="Light Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2" data-name="Background"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" data-name="Background" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" data-name="Outline" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" data-name="Outline" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" data-name="Right Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" data-name="Left Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>
@@ -0,0 +1,88 @@
1
+ import { useCallback, useState } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { SessionTraceList } from "../components/SessionTraceList";
4
+ import { TraceDetailContent } from "../components/TraceDetailContent";
5
+ import type { Trace } from "../types";
6
+
7
+ function getInitialTraceId(): string | null {
8
+ const params = new URLSearchParams(window.location.search);
9
+ return params.get("traceId");
10
+ }
11
+
12
+ interface SessionViewProps {
13
+ sessionId: string;
14
+ }
15
+
16
+ export function SessionView({ sessionId }: SessionViewProps) {
17
+ const [selectedTraceId, setSelectedTraceId] = useState<string | null>(
18
+ getInitialTraceId,
19
+ );
20
+ const [autoSelectDone, setAutoSelectDone] = useState(
21
+ () => getInitialTraceId() !== null,
22
+ );
23
+
24
+ const handleSelectTrace = (traceId: string) => {
25
+ setSelectedTraceId(traceId);
26
+
27
+ // Update URL without navigation
28
+ const url = new URL(window.location.href);
29
+ url.searchParams.set("traceId", traceId);
30
+ window.history.replaceState({}, "", url);
31
+ };
32
+
33
+ const handleTracesLoaded = useCallback(
34
+ (traces: Trace[]) => {
35
+ // Auto-select most recent trace if none selected yet
36
+ const mostRecent = traces[0];
37
+ if (!autoSelectDone && mostRecent) {
38
+ setSelectedTraceId(mostRecent.trace_id);
39
+ setAutoSelectDone(true);
40
+
41
+ // Update URL
42
+ const url = new URL(window.location.href);
43
+ url.searchParams.set("traceId", mostRecent.trace_id);
44
+ window.history.replaceState({}, "", url);
45
+ }
46
+ },
47
+ [autoSelectDone],
48
+ );
49
+
50
+ return (
51
+ <div className="h-screen flex flex-col">
52
+ {/* Header */}
53
+ <div className="border-b p-4 shrink-0">
54
+ <Button variant="outline" asChild>
55
+ <a href="/">Back to Traces</a>
56
+ </Button>
57
+ <h1 className="text-xl font-bold mt-2">Session</h1>
58
+ <div className="text-sm text-muted-foreground font-mono truncate">
59
+ {sessionId}
60
+ </div>
61
+ </div>
62
+
63
+ {/* Split pane container */}
64
+ <div className="flex flex-1 overflow-hidden">
65
+ {/* Left pane - trace list */}
66
+ <div className="w-80 border-r overflow-y-auto shrink-0">
67
+ <SessionTraceList
68
+ sessionId={sessionId}
69
+ selectedTraceId={selectedTraceId}
70
+ onSelectTrace={handleSelectTrace}
71
+ onTracesLoaded={handleTracesLoaded}
72
+ />
73
+ </div>
74
+
75
+ {/* Right pane - trace detail */}
76
+ <div className="flex-1 overflow-y-auto">
77
+ {selectedTraceId ? (
78
+ <TraceDetailContent traceId={selectedTraceId} compact />
79
+ ) : (
80
+ <div className="p-8 text-muted-foreground">
81
+ Select a trace to view details
82
+ </div>
83
+ )}
84
+ </div>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,19 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import { TraceDetailContent } from "../components/TraceDetailContent";
3
+
4
+ interface TraceDetailProps {
5
+ traceId: string;
6
+ }
7
+
8
+ export function TraceDetail({ traceId }: TraceDetailProps) {
9
+ return (
10
+ <div className="container mx-auto">
11
+ <div className="p-8 pb-0">
12
+ <Button variant="outline" asChild className="mb-4">
13
+ <a href="/">Back to Traces</a>
14
+ </Button>
15
+ </div>
16
+ <TraceDetailContent traceId={traceId} />
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,153 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "@/components/ui/card";
10
+ import { Input } from "@/components/ui/input";
11
+ import type { Trace } from "../types";
12
+
13
+ function formatDuration(startNano: number, endNano: number): string {
14
+ const ms = (endNano - startNano) / 1_000_000;
15
+ if (ms < 1000) return `${ms.toFixed(2)}ms`;
16
+ return `${(ms / 1000).toFixed(2)}s`;
17
+ }
18
+
19
+ function formatTimestamp(nanoseconds: number): string {
20
+ return new Date(nanoseconds / 1_000_000).toLocaleString();
21
+ }
22
+
23
+ function getInitialSessionId(): string {
24
+ const params = new URLSearchParams(window.location.search);
25
+ return params.get("sessionId") || "";
26
+ }
27
+
28
+ export function TraceList() {
29
+ const [traces, setTraces] = useState<Trace[]>([]);
30
+ const [loading, setLoading] = useState(true);
31
+ const [error, setError] = useState<string | null>(null);
32
+ const [sessionId, setSessionId] = useState(getInitialSessionId);
33
+ const [debouncedSessionId, setDebouncedSessionId] =
34
+ useState(getInitialSessionId);
35
+
36
+ // Debounce session ID input
37
+ useEffect(() => {
38
+ const timer = setTimeout(() => setDebouncedSessionId(sessionId), 300);
39
+ return () => clearTimeout(timer);
40
+ }, [sessionId]);
41
+
42
+ // Update URL when debounced session ID changes
43
+ useEffect(() => {
44
+ const url = new URL(window.location.href);
45
+ if (debouncedSessionId) {
46
+ url.searchParams.set("sessionId", debouncedSessionId);
47
+ } else {
48
+ url.searchParams.delete("sessionId");
49
+ }
50
+ window.history.replaceState({}, "", url);
51
+ }, [debouncedSessionId]);
52
+
53
+ // Fetch traces function
54
+ const fetchTraces = useCallback(() => {
55
+ setLoading(true);
56
+ const params = new URLSearchParams();
57
+ if (debouncedSessionId) params.set("sessionId", debouncedSessionId);
58
+ fetch(`/api/traces?${params}`)
59
+ .then((res) => {
60
+ if (!res.ok) throw new Error("Failed to fetch traces");
61
+ return res.json();
62
+ })
63
+ .then((data) => {
64
+ setTraces(data);
65
+ setLoading(false);
66
+ })
67
+ .catch((err) => {
68
+ setError(err.message);
69
+ setLoading(false);
70
+ });
71
+ }, [debouncedSessionId]);
72
+
73
+ // Fetch traces on mount and when session ID changes
74
+ useEffect(() => {
75
+ fetchTraces();
76
+ }, [fetchTraces]);
77
+
78
+ if (loading) {
79
+ return (
80
+ <div className="container mx-auto p-8">
81
+ <div className="text-muted-foreground">Loading traces...</div>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ if (error) {
87
+ return (
88
+ <div className="container mx-auto p-8">
89
+ <div className="text-red-500">Error: {error}</div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ return (
95
+ <div className="container mx-auto p-8">
96
+ <h1 className="text-2xl font-bold mb-6">Traces</h1>
97
+
98
+ <div className="flex gap-2 mb-4">
99
+ <Input
100
+ placeholder="Filter by session ID..."
101
+ value={sessionId}
102
+ onChange={(e) => setSessionId(e.target.value)}
103
+ className="max-w-sm"
104
+ />
105
+ <Button variant="outline" onClick={fetchTraces} disabled={loading}>
106
+ Refresh
107
+ </Button>
108
+ </div>
109
+
110
+ {traces.length === 0 ? (
111
+ <div className="text-muted-foreground">No traces found</div>
112
+ ) : (
113
+ <div className="space-y-2">
114
+ {traces.map((trace) => (
115
+ <a
116
+ key={trace.trace_id}
117
+ href={`/trace/${trace.trace_id}`}
118
+ className="block"
119
+ >
120
+ <Card className="hover:bg-muted/50 transition-colors cursor-pointer">
121
+ <CardHeader className="py-3">
122
+ <div className="flex items-center justify-between">
123
+ <CardTitle className="text-base font-medium">
124
+ {trace.first_span_name || "Unknown"}
125
+ </CardTitle>
126
+ <span className="text-sm text-muted-foreground">
127
+ {formatDuration(
128
+ trace.start_time_unix_nano,
129
+ trace.end_time_unix_nano,
130
+ )}
131
+ </span>
132
+ </div>
133
+ <CardDescription className="flex items-center gap-4">
134
+ <span>{trace.service_name || "Unknown service"}</span>
135
+ <span>{trace.span_count} spans</span>
136
+ </CardDescription>
137
+ </CardHeader>
138
+ <CardContent className="py-2 pt-0">
139
+ <div className="text-xs text-muted-foreground font-mono">
140
+ {trace.trace_id}
141
+ </div>
142
+ <div className="text-xs text-muted-foreground">
143
+ {formatTimestamp(trace.start_time_unix_nano)}
144
+ </div>
145
+ </CardContent>
146
+ </Card>
147
+ </a>
148
+ ))}
149
+ </div>
150
+ )}
151
+ </div>
152
+ );
153
+ }
package/src/server.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { createOtlpServer } from "@townco/otlp-server/http";
2
+ import { serve } from "bun";
3
+ import { DebuggerDb } from "./db";
4
+ import index from "./index.html";
5
+
6
+ export const DEFAULT_DEBUGGER_PORT = 4000;
7
+ export const DEFAULT_OTLP_PORT = 4318;
8
+
9
+ export interface DebuggerServerOptions {
10
+ port?: number;
11
+ otlpPort?: number;
12
+ dbPath: string;
13
+ }
14
+
15
+ export interface DebuggerServerResult {
16
+ server: ReturnType<typeof serve>;
17
+ otlpServer: ReturnType<typeof serve>;
18
+ stop: () => void;
19
+ }
20
+
21
+ export function startDebuggerServer(
22
+ options: DebuggerServerOptions,
23
+ ): DebuggerServerResult {
24
+ const {
25
+ port = DEFAULT_DEBUGGER_PORT,
26
+ otlpPort = DEFAULT_OTLP_PORT,
27
+ dbPath,
28
+ } = options;
29
+
30
+ // Start OTLP server (initializes database internally)
31
+ const otlpApp = createOtlpServer({ dbPath });
32
+ const otlpServer = serve({
33
+ fetch: otlpApp.fetch,
34
+ port: otlpPort,
35
+ });
36
+
37
+ // Create debugger database connection for reading
38
+ const db = new DebuggerDb(dbPath);
39
+
40
+ // Start debugger UI server
41
+ const server = serve({
42
+ port,
43
+ routes: {
44
+ "/api/traces": {
45
+ GET(req) {
46
+ const url = new URL(req.url);
47
+ const limit = Number.parseInt(url.searchParams.get("limit") || "50");
48
+ const offset = Number.parseInt(url.searchParams.get("offset") || "0");
49
+ const sessionId = url.searchParams.get("sessionId") || undefined;
50
+ const traces = db.listTraces(limit, offset, sessionId);
51
+ return Response.json(traces);
52
+ },
53
+ },
54
+
55
+ "/api/traces/:traceId": {
56
+ GET(req) {
57
+ const traceId = req.params.traceId;
58
+ const data = db.getTraceById(traceId);
59
+ if (!data.trace) {
60
+ return Response.json({ error: "Trace not found" }, { status: 404 });
61
+ }
62
+ return Response.json(data);
63
+ },
64
+ },
65
+
66
+ // Serve index.html for all unmatched routes (SPA routing)
67
+ "/*": index,
68
+ },
69
+
70
+ development: process.env.NODE_ENV !== "production" && {
71
+ hmr: true,
72
+ console: true,
73
+ },
74
+ });
75
+
76
+ const stop = () => {
77
+ server.stop();
78
+ otlpServer.stop();
79
+ };
80
+
81
+ return { server, otlpServer, stop };
82
+ }
package/src/types.ts ADDED
@@ -0,0 +1,48 @@
1
+ export interface Trace {
2
+ trace_id: string;
3
+ service_name: string | null;
4
+ first_span_name: string | null;
5
+ start_time_unix_nano: number;
6
+ end_time_unix_nano: number;
7
+ span_count: number;
8
+ created_at: string;
9
+ }
10
+
11
+ export interface Span {
12
+ span_id: string;
13
+ trace_id: string;
14
+ parent_span_id: string | null;
15
+ name: string;
16
+ kind: number;
17
+ start_time_unix_nano: number;
18
+ end_time_unix_nano: number;
19
+ status_code: number;
20
+ status_message: string | null;
21
+ attributes: string | null;
22
+ events: string | null;
23
+ resource_attributes: string | null;
24
+ }
25
+
26
+ export interface Log {
27
+ id: number;
28
+ trace_id: string | null;
29
+ span_id: string | null;
30
+ timestamp_unix_nano: number;
31
+ severity_number: number;
32
+ severity_text: string | null;
33
+ body: string | null;
34
+ attributes: string | null;
35
+ resource_attributes: string | null;
36
+ }
37
+
38
+ export interface SpanNode extends Span {
39
+ children: SpanNode[];
40
+ depth: number;
41
+ durationMs: number;
42
+ }
43
+
44
+ export interface TraceDetail {
45
+ trace: Trace | null;
46
+ spans: Span[];
47
+ logs: Log[];
48
+ }