Skip to content

Commit 588bdbc

Browse files
committed
fix(errors): separate informational notes from actionable alternatives in ContextError
Add optional `note` parameter to ContextError for diagnostic context, rendered as a "Note:" section after the actionable "Or:" alternatives. Fixes CLI-VN where the Or: section contained non-actionable DSN info.
1 parent eb1b19e commit 588bdbc

File tree

4 files changed

+75
-12
lines changed

4 files changed

+75
-12
lines changed

src/commands/issue/list.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,10 +1145,12 @@ async function handleResolvedTargets(
11451145

11461146
if (targets.length === 0) {
11471147
if (skippedSelfHosted) {
1148-
throw new ContextError("Organization and project", USAGE_HINT, [
1149-
`Found ${skippedSelfHosted} DSN(s) that could not be resolved`,
1150-
"You may not have access to these projects, or you can specify the target explicitly",
1151-
]);
1148+
throw new ContextError(
1149+
"Organization and project",
1150+
USAGE_HINT,
1151+
undefined,
1152+
`Found ${skippedSelfHosted} DSN(s) that could not be resolved — you may not have access to these projects`
1153+
);
11521154
}
11531155
throw new ContextError("Organization and project", USAGE_HINT);
11541156
}

src/commands/project/view.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,12 @@ const USAGE_HINT = "sentry project view <org>/<project>";
4545
*/
4646
function buildContextError(skippedSelfHosted?: number): ContextError {
4747
if (skippedSelfHosted) {
48-
return new ContextError("Organization and project", USAGE_HINT, [
49-
"Run from a directory with a Sentry-configured project",
50-
"Set SENTRY_ORG and SENTRY_PROJECT (or SENTRY_DSN) environment variables",
51-
`Found ${skippedSelfHosted} DSN(s) that could not be resolved — you may not have access to these projects`,
52-
]);
48+
return new ContextError(
49+
"Organization and project",
50+
USAGE_HINT,
51+
undefined,
52+
`Found ${skippedSelfHosted} DSN(s) that could not be resolved — you may not have access to these projects`
53+
);
5354
}
5455

5556
return new ContextError("Organization and project", USAGE_HINT);

src/lib/errors.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,16 @@ const DEFAULT_CONTEXT_ALTERNATIVES = [
166166
* @param resource - What is required (e.g., "Organization", "Trace ID and span ID")
167167
* @param command - Single-line CLI usage example (e.g., "sentry org view <org-slug>")
168168
* @param alternatives - Alternative ways to provide the context
169+
* @param note - Optional informational context (e.g., "Found 2 DSN(s) that could not be resolved").
170+
* Rendered as a separate "Note:" section after alternatives. Use this for diagnostic
171+
* information that explains what the CLI tried — keep alternatives purely actionable.
169172
* @returns Formatted multi-line error message
170173
*/
171174
function buildContextMessage(
172175
resource: string,
173176
command: string,
174-
alternatives: string[]
177+
alternatives: string[],
178+
note?: string
175179
): string {
176180
// Compound resources ("X and Y") need plural grammar
177181
const isPlural = resource.includes(" and ");
@@ -187,6 +191,9 @@ function buildContextMessage(
187191
lines.push(` - ${alt}`);
188192
}
189193
}
194+
if (note) {
195+
lines.push("", `Note: ${note}`);
196+
}
190197
return lines.join("\n");
191198
}
192199

@@ -229,23 +236,29 @@ function buildResolutionMessage(
229236
* that should use {@link ResolutionError}.
230237
* @param alternatives - Alternative ways to resolve (defaults to DSN/project detection hints).
231238
* Pass `[]` when the defaults are irrelevant (e.g., for missing positional IDs like Trace ID).
239+
* @param note - Optional informational context rendered as a separate "Note:" section.
240+
* Use for diagnostic info (e.g., "Found 2 DSN(s) that could not be resolved").
241+
* Keep alternatives purely actionable — put explanations here instead.
232242
*/
233243
export class ContextError extends CliError {
234244
readonly resource: string;
235245
readonly command: string;
236246
readonly alternatives: string[];
247+
readonly note?: string;
237248

238249
constructor(
239250
resource: string,
240251
command: string,
241-
alternatives: string[] = [...DEFAULT_CONTEXT_ALTERNATIVES]
252+
alternatives: string[] = [...DEFAULT_CONTEXT_ALTERNATIVES],
253+
note?: string
242254
) {
243255
// Include full formatted message so it's shown even when caught by external handlers
244-
super(buildContextMessage(resource, command, alternatives));
256+
super(buildContextMessage(resource, command, alternatives, note));
245257
this.name = "ContextError";
246258
this.resource = resource;
247259
this.command = command;
248260
this.alternatives = alternatives;
261+
this.note = note;
249262

250263
// Dev-time assertion: command must be a single-line CLI usage example.
251264
// Multi-line commands are a sign the caller should use ResolutionError.

test/lib/errors.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,53 @@ describe("ContextError", () => {
131131
expect(formatted).toContain("Resource is required.");
132132
expect(formatted).not.toContain("Or:");
133133
});
134+
135+
test("format() includes note section after alternatives", () => {
136+
const err = new ContextError(
137+
"Organization",
138+
"sentry org list",
139+
undefined,
140+
"Found 2 DSN(s) that could not be resolved"
141+
);
142+
const formatted = err.format();
143+
expect(formatted).toContain("Organization is required.");
144+
// Default alternatives are present
145+
expect(formatted).toContain("Or:");
146+
expect(formatted).toContain(
147+
"Run from a directory with a Sentry-configured project"
148+
);
149+
// Note appears as a separate section
150+
expect(formatted).toContain(
151+
"Note: Found 2 DSN(s) that could not be resolved"
152+
);
153+
// Note appears after alternatives
154+
const orIndex = formatted.indexOf("Or:");
155+
const noteIndex = formatted.indexOf("Note:");
156+
expect(noteIndex).toBeGreaterThan(orIndex);
157+
});
158+
159+
test("format() includes note without alternatives", () => {
160+
const err = new ContextError(
161+
"Resource",
162+
"sentry resource get",
163+
[],
164+
"Some diagnostic info"
165+
);
166+
const formatted = err.format();
167+
expect(formatted).toContain("Resource is required.");
168+
expect(formatted).not.toContain("Or:");
169+
expect(formatted).toContain("Note: Some diagnostic info");
170+
});
171+
172+
test("note field is stored on instance", () => {
173+
const err = new ContextError(
174+
"Organization",
175+
"sentry org list",
176+
undefined,
177+
"test note"
178+
);
179+
expect(err.note).toBe("test note");
180+
});
134181
});
135182

136183
describe("ResolutionError", () => {

0 commit comments

Comments
 (0)