Skip to content

Commit 4b682e8

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 5667461 commit 4b682e8

11 files changed

Lines changed: 1153 additions & 291 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 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: 52 additions & 5 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,
@@ -237,11 +271,22 @@ export class RunPresenter {
237271

238272
// Resolve agent-kind once so the tree renderer can swap icon/colour for
239273
// the current run's spans without doing per-row lookups.
240-
const isAgentRun =
241-
RunAnnotations.safeParse(run.annotations).data?.taskKind === "AGENT";
274+
const isAgentRun = RunAnnotations.safeParse(run.annotations).data?.taskKind === "AGENT";
242275

243276
//this tree starts at the passed in span (hides parent elements if there are any)
244277
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+
}
245290

246291
//we need the start offset for each item, and the total duration of the entire tree
247292
const treeRootStartTimeMs = tree ? tree?.data.startTime.getTime() : 0;
@@ -313,6 +358,8 @@ export class RunPresenter {
313358
: undefined,
314359
overridesBySpanId: traceSummary.overridesBySpanId,
315360
linkedRunIdBySpanId,
361+
isTruncated,
362+
missingAnchor,
316363
},
317364
maximumLiveReloadingSetting: repository.maximumLiveReloadingSetting,
318365
};

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: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ import { json } from "@remix-run/server-runtime";
22
import { BatchId } from "@trigger.dev/core/v3/isomorphic";
33
import { z } from "zod";
44
import { $replica } from "~/db.server";
5-
import {
6-
anyResource,
7-
createLoaderApiRoute,
8-
} from "~/services/routeBuilders/apiBuilder.server";
5+
import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
96
import { getEventRepositoryForStore } from "~/v3/eventRepository/index.server";
107
import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server";
118
import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.server";
@@ -24,13 +21,13 @@ const ParamsSchema = z.object({
2421
// pass-through control case in scripts/mollifier-api-parity.sh).
2522
type ResolvedRun =
2623
| { source: "pg"; run: Awaited<ReturnType<typeof findPgRun>> & {} }
27-
| { source: "buffer"; run: NonNullable<Awaited<ReturnType<typeof findRunByIdWithMollifierFallback>>> };
24+
| {
25+
source: "buffer";
26+
run: NonNullable<Awaited<ReturnType<typeof findRunByIdWithMollifierFallback>>>;
27+
};
2828

2929
async function findPgRun(runId: string, environmentId: string) {
30-
return runStore.findRun(
31-
{ friendlyId: runId, runtimeEnvironmentId: environmentId },
32-
$replica
33-
);
30+
return runStore.findRun({ friendlyId: runId, runtimeEnvironmentId: environmentId }, $replica);
3431
}
3532

3633
export const loader = createLoaderApiRoute(
@@ -96,10 +93,11 @@ export const loader = createLoaderApiRoute(
9693
authentication.environment.organization.id
9794
);
9895

99-
const traceSummary = await eventRepository.getTraceDetailedSummary(
96+
const traceSummary = await eventRepository.getTraceDetailedSubtreeSummary(
10097
getTaskEventStoreTableForRun(run),
10198
authentication.environment.id,
10299
run.traceId,
100+
run.spanId,
103101
run.createdAt,
104102
run.completedAt ?? undefined
105103
);

0 commit comments

Comments
 (0)