diff --git a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts index 4c0e3405cf..a4c4f82e76 100644 --- a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts @@ -9,6 +9,7 @@ import { } from "@trigger.dev/core/v3"; import { AttemptId, getMaxDuration, parseTraceparent } from "@trigger.dev/core/v3/isomorphic"; import { RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus"; +import { clickhouseClient } from "~/services/clickhouseInstance.server"; import { logger } from "~/services/logger.server"; import { rehydrateAttribute } from "~/v3/eventRepository/eventRepository.server"; import { machinePresetFromRun } from "~/v3/machinePresets.server"; @@ -210,6 +211,14 @@ export class SpanPresenter extends BasePresenter { region = workerGroup ?? null; } + // Query for runs that are replays of this run (from ClickHouse) + const replays = await this.#getRunReplays( + run.project.organization.id, + run.project.id, + run.runtimeEnvironment.id, + run.friendlyId + ); + return { id: run.id, friendlyId: run.friendlyId, @@ -276,9 +285,51 @@ export class SpanPresenter extends BasePresenter { machinePreset: machine?.name, taskEventStore: run.taskEventStore, externalTraceId, + replays, }; } + async #getRunReplays( + organizationId: string, + projectId: string, + environmentId: string, + friendlyId: string + ): Promise> { + try { + const [error, result] = await clickhouseClient.taskRuns.getRunReplays({ + organizationId, + projectId, + environmentId, + replayedFromFriendlyId: friendlyId, + }); + + if (error) { + logger.error("Error fetching run replays from ClickHouse", { + error, + organizationId, + projectId, + environmentId, + friendlyId, + }); + return []; + } + + return result.map((row) => ({ + friendlyId: row.friendly_id, + status: row.status, + })); + } catch (error) { + logger.error("Error fetching run replays from ClickHouse", { + error, + organizationId, + projectId, + environmentId, + friendlyId, + }); + return []; + } + } + async resolveSchedule(scheduleId?: string) { if (!scheduleId) { return; diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx index e8e472bfc7..39c5ee834c 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx @@ -707,6 +707,40 @@ function RunBody({ )} + {run.replays && run.replays.length > 0 && ( + + Replayed as + +
+ {run.replays.map((replay) => ( + + + + + } + content={`Jump to replay run`} + disableHoverableContent + /> + ))} +
+
+
+ )} {environment && ( Environment diff --git a/apps/webapp/app/services/runsReplicationService.server.ts b/apps/webapp/app/services/runsReplicationService.server.ts index c150eb2a00..ec8b365bdb 100644 --- a/apps/webapp/app/services/runsReplicationService.server.ts +++ b/apps/webapp/app/services/runsReplicationService.server.ts @@ -831,6 +831,7 @@ export class RunsReplicationService { concurrency_key: run.concurrencyKey ?? "", bulk_action_group_ids: run.bulkActionGroupIds ?? [], worker_queue: run.masterQueue, + replayed_from_friendly_id: run.replayedFromTaskRunFriendlyId ?? "", _version: _version.toString(), _is_deleted: event === "delete" ? 1 : 0, }; diff --git a/internal-packages/clickhouse/schema/012_add_task_runs_v2_replayed_from.sql b/internal-packages/clickhouse/schema/012_add_task_runs_v2_replayed_from.sql new file mode 100644 index 0000000000..ed35c137e2 --- /dev/null +++ b/internal-packages/clickhouse/schema/012_add_task_runs_v2_replayed_from.sql @@ -0,0 +1,10 @@ +-- +goose Up +/* +Add replayed_from_friendly_id column to track which run this was replayed from. + */ +ALTER TABLE trigger_dev.task_runs_v2 +ADD COLUMN replayed_from_friendly_id String DEFAULT ''; + +-- +goose Down +ALTER TABLE trigger_dev.task_runs_v2 +DROP COLUMN replayed_from_friendly_id; diff --git a/internal-packages/clickhouse/src/index.ts b/internal-packages/clickhouse/src/index.ts index 4aceeb92d8..14596d1882 100644 --- a/internal-packages/clickhouse/src/index.ts +++ b/internal-packages/clickhouse/src/index.ts @@ -12,6 +12,7 @@ import { getTaskUsageByOrganization, getTaskRunsCountQueryBuilder, getTaskRunTagsQueryBuilder, + getRunReplays, } from "./taskRuns.js"; import { getSpanDetailsQueryBuilder, @@ -164,6 +165,7 @@ export class ClickHouse { getCurrentRunningStats: getCurrentRunningStats(this.reader), getAverageDurations: getAverageDurations(this.reader), getTaskUsageByOrganization: getTaskUsageByOrganization(this.reader), + getRunReplays: getRunReplays(this.reader), }; } diff --git a/internal-packages/clickhouse/src/taskRuns.ts b/internal-packages/clickhouse/src/taskRuns.ts index b2410ee4b6..d9e5e96c33 100644 --- a/internal-packages/clickhouse/src/taskRuns.ts +++ b/internal-packages/clickhouse/src/taskRuns.ts @@ -45,6 +45,7 @@ export const TaskRunV2 = z.object({ concurrency_key: z.string().default(""), bulk_action_group_ids: z.array(z.string()).default([]), worker_queue: z.string().default(""), + replayed_from_friendly_id: z.string().default(""), _version: z.string(), _is_deleted: z.number().int().default(0), }); @@ -305,3 +306,41 @@ export function getTaskUsageByOrganization(ch: ClickhouseReader, settings?: Clic settings, }); } + +export const RunReplayQueryResult = z.object({ + friendly_id: z.string(), + status: z.string(), +}); + +export type RunReplayQueryResult = z.infer; + +export const RunReplayQueryParams = z.object({ + organizationId: z.string(), + projectId: z.string(), + environmentId: z.string(), + replayedFromFriendlyId: z.string(), +}); + +export function getRunReplays(ch: ClickhouseReader, settings?: ClickHouseSettings) { + return ch.query({ + name: "getRunReplays", + query: ` + SELECT + friendly_id, + status + FROM trigger_dev.task_runs_v2 FINAL + WHERE + organization_id = {organizationId: String} + AND project_id = {projectId: String} + AND environment_id = {environmentId: String} + AND replayed_from_friendly_id = {replayedFromFriendlyId: String} + AND _is_deleted = 0 + ORDER BY + created_at DESC + LIMIT 10 + `, + schema: RunReplayQueryResult, + params: RunReplayQueryParams, + settings, + }); +}