Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/lib/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
} from "./formatters/output.js";
import { isPlainOutput } from "./formatters/plain-detect.js";
import { GLOBAL_FLAGS } from "./global-flags.js";
import { setInteractivePromptsAllowed } from "./interactive-prompts.js";
import {
LOG_LEVEL_NAMES,
type LogLevelName,
Expand Down Expand Up @@ -704,6 +705,10 @@ export function buildCommand<
}
}

// Suppress interactive prompts (e.g. the org/project picker) in JSON mode so
// a prompt never blocks a scripted run or interleaves with stdout JSON.
setInteractivePromptsAllowed(!cleanFlags.json);

const stdout = (this as unknown as { stdout: Writer }).stdout;

// Reset per-invocation state
Expand Down
38 changes: 17 additions & 21 deletions src/lib/error-reporting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
* Provides two things:
*
* 1. **Silencing rules** — `OutputError`, network failures (offline/DNS/proxy),
* `ContextError` (a required value the user omitted), `AuthError` (expected
* auth states the user must act on), 401–499 `ApiError`, and 400 `ApiError`s
* that report an unparseable user search query are not sent to Sentry as
* issues. A `cli.error.silenced` metric preserves volume + user/org context.
* `AuthError` (expected auth states the user must act on), 401–499 `ApiError`,
* and 400 `ApiError`s that report an unparseable user search query are not
* sent to Sentry as issues. A `cli.error.silenced` metric preserves volume +
* user/org context.
*
* `ContextError` (a required value the user omitted) is deliberately NOT
* silenced: its volume is the signal driving auto-detection/UX improvements
* (e.g. single-org auto-select, the interactive picker), so it must stay
* visible (CLI-3B).
*
* 2. **Grouping tags** — enriches every error event with `cli_error.*` tags
* that Sentry's server-side fingerprint rules use for stable grouping.
Expand Down Expand Up @@ -48,7 +53,6 @@
*/
type SilenceReason =
| "output_error"
| "context_missing"
| "auth_expected"
| "api_user_error"
| "api_query_error"
Expand All @@ -72,18 +76,16 @@
if (isNetworkError(error)) {
return "network_error";
}
// A ContextError always means the user omitted a required value (no
// org/project could be resolved, a required ID was not provided, etc.). It is
// never a CLI bug — unlike ResolutionError, where a *provided* value that
// can't be matched may signal a product/access issue worth observing. There
// is nothing per-instance to investigate, so silence the whole class; the
// `cli.error.silenced` metric (keyed by `error_class` + `resource`) preserves
// the volume and which value was missing. (CLI-3B: ~2000 users.)
if (error instanceof ContextError) {
return "context_missing";
}
// A ContextError means the user omitted a required value (no org/project
// could be resolved, a required ID was not provided, etc.). It is NOT
// silenced: the volume of these is the signal we use to drive auto-detection
// and UX improvements (single-org auto-select, the interactive picker, the
// enriched error). Silencing it hid that signal AND skipped the fix, so it
// stays captured (CLI-3B). The accompanying `resolveOrgProjectOrGuide`
// changes aim to drive this volume down by helping users succeed instead.
//
// All AuthError reasons are expected auth states the user must act on, not
// CLI bugs: `not_authenticated` (no token), `expired` (token aged out), and

Check warning on line 88 in src/lib/error-reporting.ts

View check run for this annotation

@sentry/warden / warden: find-bugs

Un-silencing ContextError also marks Sentry sessions as crashed, skewing release-health crash rate

`telemetry.ts:238` gates session-crash marking on the same `classifySilenced()` used for Sentry issue capture: `if (!classifySilenced(e)) { markSessionCrashed(); }`. Because this PR keeps `ContextError` un-silenced (so its volume stays visible — CLI-3B), `classifySilenced(ContextError)` returns `null`, so `markSessionCrashed()` now fires for every `ContextError`. These are user-input mistakes (omitted org/project, missing required ID), not CLI crashes, and the PR description estimates ~2000 users hit this path — all of those sessions will now be counted as crashed, inflating the release-health crash-free rate. This conflates two distinct concerns: keeping the error visible as a Sentry issue (desired) vs. counting it as a session crash (undesired for an expected user-input state).
// `invalid` (a bad/insufficiently-scoped token the user supplied). `invalid`
// is now only thrown for a genuine 401/403 (see auth/login.ts) — transient
// network/server failures no longer masquerade as it — so it is safe to
Expand Down Expand Up @@ -117,12 +119,6 @@
if (error instanceof AuthError) {
attributes.auth_reason = error.reason;
}
// Preserve which required value was missing so the silenced-volume metric
// keeps sub-grouping context (e.g. "Organization and project" vs "Event ID").
// ContextError resources are a small fixed set, so cardinality stays low.
if (error instanceof ContextError) {
attributes.resource = error.resource;
}

try {
Sentry.metrics.distribution("cli.error.silenced", 1, { attributes });
Expand Down
29 changes: 29 additions & 0 deletions src/lib/interactive-prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Process-wide gate for interactive prompts.
*
* Commands that emit machine-readable output (`--json`, or `SENTRY_OUTPUT_FORMAT=json`)
* must never block on an interactive prompt or interleave prompt UI with JSON on
* stdout. The command wrapper disables prompts for such runs via
* {@link setInteractivePromptsAllowed}, and prompt sites (e.g. the org/project
* picker in `resolve-target.ts`) consult {@link interactivePromptsAllowed}
* before showing a prompt.
*
* Defaults to `true` so code paths that run outside the command wrapper (tests,
* library callers) fall back to their own TTY checks rather than being silently
* blocked.
*/

let allowed = true;

/**
* Enable or disable interactive prompts for the current command run. Called by
* the command wrapper with `false` when JSON output is active.
*/
export function setInteractivePromptsAllowed(value: boolean): void {
allowed = value;
}

/** Whether interactive prompts are currently permitted. */
export function interactivePromptsAllowed(): boolean {
return allowed;
}
Loading
Loading