@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 +21 -0
- package/package.json +38 -0
- package/src/App.tsx +103 -0
- package/src/components/AttributeViewer.tsx +26 -0
- package/src/components/LogList.tsx +72 -0
- package/src/components/SessionTraceList.tsx +116 -0
- package/src/components/SpanTree.tsx +205 -0
- package/src/components/TraceDetailContent.tsx +131 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/select.tsx +187 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/db.ts +60 -0
- package/src/env.d.ts +9 -0
- package/src/frontend.tsx +26 -0
- package/src/index.css +11 -0
- package/src/index.html +13 -0
- package/src/index.ts +20 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +1 -0
- package/src/pages/SessionView.tsx +88 -0
- package/src/pages/TraceDetail.tsx +19 -0
- package/src/pages/TraceList.tsx +153 -0
- package/src/server.ts +82 -0
- package/src/types.ts +48 -0
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
|
+
}
|