Skip to content

Commit 432c360

Browse files
committed
feat: add reverse replay tracking with "Replayed as" section in Details tab
- Add replayed_from_friendly_id column to ClickHouse task_runs_v2 table - Create migration 012_add_task_runs_v2_replayed_from.sql - Update replication service to include the new field - Add getRunReplays query to ClickHouse client (optimized with org/project/env filter) - Update SpanPresenter to query replays from ClickHouse - Add "Replayed as" section in run Details tab showing linked replay runs with status The ClickHouse query filters by organization_id, project_id, and environment_id in the correct order to match the primary key for optimal query performance. This enables users to see which runs have been replayed from the original run, addressing the feedback about tracking replay status of failed runs. Slack thread: https://triggerdotdev.slack.com/archives/C045W9WM3E1/p1767609682537389
1 parent edf5b14 commit 432c360

File tree

6 files changed

+137
-0
lines changed

6 files changed

+137
-0
lines changed

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "@trigger.dev/core/v3";
1010
import { AttemptId, getMaxDuration, parseTraceparent } from "@trigger.dev/core/v3/isomorphic";
1111
import { RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
12+
import { clickhouseClient } from "~/services/clickhouseInstance.server";
1213
import { logger } from "~/services/logger.server";
1314
import { rehydrateAttribute } from "~/v3/eventRepository/eventRepository.server";
1415
import { machinePresetFromRun } from "~/v3/machinePresets.server";
@@ -210,6 +211,14 @@ export class SpanPresenter extends BasePresenter {
210211
region = workerGroup ?? null;
211212
}
212213

214+
// Query for runs that are replays of this run (from ClickHouse)
215+
const replays = await this.#getRunReplays(
216+
run.project.organization.id,
217+
run.project.id,
218+
run.runtimeEnvironment.id,
219+
run.friendlyId
220+
);
221+
213222
return {
214223
id: run.id,
215224
friendlyId: run.friendlyId,
@@ -276,9 +285,51 @@ export class SpanPresenter extends BasePresenter {
276285
machinePreset: machine?.name,
277286
taskEventStore: run.taskEventStore,
278287
externalTraceId,
288+
replays,
279289
};
280290
}
281291

292+
async #getRunReplays(
293+
organizationId: string,
294+
projectId: string,
295+
environmentId: string,
296+
friendlyId: string
297+
): Promise<Array<{ friendlyId: string; status: string }>> {
298+
try {
299+
const [error, result] = await clickhouseClient.taskRuns.getRunReplays({
300+
organizationId,
301+
projectId,
302+
environmentId,
303+
replayedFromFriendlyId: friendlyId,
304+
});
305+
306+
if (error) {
307+
logger.error("Error fetching run replays from ClickHouse", {
308+
error,
309+
organizationId,
310+
projectId,
311+
environmentId,
312+
friendlyId,
313+
});
314+
return [];
315+
}
316+
317+
return result.map((row) => ({
318+
friendlyId: row.friendly_id,
319+
status: row.status,
320+
}));
321+
} catch (error) {
322+
logger.error("Error fetching run replays from ClickHouse", {
323+
error,
324+
organizationId,
325+
projectId,
326+
environmentId,
327+
friendlyId,
328+
});
329+
return [];
330+
}
331+
}
332+
282333
async resolveSchedule(scheduleId?: string) {
283334
if (!scheduleId) {
284335
return;

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,40 @@ function RunBody({
707707
</Property.Value>
708708
</Property.Item>
709709
)}
710+
{run.replays && run.replays.length > 0 && (
711+
<Property.Item>
712+
<Property.Label>Replayed as</Property.Label>
713+
<Property.Value>
714+
<div className="flex flex-col gap-1">
715+
{run.replays.map((replay) => (
716+
<SimpleTooltip
717+
key={replay.friendlyId}
718+
button={
719+
<TextLink
720+
to={v3RunRedirectPath(organization, project, {
721+
friendlyId: replay.friendlyId,
722+
})}
723+
className="flex items-center gap-1"
724+
>
725+
<CopyableText
726+
value={replay.friendlyId}
727+
copyValue={replay.friendlyId}
728+
asChild
729+
/>
730+
<TaskRunStatusCombo
731+
status={replay.status as any}
732+
className="text-xs"
733+
/>
734+
</TextLink>
735+
}
736+
content={`Jump to replay run`}
737+
disableHoverableContent
738+
/>
739+
))}
740+
</div>
741+
</Property.Value>
742+
</Property.Item>
743+
)}
710744
{environment && (
711745
<Property.Item>
712746
<Property.Label>Environment</Property.Label>

apps/webapp/app/services/runsReplicationService.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,7 @@ export class RunsReplicationService {
831831
concurrency_key: run.concurrencyKey ?? "",
832832
bulk_action_group_ids: run.bulkActionGroupIds ?? [],
833833
worker_queue: run.masterQueue,
834+
replayed_from_friendly_id: run.replayedFromTaskRunFriendlyId ?? "",
834835
_version: _version.toString(),
835836
_is_deleted: event === "delete" ? 1 : 0,
836837
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- +goose Up
2+
/*
3+
Add replayed_from_friendly_id column to track which run this was replayed from.
4+
*/
5+
ALTER TABLE trigger_dev.task_runs_v2
6+
ADD COLUMN replayed_from_friendly_id String DEFAULT '';
7+
8+
-- +goose Down
9+
ALTER TABLE trigger_dev.task_runs_v2
10+
DROP COLUMN replayed_from_friendly_id;

internal-packages/clickhouse/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getTaskUsageByOrganization,
1313
getTaskRunsCountQueryBuilder,
1414
getTaskRunTagsQueryBuilder,
15+
getRunReplays,
1516
} from "./taskRuns.js";
1617
import {
1718
getSpanDetailsQueryBuilder,
@@ -164,6 +165,7 @@ export class ClickHouse {
164165
getCurrentRunningStats: getCurrentRunningStats(this.reader),
165166
getAverageDurations: getAverageDurations(this.reader),
166167
getTaskUsageByOrganization: getTaskUsageByOrganization(this.reader),
168+
getRunReplays: getRunReplays(this.reader),
167169
};
168170
}
169171

internal-packages/clickhouse/src/taskRuns.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const TaskRunV2 = z.object({
4545
concurrency_key: z.string().default(""),
4646
bulk_action_group_ids: z.array(z.string()).default([]),
4747
worker_queue: z.string().default(""),
48+
replayed_from_friendly_id: z.string().default(""),
4849
_version: z.string(),
4950
_is_deleted: z.number().int().default(0),
5051
});
@@ -305,3 +306,41 @@ export function getTaskUsageByOrganization(ch: ClickhouseReader, settings?: Clic
305306
settings,
306307
});
307308
}
309+
310+
export const RunReplayQueryResult = z.object({
311+
friendly_id: z.string(),
312+
status: z.string(),
313+
});
314+
315+
export type RunReplayQueryResult = z.infer<typeof RunReplayQueryResult>;
316+
317+
export const RunReplayQueryParams = z.object({
318+
organizationId: z.string(),
319+
projectId: z.string(),
320+
environmentId: z.string(),
321+
replayedFromFriendlyId: z.string(),
322+
});
323+
324+
export function getRunReplays(ch: ClickhouseReader, settings?: ClickHouseSettings) {
325+
return ch.query({
326+
name: "getRunReplays",
327+
query: `
328+
SELECT
329+
friendly_id,
330+
status
331+
FROM trigger_dev.task_runs_v2 FINAL
332+
WHERE
333+
organization_id = {organizationId: String}
334+
AND project_id = {projectId: String}
335+
AND environment_id = {environmentId: String}
336+
AND replayed_from_friendly_id = {replayedFromFriendlyId: String}
337+
AND _is_deleted = 0
338+
ORDER BY
339+
created_at DESC
340+
LIMIT 10
341+
`,
342+
schema: RunReplayQueryResult,
343+
params: RunReplayQueryParams,
344+
settings,
345+
});
346+
}

0 commit comments

Comments
 (0)