From 5c2c83574cdf2d38cb5c1b09290f1958c70c00ac Mon Sep 17 00:00:00 2001 From: Brian Newsom Date: Fri, 7 Aug 2026 13:52:05 -0600 Subject: [PATCH 1/2] fix(studio): render spans before trace details arrive Signed-off-by: Brian Newsom --- .../IntakeDetail/SessionDetailView.tsx | 1 + .../IntakeDetail/TraceDetailView.tsx | 20 +++-- .../IntakeDetail/useSessionTrajectories.ts | 34 ++++++- .../IntakeSessionDetailRoute/index.test.tsx | 90 +++++++++++++++++++ 4 files changed, 135 insertions(+), 10 deletions(-) diff --git a/web/packages/studio/src/components/IntakeDetail/SessionDetailView.tsx b/web/packages/studio/src/components/IntakeDetail/SessionDetailView.tsx index a4d769cba2..c01f58ea75 100644 --- a/web/packages/studio/src/components/IntakeDetail/SessionDetailView.tsx +++ b/web/packages/studio/src/components/IntakeDetail/SessionDetailView.tsx @@ -180,6 +180,7 @@ export const SessionDetailView: FC = ({ sessionId={sessionId} parentBreadcrumbs={traceParentBreadcrumbs} traceSummary={trajectories.find(({ trace }) => trace.id === traceId)?.trace} + isTraceSummaryLoading={explorer.isSpansFetching && !explorer.spansLoaded} > {(trace) => ( ReactNode; } @@ -39,6 +40,7 @@ export const TraceDetailView: FC = ({ traceId, sessionId, traceSummary, + isTraceSummaryLoading = false, parentBreadcrumbs, children, }) => { @@ -53,6 +55,7 @@ export const TraceDetailView: FC = ({ // The session already owns enough trace data to keep its explorer mounted // while the selected trace's full payload hydrates in the background. const resolvedTrace = trace ?? traceSummary; + const traceNotFound = error?.response?.status === 404; const { setBreadcrumbs } = useBreadcrumbs(); const traceBreadcrumbLabel = resolvedTrace ? getTraceDisplayName(resolvedTrace) : traceId; @@ -70,7 +73,11 @@ export const TraceDetailView: FC = ({ setBreadcrumbs([...parentBreadcrumbs, { slotLabel: `Trace ${traceBreadcrumbLabel}` }]); }, [parentBreadcrumbs, setBreadcrumbs, traceBreadcrumbLabel]); - if (error?.response?.status === 404) { + if (!resolvedTrace && (isLoading || (traceNotFound && isTraceSummaryLoading))) { + return ; + } + + if (traceNotFound && !resolvedTrace) { return ( = ({ ); } - if (isLoading && !resolvedTrace) { - return ; - } - if (error && !resolvedTrace) { return ( = ({ return ( <> + {traceNotFound && ( + + Trace details are still arriving. The activity received so far is shown below. + + )} {children(resolvedTrace)} { + + const traceSummaries: Trace[] = [...traces]; + const knownTraceIds = new Set(traces.map((trace) => trace.id)); + for (const [traceId, spans] of groupedSpans) { + if (knownTraceIds.has(traceId)) continue; + const earliestSpan = spans.reduce((earliest, span) => + compareSpansByStartedAt(span, earliest) < 0 ? span : earliest + ); + traceSummaries.push({ + id: traceId, + session_id: sessionId, + workspace, + started_at: earliestSpan.started_at, + status: SpanStatus.unknown, + span_count: spans.length, + }); + } + + traceSummaries.sort((a, b) => { + const startedAtDifference = Date.parse(a.started_at) - Date.parse(b.started_at); + return startedAtDifference || a.id.localeCompare(b.id); + }); + return traceSummaries.map((trace) => { const spans = groupedSpans.get(trace.id) ?? []; return { trace, spans, spanTree: buildSpanTree(spans) }; }); - }, [sessionSpansResponse?.data, traces]); + }, [sessionId, sessionSpansResponse?.data, traces, workspace]); const explorer = useMemo( () => ({ diff --git a/web/packages/studio/src/routes/IntakeSessionDetailRoute/index.test.tsx b/web/packages/studio/src/routes/IntakeSessionDetailRoute/index.test.tsx index e39f0ab95d..1618ce58be 100644 --- a/web/packages/studio/src/routes/IntakeSessionDetailRoute/index.test.tsx +++ b/web/packages/studio/src/routes/IntakeSessionDetailRoute/index.test.tsx @@ -387,6 +387,96 @@ describe('IntakeSessionDetailRoute', () => { expect((await screen.findAllByText('Outside page span')).length).toBeGreaterThan(0); }); + it('renders received spans while trace details are still arriving', async () => { + const traceId = 'trace-still-arriving'; + const receivedSpan = { + ...mockSpanById('span-llm-001')!, + span_id: 'span-received-before-trace', + parent_span_id: 'span-root-not-received', + trace_id: traceId, + name: 'Generate response from received activity', + }; + + server.use( + http.get('*/apis/intake/v2/workspaces/:workspace/traces', () => + HttpResponse.json({ + ...mockTracesPage, + data: [], + pagination: { + ...mockTracesPage.pagination, + current_page_size: 0, + total_results: 0, + }, + }) + ), + http.get('*/apis/intake/v2/workspaces/:workspace/traces/:traceId', ({ params }) => + params['traceId'] === traceId + ? new HttpResponse(null, { status: 404 }) + : new HttpResponse(null, { status: 500 }) + ), + http.get('*/apis/intake/v2/workspaces/:workspace/spans', () => + HttpResponse.json({ + ...mockSpansPage, + data: [receivedSpan], + pagination: { + ...mockSpansPage.pagination, + current_page_size: 1, + total_results: 1, + }, + }) + ) + ); + + renderSessionDetail('session-agent-run-001', `?traceId=${traceId}`); + + expect( + await screen.findByText( + 'Trace details are still arriving. The activity received so far is shown below.' + ) + ).toBeInTheDocument(); + expect(screen.queryByText('Trace Not Found')).not.toBeInTheDocument(); + expect( + (await screen.findAllByText('Generate response from received activity')).length + ).toBeGreaterThan(0); + }); + + it('shows not found when neither trace details nor matching spans exist', async () => { + const traceId = 'trace-never-received'; + server.use( + http.get('*/apis/intake/v2/workspaces/:workspace/traces', () => + HttpResponse.json({ + ...mockTracesPage, + data: [], + pagination: { + ...mockTracesPage.pagination, + current_page_size: 0, + total_results: 0, + }, + }) + ), + http.get( + '*/apis/intake/v2/workspaces/:workspace/traces/:traceId', + () => new HttpResponse(null, { status: 404 }) + ), + http.get('*/apis/intake/v2/workspaces/:workspace/spans', () => + HttpResponse.json({ + ...mockSpansPage, + data: [], + pagination: { + ...mockSpansPage.pagination, + current_page_size: 0, + total_results: 0, + }, + }) + ) + ); + + renderSessionDetail('session-agent-run-001', `?traceId=${traceId}`); + + expect(await screen.findByText('Trace Not Found')).toBeInTheDocument(); + expect(screen.queryByText(/Trace details are still arriving/)).not.toBeInTheDocument(); + }); + it('rejects a trace deep link that belongs to another session', async () => { renderSessionDetail('session-agent-run-001', '?traceId=trace-agent-run-002'); From 5b229794c76f5969701dd0e67b98817a5152e886 Mon Sep 17 00:00:00 2001 From: Brian Newsom Date: Fri, 7 Aug 2026 14:09:48 -0600 Subject: [PATCH 2/2] fix(studio): preserve partial trace loading errors Signed-off-by: Brian Newsom --- .../IntakeDetail/SessionDetailView.tsx | 5 +++- .../IntakeDetail/TraceDetailView.tsx | 20 +++++++++++-- .../IntakeSessionDetailRoute/index.test.tsx | 30 +++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/web/packages/studio/src/components/IntakeDetail/SessionDetailView.tsx b/web/packages/studio/src/components/IntakeDetail/SessionDetailView.tsx index c01f58ea75..9b058820b4 100644 --- a/web/packages/studio/src/components/IntakeDetail/SessionDetailView.tsx +++ b/web/packages/studio/src/components/IntakeDetail/SessionDetailView.tsx @@ -180,7 +180,10 @@ export const SessionDetailView: FC = ({ sessionId={sessionId} parentBreadcrumbs={traceParentBreadcrumbs} traceSummary={trajectories.find(({ trace }) => trace.id === traceId)?.trace} - isTraceSummaryLoading={explorer.isSpansFetching && !explorer.spansLoaded} + traceSummaryStatus={ + explorer.spansLoaded ? 'resolved' : explorer.spansError ? 'error' : 'loading' + } + traceSummaryErrorMessage={explorer.spansError?.message} > {(trace) => ( ReactNode; } @@ -40,7 +41,8 @@ export const TraceDetailView: FC = ({ traceId, sessionId, traceSummary, - isTraceSummaryLoading = false, + traceSummaryStatus, + traceSummaryErrorMessage, parentBreadcrumbs, children, }) => { @@ -73,10 +75,22 @@ export const TraceDetailView: FC = ({ setBreadcrumbs([...parentBreadcrumbs, { slotLabel: `Trace ${traceBreadcrumbLabel}` }]); }, [parentBreadcrumbs, setBreadcrumbs, traceBreadcrumbLabel]); - if (!resolvedTrace && (isLoading || (traceNotFound && isTraceSummaryLoading))) { + if (!resolvedTrace && (isLoading || (traceNotFound && traceSummaryStatus === 'loading'))) { return ; } + if (traceNotFound && !resolvedTrace && traceSummaryStatus === 'error') { + return ( + } + slotHeading="Error loading trace activity" + slotSubheading={traceSummaryErrorMessage} + /> + ); + } + if (traceNotFound && !resolvedTrace) { return ( { expect(screen.queryByText(/Trace details are still arriving/)).not.toBeInTheDocument(); }); + it('shows an error when trace details are missing and session spans fail to load', async () => { + const traceId = 'trace-with-failed-activity-request'; + server.use( + http.get('*/apis/intake/v2/workspaces/:workspace/traces', () => + HttpResponse.json({ + ...mockTracesPage, + data: [], + pagination: { + ...mockTracesPage.pagination, + current_page_size: 0, + total_results: 0, + }, + }) + ), + http.get( + '*/apis/intake/v2/workspaces/:workspace/traces/:traceId', + () => new HttpResponse(null, { status: 404 }) + ), + http.get( + '*/apis/intake/v2/workspaces/:workspace/spans', + () => new HttpResponse(null, { status: 500 }) + ) + ); + + renderSessionDetail('session-agent-run-001', `?traceId=${traceId}`); + + expect(await screen.findByText('Error loading trace activity')).toBeInTheDocument(); + expect(screen.queryByText('Trace Not Found')).not.toBeInTheDocument(); + }); + it('rejects a trace deep link that belongs to another session', async () => { renderSessionDetail('session-agent-run-001', '?traceId=trace-agent-run-002');