Skip to content

Commit 8766e4c

Browse files
committed
fix(project): disambiguate 404 errors from create endpoint
The /teams/{org}/{team}/projects/ endpoint returns 404 for both a bad org and a bad team. Previously we always blamed the team, which was misleading when --team was explicit and the org was auto-detected wrong. Now on 404 we call listTeams(orgSlug) to check: - If it succeeds → team is wrong, show available teams - If it fails → org is wrong, show user's actual organizations Only adds an API call on the error path, never on the happy path.
1 parent 1726210 commit 8766e4c

File tree

2 files changed

+80
-7
lines changed

2 files changed

+80
-7
lines changed

src/commands/project/create.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
*/
77

88
import type { SentryContext } from "../../context.js";
9-
import { createProject, tryGetPrimaryDsn } from "../../lib/api-client.js";
9+
import {
10+
createProject,
11+
listOrganizations,
12+
listTeams,
13+
tryGetPrimaryDsn,
14+
} from "../../lib/api-client.js";
1015
import { parseOrgPrefixedArg } from "../../lib/arg-parsing.js";
1116
import { buildCommand } from "../../lib/command.js";
1217
import { ApiError, CliError, ContextError } from "../../lib/errors.js";
@@ -81,6 +86,55 @@ function buildPlatformError(nameArg: string, platform?: string): string {
8186
);
8287
}
8388

89+
/**
90+
* Disambiguate a 404 from the create project endpoint.
91+
*
92+
* The `/teams/{org}/{team}/projects/` endpoint returns 404 for both
93+
* a bad org and a bad team. This helper calls `listTeams` to determine
94+
* which is wrong, then throws an actionable error.
95+
*
96+
* Only called on the error path — no cost to the happy path.
97+
*/
98+
async function handleCreateProject404(
99+
orgSlug: string,
100+
teamSlug: string,
101+
name: string,
102+
platform: string
103+
): Promise<never> {
104+
// If listTeams succeeds, the org is valid and the team is wrong
105+
const teams = await listTeams(orgSlug).catch(() => null);
106+
107+
if (teams !== null) {
108+
if (teams.length > 0) {
109+
const teamList = teams.map((t) => ` ${t.slug}`).join("\n");
110+
throw new CliError(
111+
`Team '${teamSlug}' not found in ${orgSlug}.\n\n` +
112+
`Available teams:\n\n${teamList}\n\n` +
113+
"Try:\n" +
114+
` sentry project create ${orgSlug}/${name} ${platform} --team <team-slug>`
115+
);
116+
}
117+
throw new CliError(
118+
`No teams found in ${orgSlug}.\n\n` +
119+
"Create a team first, then try again."
120+
);
121+
}
122+
123+
// listTeams also failed — org is likely wrong
124+
let orgHint = `Specify org explicitly: ${USAGE_HINT}`;
125+
try {
126+
const orgs = await listOrganizations();
127+
if (orgs.length > 0) {
128+
const orgList = orgs.map((o) => ` ${o.slug}`).join("\n");
129+
orgHint = `Your organizations:\n\n${orgList}`;
130+
}
131+
} catch {
132+
// Best-effort — if this also fails, use the generic hint
133+
}
134+
135+
throw new CliError(`Organization '${orgSlug}' not found.\n\n${orgHint}`);
136+
}
137+
84138
/**
85139
* Create a project with user-friendly error handling.
86140
* Wraps API errors with actionable messages instead of raw HTTP status codes.
@@ -105,11 +159,7 @@ async function createProjectWithErrors(
105159
throw new CliError(buildPlatformError(`${orgSlug}/${name}`, platform));
106160
}
107161
if (error.status === 404) {
108-
throw new CliError(
109-
`Team '${teamSlug}' not found in ${orgSlug}.\n\n` +
110-
"Check the team slug and try again:\n" +
111-
` sentry project create ${orgSlug}/${name} ${platform} --team <team-slug>`
112-
);
162+
await handleCreateProject404(orgSlug, teamSlug, name, platform);
113163
}
114164
throw new CliError(
115165
`Failed to create project '${name}' in ${orgSlug}.\n\n` +

test/commands/project/create.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ describe("project create", () => {
206206
expect(err.message).toContain("sentry project view");
207207
});
208208

209-
test("handles 404 from createProject as team-not-found", async () => {
209+
test("handles 404 from createProject as team-not-found with available teams", async () => {
210210
createProjectSpy.mockRejectedValue(
211211
new ApiError("API request failed: 404 Not Found", 404)
212212
);
@@ -219,9 +219,32 @@ describe("project create", () => {
219219
.catch((e: Error) => e);
220220
expect(err).toBeInstanceOf(CliError);
221221
expect(err.message).toContain("Team 'engineering' not found");
222+
expect(err.message).toContain("Available teams:");
223+
expect(err.message).toContain("engineering");
222224
expect(err.message).toContain("--team <team-slug>");
223225
});
224226

227+
test("handles 404 from createProject with bad org — shows user's orgs", async () => {
228+
createProjectSpy.mockRejectedValue(
229+
new ApiError("API request failed: 404 Not Found", 404)
230+
);
231+
// listTeams also fails → org is bad
232+
listTeamsSpy.mockRejectedValue(
233+
new ApiError("API request failed: 404 Not Found", 404)
234+
);
235+
236+
const { context } = createMockContext();
237+
const func = await createCommand.loader();
238+
239+
const err = await func
240+
.call(context, { json: false, team: "backend" }, "my-app", "node")
241+
.catch((e: Error) => e);
242+
expect(err).toBeInstanceOf(CliError);
243+
expect(err.message).toContain("Organization 'acme-corp' not found");
244+
expect(err.message).toContain("Your organizations");
245+
expect(err.message).toContain("other-org");
246+
});
247+
225248
test("handles 400 invalid platform with platform list", async () => {
226249
createProjectSpy.mockRejectedValue(
227250
new ApiError(

0 commit comments

Comments
 (0)