Skip to content

Commit c8958ac

Browse files
committed
Apply run options in replays
1 parent cfd8556 commit c8958ac

File tree

5 files changed

+117
-99
lines changed

5 files changed

+117
-99
lines changed

apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { TabButton, TabContainer } from "~/components/primitives/Tabs";
3636
import { TextLink } from "~/components/primitives/TextLink";
3737
import { type loader } from "~/routes/resources.taskruns.$runParam.replay";
3838
import { docsPath } from "~/utils/pathBuilder";
39-
import { ReplayTaskData } from "~/v3/replayTask";
39+
import { ReplayRunData } from "~/v3/replayTask";
4040
import { RectangleStackIcon } from "@heroicons/react/20/solid";
4141
import { Badge } from "~/components/primitives/Badge";
4242
import { RunTagInput } from "./RunTagInput";
@@ -148,27 +148,6 @@ function ReplayForm({
148148
replayData.payloadType === "application/json" ||
149149
replayData.payloadType === "application/super+json";
150150

151-
const submitForm = useCallback(
152-
(e: React.FormEvent<HTMLFormElement>) => {
153-
const formData = new FormData(e.currentTarget);
154-
const data: Record<string, string> = {
155-
environment: formData.get("environment") as string,
156-
failedRedirect: formData.get("failedRedirect") as string,
157-
};
158-
159-
if (editablePayload) {
160-
data.payload = currentPayloadJson.current;
161-
}
162-
163-
submit(data, {
164-
action: formAction,
165-
method: "post",
166-
});
167-
e.preventDefault();
168-
},
169-
[currentPayloadJson]
170-
);
171-
172151
const [tab, setTab] = useState<"payload" | "metadata">("payload");
173152

174153
const { defaultTaskQueue } = replayData;
@@ -217,16 +196,16 @@ function ReplayForm({
217196
submit(formData, { method: "POST", action: formAction });
218197
},
219198
onValidate({ formData }) {
220-
return parse(formData, { schema: ReplayTaskData });
199+
return parse(formData, { schema: ReplayRunData });
221200
},
222201
});
223202

224203
return (
225204
<Form
226205
action={formAction}
227206
method="post"
228-
onSubmit={(e) => submitForm(e)}
229207
className="flex flex-1 flex-col overflow-hidden px-3"
208+
{...form.props}
230209
>
231210
<input type="hidden" name="failedRedirect" value={failedRedirect} />
232211

@@ -242,16 +221,16 @@ function ReplayForm({
242221
<div className="rounded-smbg-charcoal-900 mb-3 h-full min-h-40 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
243222
<JSONEditor
244223
autoFocus
245-
defaultValue={!tab || tab === "payload" ? defaultPayloadJson : defaultMetadataJson}
224+
defaultValue={tab === "payload" ? defaultPayloadJson : defaultMetadataJson}
246225
readOnly={false}
247226
basicSetup
248227
onChange={(v) => {
249-
if (!tab || tab === "payload") {
228+
if (tab === "payload") {
250229
currentPayloadJson.current = v;
251-
setDefaultPayloadJson(v);
230+
setPayload(v);
252231
} else {
253232
currentMetadataJson.current = v;
254-
setDefaultMetadataJson(v);
233+
setMetadata(v);
255234
}
256235
}}
257236
height="100%"
@@ -261,7 +240,7 @@ function ReplayForm({
261240
<TabContainer className="flex grow items-baseline justify-between self-end border-none">
262241
<div className="flex gap-5">
263242
<TabButton
264-
isActive={!tab || tab === "payload"}
243+
isActive={tab === "payload"}
265244
layoutId="replay-editor"
266245
onClick={() => {
267246
setTab("payload");
@@ -505,8 +484,7 @@ function ReplayForm({
505484
<InputGroup className="flex flex-row items-center">
506485
<Label>Replay this run in</Label>
507486
<Select
508-
id="environment"
509-
name="environment"
487+
{...conform.select(environment)}
510488
placeholder="Select an environment"
511489
defaultValue={replayData.environment.id}
512490
items={replayData.environments}

apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server";
1414
import parseDuration from "parse-duration";
1515
import { findCurrentWorkerDeployment } from "~/v3/models/workerDeployment.server";
1616
import { queueTypeFromType } from "~/presenters/v3/QueueRetrievePresenter.server";
17+
import { ReplayRunData } from "~/v3/replayTask";
1718

1819
const ParamSchema = z.object({
1920
runParam: z.string(),
@@ -179,26 +180,18 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
179180
});
180181
}
181182

182-
const FormSchema = z.object({
183-
environment: z.string().optional(),
184-
payload: z.string().optional(),
185-
failedRedirect: z.string(),
186-
});
187-
188183
export const action: ActionFunction = async ({ request, params }) => {
189-
const userId = await requireUserId(request);
190-
191184
const { runParam } = ParamSchema.parse(params);
192185

193186
const formData = await request.formData();
194-
const submission = parse(formData, { schema: FormSchema });
187+
const submission = parse(formData, { schema: ReplayRunData });
195188

196189
if (!submission.value) {
197190
return json(submission);
198191
}
199192

200193
try {
201-
const taskRun = await prisma.taskRun.findUnique({
194+
const taskRun = await prisma.taskRun.findFirst({
202195
where: {
203196
friendlyId: runParam,
204197
},
@@ -224,6 +217,18 @@ export const action: ActionFunction = async ({ request, params }) => {
224217
const newRun = await replayRunService.call(taskRun, {
225218
environmentId: submission.value.environment,
226219
payload: submission.value.payload,
220+
metadata: submission.value.metadata,
221+
tags: submission.value.tags,
222+
queue: submission.value.queue,
223+
concurrencyKey: submission.value.concurrencyKey,
224+
maxAttempts: submission.value.maxAttempts,
225+
maxDurationSeconds: submission.value.maxDurationSeconds,
226+
machine: submission.value.machine,
227+
delaySeconds: submission.value.delaySeconds,
228+
idempotencyKey: submission.value.idempotencyKey,
229+
idempotencyKeyTTLSeconds: submission.value.idempotencyKeyTTLSeconds,
230+
ttlSeconds: submission.value.ttlSeconds,
231+
version: submission.value.version,
227232
});
228233

229234
if (!newRun) {

apps/webapp/app/v3/replayTask.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,47 @@
11
import { z } from "zod";
22
import { RunOptionsData } from "./testTask";
33

4-
export const ReplayTaskData = z
4+
export const ReplayRunData = z
55
.object({
66
environment: z.string().optional(),
7-
payload: z.string().optional(),
8-
metadata: z.string().optional(),
7+
payload: z
8+
.string()
9+
.optional()
10+
.transform((val, ctx) => {
11+
if (!val) {
12+
return {};
13+
}
14+
15+
try {
16+
return JSON.parse(val);
17+
} catch {
18+
ctx.addIssue({
19+
code: z.ZodIssueCode.custom,
20+
message: "Payload must be a valid JSON string",
21+
});
22+
return z.NEVER;
23+
}
24+
}),
25+
metadata: z
26+
.string()
27+
.optional()
28+
.transform((val, ctx) => {
29+
if (!val) {
30+
return {};
31+
}
32+
33+
try {
34+
return JSON.parse(val);
35+
} catch {
36+
ctx.addIssue({
37+
code: z.ZodIssueCode.custom,
38+
message: "Metadata must be a valid JSON string",
39+
});
40+
return z.NEVER;
41+
}
42+
}),
943
failedRedirect: z.string(),
1044
})
1145
.and(RunOptionsData);
1246

13-
export type ReplayTaskData = z.infer<typeof ReplayTaskData>;
47+
export type ReplayRunData = z.infer<typeof ReplayRunData>;

apps/webapp/app/v3/services/replayTaskRun.server.ts

Lines changed: 53 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
import {
2+
type MachinePresetName,
23
conditionallyImportPacket,
3-
IOPacket,
44
parsePacket,
5-
RunTags,
6-
stringifyIO,
75
} from "@trigger.dev/core/v3";
8-
import { replaceSuperJsonPayload } from "@trigger.dev/core/v3/utils/ioSerialization";
9-
import { TaskRun } from "@trigger.dev/database";
6+
import { type TaskRun } from "@trigger.dev/database";
107
import { findEnvironmentById } from "~/models/runtimeEnvironment.server";
118
import { getTagsForRunId } from "~/models/taskRunTag.server";
129
import { logger } from "~/services/logger.server";
1310
import { BaseService } from "./baseService.server";
1411
import { OutOfEntitlementError, TriggerTaskService } from "./triggerTask.server";
12+
import { type RunOptionsData } from "../testTask";
1513

1614
type OverrideOptions = {
1715
environmentId?: string;
18-
payload?: string;
19-
};
16+
payload?: unknown;
17+
metadata?: unknown;
18+
} & RunOptionsData;
2019

2120
export class ReplayTaskRunService extends BaseService {
22-
public async call(existingTaskRun: TaskRun, overrideOptions?: OverrideOptions) {
21+
public async call(existingTaskRun: TaskRun, overrideOptions: OverrideOptions = {}) {
2322
const authenticatedEnvironment = await findEnvironmentById(
24-
overrideOptions?.environmentId ?? existingTaskRun.runtimeEnvironmentId
23+
overrideOptions.environmentId ?? existingTaskRun.runtimeEnvironmentId
2524
);
2625
if (!authenticatedEnvironment) {
2726
return;
@@ -36,57 +35,41 @@ export class ReplayTaskRunService extends BaseService {
3635
taskRunFriendlyId: existingTaskRun.friendlyId,
3736
});
3837

39-
let payloadPacket: IOPacket;
40-
41-
if (overrideOptions?.payload) {
42-
if (existingTaskRun.payloadType === "application/super+json") {
43-
const newPayload = await replaceSuperJsonPayload(
44-
existingTaskRun.payload,
45-
overrideOptions.payload
46-
);
47-
payloadPacket = await stringifyIO(newPayload);
48-
} else {
49-
payloadPacket = await conditionallyImportPacket({
50-
data: overrideOptions.payload,
51-
dataType: existingTaskRun.payloadType,
52-
});
53-
}
54-
} else {
55-
payloadPacket = await conditionallyImportPacket({
38+
const getExistingPayload = async () => {
39+
const existingPayloadPacket = await conditionallyImportPacket({
5640
data: existingTaskRun.payload,
5741
dataType: existingTaskRun.payloadType,
5842
});
59-
}
6043

61-
const parsedPayload =
62-
payloadPacket.dataType === "application/json"
63-
? await parsePacket(payloadPacket)
64-
: payloadPacket.data;
65-
66-
logger.info("Replaying task run payload", {
67-
taskRunId: existingTaskRun.id,
68-
taskRunFriendlyId: existingTaskRun.friendlyId,
69-
payloadPacketType: payloadPacket.dataType,
70-
});
44+
return existingPayloadPacket.dataType === "application/json"
45+
? await parsePacket(existingPayloadPacket)
46+
: existingPayloadPacket.data;
47+
};
7148

72-
const metadata = existingTaskRun.seedMetadata
73-
? await parsePacket({
74-
data: existingTaskRun.seedMetadata,
75-
dataType: existingTaskRun.seedMetadataType,
76-
})
77-
: undefined;
49+
const payload = overrideOptions.payload ?? (await getExistingPayload());
50+
const metadata =
51+
overrideOptions.metadata ??
52+
(existingTaskRun.seedMetadata
53+
? await parsePacket({
54+
data: existingTaskRun.seedMetadata,
55+
dataType: existingTaskRun.seedMetadataType,
56+
})
57+
: undefined);
7858

7959
try {
80-
const tags = await getTagsForRunId({
81-
friendlyId: existingTaskRun.friendlyId,
82-
environmentId: authenticatedEnvironment.id,
83-
});
60+
const tags =
61+
overrideOptions.tags ??
62+
(
63+
await getTagsForRunId({
64+
friendlyId: existingTaskRun.friendlyId,
65+
environmentId: authenticatedEnvironment.id,
66+
})
67+
)?.map((t) => t.name);
8468

85-
//get the queue from the original run, so we can use the same settings on the replay
8669
const taskQueue = await this._prisma.taskQueue.findFirst({
8770
where: {
8871
runtimeEnvironmentId: authenticatedEnvironment.id,
89-
name: existingTaskRun.queue,
72+
name: overrideOptions.queue ?? existingTaskRun.queue,
9073
},
9174
});
9275

@@ -95,18 +78,34 @@ export class ReplayTaskRunService extends BaseService {
9578
existingTaskRun.taskIdentifier,
9679
authenticatedEnvironment,
9780
{
98-
payload: parsedPayload,
81+
payload,
9982
options: {
10083
queue: taskQueue
10184
? {
10285
name: taskQueue.name,
10386
}
10487
: undefined,
105-
concurrencyKey: existingTaskRun.concurrencyKey ?? undefined,
10688
test: existingTaskRun.isTest,
107-
payloadType: payloadPacket.dataType,
108-
tags: tags?.map((t) => t.name) as RunTags,
109-
metadata,
89+
tags,
90+
metadata: metadata,
91+
delay: overrideOptions.delaySeconds
92+
? new Date(Date.now() + overrideOptions.delaySeconds * 1000)
93+
: undefined,
94+
ttl: overrideOptions.ttlSeconds,
95+
idempotencyKey: overrideOptions.idempotencyKey,
96+
idempotencyKeyTTL: overrideOptions.idempotencyKeyTTLSeconds
97+
? `${overrideOptions.idempotencyKeyTTLSeconds}s`
98+
: undefined,
99+
concurrencyKey:
100+
overrideOptions.concurrencyKey ?? existingTaskRun.concurrencyKey ?? undefined,
101+
maxAttempts: overrideOptions.maxAttempts,
102+
maxDuration: overrideOptions.maxDurationSeconds,
103+
machine:
104+
overrideOptions.machine ??
105+
(existingTaskRun.machinePreset as MachinePresetName) ??
106+
undefined,
107+
lockToVersion:
108+
overrideOptions.version === "latest" ? undefined : overrideOptions.version,
110109
},
111110
},
112111
{

apps/webapp/app/v3/testTask.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export const RunOptionsData = z.object({
4848
version: z.string().optional(),
4949
});
5050

51+
export type RunOptionsData = z.infer<typeof RunOptionsData>;
52+
5153
export const TestTaskData = z
5254
.discriminatedUnion("triggerSource", [
5355
z.object({

0 commit comments

Comments
 (0)