Skip to content

Commit c9d82a7

Browse files
committed
fix(webapp): fetch run-scoped trace subtrees for large traces
Large traces capped by row limits could leave child runs with an empty trace view when their span fell outside the first time-ordered slice. Fetch the anchor span subtree for the dashboard and trace API, surface truncation in the UI, and document the run-scoped response shape.
1 parent b1987dc commit c9d82a7

12 files changed

Lines changed: 1383 additions & 312 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Fix run-scoped trace subtree fetching so ancestor spans are loaded regardless of the anchor run's time window. Ancestors start before the anchor run and are fetched by explicit span IDs, so applying the anchor's `startCreatedAt` filter wrongly excluded them — which meant cancellation/error overrides from an ancestor never propagated down to the anchor subtree (e.g. a child span stayed PARTIAL when its parent run was cancelled). Ancestor fetches now skip the time window.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Fix empty trace views for child and nested runs in very large traces. The dashboard and retrieve-trace API now return the requested run's span subtree.

apps/webapp/app/presenters/v3/RunPresenter.server.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { millisecondsToNanoseconds, RunAnnotations } from "@trigger.dev/core/v3";
22
import { createTreeFromFlatItems, flattenTree } from "~/components/primitives/TreeView/TreeView";
33
import { prisma, type PrismaClient } from "~/db.server";
4+
import { logger } from "~/services/logger.server";
45
import { createTimelineSpanEventsFromSpanEvents } from "~/utils/timelineSpanEvents";
56
import { getUsername } from "~/utils/username";
67
import { SpanSummary } from "~/v3/eventRepository/eventRepository.types";
@@ -179,16 +180,49 @@ export class RunPresenter {
179180
run.runtimeEnvironment.organizationId
180181
);
181182

182-
// get the events
183+
const traceTimeBounds = {
184+
startCreatedAt: run.rootTaskRun?.createdAt ?? run.createdAt,
185+
endCreatedAt: run.completedAt ?? undefined,
186+
};
187+
188+
// Fast path: full trace summary. Slow path: subtree fetch when the anchor
189+
// span fell past the row cap (large traces ordered by start_time ASC).
183190
let traceSummary = await repository.getTraceSummary(
184191
getTaskEventStoreTableForRun(run),
185192
run.runtimeEnvironment.id,
186193
run.traceId,
187-
run.rootTaskRun?.createdAt ?? run.createdAt,
188-
run.completedAt ?? undefined,
194+
traceTimeBounds.startCreatedAt,
195+
traceTimeBounds.endCreatedAt,
189196
{ includeDebugLogs: showDebug }
190197
);
191198

199+
let isTruncated = traceSummary?.isTruncated ?? false;
200+
const hasAnchorSpan = traceSummary?.spans.some((span) => span.id === run.spanId) ?? false;
201+
202+
if (traceSummary && !hasAnchorSpan) {
203+
logger.warn("Trace summary missing anchor span, falling back to subtree fetch", {
204+
runId: run.friendlyId,
205+
spanId: run.spanId,
206+
traceId: run.traceId,
207+
spanCount: traceSummary.spans.length,
208+
});
209+
210+
const subtreeSummary = await repository.getTraceSubtreeSummary(
211+
getTaskEventStoreTableForRun(run),
212+
run.runtimeEnvironment.id,
213+
run.traceId,
214+
run.spanId,
215+
traceTimeBounds.startCreatedAt,
216+
traceTimeBounds.endCreatedAt,
217+
{ includeDebugLogs: showDebug }
218+
);
219+
220+
if (subtreeSummary) {
221+
traceSummary = subtreeSummary;
222+
isTruncated = subtreeSummary.isTruncated ?? false;
223+
}
224+
}
225+
192226
if (!traceSummary) {
193227
const spanSummary: SpanSummary = {
194228
id: run.spanId,
@@ -241,6 +275,18 @@ export class RunPresenter {
241275

242276
//this tree starts at the passed in span (hides parent elements if there are any)
243277
const tree = createTreeFromFlatItems(traceSummary.spans, run.spanId);
278+
const missingAnchor = !traceSummary.spans.some((span) => span.id === run.spanId) || !tree;
279+
280+
if (missingAnchor) {
281+
logger.warn("Trace view anchor span not found in trace summary", {
282+
runId: run.friendlyId,
283+
spanId: run.spanId,
284+
traceId: run.traceId,
285+
spanCount: traceSummary.spans.length,
286+
});
287+
288+
isTruncated = true;
289+
}
244290

245291
//we need the start offset for each item, and the total duration of the entire tree
246292
const treeRootStartTimeMs = tree ? tree?.data.startTime.getTime() : 0;
@@ -312,6 +358,8 @@ export class RunPresenter {
312358
: undefined,
313359
overridesBySpanId: traceSummary.overridesBySpanId,
314360
linkedRunIdBySpanId,
361+
isTruncated,
362+
missingAnchor,
315363
},
316364
maximumLiveReloadingSetting: repository.maximumLiveReloadingSetting,
317365
};

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
3636
import { PageBody } from "~/components/layout/AppLayout";
3737
import { Badge } from "~/components/primitives/Badge";
3838
import { Button, LinkButton } from "~/components/primitives/Buttons";
39+
import { Callout } from "~/components/primitives/Callout";
3940
import { CopyableText } from "~/components/primitives/CopyableText";
4041
import { DateTimeShort } from "~/components/primitives/DateTime";
4142
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
@@ -599,8 +600,16 @@ function TraceView({
599600
return <></>;
600601
}
601602

602-
const { events, duration, rootSpanStatus, rootStartedAt, queuedDuration, overridesBySpanId } =
603-
trace;
603+
const {
604+
events,
605+
duration,
606+
rootSpanStatus,
607+
rootStartedAt,
608+
queuedDuration,
609+
overridesBySpanId,
610+
isTruncated = false,
611+
missingAnchor = false,
612+
} = trace;
604613

605614
const changeToSpan = useDebounce((selectedSpan: string) => {
606615
replaceSearchParam("span", selectedSpan, { replace: true });
@@ -647,31 +656,44 @@ function TraceView({
647656
id={resizableSettings.parent.main.id}
648657
min={resizableSettings.parent.main.min}
649658
>
650-
<TasksTreeView
651-
selectedId={selectedSpanId}
652-
key={events[0]?.id ?? "-"}
653-
events={events}
654-
onSelectedIdChanged={(selectedSpan) => {
655-
//instantly close the panel if no span is selected
656-
if (!selectedSpan) {
657-
replaceSearchParam("span");
658-
return;
659-
}
660-
661-
changeToSpan(selectedSpan);
662-
}}
663-
totalDuration={duration}
664-
rootSpanStatus={rootSpanStatus}
665-
rootStartedAt={rootStartedAt ? new Date(rootStartedAt) : undefined}
666-
queuedDuration={queuedDuration}
667-
environmentType={run.environment.type}
668-
shouldLiveReload={isLiveReloading}
669-
maximumLiveReloadingSetting={maximumLiveReloadingSetting}
670-
rootRun={run.rootTaskRun}
671-
parentRun={run.parentTaskRun}
672-
isCompleted={run.completedAt !== null}
673-
treeSnapshot={resizable.tree as ResizableSnapshot}
674-
/>
659+
<div className="flex h-full flex-col overflow-hidden">
660+
{isTruncated && (
661+
<div className="shrink-0 border-b border-charcoal-700 px-3 py-2">
662+
<Callout variant="warning" className="text-sm">
663+
{missingAnchor
664+
? "Trace too large to display completely."
665+
: "This run's trace is partially displayed because it exceeds the view limit."}
666+
</Callout>
667+
</div>
668+
)}
669+
<div className="min-h-0 flex-1">
670+
<TasksTreeView
671+
selectedId={selectedSpanId}
672+
key={events[0]?.id ?? "-"}
673+
events={events}
674+
onSelectedIdChanged={(selectedSpan) => {
675+
//instantly close the panel if no span is selected
676+
if (!selectedSpan) {
677+
replaceSearchParam("span");
678+
return;
679+
}
680+
681+
changeToSpan(selectedSpan);
682+
}}
683+
totalDuration={duration}
684+
rootSpanStatus={rootSpanStatus}
685+
rootStartedAt={rootStartedAt ? new Date(rootStartedAt) : undefined}
686+
queuedDuration={queuedDuration}
687+
environmentType={run.environment.type}
688+
shouldLiveReload={isLiveReloading}
689+
maximumLiveReloadingSetting={maximumLiveReloadingSetting}
690+
rootRun={run.rootTaskRun}
691+
parentRun={run.parentTaskRun}
692+
isCompleted={run.completedAt !== null}
693+
treeSnapshot={resizable.tree as ResizableSnapshot}
694+
/>
695+
</div>
696+
</div>
675697
</ResizablePanel>
676698
<ResizableHandle
677699
id={resizableSettings.parent.handleId}

apps/webapp/app/routes/api.v1.runs.$runId.trace.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,11 @@ export const loader = createLoaderApiRoute(
9393
authentication.environment.organization.id
9494
);
9595

96-
const traceSummary = await eventRepository.getTraceDetailedSummary(
96+
const traceSummary = await eventRepository.getTraceDetailedSubtreeSummary(
9797
getTaskEventStoreTableForRun(run),
9898
authentication.environment.id,
9999
run.traceId,
100+
run.spanId,
100101
run.createdAt,
101102
run.completedAt ?? undefined
102103
);

0 commit comments

Comments
 (0)