Skip to content
30 changes: 20 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,18 +563,28 @@ All CLI errors extend the `CliError` base class from `src/lib/errors.ts`:

```typescript
// Error hierarchy in src/lib/errors.ts
CliError (base)
├── ApiError (HTTP/API failures - status, detail, endpoint)
├── AuthError (authentication - reason: 'not_authenticated' | 'expired' | 'invalid')
├── ConfigError (configuration - suggestion?)
├── ContextError (missing context - resource, command, alternatives)
├── ResolutionError (value provided but not found - resource, headline, hint, suggestions)
├── ValidationError (input validation - field?)
├── DeviceFlowError (OAuth flow - code)
├── SeerError (Seer AI - reason: 'not_enabled' | 'no_budget' | 'ai_disabled')
└── UpgradeError (upgrade - reason: 'unknown_method' | 'network_error' | 'execution_failed' | 'version_not_found')
// Exit codes are defined in the EXIT constant object — use EXIT.* constants
// when constructing errors, never hardcode numeric exit codes outside errors.ts.
CliError (base, exitCode=1)
├── HostScopeError (exitCode=13)
├── ApiError (exitCode=30 — HTTP/API failures)
├── AuthError (exitCode=10–12 by reason — 'not_authenticated' | 'expired' | 'invalid')
├── ConfigError (exitCode=20 — configuration/DSN)
├── OutputError (exitCode=60 — data rendered, but operation failed)
├── ContextError (exitCode=22 — missing context)
├── ResolutionError (exitCode=23 — value provided but not found)
├── ValidationError (exitCode=21 — input validation)
├── DeviceFlowError (exitCode=51 — OAuth flow)
├── SeerError (exitCode=40–42 by reason — 'not_enabled' | 'no_budget' | 'ai_disabled')
├── TimeoutError (exitCode=31 — operation timed out)
├── UpgradeError (exitCode=50 — upgrade failures)
└── WizardError (exitCode=61–64 by workflow step — init wizard error)
```

> Exit code ranges: 1x=auth, 2x=input/config, 3x=API/network, 4x=feature/billing,
> 5x=operations, 6x=command-specific. See `EXIT` in `src/lib/errors.ts` and
> https://cli.sentry.dev/exit-codes/ for the full reference.

**Choosing between ContextError, ResolutionError, and ValidationError:**

| Scenario | Error Class | Example |
Expand Down
16 changes: 16 additions & 0 deletions docs/src/content/docs/agent-guidance.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ The `sentry` CLI follows conventions from well-known tools — if you're familia
- Never store or log authentication tokens — the CLI manages credentials automatically
- If the CLI reports the wrong org/project, override with explicit `<org>/<project>` arguments

## Exit Codes

The CLI uses semantic exit codes. Key ranges for agents:

| Range | Meaning | Agent Action |
|-------|---------|-------------|
| 0 | Success | Proceed normally |
| 10–19 | Auth error | Prompt user to run `sentry auth login` |
| 20–29 | Input error | Check command arguments and retry |
| 30–39 | API error | Retry or report to user |
| 40–49 | Feature unavailable | Inform user about plan/settings |
| 50–59 | Operation error | Report to user |
| 60–69 | Command-specific | Check stderr for details |

See [Exit Codes](/exit-codes/) for the complete reference.

## Workflow Patterns

### Investigate an Issue
Expand Down
100 changes: 100 additions & 0 deletions docs/src/content/docs/exit-codes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
title: Exit Codes
description: Exit code reference for scripting and automation with the Sentry CLI
---

The CLI uses semantic exit codes so scripts, CI pipelines, and AI agents can
react to failure categories without parsing stderr.

## Exit Code Ranges

| Range | Category | Description |
|-------|----------|-------------|
| 0 | Success | Command completed successfully |
| 1 | General | Unexpected or unclassified error |
| 10–19 | Auth | Authentication and authorization failures |
| 20–29 | Input | Configuration, validation, and resolution errors |
| 30–39 | API | Sentry API and network errors |
| 40–49 | Feature | Feature availability and billing issues |
| 50–59 | Operations | Upgrade and OAuth flow errors |
| 60–69 | Command | Command-specific non-standard exits |

## Complete Reference

| Code | Name | Description |
|------|------|-------------|
| 0 | Success | Command completed successfully |
| 1 | General Error | Unexpected error or unclassified failure |
| 10 | Not Authenticated | No credentials found — run `sentry auth login` |
| 11 | Token Expired | Auth token expired — re-authenticate |
| 12 | Token Invalid | Auth token rejected by the server |
| 13 | Host Scope | Request blocked — credentials don't match the target host |
| 20 | Config Error | Configuration or DSN problem |
| 21 | Validation Error | Invalid input (malformed ID, bad flag value, etc.) |
| 22 | Missing Context | Required context (org, project) could not be determined |
| 23 | Not Found | A user-provided identifier could not be resolved |
| 30 | API Error | Sentry API returned an error response |
| 31 | Timeout | Operation exceeded its time limit |
| 40 | Seer Not Enabled | Seer is not enabled for the organization |
| 41 | Seer No Budget | Seer requires a paid plan |
| 42 | AI Disabled | AI features disabled by organization admin |
| 50 | Upgrade Error | CLI upgrade operation failed |
| 51 | Device Flow Error | OAuth device authorization flow failed |
| 60 | Output Error | Command produced output but the operation failed |
| 61 | Wizard Error | Interactive setup wizard encountered an error |
| 62 | Wizard Deps | Wizard dependency installation failed |
| 63 | Wizard Codemod | Wizard codemod plan or apply failed |
| 64 | Wizard Verify | User stopped wizard after verification step |

## Scripting Examples

### Bash

```bash
sentry issue list my-org/
code=$?

case $code in
0) echo "Success" ;;
1?) echo "Auth problem (code $code) — run: sentry auth login" ;;
2?) echo "Input/config problem (code $code)" ;;
3?) echo "API/network error (code $code)" ;;
4?) echo "Feature not available (code $code)" ;;
*) echo "Failed with exit code $code" ;;
esac
```

### Python

```python
import subprocess

result = subprocess.run(["sentry", "issue", "list", "my-org/"], capture_output=True)

if result.returncode == 0:
print("Success")
elif 10 <= result.returncode <= 19:
print("Auth error — run: sentry auth login")
elif 20 <= result.returncode <= 29:
print("Input/config error")
elif 30 <= result.returncode <= 39:
print("API/network error")
elif 40 <= result.returncode <= 49:
print("Feature not available")
```

## Notes

- Exit codes below 128 are safe from collision with Unix signal exits (128+N).
- The `sentry api` command renders API error responses to stdout and exits
with code 60 (Output Error), not 30 (API Error). This matches the `gh api`
convention — the error response body is useful output. Parse the HTTP status
from `--verbose` output or the JSON error body if you need to distinguish
API error categories.
- The `sentry init` wizard maps its internal workflow exit codes to CLI
exit codes: platform not detected → 20 (Config), dependency install
failed → 62 (Wizard Deps), codemod failed → 63 (Wizard Codemod),
verification stopped → 64 (Wizard Verify), other → 61 (Wizard).
- [Stricli](https://bloomberg.github.io/stricli/) (the CLI framework) uses
negative exit codes (-5 to -1) for framework-level errors like unknown
commands or invalid arguments. These appear as 251–255 in unsigned form.
16 changes: 16 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ The `sentry` CLI follows conventions from well-known tools — if you're familia
- Never store or log authentication tokens — the CLI manages credentials automatically
- If the CLI reports the wrong org/project, override with explicit `<org>/<project>` arguments

### Exit Codes

The CLI uses semantic exit codes. Key ranges for agents:

| Range | Meaning | Agent Action |
|-------|---------|-------------|
| 0 | Success | Proceed normally |
| 10–19 | Auth error | Prompt user to run `sentry auth login` |
| 20–29 | Input error | Check command arguments and retry |
| 30–39 | API error | Retry or report to user |
| 40–49 | Feature unavailable | Inform user about plan/settings |
| 50–59 | Operation error | Report to user |
| 60–69 | Command-specific | Check stderr for details |

See [Exit Codes](/exit-codes/) for the complete reference.

### Workflow Patterns

#### Investigate an Issue
Expand Down
56 changes: 37 additions & 19 deletions src/commands/log/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
} from "../../lib/arg-parsing.js";
import { openInBrowser } from "../../lib/browser.js";
import { buildCommand } from "../../lib/command.js";
import { ContextError, ValidationError } from "../../lib/errors.js";
import {
ContextError,
ResolutionError,
ValidationError,
} from "../../lib/errors.js";
import { formatLogDetails } from "../../lib/formatters/index.js";
import { filterFields } from "../../lib/formatters/json.js";
import { CommandOutput } from "../../lib/formatters/output.js";
Expand Down Expand Up @@ -345,7 +349,7 @@ function retentionSuffix(logId: string): string {
* @param logIds - Requested IDs
* @param org - Organization slug
* @param project - Project slug
* @throws {ValidationError} Always
* @throws {ResolutionError} Always
*/
function throwNotFoundError(
logIds: string[],
Expand All @@ -356,33 +360,47 @@ function throwNotFoundError(
// edit in `retention.ts` keeps this message in sync with the
// deterministic retention-aware path.
const retentionDays = RETENTION_DAYS.log;
const genericHint = retentionDays
? `Make sure the log IDs are correct and were sent within the last ${retentionDays} days.`
: "Make sure the log IDs are correct.";

if (logIds.length === 1) {
const id = logIds[0] ?? "";
const suffix = retentionSuffix(id);
const hint = suffix
? `This log is no longer retrievable.${suffix}`
: genericHint.replace("log IDs are correct", "log ID is correct");
throw new ValidationError(
`No log found with ID "${id}" in ${org}/${project}.\n\n${hint}`
let suggestions: string[];
if (suffix) {
suggestions = [`This log is no longer retrievable.${suffix}`];
} else if (retentionDays) {
suggestions = [
`Make sure the log ID is correct and was sent within the last ${retentionDays} days`,
];
} else {
suggestions = ["Make sure the log ID is correct"];
}
throw new ResolutionError(
`Log '${id}'`,
`not found in ${org}/${project}`,
`sentry log view ${org}/${project}/${id}`,
suggestions
);
}

// Multiple IDs — compute the retention suffix once per ID so both the
// inline annotation and the "any expired?" check reuse the same decode.
// ID list and the "any expired?" check reuse the same decode.
const suffixed = logIds.map((id) => ({ id, suffix: retentionSuffix(id) }));
const annotated = suffixed
.map(({ id, suffix }) => ` - \`${id}\`${suffix}`)
.join("\n");
const anyExpired = suffixed.some(({ suffix }) => suffix !== "");
const hint = anyExpired
? "Expired log IDs are no longer retrievable. Check non-expired IDs and re-run."
: genericHint;
throw new ValidationError(
`No logs found with any of the following IDs in ${org}/${project}:\n${annotated}\n\n${hint}`
const idList = suffixed.map(({ id, suffix }) => `${id}${suffix}`);
let hint: string;
if (anyExpired) {
hint =
"Expired log IDs are no longer retrievable — check non-expired IDs and re-run";
} else if (retentionDays) {
hint = `Make sure the log IDs are correct and were sent within the last ${retentionDays} days`;
} else {
hint = "Make sure the log IDs are correct";
}
throw new ResolutionError(
`${idList.length} log(s)`,
`not found in ${org}/${project}`,
hint,
idList.map((id) => `ID: ${id}`)
);
}

Expand Down
18 changes: 13 additions & 5 deletions src/commands/trace/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import {
} from "../../lib/arg-parsing.js";
import { openInBrowser } from "../../lib/browser.js";
import { buildCommand } from "../../lib/command.js";
import { ContextError, ValidationError } from "../../lib/errors.js";
import {
ContextError,
ResolutionError,
ValidationError,
} from "../../lib/errors.js";
import {
computeTraceSummary,
formatSimpleSpanTree,
Expand Down Expand Up @@ -558,10 +562,14 @@ export const viewCommand = buildCommand({
});

if (spans.length === 0) {
throw new ValidationError(
`No trace found with ID "${traceId}".\n\n` +
"The ID format is valid but no matching trace exists in this project. " +
"Check that you are querying the right org/project, or the trace may be past your plan's retention window."
throw new ResolutionError(
`Trace '${traceId}'`,
"not found",
`sentry trace view ${org}/${project ?? "<project>"}/${traceId}`,
[
"Check that you are querying the right org/project",
"The trace may be past your plan's retention window",
]
);
}

Expand Down
Loading
Loading