Skip to content

Commit c702d6a

Browse files
authored
v3: better task metadata errors (#991)
* v3: better handle task metadata parse errors, and display nicely formatted errors (dev, deploy, UI) * Add changeset
1 parent 9af2570 commit c702d6a

File tree

21 files changed

+360
-29
lines changed

21 files changed

+360
-29
lines changed

.changeset/breezy-gorillas-mate.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"trigger.dev": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
better handle task metadata parse errors, and display nicely formatted errors

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"type": "node-terminal",
4242
"request": "launch",
4343
"name": "Debug V3 Deploy CLI",
44-
"command": "pnpm exec trigger.dev deploy --skip-deploy",
44+
"command": "pnpm exec trigger.dev deploy",
4545
"cwd": "${workspaceFolder}/references/v3-catalog",
4646
"sourceMaps": true
4747
},

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
import {
2+
DeploymentErrorData,
3+
TaskMetadataFailedToParseData,
4+
groupTaskMetadataIssuesByTask,
5+
} from "@trigger.dev/core/v3";
16
import { WorkerDeployment, WorkerDeploymentStatus } from "@trigger.dev/database";
7+
import { z } from "zod";
28
import { PrismaClient, prisma } from "~/db.server";
39
import { Organization } from "~/models/organization.server";
410
import { Project } from "~/models/project.server";
511
import { User } from "~/models/user.server";
12+
import { safeJsonParse } from "~/utils/json";
613
import { getUsername } from "~/utils/username";
714

815
export class DeploymentPresenter {
@@ -51,6 +58,7 @@ export class DeploymentPresenter {
5158
id: true,
5259
shortCode: true,
5360
version: true,
61+
errorData: true,
5462
environment: {
5563
select: {
5664
id: true,
@@ -120,7 +128,81 @@ export class DeploymentPresenter {
120128
userName: getUsername(deployment.environment.orgMember?.user),
121129
},
122130
deployedBy: deployment.triggeredBy,
131+
errorData: this.#prepareErrorData(deployment.errorData),
123132
},
124133
};
125134
}
135+
136+
#prepareErrorData(errorData: WorkerDeployment["errorData"]) {
137+
if (!errorData) {
138+
return;
139+
}
140+
141+
const parsedErrorData = DeploymentErrorData.safeParse(errorData);
142+
143+
if (!parsedErrorData.success) {
144+
return;
145+
}
146+
147+
if (parsedErrorData.data.name === "TaskMetadataParseError") {
148+
const errorJson = safeJsonParse(parsedErrorData.data.stack);
149+
150+
if (errorJson) {
151+
const parsedError = TaskMetadataFailedToParseData.safeParse(errorJson);
152+
153+
if (parsedError.success) {
154+
return {
155+
name: parsedErrorData.data.name,
156+
message: parsedErrorData.data.message,
157+
stack: createTaskMetadataFailedErrorStack(parsedError.data),
158+
};
159+
} else {
160+
return {
161+
name: parsedErrorData.data.name,
162+
message: parsedErrorData.data.message,
163+
};
164+
}
165+
} else {
166+
return {
167+
name: parsedErrorData.data.name,
168+
message: parsedErrorData.data.message,
169+
};
170+
}
171+
}
172+
173+
return {
174+
name: parsedErrorData.data.name,
175+
message: parsedErrorData.data.message,
176+
stack: parsedErrorData.data.stack,
177+
};
178+
}
179+
}
180+
181+
function createTaskMetadataFailedErrorStack(
182+
data: z.infer<typeof TaskMetadataFailedToParseData>
183+
): string {
184+
const stack = [];
185+
186+
const groupedIssues = groupTaskMetadataIssuesByTask(data.tasks, data.zodIssues);
187+
188+
for (const key in groupedIssues) {
189+
const taskWithIssues = groupedIssues[key];
190+
191+
if (!taskWithIssues) {
192+
continue;
193+
}
194+
195+
stack.push("\n");
196+
stack.push(` ❯ ${taskWithIssues.exportName} in ${taskWithIssues.filePath}`);
197+
198+
for (const issue of taskWithIssues.issues) {
199+
if (issue.path) {
200+
stack.push(` x ${issue.path} ${issue.message}`);
201+
} else {
202+
stack.push(` x ${issue.message}`);
203+
}
204+
}
205+
}
206+
207+
return stack.join("\n");
126208
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.deployments.$deploymentParam/route.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime";
33
import { typedjson, useTypedLoaderData } from "remix-typedjson";
44
import { ExitIcon } from "~/assets/icons/ExitIcon";
55
import { UserAvatar } from "~/components/UserProfilePhoto";
6+
import { CodeBlock } from "~/components/code/CodeBlock";
67
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
78
import { Badge } from "~/components/primitives/Badge";
89
import { LinkButton } from "~/components/primitives/Buttons";
@@ -156,6 +157,26 @@ export default function Page() {
156157
</TableBody>
157158
</Table>
158159
</div>
160+
) : deployment.errorData ? (
161+
<div className="flex flex-col">
162+
{deployment.errorData.stack ? (
163+
<CodeBlock
164+
language="markdown"
165+
rowTitle={deployment.errorData.message}
166+
code={deployment.errorData.stack}
167+
maxLines={20}
168+
/>
169+
) : (
170+
<div className="flex flex-col">
171+
<Paragraph
172+
variant="base/bright"
173+
className="w-full border-b border-grid-dimmed py-2.5"
174+
>
175+
{deployment.errorData.message}
176+
</Paragraph>
177+
</div>
178+
)}
179+
</div>
159180
) : null}
160181
</div>
161182
</div>

apps/webapp/app/utils/json.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { z } from "zod";
22

3-
export function safeJsonParse(json: string): unknown {
3+
export function safeJsonParse(json?: string): unknown {
4+
if (!json) {
5+
return;
6+
}
7+
48
try {
59
return JSON.parse(json);
610
} catch (e) {

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { marqs, sanitizeQueueName } from "~/v3/marqs/index.server";
88
import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion";
99
import { BaseService } from "./baseService.server";
1010
import { projectPubSub } from "./projectPubSub.server";
11+
import { env } from "~/env.server";
1112

1213
export class CreateBackgroundWorkerService extends BaseService {
1314
public async call(
@@ -110,7 +111,7 @@ export class CreateBackgroundWorkerService extends BaseService {
110111
export async function createBackgroundTasks(
111112
tasks: TaskResource[],
112113
worker: BackgroundWorker,
113-
env: AuthenticatedEnvironment,
114+
environment: AuthenticatedEnvironment,
114115
prisma: PrismaClientOrTransaction
115116
) {
116117
for (const task of tasks) {
@@ -137,6 +138,18 @@ export async function createBackgroundTasks(
137138
queueName = sanitizeQueueName(`task/${task.id}`);
138139
}
139140

141+
const concurrencyLimit =
142+
typeof task.queue?.concurrencyLimit === "number"
143+
? Math.max(
144+
Math.min(
145+
task.queue.concurrencyLimit,
146+
environment.maximumConcurrencyLimit,
147+
environment.organization.maximumConcurrencyLimit
148+
),
149+
0
150+
)
151+
: null;
152+
140153
const taskQueue = await prisma.taskQueue.upsert({
141154
where: {
142155
runtimeEnvironmentId_name: {
@@ -145,13 +158,13 @@ export async function createBackgroundTasks(
145158
},
146159
},
147160
update: {
148-
concurrencyLimit: task.queue?.concurrencyLimit,
161+
concurrencyLimit,
149162
rateLimit: task.queue?.rateLimit,
150163
},
151164
create: {
152165
friendlyId: generateFriendlyId("queue"),
153166
name: queueName,
154-
concurrencyLimit: task.queue?.concurrencyLimit,
167+
concurrencyLimit,
155168
runtimeEnvironmentId: worker.runtimeEnvironmentId,
156169
projectId: worker.projectId,
157170
rateLimit: task.queue?.rateLimit,
@@ -160,7 +173,11 @@ export async function createBackgroundTasks(
160173
});
161174

162175
if (taskQueue.concurrencyLimit) {
163-
await marqs?.updateQueueConcurrencyLimits(env, taskQueue.name, taskQueue.concurrencyLimit);
176+
await marqs?.updateQueueConcurrencyLimits(
177+
environment,
178+
taskQueue.name,
179+
taskQueue.concurrencyLimit
180+
);
164181
}
165182
} catch (error) {
166183
if (error instanceof Prisma.PrismaClientKnownRequestError) {

packages/cli-v3/src/commands/deploy.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { depot } from "@depot/cli";
33
import { context, trace } from "@opentelemetry/api";
44
import {
55
ResolvedConfig,
6+
TaskMetadataFailedToParseData,
67
detectDependencyVersion,
78
flattenAttributes,
89
recordSpanException,
@@ -49,9 +50,11 @@ import { bundleDependenciesPlugin, workerSetupImportConfigPlugin } from "../util
4950
import { chalkError, chalkPurple, chalkWarning } from "../utilities/cliOutput";
5051
import {
5152
logESMRequireError,
53+
logTaskMetadataParseError,
5254
parseBuildErrorStack,
5355
parseNpmInstallError,
5456
} from "../utilities/deployErrors";
57+
import { safeJsonParse } from "../utilities/safeJsonParse";
5558

5659
const DeployCommandOptions = CommonCommandOptions.extend({
5760
skipTypecheck: z.boolean().default(false),
@@ -401,6 +404,24 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
401404
}
402405
case "FAILED": {
403406
if (finishedDeployment.errorData) {
407+
if (finishedDeployment.errorData.name === "TaskMetadataParseError") {
408+
const errorJson = safeJsonParse(finishedDeployment.errorData.stack);
409+
410+
if (errorJson) {
411+
const parsedError = TaskMetadataFailedToParseData.safeParse(errorJson);
412+
413+
if (parsedError.success) {
414+
deploymentSpinner.stop(`Deployment encountered an error. ${deploymentLink}`);
415+
416+
logTaskMetadataParseError(parsedError.data.zodIssues, parsedError.data.tasks);
417+
418+
throw new SkipLoggingError(
419+
`Deployment encountered an error: ${finishedDeployment.errorData.name}`
420+
);
421+
}
422+
}
423+
}
424+
404425
const parsedError = finishedDeployment.errorData.stack
405426
? parseBuildErrorStack(finishedDeployment.errorData)
406427
: finishedDeployment.errorData.message;

packages/cli-v3/src/commands/dev.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@ import {
3838
import { logger } from "../utilities/logger.js";
3939
import { isLoggedIn } from "../utilities/session.js";
4040
import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles";
41-
import { UncaughtExceptionError } from "../workers/common/errors";
41+
import { TaskMetadataParseError, UncaughtExceptionError } from "../workers/common/errors";
4242
import { BackgroundWorker, BackgroundWorkerCoordinator } from "../workers/dev/backgroundWorker.js";
4343
import { runtimeCheck } from "../utilities/runtimeCheck";
4444
import {
4545
logESMRequireError,
46+
logTaskMetadataParseError,
4647
parseBuildErrorStack,
4748
parseNpmInstallError,
4849
} from "../utilities/deployErrors";
@@ -548,7 +549,10 @@ function useDev({
548549
backgroundWorker
549550
);
550551
} catch (e) {
551-
if (e instanceof UncaughtExceptionError) {
552+
if (e instanceof TaskMetadataParseError) {
553+
logTaskMetadataParseError(e.zodIssues, e.tasks);
554+
return;
555+
} else if (e instanceof UncaughtExceptionError) {
552556
const parsedBuildError = parseBuildErrorStack(e.originalError);
553557

554558
if (typeof parsedBuildError !== "string") {

packages/cli-v3/src/utilities/deployErrors.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import chalk from "chalk";
22
import { relative } from "node:path";
3-
import { chalkError, chalkPurple, chalkGrey, chalkGreen } from "./cliOutput";
3+
import { chalkError, chalkPurple, chalkGrey, chalkGreen, chalkWarning } from "./cliOutput";
44
import { logger } from "./logger";
55
import { ReadConfigResult } from "./configFiles";
6+
import { TaskMetadataParseError } from "../workers/common/errors";
7+
import { z } from "zod";
8+
import { groupTaskMetadataIssuesByTask } from "@trigger.dev/core/v3";
69

710
export type ESMRequireError = {
811
type: "esm-require-error";
@@ -144,3 +147,35 @@ export function parseNpmInstallError(error: unknown): NpmInstallError {
144147

145148
return "Unknown error";
146149
}
150+
151+
export function logTaskMetadataParseError(zodIssues: z.ZodIssue[], tasks: any) {
152+
logger.log(
153+
`\n${chalkError("X Error:")} Failed to start. The following ${
154+
zodIssues.length === 1 ? "task issue was" : "task issues were"
155+
} found:`
156+
);
157+
158+
const groupedIssues = groupTaskMetadataIssuesByTask(tasks, zodIssues);
159+
160+
for (const key in groupedIssues) {
161+
const taskWithIssues = groupedIssues[key];
162+
163+
if (!taskWithIssues) {
164+
continue;
165+
}
166+
167+
logger.log(
168+
`\n ${chalkWarning("❯")} ${taskWithIssues.exportName} ${chalkGrey("in")} ${
169+
taskWithIssues.filePath
170+
}`
171+
);
172+
173+
for (const issue of taskWithIssues.issues) {
174+
if (issue.path) {
175+
logger.log(` ${chalkError("x")} ${issue.path} ${chalkGrey(issue.message)}`);
176+
} else {
177+
logger.log(` ${chalkError("x")} ${chalkGrey(issue.message)}`);
178+
}
179+
}
180+
}
181+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export function safeJsonParse(json?: string): unknown {
2+
if (!json) {
3+
return undefined;
4+
}
5+
6+
try {
7+
return JSON.parse(json);
8+
} catch {
9+
return undefined;
10+
}
11+
}

packages/cli-v3/src/workers/common/errors.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { z } from "zod";
2+
13
export class UncaughtExceptionError extends Error {
24
constructor(
35
public readonly originalError: { name: string; message: string; stack?: string },
@@ -8,3 +10,14 @@ export class UncaughtExceptionError extends Error {
810
this.name = "UncaughtExceptionError";
911
}
1012
}
13+
14+
export class TaskMetadataParseError extends Error {
15+
constructor(
16+
public readonly zodIssues: z.ZodIssue[],
17+
public readonly tasks: any
18+
) {
19+
super(`Failed to parse task metadata`);
20+
21+
this.name = "TaskMetadataParseError";
22+
}
23+
}

0 commit comments

Comments
 (0)