Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export const SessionDetailView: FC<SessionDetailViewProps> = ({
sessionId={sessionId}
parentBreadcrumbs={traceParentBreadcrumbs}
traceSummary={trajectories.find(({ trace }) => trace.id === traceId)?.trace}
isTraceSummaryLoading={explorer.isSpansFetching && !explorer.spansLoaded}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
>
{(trace) => (
<TraceSpanAccordions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { IntakeAccordion } from '@nemo/common/src/components/IntakeAccordion';
import { useGetTrace } from '@nemo/sdk/generated/platform/api';
import type { Trace } from '@nemo/sdk/generated/platform/schema';
import { Stack, StatusMessage, Text } from '@nvidia/foundations-react-core';
import { Banner, Stack, StatusMessage, Text } from '@nvidia/foundations-react-core';
import { KeyValueRows } from '@studio/components/IntakeDetail/IntakeComponents/KeyValueRows';
import { RawJsonDebug } from '@studio/components/IntakeDetail/IntakeComponents/RawJsonDebug';
import {
Expand All @@ -29,6 +29,7 @@ interface TraceDetailViewProps {
traceId: string;
sessionId: string;
traceSummary?: Trace;
isTraceSummaryLoading?: boolean;
parentBreadcrumbs: BreadcrumbsItemProps[];
children: (trace: Trace) => ReactNode;
}
Expand All @@ -39,6 +40,7 @@ export const TraceDetailView: FC<TraceDetailViewProps> = ({
traceId,
sessionId,
traceSummary,
isTraceSummaryLoading = false,
parentBreadcrumbs,
children,
}) => {
Expand All @@ -53,6 +55,7 @@ export const TraceDetailView: FC<TraceDetailViewProps> = ({
// 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;
Expand All @@ -70,7 +73,11 @@ export const TraceDetailView: FC<TraceDetailViewProps> = ({
setBreadcrumbs([...parentBreadcrumbs, { slotLabel: `Trace ${traceBreadcrumbLabel}` }]);
}, [parentBreadcrumbs, setBreadcrumbs, traceBreadcrumbLabel]);

if (error?.response?.status === 404) {
if (!resolvedTrace && (isLoading || (traceNotFound && isTraceSummaryLoading))) {
return <Loading description="Loading trace..." />;
}

if (traceNotFound && !resolvedTrace) {
return (
<NotFound
subheader="Trace Not Found"
Expand All @@ -79,10 +86,6 @@ export const TraceDetailView: FC<TraceDetailViewProps> = ({
);
}

if (isLoading && !resolvedTrace) {
return <Loading description="Loading trace..." />;
}

if (error && !resolvedTrace) {
return (
<StatusMessage
Expand Down Expand Up @@ -110,6 +113,11 @@ export const TraceDetailView: FC<TraceDetailViewProps> = ({

return (
<>
{traceNotFound && (
<Banner kind="inline" status="info">
Trace details are still arriving. The activity received so far is shown below.
</Banner>
)}
{children(resolvedTrace)}
<IntakeAccordion
variant="section"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
// SPDX-License-Identifier: Apache-2.0

import { useGetSession, useListSpans, useListTraces } from '@nemo/sdk/generated/platform/api';
import type { Span } from '@nemo/sdk/generated/platform/schema';
import { type Span, SpanStatus, type Trace } from '@nemo/sdk/generated/platform/schema';
import { type SessionExplorerData } from '@studio/components/IntakeDetail/TraceSpanAccordions';
import { buildSpanTree, type SessionTrajectory } from '@studio/util/intakeTelemetry';
import {
buildSpanTree,
compareSpansByStartedAt,
type SessionTrajectory,
} from '@studio/util/intakeTelemetry';
import { useMemo } from 'react';

const SESSION_TRACES_PAGE_SIZE = 1000;
Expand Down Expand Up @@ -57,11 +61,33 @@ export function useSessionTrajectories(workspace: string, sessionId: string) {
traceSpans.push(span);
groupedSpans.set(span.trace_id, traceSpans);
}
return traces.map((trace) => {

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<SessionExplorerData>(
() => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
Loading