Skip to content

Commit 72710b7

Browse files
committed
fix(errors): improve ContextError wording for auto-detect failures
When org/project auto-detection fails, the error now says "Could not auto-detect organization and project" instead of the confusing "Organization and project are required", and instructs users to "Provide them explicitly" with the command syntax. The change detects auto-detect mode by checking whether `alternatives` was omitted (all auto-detect call sites omit it, while required-input sites pass `[]` or custom arrays). No API breaking change — existing call sites work unchanged.
1 parent ec08c24 commit 72710b7

13 files changed

Lines changed: 113 additions & 85 deletions

File tree

AGENTS.md

Lines changed: 56 additions & 58 deletions
Large diffs are not rendered by default.

src/lib/errors.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export class OutputError extends CliError {
154154
}
155155

156156
const DEFAULT_CONTEXT_ALTERNATIVES = [
157-
"Run from a directory with a Sentry-configured project",
157+
"Run from a directory with a Sentry DSN in source code or .env files",
158158
"Set SENTRY_ORG and SENTRY_PROJECT (or SENTRY_DSN) environment variables",
159159
"Run 'sentry org list' to find your organization slug",
160160
"Run 'sentry project list <org>/' to find project slugs",
@@ -169,22 +169,36 @@ const DEFAULT_CONTEXT_ALTERNATIVES = [
169169
* @param note - Optional informational context (e.g., "Found 2 DSN(s) that could not be resolved").
170170
* Rendered as a separate "Note:" section after alternatives. Use this for diagnostic
171171
* information that explains what the CLI tried — keep alternatives purely actionable.
172+
* @param isAutoDetect - When true, the headline explains that auto-detection was attempted
173+
* and failed rather than stating the value "is required". Callers that omit `alternatives`
174+
* (using defaults) trigger this automatically via the {@link ContextError} constructor.
172175
* @returns Formatted multi-line error message
173176
*/
174177
function buildContextMessage(
175178
resource: string,
176179
command: string,
177180
alternatives: string[],
178-
note?: string
181+
options?: { note?: string; isAutoDetect?: boolean }
179182
): string {
183+
const { note, isAutoDetect } = options ?? {};
180184
// Compound resources ("X and Y") need plural grammar
181185
const isPlural = resource.includes(" and ");
182-
const lines = [
183-
`${resource} ${isPlural ? "are" : "is"} required.`,
184-
"",
185-
`Specify ${isPlural ? "them" : "it"} using:`,
186-
` ${command}`,
187-
];
186+
const pronoun = isPlural ? "them" : "it";
187+
188+
const lines = isAutoDetect
189+
? [
190+
`Could not auto-detect ${resource.toLowerCase()}.`,
191+
"",
192+
`Provide ${pronoun} explicitly:`,
193+
` ${command}`,
194+
]
195+
: [
196+
`${resource} ${isPlural ? "are" : "is"} required.`,
197+
"",
198+
`Specify ${pronoun} using:`,
199+
` ${command}`,
200+
];
201+
188202
if (alternatives.length > 0) {
189203
lines.push("", "Or:");
190204
for (const alt of alternatives) {
@@ -229,6 +243,10 @@ function buildResolutionMessage(
229243
* user **provided** a value that couldn't be matched, use {@link ResolutionError}
230244
* instead. For malformed input, use {@link ValidationError}.
231245
*
246+
* When `alternatives` is omitted (using defaults), the error assumes auto-detection
247+
* was attempted and produces a "Could not auto-detect ..." headline. When `alternatives`
248+
* is explicitly provided (including `[]`), the error uses "... is/are required." instead.
249+
*
232250
* @param resource - What is required (e.g., "Organization", "Organization and project").
233251
* Use " and " to join compound resources — triggers plural grammar ("are required").
234252
* @param command - **Single-line** CLI usage example (e.g., "sentry org view <org-slug>").
@@ -249,15 +267,26 @@ export class ContextError extends CliError {
249267
constructor(
250268
resource: string,
251269
command: string,
252-
alternatives: string[] = [...DEFAULT_CONTEXT_ALTERNATIVES],
270+
alternatives?: string[],
253271
note?: string
254272
) {
273+
// When alternatives is omitted, auto-detection was tried and failed
274+
const isAutoDetect = alternatives === undefined;
275+
const resolvedAlternatives = alternatives ?? [
276+
...DEFAULT_CONTEXT_ALTERNATIVES,
277+
];
278+
255279
// Include full formatted message so it's shown even when caught by external handlers
256-
super(buildContextMessage(resource, command, alternatives, note));
280+
super(
281+
buildContextMessage(resource, command, resolvedAlternatives, {
282+
note,
283+
isAutoDetect,
284+
})
285+
);
257286
this.name = "ContextError";
258287
this.resource = resource;
259288
this.command = command;
260-
this.alternatives = alternatives;
289+
this.alternatives = resolvedAlternatives;
261290
this.note = note;
262291

263292
// Dev-time assertion: command must be a single-line CLI usage example.

test/commands/dashboard/list.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ describe("dashboard list command", () => {
459459
const func = await listCommand.loader();
460460

461461
await expect(func.call(context, defaultFlags())).rejects.toThrow(
462-
"Organization"
462+
"organization"
463463
);
464464
});
465465
});

test/commands/event/list.func.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,6 @@ describe("event list command func()", () => {
268268
{ limit: 25, json: false, full: false, period: "7d" },
269269
"123456789"
270270
)
271-
).rejects.toThrow("Organization");
271+
).rejects.toThrow("organization");
272272
});
273273
});

test/commands/issue/events.func.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,6 @@ describe("eventsCommand.func()", () => {
492492
{ limit: 25, json: false, full: false, period: "7d" },
493493
"123456789"
494494
)
495-
).rejects.toThrow("Organization");
495+
).rejects.toThrow("organization");
496496
});
497497
});

test/commands/issue/utils.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ describe("resolveOrgAndIssueId", () => {
153153
cwd: getConfigDir(),
154154
command: "explain",
155155
})
156-
).rejects.toThrow("Organization");
156+
).rejects.toThrow("organization");
157157
});
158158

159159
test("resolves numeric ID when API response includes subdomain-style permalink", async () => {
@@ -1803,7 +1803,7 @@ describe("resolveOrgAndIssueId: magic @ selectors", () => {
18031803
cwd: getConfigDir(),
18041804
command: "view",
18051805
})
1806-
).rejects.toThrow("Organization");
1806+
).rejects.toThrow("organization");
18071807
});
18081808
});
18091809

test/commands/log/list.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,7 +799,7 @@ describe("listCommand.func — trace mode org resolution failure", () => {
799799
expect.unreachable("Should have thrown");
800800
} catch (error) {
801801
expect(error).toBeInstanceOf(ContextError);
802-
expect((error as ContextError).message).toContain("Organization");
802+
expect((error as ContextError).message).toContain("organization");
803803
}
804804
});
805805
});

test/commands/release/finalize.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe("release finalize", () => {
9898
const func = await finalizeCommand.loader();
9999

100100
await expect(func.call(context, { json: false }, "1.0.0")).rejects.toThrow(
101-
"Organization"
101+
"organization"
102102
);
103103
});
104104
});

test/commands/release/view.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ describe("release view", () => {
200200

201201
await expect(
202202
func.call(context, { fresh: false, json: false }, "1.0.0")
203-
).rejects.toThrow("Organization");
203+
).rejects.toThrow("organization");
204204
});
205205

206206
test("displays per-project health data in human mode", async () => {

test/commands/trace/list.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ describe("resolveOrgProjectFromArg", () => {
133133
expect.unreachable("Should have thrown");
134134
} catch (error) {
135135
expect(error).toBeInstanceOf(ContextError);
136-
expect((error as ContextError).message).toContain("Project");
136+
expect((error as ContextError).message).toContain("project");
137137
expect((error as ContextError).message).toContain("my-org/<project>");
138138
}
139139
});

0 commit comments

Comments
 (0)