diff --git a/src/commands/issue/list.ts b/src/commands/issue/list.ts index 7183ce87d..f9d4db840 100644 --- a/src/commands/issue/list.ts +++ b/src/commands/issue/list.ts @@ -1145,10 +1145,12 @@ async function handleResolvedTargets( if (targets.length === 0) { if (skippedSelfHosted) { - throw new ContextError("Organization and project", USAGE_HINT, [ - `Found ${skippedSelfHosted} DSN(s) that could not be resolved`, - "You may not have access to these projects, or you can specify the target explicitly", - ]); + throw new ContextError( + "Organization and project", + USAGE_HINT, + undefined, + `Found ${skippedSelfHosted} DSN(s) that could not be resolved — you may not have access to these projects` + ); } throw new ContextError("Organization and project", USAGE_HINT); } diff --git a/src/commands/project/view.ts b/src/commands/project/view.ts index 1e8743031..acb4f6813 100644 --- a/src/commands/project/view.ts +++ b/src/commands/project/view.ts @@ -45,11 +45,12 @@ const USAGE_HINT = "sentry project view /"; */ function buildContextError(skippedSelfHosted?: number): ContextError { if (skippedSelfHosted) { - return new ContextError("Organization and project", USAGE_HINT, [ - "Run from a directory with a Sentry-configured project", - "Set SENTRY_ORG and SENTRY_PROJECT (or SENTRY_DSN) environment variables", - `Found ${skippedSelfHosted} DSN(s) that could not be resolved — you may not have access to these projects`, - ]); + return new ContextError( + "Organization and project", + USAGE_HINT, + undefined, + `Found ${skippedSelfHosted} DSN(s) that could not be resolved — you may not have access to these projects` + ); } return new ContextError("Organization and project", USAGE_HINT); diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 78484d14d..29c008c97 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -166,12 +166,16 @@ const DEFAULT_CONTEXT_ALTERNATIVES = [ * @param resource - What is required (e.g., "Organization", "Trace ID and span ID") * @param command - Single-line CLI usage example (e.g., "sentry org view ") * @param alternatives - Alternative ways to provide the context + * @param note - Optional informational context (e.g., "Found 2 DSN(s) that could not be resolved"). + * Rendered as a separate "Note:" section after alternatives. Use this for diagnostic + * information that explains what the CLI tried — keep alternatives purely actionable. * @returns Formatted multi-line error message */ function buildContextMessage( resource: string, command: string, - alternatives: string[] + alternatives: string[], + note?: string ): string { // Compound resources ("X and Y") need plural grammar const isPlural = resource.includes(" and "); @@ -187,6 +191,9 @@ function buildContextMessage( lines.push(` - ${alt}`); } } + if (note) { + lines.push("", `Note: ${note}`); + } return lines.join("\n"); } @@ -229,23 +236,29 @@ function buildResolutionMessage( * that should use {@link ResolutionError}. * @param alternatives - Alternative ways to resolve (defaults to DSN/project detection hints). * Pass `[]` when the defaults are irrelevant (e.g., for missing positional IDs like Trace ID). + * @param note - Optional informational context rendered as a separate "Note:" section. + * Use for diagnostic info (e.g., "Found 2 DSN(s) that could not be resolved"). + * Keep alternatives purely actionable — put explanations here instead. */ export class ContextError extends CliError { readonly resource: string; readonly command: string; readonly alternatives: string[]; + readonly note?: string; constructor( resource: string, command: string, - alternatives: string[] = [...DEFAULT_CONTEXT_ALTERNATIVES] + alternatives: string[] = [...DEFAULT_CONTEXT_ALTERNATIVES], + note?: string ) { // Include full formatted message so it's shown even when caught by external handlers - super(buildContextMessage(resource, command, alternatives)); + super(buildContextMessage(resource, command, alternatives, note)); this.name = "ContextError"; this.resource = resource; this.command = command; this.alternatives = alternatives; + this.note = note; // Dev-time assertion: command must be a single-line CLI usage example. // Multi-line commands are a sign the caller should use ResolutionError. diff --git a/test/lib/errors.test.ts b/test/lib/errors.test.ts index 89ba31291..083ab25a2 100644 --- a/test/lib/errors.test.ts +++ b/test/lib/errors.test.ts @@ -131,6 +131,53 @@ describe("ContextError", () => { expect(formatted).toContain("Resource is required."); expect(formatted).not.toContain("Or:"); }); + + test("format() includes note section after alternatives", () => { + const err = new ContextError( + "Organization", + "sentry org list", + undefined, + "Found 2 DSN(s) that could not be resolved" + ); + const formatted = err.format(); + expect(formatted).toContain("Organization is required."); + // Default alternatives are present + expect(formatted).toContain("Or:"); + expect(formatted).toContain( + "Run from a directory with a Sentry-configured project" + ); + // Note appears as a separate section + expect(formatted).toContain( + "Note: Found 2 DSN(s) that could not be resolved" + ); + // Note appears after alternatives + const orIndex = formatted.indexOf("Or:"); + const noteIndex = formatted.indexOf("Note:"); + expect(noteIndex).toBeGreaterThan(orIndex); + }); + + test("format() includes note without alternatives", () => { + const err = new ContextError( + "Resource", + "sentry resource get", + [], + "Some diagnostic info" + ); + const formatted = err.format(); + expect(formatted).toContain("Resource is required."); + expect(formatted).not.toContain("Or:"); + expect(formatted).toContain("Note: Some diagnostic info"); + }); + + test("note field is stored on instance", () => { + const err = new ContextError( + "Organization", + "sentry org list", + undefined, + "test note" + ); + expect(err.note).toBe("test note"); + }); }); describe("ResolutionError", () => {