Skip to content

Commit 6f1d9e3

Browse files
authored
fix(project-create): preserve ApiError type so 4xx errors are silenced (#775)
## Summary Fix [CLI-196](https://sentry.sentry.io/issues/7420270756/) where `project create` failures were being captured as Sentry issues even for user-permission errors (17 events from 5 users across 5 orgs in 24h). ## Root cause `createProjectWithErrors` catches `ApiError` from `createProjectWithDsn`. For unhandled status codes, it was wrapping in a generic `CliError`: ```ts throw new CliError( `Failed to create project '${name}' in ${orgSlug}.\n\n` + `API error (${error.status}): ${error.detail ?? error.message}` ); ``` This **loses the `status` field**. `classifySilenced` in `error-reporting.ts` only silences `ApiError` with status 401–499, so 403 "Your organization has disabled this feature for members" became a Sentry issue. ## Fix Re-throw as `ApiError` preserving `status`/`detail`/`endpoint`. User-facing output is unchanged (`ApiError` extends `CliError` and `.format()` renders the wrapped message). The 4xx silencing now applies — 403s go to the `cli.error.silenced` metric with org/user context, not to Sentry as noisy issues. 5xx and network errors (status 0) continue to be captured as legitimate signals. Test updated to assert the preserved `ApiError` type and `status: 403`.
1 parent c7643e5 commit 6f1d9e3

2 files changed

Lines changed: 25 additions & 7 deletions

File tree

src/commands/project/create.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,19 @@ async function createProjectWithErrors(opts: {
255255
// createProjectWithDsn's return type differs from SentryProject
256256
return await (handleCreateProject404(opts) as never);
257257
}
258-
throw new CliError(
259-
`Failed to create project '${name}' in ${orgSlug}.\n\n` +
260-
`API error (${error.status}): ${error.detail ?? error.message}`
258+
// Re-throw as ApiError (not CliError) so the 401–499 user-error
259+
// silencing in error-reporting.ts applies — e.g. 403 "Your organization
260+
// has disabled this feature for members" is a permission issue, not a
261+
// CLI bug. 5xx and network errors still get captured.
262+
//
263+
// The message is kept short — ApiError.format() appends `detail` and
264+
// `endpoint` on separate lines, so embedding them in the message would
265+
// duplicate the output.
266+
throw new ApiError(
267+
`Failed to create project '${name}' in ${orgSlug} (HTTP ${error.status}).`,
268+
error.status,
269+
error.detail,
270+
error.endpoint
261271
);
262272
}
263273
throw error;

test/commands/project/create.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -417,20 +417,28 @@ describe("project create", () => {
417417
expect(err.message).toContain("Common platforms:");
418418
});
419419

420-
test("wraps other API errors with context", async () => {
420+
test("wraps other API errors with context, preserving ApiError type", async () => {
421421
createProjectSpy.mockRejectedValue(
422422
new ApiError("API request failed: 403 Forbidden", 403, "No permission")
423423
);
424424

425425
const { context } = createMockContext();
426426
const func = await createCommand.loader();
427427

428-
const err = await func
428+
const err = (await func
429429
.call(context, { json: false }, "my-app", "node")
430-
.catch((e: Error) => e);
431-
expect(err).toBeInstanceOf(CliError);
430+
.catch((e: Error) => e)) as ApiError;
431+
// Stays ApiError (not a plain CliError wrapper) so the 401–499
432+
// user-error silencing in error-reporting.ts still applies.
433+
expect(err).toBeInstanceOf(ApiError);
434+
expect(err.status).toBe(403);
435+
expect(err.detail).toBe("No permission");
432436
expect(err.message).toContain("Failed to create project");
433437
expect(err.message).toContain("403");
438+
// Detail is NOT duplicated in message — ApiError.format() appends it.
439+
expect(err.message).not.toContain("No permission");
440+
// But format() surfaces it for the user
441+
expect(err.format()).toContain("No permission");
434442
});
435443

436444
test("outputs JSON when --json flag is set", async () => {

0 commit comments

Comments
 (0)