Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions apps/webapp/app/presenters/v3/SpanPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Array<{ friendlyId: string; status: string }>> {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,40 @@ function RunBody({
</Property.Value>
</Property.Item>
)}
{run.replays && run.replays.length > 0 && (
<Property.Item>
<Property.Label>Replayed as</Property.Label>
<Property.Value>
<div className="flex flex-col gap-1">
{run.replays.map((replay) => (
<SimpleTooltip
key={replay.friendlyId}
button={
<TextLink
to={v3RunRedirectPath(organization, project, {
friendlyId: replay.friendlyId,
})}
className="flex items-center gap-1"
>
<CopyableText
value={replay.friendlyId}
copyValue={replay.friendlyId}
asChild
/>
<TaskRunStatusCombo
status={replay.status as any}
className="text-xs"
/>
</TextLink>
}
content={`Jump to replay run`}
disableHoverableContent
/>
))}
</div>
</Property.Value>
</Property.Item>
)}
Comment on lines +710 to +743
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for SpanRun type definition
rg "type SpanRun" apps/webapp --type ts -A 20

Repository: triggerdotdev/trigger.dev

Length of output: 5469


🏁 Script executed:

# Search for TaskRunStatus type/enum definition
rg "type TaskRunStatus|enum TaskRunStatus|export.*TaskRunStatus" apps/webapp --type ts -B 2 -A 10

Repository: triggerdotdev/trigger.dev

Length of output: 24510


🏁 Script executed:

# Check how replays is typed in the SpanPresenter
rg "replays" apps/webapp --type ts -B 3 -A 3

Repository: triggerdotdev/trigger.dev

Length of output: 4525


🏁 Script executed:

# Check the TaskRunStatusCombo component to see what type it expects
rg -A 10 "interface.*Props|type.*Props" apps/webapp/app/components/runs/v3/TaskRunStatusCombo.tsx --type ts

Repository: triggerdotdev/trigger.dev

Length of output: 166


🏁 Script executed:

# Get the full SpanRun type definition by looking at Result type
rg "type Result" apps/webapp/app/presenters/v3/SpanPresenter.server.ts -B 5 -A 15

Repository: triggerdotdev/trigger.dev

Length of output: 820


🏁 Script executed:

# Look at the #getRunReplays method to see what it returns
rg "#getRunReplays|getRunReplays" apps/webapp/app/presenters/v3/SpanPresenter.server.ts -A 30

Repository: triggerdotdev/trigger.dev

Length of output: 2125


🏁 Script executed:

# Check what TaskRunStatus type is imported from
rg "import.*TaskRunStatus" apps/webapp/app/components/runs/v3/TaskRunStatus.tsx

Repository: triggerdotdev/trigger.dev

Length of output: 128


🏁 Script executed:

# Look for the actual replay object structure in SpanPresenter
rg "friendlyId.*status" apps/webapp/app/presenters/v3/SpanPresenter.server.ts -B 5 -A 5

Repository: triggerdotdev/trigger.dev

Length of output: 409


Remove the as any type assertion and properly type the replay status field.

Line 731 uses as any to cast replay.status to satisfy TaskRunStatusCombo, which expects a TaskRunStatus type. The issue stems from the #getRunReplays() method returning status: string instead of status: TaskRunStatus.

Fix this by updating the return type of #getRunReplays() in SpanPresenter.server.ts to:

Promise<Array<{ friendlyId: string; status: TaskRunStatus }>>

This ensures type safety without needing the as any cast. The method can validate or coerce the ClickHouse string values to the proper TaskRunStatus type before returning.

🤖 Prompt for AI Agents
In
@apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
around lines 710-743, The replay status is being cast with `as any` for
`TaskRunStatusCombo`; update the `#getRunReplays()` method in
SpanPresenter.server.ts to return Promise<Array<{ friendlyId: string; status:
TaskRunStatus }>> instead of status: string, and convert/validate the ClickHouse
string values to the TaskRunStatus enum before returning so callers (e.g., use
of `replay.status` in TaskRunStatusCombo) no longer need `as any`; locate
`#getRunReplays()` and the `replay.status` usage to implement the type change
and any necessary coercion/validation logic.

{environment && (
<Property.Item>
<Property.Label>Environment</Property.Label>
Expand Down
1 change: 1 addition & 0 deletions apps/webapp/app/services/runsReplicationService.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions internal-packages/clickhouse/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getTaskUsageByOrganization,
getTaskRunsCountQueryBuilder,
getTaskRunTagsQueryBuilder,
getRunReplays,
} from "./taskRuns.js";
import {
getSpanDetailsQueryBuilder,
Expand Down Expand Up @@ -164,6 +165,7 @@ export class ClickHouse {
getCurrentRunningStats: getCurrentRunningStats(this.reader),
getAverageDurations: getAverageDurations(this.reader),
getTaskUsageByOrganization: getTaskUsageByOrganization(this.reader),
getRunReplays: getRunReplays(this.reader),
};
}

Expand Down
39 changes: 39 additions & 0 deletions internal-packages/clickhouse/src/taskRuns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
Expand Down Expand Up @@ -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<typeof RunReplayQueryResult>;

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,
});
}