Skip to content

Commit 9b431a0

Browse files
committed
Improve the display of the idempotency section in the run details inspector
1 parent 9758835 commit 9b431a0

File tree

3 files changed

+125
-112
lines changed

3 files changed

+125
-112
lines changed

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
type V3TaskRunContext,
99
} from "@trigger.dev/core/v3";
1010
import { AttemptId, getMaxDuration, parseTraceparent } from "@trigger.dev/core/v3/isomorphic";
11-
import { getUserProvidedIdempotencyKey } from "@trigger.dev/core/v3/serverOnly";
11+
import {
12+
getIdempotencyKeyScope,
13+
getUserProvidedIdempotencyKey,
14+
} from "@trigger.dev/core/v3/serverOnly";
1215
import { RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
1316
import { logger } from "~/services/logger.server";
1417
import { rehydrateAttribute } from "~/v3/eventRepository/eventRepository.server";
@@ -232,6 +235,8 @@ export class SpanPresenter extends BasePresenter {
232235
environmentId: run.runtimeEnvironment.id,
233236
idempotencyKey: getUserProvidedIdempotencyKey(run),
234237
idempotencyKeyExpiresAt: run.idempotencyKeyExpiresAt,
238+
idempotencyKeyScope: getIdempotencyKeyScope(run),
239+
idempotencyKeyStatus: this.getIdempotencyKeyStatus(run),
235240
debounce: run.debounce as { key: string; delay: string; createdAt: Date } | null,
236241
schedule: await this.resolveSchedule(run.scheduleId ?? undefined),
237242
queue: {
@@ -277,6 +282,30 @@ export class SpanPresenter extends BasePresenter {
277282
};
278283
}
279284

285+
private getIdempotencyKeyStatus(run: {
286+
idempotencyKey: string | null;
287+
idempotencyKeyExpiresAt: Date | null;
288+
idempotencyKeyOptions: unknown;
289+
}): "active" | "inactive" | "expired" | undefined {
290+
// No idempotency configured if no scope exists
291+
const scope = getIdempotencyKeyScope(run);
292+
if (!scope) {
293+
return undefined;
294+
}
295+
296+
// Check if expired first (takes precedence)
297+
if (run.idempotencyKeyExpiresAt && run.idempotencyKeyExpiresAt < new Date()) {
298+
return "expired";
299+
}
300+
301+
// Check if reset (hash is null but options exist)
302+
if (run.idempotencyKey === null) {
303+
return "inactive";
304+
}
305+
306+
return "active";
307+
}
308+
280309
async resolveSchedule(scheduleId?: string) {
281310
if (!scheduleId) {
282311
return;
@@ -706,5 +735,4 @@ export class SpanPresenter extends BasePresenter {
706735

707736
return parsedTraceparent?.traceId;
708737
}
709-
710738
}

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.idempotencyKey.reset.tsx

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,16 @@
1-
import { parse } from "@conform-to/zod";
21
import { type ActionFunction, json } from "@remix-run/node";
3-
import { z } from "zod";
42
import { prisma } from "~/db.server";
5-
import { jsonWithErrorMessage } from "~/models/message.server";
3+
import { jsonWithErrorMessage, jsonWithSuccessMessage } from "~/models/message.server";
64
import { logger } from "~/services/logger.server";
75
import { requireUserId } from "~/services/session.server";
86
import { ResetIdempotencyKeyService } from "~/v3/services/resetIdempotencyKey.server";
97
import { v3RunParamsSchema } from "~/utils/pathBuilder";
108

11-
export const resetIdempotencyKeySchema = z.object({
12-
taskIdentifier: z.string().min(1, "Task identifier is required"),
13-
});
14-
159
export const action: ActionFunction = async ({ request, params }) => {
1610
const userId = await requireUserId(request);
17-
const { projectParam, organizationSlug, envParam, runParam } =
18-
v3RunParamsSchema.parse(params);
19-
20-
const formData = await request.formData();
21-
const submission = parse(formData, { schema: resetIdempotencyKeySchema });
22-
23-
if (!submission.value) {
24-
return json(submission);
25-
}
11+
const { projectParam, organizationSlug, envParam, runParam } = v3RunParamsSchema.parse(params);
2612

2713
try {
28-
const { taskIdentifier } = submission.value;
29-
3014
const taskRun = await prisma.taskRun.findFirst({
3115
where: {
3216
friendlyId: runParam,
@@ -54,21 +38,11 @@ export const action: ActionFunction = async ({ request, params }) => {
5438
});
5539

5640
if (!taskRun) {
57-
submission.error = { runParam: ["Run not found"] };
58-
return json(submission);
41+
return jsonWithErrorMessage({}, request, "Run not found");
5942
}
6043

6144
if (!taskRun.idempotencyKey) {
62-
return jsonWithErrorMessage(
63-
submission,
64-
request,
65-
"This run does not have an idempotency key"
66-
);
67-
}
68-
69-
if (taskRun.taskIdentifier !== taskIdentifier) {
70-
submission.error = { taskIdentifier: ["Task identifier does not match this run"] };
71-
return json(submission);
45+
return jsonWithErrorMessage({}, request, "This run does not have an idempotency key");
7246
}
7347

7448
const environment = await prisma.runtimeEnvironment.findUnique({
@@ -85,22 +59,18 @@ export const action: ActionFunction = async ({ request, params }) => {
8559
});
8660

8761
if (!environment) {
88-
return jsonWithErrorMessage(
89-
submission,
90-
request,
91-
"Environment not found"
92-
);
62+
return jsonWithErrorMessage({}, request, "Environment not found");
9363
}
9464

9565
const service = new ResetIdempotencyKeyService();
9666

97-
await service.call(taskRun.idempotencyKey, taskIdentifier, {
67+
await service.call(taskRun.idempotencyKey, taskRun.taskIdentifier, {
9868
...environment,
9969
organizationId: environment.project.organizationId,
10070
organization: environment.project.organization,
10171
});
10272

103-
return json({ success: true });
73+
return jsonWithSuccessMessage({}, request, "Idempotency key reset successfully");
10474
} catch (error) {
10575
if (error instanceof Error) {
10676
logger.error("Failed to reset idempotency key", {
@@ -110,15 +80,11 @@ export const action: ActionFunction = async ({ request, params }) => {
11080
stack: error.stack,
11181
},
11282
});
113-
return jsonWithErrorMessage(
114-
submission,
115-
request,
116-
`Failed to reset idempotency key: ${error.message}`
117-
);
83+
return jsonWithErrorMessage({}, request, `Failed to reset idempotency key: ${error.message}`);
11884
} else {
11985
logger.error("Failed to reset idempotency key", { error });
12086
return jsonWithErrorMessage(
121-
submission,
87+
{},
12288
request,
12389
`Failed to reset idempotency key: ${JSON.stringify(error)}`
12490
);

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

Lines changed: 86 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
ArrowPathIcon,
3+
BookOpenIcon,
34
CheckIcon,
5+
ClockIcon,
46
CloudArrowDownIcon,
57
EnvelopeIcon,
68
QueueListIcon,
@@ -159,7 +161,8 @@ export function SpanView({
159161
return null;
160162
}
161163

162-
if (fetcher.state !== "idle" || fetcher.data === undefined) {
164+
// Only show loading spinner when there's no data yet, not during revalidation
165+
if (fetcher.data === undefined) {
163166
return (
164167
<div
165168
className={cn(
@@ -308,32 +311,6 @@ function RunBody({
308311
const tab = value("tab");
309312
const resetFetcher = useTypedFetcher<typeof resetIdempotencyKeyAction>();
310313

311-
// Handle toast messages from the reset action
312-
useEffect(() => {
313-
if (resetFetcher.data && resetFetcher.state === "idle") {
314-
// Check if the response indicates success
315-
if (
316-
resetFetcher.data &&
317-
typeof resetFetcher.data === "object" &&
318-
"success" in resetFetcher.data &&
319-
resetFetcher.data.success === true
320-
) {
321-
toast.custom(
322-
(t) => (
323-
<ToastUI
324-
variant="success"
325-
message="Idempotency key reset successfully"
326-
t={t as string}
327-
/>
328-
),
329-
{
330-
duration: 5000,
331-
}
332-
);
333-
}
334-
}
335-
}, [resetFetcher.data, resetFetcher.state]);
336-
337314
return (
338315
<div className="grid h-full max-h-full grid-rows-[2.5rem_2rem_1fr_3.25rem] overflow-hidden bg-background-bright">
339316
<div className="flex items-center justify-between gap-2 overflow-x-hidden px-3 pr-2">
@@ -443,6 +420,12 @@ function RunBody({
443420
/>
444421
</Property.Value>
445422
</Property.Item>
423+
<Property.Item>
424+
<Property.Label>Run ID</Property.Label>
425+
<Property.Value>
426+
<CopyableText value={run.friendlyId} copyValue={run.friendlyId} asChild />
427+
</Property.Value>
428+
</Property.Item>
446429
{run.relationships.root ? (
447430
run.relationships.root.isParent ? (
448431
<Property.Item>
@@ -581,38 +564,45 @@ function RunBody({
581564
</Property.Item>
582565
)}
583566
<Property.Item>
584-
<Property.Label>Idempotency</Property.Label>
585-
<Property.Value>
586-
<div className="flex items-start justify-between gap-2">
587-
<div className="flex-1">
588-
{run.idempotencyKey ? (
589-
<CopyableText
590-
value={run.idempotencyKey}
591-
copyValue={run.idempotencyKey}
592-
asChild
593-
/>
594-
) : (
595-
<div className="break-all"></div>
596-
)}
597-
{run.idempotencyKey && (
598-
<div>
599-
Expires:{" "}
600-
{run.idempotencyKeyExpiresAt ? (
601-
<DateTime date={run.idempotencyKeyExpiresAt} />
602-
) : (
603-
"–"
604-
)}
605-
</div>
606-
)}
607-
</div>
608-
{run.idempotencyKey && (
567+
<Property.Label>
568+
<div className="flex items-center justify-between">
569+
<span className="flex items-center gap-1">
570+
Idempotency
571+
<InfoIconTooltip
572+
content={
573+
<div className="flex max-w-xs flex-col gap-2 p-1">
574+
<Paragraph variant="small">
575+
Idempotency keys prevent duplicate task runs. If you trigger a task
576+
with the same key twice, the second request returns the original run.
577+
</Paragraph>
578+
<Paragraph variant="small">
579+
<strong>Scope:</strong> <strong>global</strong> applies across all
580+
runs, <strong>run</strong> is unique to a parent run, and{" "}
581+
<strong>attempt</strong> is unique to a specific attempt.
582+
</Paragraph>
583+
<Paragraph variant="small">
584+
<strong>Status:</strong> <strong>Active</strong> means duplicates are
585+
blocked, <strong>Expired</strong> means the TTL has passed, and{" "}
586+
<strong>Inactive</strong> means the key was reset or cleared.
587+
</Paragraph>
588+
<LinkButton
589+
to={docsPath("idempotency")}
590+
variant="docs/small"
591+
LeadingIcon={BookOpenIcon}
592+
>
593+
Read docs
594+
</LinkButton>
595+
</div>
596+
}
597+
/>
598+
</span>
599+
{run.idempotencyKeyStatus === "active" ? (
609600
<resetFetcher.Form
610601
method="post"
611602
action={v3RunIdempotencyKeyResetPath(organization, project, environment, {
612-
friendlyId: runParam,
603+
friendlyId: run.friendlyId,
613604
})}
614605
>
615-
<input type="hidden" name="taskIdentifier" value={run.taskIdentifier} />
616606
<Button
617607
type="submit"
618608
variant="minimal/small"
@@ -622,8 +612,49 @@ function RunBody({
622612
{resetFetcher.state === "submitting" ? "Resetting..." : "Reset"}
623613
</Button>
624614
</resetFetcher.Form>
625-
)}
615+
) : run.idempotencyKeyStatus === "expired" ? (
616+
<span className="flex items-center gap-1 text-xs text-amber-500">
617+
<ClockIcon className="size-4" />
618+
Expired
619+
</span>
620+
) : run.idempotencyKeyStatus === "inactive" ? (
621+
<span className="text-xs text-text-dimmed">Inactive</span>
622+
) : null}
626623
</div>
624+
</Property.Label>
625+
<Property.Value>
626+
{run.idempotencyKeyStatus ? (
627+
<>
628+
<div>
629+
<span className="text-text-dimmed">Key: </span>
630+
{run.idempotencyKey ? (
631+
<CopyableText
632+
value={run.idempotencyKey}
633+
copyValue={run.idempotencyKey}
634+
asChild
635+
/>
636+
) : (
637+
"–"
638+
)}
639+
</div>
640+
<div>
641+
<span className="text-text-dimmed">Scope: </span>
642+
{run.idempotencyKeyScope ?? "–"}
643+
</div>
644+
<div>
645+
<span className="text-text-dimmed">
646+
{run.idempotencyKeyStatus === "expired" ? "Expired: " : "Expires: "}
647+
</span>
648+
{run.idempotencyKeyExpiresAt ? (
649+
<DateTime date={run.idempotencyKeyExpiresAt} />
650+
) : (
651+
"–"
652+
)}
653+
</div>
654+
</>
655+
) : (
656+
"–"
657+
)}
627658
</Property.Value>
628659
</Property.Item>
629660
<Property.Item>
@@ -859,18 +890,6 @@ function RunBody({
859890
: "–"}
860891
</Property.Value>
861892
</Property.Item>
862-
<Property.Item>
863-
<Property.Label>Run ID</Property.Label>
864-
<Property.Value>
865-
<CopyableText value={run.friendlyId} copyValue={run.friendlyId} asChild />
866-
</Property.Value>
867-
</Property.Item>
868-
<Property.Item>
869-
<Property.Label>Internal ID</Property.Label>
870-
<Property.Value>
871-
<CopyableText value={run.id} copyValue={run.id} asChild />
872-
</Property.Value>
873-
</Property.Item>
874893
<Property.Item>
875894
<Property.Label>Run Engine</Property.Label>
876895
<Property.Value>{run.engine}</Property.Value>

0 commit comments

Comments
 (0)