Skip to content

Commit dd7503f

Browse files
authored
feat: add error state for trace and spans load (#941)
* feat: add error state for trace and spans load * fix: fix comment
1 parent 73d3198 commit dd7503f

File tree

5 files changed

+129
-57
lines changed

5 files changed

+129
-57
lines changed

frontend/app/api/projects/[projectId]/traces/[traceId]/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ export async function GET(
2121
return NextResponse.json(trace);
2222
} catch (error) {
2323
console.error("Error fetching trace:", error);
24-
return NextResponse.json({ error: "Failed to fetch trace" }, { status: 500 });
24+
return NextResponse.json(
25+
{
26+
error: error instanceof Error ? error.message : "Failed to fetch trace",
27+
},
28+
{ status: 500 }
29+
);
2530
}
2631
}
2732

frontend/components/traces/trace-view/header.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,13 @@ const Header = ({ handleClose }: HeaderProps) => {
6363
<Button variant={"ghost"} className="px-0" onClick={handleClose}>
6464
<ChevronsRight />
6565
</Button>
66-
<Link passHref href={`/project/${projectId}/traces/${trace?.id}?${fullScreenParams.toString()}`}>
67-
<Button variant="ghost" className="px-0 mr-1">
68-
<Expand className="w-4 h-4" size={16} />
69-
</Button>
70-
</Link>
66+
{trace && (
67+
<Link passHref href={`/project/${projectId}/traces/${trace?.id}?${fullScreenParams.toString()}`}>
68+
<Button variant="ghost" className="px-0 mr-1">
69+
<Expand className="w-4 h-4" size={16} />
70+
</Button>
71+
</Link>
72+
)}
7173
<TooltipProvider delayDuration={0}>
7274
<Tooltip>
7375
<TooltipTrigger asChild>
@@ -88,7 +90,7 @@ const Header = ({ handleClose }: HeaderProps) => {
8890
<>
8991
<Tooltip>
9092
<TooltipTrigger asChild>
91-
<Button onClick={navigateDown} className="hover:bg-secondary px-1.5" variant="ghost">
93+
<Button disabled={!trace} onClick={navigateDown} className="hover:bg-secondary px-1.5" variant="ghost">
9294
<ChevronDown className="w-4 h-4" />
9395
</Button>
9496
</TooltipTrigger>
@@ -104,7 +106,7 @@ const Header = ({ handleClose }: HeaderProps) => {
104106
</Tooltip>
105107
<Tooltip>
106108
<TooltipTrigger asChild>
107-
<Button onClick={navigateUp} className="hover:bg-secondary px-1.5" variant="ghost">
109+
<Button disabled={!trace} onClick={navigateUp} className="hover:bg-secondary px-1.5" variant="ghost">
108110
<ChevronUp className="w-4 h-4" />
109111
</Button>
110112
</TooltipTrigger>
@@ -123,6 +125,7 @@ const Header = ({ handleClose }: HeaderProps) => {
123125
<Tooltip>
124126
<TooltipTrigger asChild>
125127
<Button
128+
disabled={!trace}
126129
className="hover:bg-secondary px-1.5"
127130
variant="ghost"
128131
onClick={() => setBrowserSession(!browserSession)}

frontend/components/traces/trace-view/index.tsx

Lines changed: 103 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { get } from "lodash";
2-
import { ChartNoAxesGantt, ListFilter, Minus, Plus, Search, Sparkles } from "lucide-react";
2+
import { AlertTriangle, ChartNoAxesGantt, ListFilter, Minus, Plus, Search, Sparkles } from "lucide-react";
33
import { useParams, usePathname, useRouter, useSearchParams } from "next/navigation";
44
import React, { useCallback, useEffect, useMemo } from "react";
55

@@ -27,7 +27,6 @@ import { StatefulFilter, StatefulFilterList } from "@/components/ui/datatable-fi
2727
import { useFiltersContextProvider } from "@/components/ui/datatable-filter/context";
2828
import { DatatableFilter } from "@/components/ui/datatable-filter/utils";
2929
import { Skeleton } from "@/components/ui/skeleton";
30-
import { useToast } from "@/lib/hooks/use-toast";
3130
import { SpanType } from "@/lib/traces/types";
3231
import { cn } from "@/lib/utils.ts";
3332

@@ -52,7 +51,6 @@ const PureTraceView = ({ traceId, spanId, onClose, propsTrace }: TraceViewProps)
5251
const router = useRouter();
5352
const pathName = usePathname();
5453
const { projectId } = useParams();
55-
const { toast } = useToast();
5654

5755
// Data states
5856
const {
@@ -66,6 +64,10 @@ const PureTraceView = ({ traceId, spanId, onClose, propsTrace }: TraceViewProps)
6664
isTraceLoading,
6765
setIsTraceLoading,
6866
setIsSpansLoading,
67+
traceError,
68+
setTraceError,
69+
spansError,
70+
setSpansError,
6971
} = useTraceViewStoreContext((state) => ({
7072
selectedSpan: state.selectedSpan,
7173
setSelectedSpan: state.setSelectedSpan,
@@ -77,6 +79,10 @@ const PureTraceView = ({ traceId, spanId, onClose, propsTrace }: TraceViewProps)
7779
isSpansLoading: state.isSpansLoading,
7880
setIsSpansLoading: state.setIsSpansLoading,
7981
setIsTraceLoading: state.setIsTraceLoading,
82+
traceError: state.traceError,
83+
setTraceError: state.setTraceError,
84+
spansError: state.spansError,
85+
setSpansError: state.setSpansError,
8086
}));
8187

8288
// UI states
@@ -135,31 +141,31 @@ const PureTraceView = ({ traceId, spanId, onClose, propsTrace }: TraceViewProps)
135141
const handleFetchTrace = useCallback(async () => {
136142
try {
137143
setIsTraceLoading(true);
144+
setTraceError(undefined);
145+
138146
if (propsTrace) {
139147
setTrace(propsTrace);
140148
} else {
141149
const response = await fetch(`/api/projects/${projectId}/traces/${traceId}`);
150+
142151
if (!response.ok) {
143-
toast({
144-
variant: "destructive",
145-
title: "Error",
146-
description: "Failed to load trace. Please try again.",
147-
});
152+
const errorData = await response.json().catch(() => ({ error: "Unknown error" }));
153+
const errorMessage = errorData.error || "Failed to load trace";
154+
155+
setTraceError(errorMessage);
148156
return;
149157
}
158+
150159
const traceData = (await response.json()) as TraceViewTrace;
151160
setTrace(traceData);
152161
}
153162
} catch (e) {
154-
toast({
155-
variant: "destructive",
156-
title: "Error",
157-
description: "Failed to load trace. Please try again.",
158-
});
163+
const errorMessage = e instanceof Error ? e.message : "Failed to load trace. Please try again.";
164+
setTraceError(errorMessage);
159165
} finally {
160166
setIsTraceLoading(false);
161167
}
162-
}, [projectId, propsTrace, setBrowserSession, setIsTraceLoading, setTrace, toast, traceId]);
168+
}, [projectId, propsTrace, setIsTraceLoading, setTrace, setTraceError, traceId]);
163169

164170
const handleSpanSelect = useCallback(
165171
(span?: TraceViewSpan) => {
@@ -186,6 +192,7 @@ const PureTraceView = ({ traceId, spanId, onClose, propsTrace }: TraceViewProps)
186192
async (search: string, searchIn: string[], filters: DatatableFilter[]) => {
187193
try {
188194
setIsSpansLoading(true);
195+
setSpansError(undefined);
189196

190197
const params = new URLSearchParams();
191198
if (search) {
@@ -201,8 +208,16 @@ const PureTraceView = ({ traceId, spanId, onClose, propsTrace }: TraceViewProps)
201208

202209
const url = `/api/projects/${projectId}/traces/${traceId}/spans?${params.toString()}`;
203210
const response = await fetch(url);
204-
const results = (await response.json()) as TraceViewSpan[];
205211

212+
if (!response.ok) {
213+
const errorData = await response.json().catch(() => ({ error: "Unknown error" }));
214+
const errorMessage = errorData.error || "Failed to load spans";
215+
216+
setSpansError(errorMessage);
217+
return;
218+
}
219+
220+
const results = (await response.json()) as TraceViewSpan[];
206221
const spans = search || filters?.length > 0 ? results : enrichSpansWithPending(results);
207222

208223
setSpans(spans);
@@ -219,19 +234,25 @@ const PureTraceView = ({ traceId, spanId, onClose, propsTrace }: TraceViewProps)
219234
setSelectedSpan(undefined);
220235
}
221236
} catch (e) {
237+
const errorMessage = e instanceof Error ? e.message : "Failed to load spans";
238+
setSpansError(errorMessage);
239+
222240
console.error(e);
223241
} finally {
224242
setIsSpansLoading(false);
225243
}
226244
},
227245
[
228-
trace,
229246
setIsSpansLoading,
230-
search,
247+
setSpansError,
248+
setSearch,
249+
setSearchEnabled,
231250
projectId,
232251
traceId,
233252
setSpans,
234-
setSearch,
253+
hasBrowserSession,
254+
setHasBrowserSession,
255+
setBrowserSession,
235256
spanId,
236257
searchParams,
237258
spanPath,
@@ -278,7 +299,7 @@ const PureTraceView = ({ traceId, spanId, onClose, propsTrace }: TraceViewProps)
278299
setSearchEnabled(!searchEnabled);
279300
}, [fetchSpans, searchEnabled, setSearch, setSearchEnabled, search]);
280301

281-
const isLoading = !trace || (isSpansLoading && isTraceLoading);
302+
const isLoading = isTraceLoading || (isSpansLoading && !traceError && !spansError);
282303

283304
useEffect(() => {
284305
if (!isSpansLoading) {
@@ -304,8 +325,20 @@ const PureTraceView = ({ traceId, spanId, onClose, propsTrace }: TraceViewProps)
304325
setBrowserSession(false);
305326
setSearch("");
306327
setSearchEnabled(false);
328+
setTraceError(undefined);
329+
setSpansError(undefined);
307330
};
308-
}, [traceId, projectId, filters, setSpans, setBrowserSession, setSearch, setSearchEnabled]);
331+
}, [
332+
traceId,
333+
projectId,
334+
filters,
335+
setSpans,
336+
setBrowserSession,
337+
setSearch,
338+
setSearchEnabled,
339+
setTraceError,
340+
setSpansError,
341+
]);
309342

310343
useEffect(() => {
311344
if (!traceId || !projectId) {
@@ -353,6 +386,21 @@ const PureTraceView = ({ traceId, spanId, onClose, propsTrace }: TraceViewProps)
353386
);
354387
}
355388

389+
if (traceError) {
390+
return (
391+
<div className="flex flex-col h-full w-full overflow-hidden">
392+
<Header handleClose={handleClose} />
393+
<div className="flex flex-col items-center justify-center flex-1 p-8 text-center">
394+
<div className="max-w-md mx-auto">
395+
<AlertTriangle className="w-12 h-12 text-destructive mx-auto mb-4" />
396+
<h3 className="text-lg font-semibold text-destructive mb-4">Error Loading Trace</h3>
397+
<p className="text-sm text-muted-foreground">{traceError}</p>
398+
</div>
399+
</div>
400+
</div>
401+
);
402+
}
403+
356404
return (
357405
<ScrollContextProvider>
358406
<div className="flex flex-col h-full w-full overflow-hidden">
@@ -430,35 +478,43 @@ const PureTraceView = ({ traceId, spanId, onClose, propsTrace }: TraceViewProps)
430478
className="rounded-none w-full border-0 border-b ring-0 bg-background"
431479
/>
432480
)}
433-
<>
434-
{tab === "chat" && (
435-
<Chat
436-
trace={trace}
437-
onSetSpanId={(spanId) => {
438-
const span = spans.find((span) => span.spanId === spanId);
439-
if (span) {
440-
handleSpanSelect(span);
441-
}
442-
}}
443-
/>
444-
)}
481+
{spansError ? (
482+
<div className="flex flex-col items-center justify-center flex-1 p-4 text-center">
483+
<AlertTriangle className="w-8 h-8 text-destructive mb-3" />
484+
<h4 className="text-sm font-semibold text-destructive mb-2">Error Loading Spans</h4>
485+
<p className="text-xs text-muted-foreground">{spansError}</p>
486+
</div>
487+
) : (
445488
<>
446-
{tab === "timeline" && <Timeline />}
447-
{tab === "tree" &&
448-
(isSpansLoading ? (
449-
<div className="flex flex-col gap-2 p-2 pb-4 w-full min-w-full">
450-
<Skeleton className="h-8 w-full" />
451-
<Skeleton className="h-8 w-full" />
452-
<Skeleton className="h-8 w-full" />
453-
</div>
454-
) : (
455-
<div className="flex flex-1 overflow-hidden relative">
456-
<Tree onSpanSelect={handleSpanSelect} />
457-
<Minimap onSpanSelect={handleSpanSelect} />
458-
</div>
459-
))}
489+
{tab === "chat" && trace && (
490+
<Chat
491+
trace={trace}
492+
onSetSpanId={(spanId) => {
493+
const span = spans.find((span) => span.spanId === spanId);
494+
if (span) {
495+
handleSpanSelect(span);
496+
}
497+
}}
498+
/>
499+
)}
500+
<>
501+
{tab === "timeline" && <Timeline />}
502+
{tab === "tree" &&
503+
(isSpansLoading ? (
504+
<div className="flex flex-col gap-2 p-2 pb-4 w-full min-w-full">
505+
<Skeleton className="h-8 w-full" />
506+
<Skeleton className="h-8 w-full" />
507+
<Skeleton className="h-8 w-full" />
508+
</div>
509+
) : (
510+
<div className="flex flex-1 overflow-hidden relative">
511+
<Tree onSpanSelect={handleSpanSelect} />
512+
<Minimap onSpanSelect={handleSpanSelect} />
513+
</div>
514+
))}
515+
</>
460516
</>
461-
</>
517+
)}
462518
<div
463519
className="absolute top-0 right-0 h-full cursor-col-resize z-50 group w-2"
464520
onMouseDown={handleResizeTreeView}

frontend/components/traces/trace-view/trace-view-store.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ export type TraceViewTrace = {
5656
interface TraceViewStoreState {
5757
trace?: TraceViewTrace;
5858
isTraceLoading: boolean;
59+
traceError?: string;
5960
spans: TraceViewSpan[];
6061
spanPath: string[] | null;
6162
isSpansLoading: boolean;
63+
spansError?: string;
6264
searchEnabled: boolean;
6365
selectedSpan?: TraceViewSpan;
6466
browserSession: boolean;
@@ -73,7 +75,9 @@ interface TraceViewStoreState {
7375

7476
interface TraceViewStoreActions {
7577
setTrace: (trace?: TraceViewTrace | ((prevTrace?: TraceViewTrace) => TraceViewTrace | undefined)) => void;
78+
setTraceError: (error?: string) => void;
7679
setSpans: (spans: TraceViewSpan[] | ((prevSpans: TraceViewSpan[]) => TraceViewSpan[])) => void;
80+
setSpansError: (error?: string) => void;
7781
setIsTraceLoading: (isTraceLoading: boolean) => void;
7882
setIsSpansLoading: (isSpansLoading: boolean) => void;
7983
setSelectedSpan: (span?: TraceViewSpan) => void;
@@ -106,8 +110,10 @@ const createTraceViewStore = () =>
106110
(set, get) => ({
107111
trace: undefined,
108112
isTraceLoading: false,
113+
traceError: undefined,
109114
spans: [],
110115
isSpansLoading: false,
116+
spansError: undefined,
111117
selectedSpan: undefined,
112118
browserSession: false,
113119
sessionTime: undefined,
@@ -130,6 +136,8 @@ const createTraceViewStore = () =>
130136
set({ trace });
131137
}
132138
},
139+
setTraceError: (traceError) => set({ traceError }),
140+
setSpansError: (spansError) => set({ spansError }),
133141
updateTraceVisibility: (visibility) => {
134142
get().setTrace((trace) => {
135143
if (trace) {

frontend/lib/actions/trace/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export async function updateTraceVisibility(params: z.infer<typeof UpdateTraceVi
165165
});
166166
}
167167

168-
export async function getTrace(input: z.infer<typeof GetTraceSchema>): Promise<TraceViewTrace> {
168+
export async function getTrace(input: z.infer<typeof GetTraceSchema>): Promise<TraceViewTrace | undefined> {
169169
const { traceId, projectId } = GetTraceSchema.parse(input);
170170

171171
const sharedTrace = await db.query.sharedTraces.findFirst({
@@ -198,7 +198,7 @@ export async function getTrace(input: z.infer<typeof GetTraceSchema>): Promise<T
198198
});
199199

200200
if (!trace) {
201-
throw new Error("Trace not found.");
201+
return undefined;
202202
}
203203

204204
return {

0 commit comments

Comments
 (0)