Skip to content

Commit 36ea32e

Browse files
MariefayTrigger.dev RepoOps
authored andcommitted
fix(webapp): treat final runs without completedAt as finished
Some cancelled runs never get a completion time. Their run page now stops live-updating once they're done, and their trace reads stop shortly after the run's last update instead of scanning up to the present. Mono-RevId: da61b00e64a77644ae169308b445dfbf8c3ae0ef
1 parent 7e14b8e commit 36ea32e

18 files changed

Lines changed: 308 additions & 66 deletions
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 the run page continuing to live-update forever for some cancelled runs. These runs now show as finished, and loading their trace is faster.

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { applyAncestorOverrides, buildTraceView } from "~/v3/eventRepository/tra
1515
import { runStore } from "~/v3/runStore.server";
1616
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
1717
import { runTriggeredAt } from "~/v3/runTimestamps";
18+
import { getRunFinishedAt, getTraceStartTimeEnd } from "~/v3/eventRepository/traceInsertedAtBound";
1819

1920
export class RunEnvironmentMismatchError extends Error {
2021
constructor(message: string) {
@@ -97,6 +98,7 @@ export class RunPresenter {
9798
queueTimestamp: true,
9899
scheduleId: true,
99100
completedAt: true,
101+
updatedAt: true,
100102
logsDeletedAt: true,
101103
annotations: true,
102104
rootTaskRun: {
@@ -166,6 +168,7 @@ export class RunPresenter {
166168
isFinished: isFinalRunStatus(run.status),
167169
startedAt: run.startedAt,
168170
completedAt: run.completedAt,
171+
finishedAt: getRunFinishedAt(run) ?? null,
169172
logsDeletedAt: showDeletedLogs ? null : run.logsDeletedAt,
170173
rootTaskRun: run.rootTaskRun,
171174
parentTaskRun: run.parentTaskRun,
@@ -195,7 +198,7 @@ export class RunPresenter {
195198
// A child view only reads its own lifetime, not the root's.
196199
const traceTimeBounds = {
197200
startCreatedAt: run.createdAt,
198-
endCreatedAt: run.completedAt ?? undefined,
201+
endCreatedAt: getTraceStartTimeEnd(run),
199202
};
200203

201204
// Resolve agent-kind once so the tree renderer can swap icon/colour for

‎apps/webapp/app/presenters/v3/SpanPresenter.server.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
type ResolvedRunLockedWorker,
5353
} from "~/v3/runOpsMigration/controlPlaneResolver.server";
5454
import type { AuthenticatedEnvironment } from "@trigger.dev/core/v3/auth/environment";
55+
import { getTraceStartTimeEnd } from "~/v3/eventRepository/traceInsertedAtBound";
5556

5657
export type PromptSpanData = {
5758
slug: string;
@@ -158,8 +159,10 @@ export class SpanPresenter extends BasePresenter {
158159
runtimeEnvironmentId: true,
159160
projectId: true,
160161
taskEventStore: true,
162+
status: true,
161163
createdAt: true,
162164
completedAt: true,
165+
updatedAt: true,
163166
},
164167
},
165168
this._prisma
@@ -227,7 +230,7 @@ export class SpanPresenter extends BasePresenter {
227230
environmentId: parentRun.runtimeEnvironmentId,
228231
projectId: parentRun.projectId,
229232
createdAt: parentRun.createdAt,
230-
completedAt: parentRun.completedAt,
233+
endCreatedAt: getTraceStartTimeEnd(parentRun),
231234
eventRepository: repository,
232235
showDebug,
233236
});
@@ -693,7 +696,7 @@ export class SpanPresenter extends BasePresenter {
693696
environmentId,
694697
projectId,
695698
createdAt,
696-
completedAt,
699+
endCreatedAt,
697700
showDebug,
698701
}: {
699702
eventRepository: IEventRepository;
@@ -703,7 +706,7 @@ export class SpanPresenter extends BasePresenter {
703706
projectId: string;
704707
eventStore: TaskEventStoreTable;
705708
createdAt: Date;
706-
completedAt: Date | null;
709+
endCreatedAt: Date | undefined;
707710
showDebug: boolean;
708711
}) {
709712
const span = await eventRepository.getSpan(
@@ -712,7 +715,7 @@ export class SpanPresenter extends BasePresenter {
712715
spanId,
713716
traceId,
714717
createdAt,
715-
completedAt ?? undefined,
718+
endCreatedAt,
716719
{ includeDebugLogs: showDebug }
717720
);
718721

‎apps/webapp/app/presenters/v3/TraceChunkPresenter.server.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getEventRepositoryForStore } from "~/v3/eventRepository/index.server";
55
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
66
import { runStore } from "~/v3/runStore.server";
77
import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server";
8+
import { getTraceStartTimeEnd } from "~/v3/eventRepository/traceInsertedAtBound";
89

910
export class TraceChunkPresenter {
1011
#prismaClient: PrismaClient;
@@ -47,7 +48,9 @@ export class TraceChunkPresenter {
4748
createdAt: true,
4849
taskEventStore: true,
4950
traceId: true,
51+
status: true,
5052
completedAt: true,
53+
updatedAt: true,
5154
logsDeletedAt: true,
5255
runtimeEnvironmentId: true,
5356
},
@@ -91,7 +94,7 @@ export class TraceChunkPresenter {
9194
const storeTable = getTaskEventStoreTableForRun(run);
9295
// A child view only reads its own lifetime, not the root's.
9396
const startCreatedAt = run.createdAt;
94-
const endCreatedAt = run.completedAt ?? undefined;
97+
const endCreatedAt = getTraceStartTimeEnd(run);
9598

9699
if (filter === "errors") {
97100
const errors = await repository.getTraceErrorEvents(

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

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ export default function Page() {
481481
const environment = useEnvironment();
482482
const isConnected = useCrossEngineIsConnected({
483483
logCount: trace?.events.length ?? 0,
484-
isCompleted: run.completedAt !== null,
484+
isCompleted: run.finishedAt !== null,
485485
});
486486
const { value } = useSearchParams();
487487
const tableState = decodeURIComponent(value("tableState") ?? "");
@@ -711,17 +711,17 @@ const INSPECTOR_REFRESH_MS = 5_000;
711711
// final rows that become visible late still land, but not long after completion.
712712
const LIVE_TAIL_UNRESOLVED_ROOT_MS = 5 * 60_000;
713713

714-
function msSinceCompleted(run: { completedAt: string | null }): number | null {
715-
return run.completedAt ? Date.now() - new Date(run.completedAt).getTime() : null;
714+
function msSinceFinished(run: { finishedAt: string | null }): number | null {
715+
return run.finishedAt ? Date.now() - new Date(run.finishedAt).getTime() : null;
716716
}
717717

718718
function shouldLiveReload(
719-
run: { completedAt: string | null },
719+
run: { finishedAt: string | null },
720720
rootSpanStatus: "executing" | "completed" | "failed"
721721
): boolean {
722-
const sinceCompleted = msSinceCompleted(run);
723-
if (sinceCompleted === null || sinceCompleted < 30_000) return true;
724-
return rootSpanStatus === "executing" && sinceCompleted < LIVE_TAIL_UNRESOLVED_ROOT_MS;
722+
const sinceFinished = msSinceFinished(run);
723+
if (sinceFinished === null || sinceFinished < 30_000) return true;
724+
return rootSpanStatus === "executing" && sinceFinished < LIVE_TAIL_UNRESOLVED_ROOT_MS;
725725
}
726726

727727
// Legacy path (live tail off): revalidate on ping, with the old size cap.
@@ -732,10 +732,10 @@ function shouldLiveReloadLegacy({
732732
}: {
733733
events: TraceEvent[];
734734
maximumLiveReloadingSetting: number;
735-
run: { completedAt: string | null };
735+
run: { finishedAt: string | null };
736736
}): boolean {
737737
if (events.length > maximumLiveReloadingSetting) return false;
738-
if (run.completedAt && new Date(run.completedAt).getTime() < Date.now() - 30_000) return false;
738+
if (run.finishedAt && new Date(run.finishedAt).getTime() < Date.now() - 30_000) return false;
739739
return true;
740740
}
741741

@@ -785,7 +785,7 @@ function TraceView({
785785
const traceIsTruncated = isTruncated || progressiveIsTruncated;
786786

787787
// The page data still shows the run unfinished, so a finish reload is useful.
788-
const runDataUnfinished = !run.isFinished && run.completedAt === null;
788+
const runDataUnfinished = !run.isFinished && run.finishedAt === null;
789789

790790
// Display only: live reload and finish detection keep using the unsettled root.
791791
const settled = useMemo(
@@ -794,9 +794,9 @@ function TraceView({
794794
events,
795795
duration,
796796
rootSpanStatus,
797-
run: { status: run.status, completedAt: run.completedAt },
797+
run: { status: run.status, finishedAt: run.finishedAt },
798798
}),
799-
[events, duration, rootSpanStatus, run.status, run.completedAt]
799+
[events, duration, rootSpanStatus, run.status, run.finishedAt]
800800
);
801801

802802
const changeToSpan = useDebounce((selectedSpan: string) => {
@@ -814,8 +814,8 @@ function TraceView({
814814
});
815815

816816
const traceStatus = getTraceStatus({
817-
// Same rule as live reload: only an end time means the run is done.
818-
runFinished: run.completedAt !== null,
817+
// Same rule as live reload: only a finished time means the run is done.
818+
runFinished: run.finishedAt !== null,
819819
rootSpanStatus,
820820
isComplete,
821821
isTruncated: traceIsTruncated,
@@ -888,8 +888,8 @@ function TraceView({
888888

889889
// SSE is off during the background load, so pick up rows written meanwhile. Runs
890890
// even if live reload already stopped, as long as the run finished recently.
891-
const sinceCompleted = msSinceCompleted(run);
892-
const recentlyActive = sinceCompleted === null || sinceCompleted < LIVE_TAIL_UNRESOLVED_ROOT_MS;
891+
const sinceFinished = msSinceFinished(run);
892+
const recentlyActive = sinceFinished === null || sinceFinished < LIVE_TAIL_UNRESOLVED_ROOT_MS;
893893
const prevIsCompleteRef = useRef(isComplete);
894894
useEffect(() => {
895895
const wasComplete = prevIsCompleteRef.current;
@@ -969,7 +969,7 @@ function TraceView({
969969
environmentType={run.environment.type}
970970
rootRun={run.rootTaskRun}
971971
parentRun={run.parentTaskRun}
972-
isCompleted={run.completedAt !== null}
972+
isCompleted={run.finishedAt !== null}
973973
treeSnapshot={resizable.tree as ResizableSnapshot}
974974
errorsOnly={errorsOnly}
975975
filterText={filterText}
@@ -1061,11 +1061,11 @@ function NoLogsView({
10611061

10621062
const logRetention = plan?.v3Subscription?.plan?.limits.logRetentionDays.number ?? 30;
10631063

1064-
const completedAt = run.completedAt ? new Date(run.completedAt) : undefined;
1064+
const finishedAt = run.finishedAt ? new Date(run.finishedAt) : undefined;
10651065
const now = new Date();
10661066

1067-
const daysSinceCompleted = completedAt
1068-
? Math.floor((now.getTime() - completedAt.getTime()) / (1000 * 60 * 60 * 24))
1067+
const daysSinceCompleted = finishedAt
1068+
? Math.floor((now.getTime() - finishedAt.getTime()) / (1000 * 60 * 60 * 24))
10691069
: undefined;
10701070

10711071
const isWithinLogRetention =
@@ -1089,7 +1089,7 @@ function NoLogsView({
10891089
// Buffered runs always carry a synthetic trace (see
10901090
// buildSyntheticTraceForBufferedRun) so they never reach
10911091
// this branch — the message here is the pre-mollifier
1092-
// copy for runs with no completedAt and no logs.
1092+
// copy for runs with no finished time and no logs.
10931093
<InfoPanel variant="info" icon={InformationCircleIcon} title="We delete old logs">
10941094
<Paragraph variant="small">
10951095
We tidy up older logs to keep things running smoothly.

‎apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/settleUnloadedRoot.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type SettleInput<E extends SettleableEvent> = {
1717
events: E[];
1818
duration: number;
1919
rootSpanStatus: RootSpanStatus;
20-
run: { status: TaskRunStatus; completedAt: Date | string | null };
20+
run: { status: TaskRunStatus; finishedAt: Date | string | null };
2121
};
2222

2323
type Settled<E> = { events: E[]; duration: number; rootSpanStatus: RootSpanStatus };
@@ -38,13 +38,13 @@ function rootOutcome(
3838
return undefined;
3939
}
4040

41-
/** Ends a finished run's root at `completedAt` while its completion row is missing. */
41+
/** Ends a finished run's root at its finished time while its completion row is missing. */
4242
export function settleUnloadedRoot<E extends SettleableEvent>(input: SettleInput<E>): Settled<E> {
4343
const { events, duration, rootSpanStatus, run } = input;
4444
const unchanged = { events, duration, rootSpanStatus };
4545

4646
const root = events[0];
47-
if (!run.completedAt || rootSpanStatus !== "executing" || !root?.data.isPartial) {
47+
if (!run.finishedAt || rootSpanStatus !== "executing" || !root?.data.isPartial) {
4848
return unchanged;
4949
}
5050

@@ -53,7 +53,7 @@ export function settleUnloadedRoot<E extends SettleableEvent>(input: SettleInput
5353

5454
const rootDuration = Math.max(
5555
0,
56-
(new Date(run.completedAt).getTime() - new Date(root.data.startTime).getTime()) * 1_000_000
56+
(new Date(run.finishedAt).getTime() - new Date(root.data.startTime).getTime()) * 1_000_000
5757
);
5858

5959
const settledRoot = {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server";
44
import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
55
import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
66
import { getEventRepositoryForStore } from "~/v3/eventRepository/index.server";
7+
import { getTraceStartTimeEnd } from "~/v3/eventRepository/traceInsertedAtBound";
78

89
const ParamsSchema = z.object({
910
runId: z.string(), // This is the run friendly ID
@@ -56,7 +57,7 @@ export const loader = createLoaderApiRoute(
5657
run.traceId,
5758
run.friendlyId,
5859
run.createdAt,
59-
run.completedAt ?? undefined
60+
getTraceStartTimeEnd(run)
6061
);
6162

6263
// TODO: return only relevant fields, avoid returning the whole events

‎apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { $replica } from "~/db.server";
55
import { extractAISpanData } from "~/components/runs/v3/ai";
66
import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
77
import { getEventRepositoryForStore } from "~/v3/eventRepository/index.server";
8+
import { getTraceStartTimeEnd } from "~/v3/eventRepository/traceInsertedAtBound";
89
import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server";
910
import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.server";
1011
import { buildSyntheticSpanDetailBody } from "~/v3/mollifier/syntheticApiResponses.server";
@@ -106,7 +107,7 @@ export const loader = createLoaderApiRoute(
106107
params.spanId,
107108
run.traceId,
108109
run.createdAt,
109-
run.completedAt ?? undefined
110+
getTraceStartTimeEnd(run)
110111
);
111112

112113
if (!span) {

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { env } from "~/env.server";
88
import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
99
import { clampToEmergencySpanCap } from "~/v3/eventRepository/emergencySpanCap.server";
1010
import { getEventRepositoryForStore } from "~/v3/eventRepository/index.server";
11-
import { getTraceInsertedAtEnd } from "~/v3/eventRepository/traceInsertedAtBound";
11+
import {
12+
getTraceInsertedAtEnd,
13+
getTraceStartTimeEnd,
14+
} from "~/v3/eventRepository/traceInsertedAtBound";
1215
import { getRunTracePage } from "~/v3/eventRepository/runTracePage.server";
1316
import { FEATURE_FLAG } from "~/v3/featureFlags";
1417
import { makeFlag } from "~/v3/featureFlags.server";
@@ -128,7 +131,7 @@ export const loader = createLoaderApiRoute(
128131
run.traceId,
129132
run.spanId,
130133
run.createdAt,
131-
run.completedAt ?? undefined,
134+
getTraceStartTimeEnd(run),
132135
{ insertedAtEnd: getTraceInsertedAtEnd(run) }
133136
);
134137

‎apps/webapp/app/routes/resources.runs.$runParam.logs.download.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createGzip } from "zlib";
99
import { Readable } from "stream";
1010
import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server";
1111
import { getEventRepositoryForStore } from "~/v3/eventRepository/index.server";
12+
import { getTraceStartTimeEnd } from "~/v3/eventRepository/traceInsertedAtBound";
1213
import {
1314
getTraceExportFormat,
1415
streamTraceExport,
@@ -42,8 +43,10 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
4243
traceId: true,
4344
organizationId: true,
4445
runtimeEnvironmentId: true,
46+
status: true,
4547
createdAt: true,
4648
completedAt: true,
49+
updatedAt: true,
4750
taskEventStore: true,
4851
taskIdentifier: true,
4952
},
@@ -106,7 +109,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
106109
run.runtimeEnvironmentId,
107110
run.traceId,
108111
run.createdAt,
109-
run.completedAt ?? undefined,
112+
getTraceStartTimeEnd(run),
110113
{ includeDebugLogs: showDebug }
111114
);
112115

0 commit comments

Comments
 (0)