From f6df42a70ae007009350b430e3582c59c9cc86c9 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 28 Feb 2026 10:33:50 +0000 Subject: [PATCH 1/3] refactor(api): wire listIssuesPaginated through @sentry/api SDK for type safety - Add `unwrapPaginatedResult` helper that unwraps SDK result data while also extracting the Link header for cursor pagination - Export `IssueSort` type derived from `ListAnOrganizationSissuesData` so sort options stay in sync with the SDK automatically - Replace hand-rolled `orgScopedRequestPaginated` call in `listIssuesPaginated` with `listAnOrganization_sIssues` + `getOrgSdkConfig` - Update `listIssuesAllPages` sort parameter to use the exported `IssueSort` - Remove the now-redundant hand-maintained sort union literal --- AGENTS.md | 435 ++++++++++++++++++++++++++++++++++++++++++ src/lib/api-client.ts | 80 ++++++-- 2 files changed, 494 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 709dcebf..2de6ba9f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1231,3 +1231,438 @@ mock.module("./some-module", () => ({ * **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. + + +## Long-term Knowledge + +### Architecture + + +* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. + +* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. + +* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. + +* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. + +* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. + +### Decision + + +* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. + +* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. + +* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) + +### Gotcha + + +* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components + +* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling + +* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. + +* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. + +* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. + +* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. + +* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. + +* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). + +* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. + +* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. + +* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. + +* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. + +* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. + +### Pattern + + +* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits + +* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. + +* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. + +* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. + +* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. + +* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. + +* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. + +* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. + +* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. + +* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require + +### Preference + + +* **General coding preference**: Prefer explicit error handling over silent failures + +* **Code style**: User prefers no backwards-compat shims, fix callers directly + +* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. + + + +## Long-term Knowledge + +### Architecture + + +* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. + +* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. + +* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. + +* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. + +* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. + +### Decision + + +* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. + +* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. + +* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) + +### Gotcha + + +* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components + +* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling + +* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. + +* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. + +* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. + +* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. + +* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. + +* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). + +* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. + +* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. + +* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. + +* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. + +* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. + +### Pattern + + +* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits + +* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. + +* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. + +* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. + +* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. + +* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. + +* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. + +* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. + +* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. + +* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require + +### Preference + + +* **General coding preference**: Prefer explicit error handling over silent failures + +* **Code style**: User prefers no backwards-compat shims, fix callers directly + +* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. + + + +## Long-term Knowledge + +### Architecture + + +* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. + +* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. + +* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. + +* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. + +* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. + +### Decision + + +* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. + +* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. + +* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) + +### Gotcha + + +* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components + +* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling + +* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. + +* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. + +* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. + +* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. + +* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. + +* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). + +* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. + +* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. + +* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. + +* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. + +* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. + +### Pattern + + +* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits + +* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. + +* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. + +* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. + +* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. + +* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. + +* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. + +* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. + +* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. + +* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require + +### Preference + + +* **General coding preference**: Prefer explicit error handling over silent failures + +* **Code style**: User prefers no backwards-compat shims, fix callers directly + +* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. + + + +## Long-term Knowledge + +### Architecture + + +* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. + +* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. + +* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. + +* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. + +* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. + +### Decision + + +* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. + +* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. + +* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) + +### Gotcha + + +* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components + +* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling + +* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. + +* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. + +* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. + +* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. + +* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. + +* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). + +* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. + +* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. + +* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. + +* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. + +* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. + +### Pattern + + +* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits + +* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. + +* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. + +* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. + +* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. + +* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. + +* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. + +* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. + +* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. + +* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require + +### Preference + + +* **General coding preference**: Prefer explicit error handling over silent failures + +* **Code style**: User prefers no backwards-compat shims, fix callers directly + +* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. + + + +## Long-term Knowledge + +### Architecture + + +* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. + +* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. + +* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. + +* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. + +* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. + +### Decision + + +* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. + +* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. + +* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) + +### Gotcha + + +* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components + +* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling + +* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. + +* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. + +* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. + +* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. + +* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. + +* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). + +* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. + +* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. + +* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. + +* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. + +* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. + +### Pattern + + +* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits + +* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. + +* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. + +* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. + +* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. + +* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. + +* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. + +* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. + +* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. + +* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require + +### Preference + + +* **General coding preference**: Prefer explicit error handling over silent failures + +* **Code style**: User prefers no backwards-compat shims, fix callers directly + +* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. + diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index fb5f7b3f..03d40e09 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -8,7 +8,9 @@ * Falls back to raw requests for internal/undocumented endpoints. */ +import type { ListAnOrganizationSissuesData } from "@sentry/api"; import { + listAnOrganization_sIssues, listAnOrganization_sTeams, listAProject_sClientKeys, listAProject_sTeams, @@ -129,6 +131,26 @@ function unwrapResult( return data as T; } +/** + * Unwrap an @sentry/api SDK result AND extract pagination from the Link header. + * + * Unlike {@link unwrapResult} which discards the Response, this preserves the + * Link header for cursor-based pagination. Use for SDK-backed paginated endpoints. + * + * @param result - The result from an SDK function call (includes `response`) + * @param context - Human-readable context for error messages + * @returns Data and optional next-page cursor + */ +function unwrapPaginatedResult( + result: { data: T; error: undefined } | { data: undefined; error: unknown }, + context: string +): PaginatedResponse { + const response = (result as { response?: Response }).response; + const data = unwrapResult(result, context); + const { nextCursor } = parseLinkHeader(response?.headers.get("link") ?? null); + return { data, nextCursor }; +} + /** * Build URLSearchParams from an options object, filtering out undefined values. * Supports string arrays for repeated keys (e.g., { tags: ["a", "b"] } → tags=a&tags=b). @@ -995,24 +1017,33 @@ export async function getProjectKeys( // Issue functions +/** + * Sort options for issue listing, derived from the @sentry/api SDK types. + * Uses the SDK type directly for compile-time safety against parameter drift. + */ +export type IssueSort = NonNullable< + NonNullable["sort"] +>; + /** * List issues for a project with pagination control. - * Returns a single page of results with cursor metadata for manual pagination. - * Uses the org-scoped endpoint with a `project:{slug}` filter. + * + * Uses the @sentry/api SDK's `listAnOrganization_sIssues` for type-safe + * query parameters, and extracts pagination from the response Link header. * * @param orgSlug - Organization slug - * @param projectSlug - Project slug + * @param projectSlug - Project slug (empty string for org-wide listing) * @param options - Query and pagination options * @returns Single page of issues with cursor metadata */ -export function listIssuesPaginated( +export async function listIssuesPaginated( orgSlug: string, projectSlug: string, options: { query?: string; cursor?: string; perPage?: number; - sort?: "date" | "new" | "freq" | "user"; + sort?: IssueSort; statsPeriod?: string; } = {} ): Promise> { @@ -1022,21 +1053,28 @@ export function listIssuesPaginated( const projectFilter = projectSlug ? `project:${projectSlug}` : ""; const fullQuery = [projectFilter, options.query].filter(Boolean).join(" "); - return orgScopedRequestPaginated( - `/organizations/${orgSlug}/issues/`, - { - params: { - // Convert empty string to undefined so ky omits the param entirely; - // sending `query=` causes the Sentry API to behave differently than - // omitting the parameter. - query: fullQuery || undefined, - cursor: options.cursor, - // The issues endpoint uses `limit` (not `per_page`) to control page size. - limit: options.perPage ?? 25, - sort: options.sort, - statsPeriod: options.statsPeriod, - }, - } + const config = await getOrgSdkConfig(orgSlug); + + const result = await listAnOrganization_sIssues({ + ...config, + path: { organization_id_or_slug: orgSlug }, + query: { + // Convert empty string to undefined so the SDK omits the param entirely; + // sending `query=` causes the Sentry API to behave differently than + // omitting the parameter. + query: fullQuery || undefined, + cursor: options.cursor, + limit: options.perPage ?? 25, + sort: options.sort, + statsPeriod: options.statsPeriod, + }, + }); + + return unwrapPaginatedResult( + result as + | { data: SentryIssue[]; error: undefined } + | { data: undefined; error: unknown }, + "Failed to list issues" ); } @@ -1071,7 +1109,7 @@ export async function listIssuesAllPages( options: { query?: string; limit: number; - sort?: "date" | "new" | "freq" | "user"; + sort?: IssueSort; statsPeriod?: string; /** Resume pagination from this cursor instead of starting from the beginning. */ startCursor?: string; From f1e12a904c6025f9c92ee441f99896bf7dc41ce2 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 28 Feb 2026 13:25:12 +0000 Subject: [PATCH 2/3] chore: remove duplicated Long-term Knowledge sections from AGENTS.md Lore appended 12 duplicate copies of the Long-term Knowledge section (~1044 extra lines) below the canonical auto-maintained section. Keep only the single authoritative copy (lines 626-711). --- AGENTS.md | 957 ------------------------------------------------------ 1 file changed, 957 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2de6ba9f..fb92cb28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -709,960 +709,3 @@ mock.module("./some-module", () => ({ * **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - - -## Long-term Knowledge - -### Architecture - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. - -* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. - -* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. - -* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. - -### Decision - - -* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. - -* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. - -* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) - -### Gotcha - - -* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components - -* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling - -* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. - -* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. - -* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. - -* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. - -* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). - -* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. - -* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. - -* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. - -* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. - -* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. - -### Pattern - - -* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits - -* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. - -* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. - -* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. - -* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. - -* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. - -* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. - -* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. - -* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. - -* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require - -### Preference - - -* **General coding preference**: Prefer explicit error handling over silent failures - -* **Code style**: User prefers no backwards-compat shims, fix callers directly - -* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - - - -## Long-term Knowledge - -### Architecture - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. - -* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. - -* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. - -* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. - -### Decision - - -* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. - -* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. - -* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) - -### Gotcha - - -* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components - -* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling - -* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. - -* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. - -* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. - -* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. - -* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). - -* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. - -* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. - -* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. - -* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. - -* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. - -### Pattern - - -* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits - -* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. - -* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. - -* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. - -* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. - -* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. - -* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. - -* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. - -* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. - -* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require - -### Preference - - -* **General coding preference**: Prefer explicit error handling over silent failures - -* **Code style**: User prefers no backwards-compat shims, fix callers directly - -* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - - - -## Long-term Knowledge - -### Architecture - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. - -* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. - -* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. - -* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. - -### Decision - - -* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. - -* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. - -* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) - -### Gotcha - - -* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components - -* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling - -* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. - -* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. - -* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. - -* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. - -* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). - -* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. - -* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. - -* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. - -* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. - -* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. - -### Pattern - - -* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits - -* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. - -* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. - -* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. - -* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. - -* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. - -* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. - -* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. - -* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. - -* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require - -### Preference - - -* **General coding preference**: Prefer explicit error handling over silent failures - -* **Code style**: User prefers no backwards-compat shims, fix callers directly - -* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - - - -## Long-term Knowledge - -### Architecture - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. - -* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. - -* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. - -* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. - -### Decision - - -* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. - -* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. - -* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) - -### Gotcha - - -* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components - -* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling - -* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. - -* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. - -* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. - -* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. - -* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). - -* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. - -* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. - -* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. - -* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. - -* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. - -### Pattern - - -* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits - -* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. - -* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. - -* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. - -* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. - -* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. - -* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. - -* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. - -* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. - -* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require - -### Preference - - -* **General coding preference**: Prefer explicit error handling over silent failures - -* **Code style**: User prefers no backwards-compat shims, fix callers directly - -* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - - - -## Long-term Knowledge - -### Architecture - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. - -* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. - -* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. - -* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. - -### Decision - - -* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. - -* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. - -* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) - -### Gotcha - - -* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components - -* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling - -* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. - -* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. - -* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. - -* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. - -* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). - -* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. - -* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. - -* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. - -* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. - -* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. - -### Pattern - - -* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits - -* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. - -* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. - -* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. - -* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. - -* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. - -* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. - -* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. - -* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. - -* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require - -### Preference - - -* **General coding preference**: Prefer explicit error handling over silent failures - -* **Code style**: User prefers no backwards-compat shims, fix callers directly - -* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - - - -## Long-term Knowledge - -### Architecture - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. - -* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. - -* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. - -* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. - -### Decision - - -* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. - -* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. - -* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) - -### Gotcha - - -* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components - -* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling - -* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. - -* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. - -* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. - -* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. - -* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). - -* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. - -* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. - -* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. - -* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. - -* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. - -### Pattern - - -* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits - -* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. - -* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. - -* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. - -* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. - -* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. - -* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. - -* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. - -* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. - -* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require - -### Preference - - -* **General coding preference**: Prefer explicit error handling over silent failures - -* **Code style**: User prefers no backwards-compat shims, fix callers directly - -* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - - - -## Long-term Knowledge - -### Architecture - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. - -* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. - -* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. - -* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. - -### Decision - - -* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. - -* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. - -* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) - -### Gotcha - - -* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components - -* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling - -* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. - -* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. - -* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. - -* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. - -* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). - -* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. - -* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. - -* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. - -* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. - -* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. - -### Pattern - - -* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits - -* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. - -* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. - -* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. - -* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. - -* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. - -* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. - -* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. - -* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. - -* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require - -### Preference - - -* **General coding preference**: Prefer explicit error handling over silent failures - -* **Code style**: User prefers no backwards-compat shims, fix callers directly - -* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - - - -## Long-term Knowledge - -### Architecture - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. - -* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. - -* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. - -* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. - -### Decision - - -* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. - -* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. - -* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) - -### Gotcha - - -* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components - -* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling - -* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. - -* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. - -* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. - -* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. - -* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). - -* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. - -* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. - -* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. - -* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. - -* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. - -### Pattern - - -* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits - -* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. - -* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. - -* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. - -* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. - -* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. - -* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. - -* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. - -* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. - -* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require - -### Preference - - -* **General coding preference**: Prefer explicit error handling over silent failures - -* **Code style**: User prefers no backwards-compat shims, fix callers directly - -* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - - - -## Long-term Knowledge - -### Architecture - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. - -* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. - -* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. - -* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. - -### Decision - - -* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. - -* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. - -* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) - -### Gotcha - - -* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components - -* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling - -* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. - -* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. - -* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. - -* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. - -* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). - -* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. - -* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. - -* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. - -* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. - -* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. - -### Pattern - - -* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits - -* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. - -* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. - -* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. - -* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. - -* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. - -* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. - -* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. - -* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. - -* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require - -### Preference - - -* **General coding preference**: Prefer explicit error handling over silent failures - -* **Code style**: User prefers no backwards-compat shims, fix callers directly - -* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - - - -## Long-term Knowledge - -### Architecture - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. - -* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. - -* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. - -* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. - -### Decision - - -* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. - -* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. - -* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) - -### Gotcha - - -* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components - -* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling - -* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. - -* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. - -* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. - -* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. - -* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). - -* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. - -* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. - -* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. - -* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. - -* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. - -### Pattern - - -* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits - -* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. - -* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. - -* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. - -* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. - -* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. - -* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. - -* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. - -* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. - -* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require - -### Preference - - -* **General coding preference**: Prefer explicit error handling over silent failures - -* **Code style**: User prefers no backwards-compat shims, fix callers directly - -* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - - - -## Long-term Knowledge - -### Architecture - - -* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry's event-fetching API endpoint is \`GET /api/0/projects/{org}/{project}/events/{event\_id}/\` — requires both org and project in the URL path. There is NO equivalent of the legacy \`/api/0/issues/{id}/\` endpoint for events. Event IDs (UUIDs) are project-scoped in Sentry's storage layer (ClickHouse/Snuba). Contrast with issues: \`getIssue()\` uses \`/api/0/issues/{id}/\` which works WITHOUT org context (issues have global numeric IDs). The issue response includes \`project.slug\` and \`organization.slug\`, enabling a two-step lookup: fetch issue → extract org/project → fetch event. For traces: \`getDetailedTrace()\` uses \`/organizations/{org}/trace/{traceId}/\` — needs only org, not project. Possible workaround for event view without org/project: use the Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\` and \`dataset=errors\` to search across all projects in an org. - -* **Sentry CLI has two distribution channels with different runtimes**: The Sentry CLI ships via two completely independent build pipelines: 1. \*\*Standalone binary\*\* (GitHub Releases): Built with \`Bun.build()\` + \`compile: true\` via \`script/build.ts\`. Produces native executables (\`sentry-{platform}-{arch}\`) with Bun runtime embedded. Runs under Bun. 2. \*\*npm package\*\*: Built with esbuild via \`script/bundle.ts\`. Produces a single minified CJS file (\`dist/bin.cjs\`) with \`#!/usr/bin/env node\` shebang. Requires Node.js 22+ (for \`node:sqlite\`). Key esbuild settings: \`platform: 'node'\`, \`target: 'node22'\`, \`format: 'cjs'\`. Aliases \`@sentry/bun\` → \`@sentry/node\`. Injects Bun API polyfills from \`script/node-polyfills.ts\`. Bun API polyfills cover: \`Bun.file()\`, \`Bun.write()\`, \`Bun.which()\`, \`Bun.spawn()\`, \`Bun.sleep()\`, \`Bun.Glob\`, \`Bun.randomUUIDv7()\`, \`Bun.semver.order()\`, and \`bun:sqlite\` (→ \`node:sqlite\` DatabaseSync). The npm bundle is CJS, so \`require()\` calls in source are native and resolved at bundle time by esbuild — no ESM/CJS conflict. CI smoke-tests with \`node dist/bin.cjs --help\` on Node 22 and 24. - -* **gh CLI config directory convention and XDG compliance**: The \`gh\` CLI (GitHub CLI), which is the explicit UX model for Sentry CLI per AGENTS.md, stores config at: \`$GH\_CONFIG\_DIR\` > \`$XDG\_CONFIG\_HOME/gh\` > \`$HOME/.config/gh\` (follows XDG on all platforms including macOS). Most other major CLIs (docker, aws, kubectl, cargo) use \`~/.toolname/\` rather than XDG. macOS \`~/Library/Application Support/\` is Apple-blessed for app data but uncommon for CLI tools and surprising to developers. The Sentry CLI currently uses \`~/.sentry/\` with \`SENTRY\_CONFIG\_DIR\` as an override env var. - -* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The Sentry CLI API client (src/lib/api-client.ts) guarantees that all errors thrown by API functions like getCurrentUser() are CliError subclasses (ApiError or AuthError). In unwrapResult(), known error types (AuthError, ApiError) are re-thrown directly, and everything else — including raw network TypeErrors from ky — goes through throwApiError() which wraps them as ApiError. This means command implementations do NOT need their own try-catch for error display: the central error handler in app.ts exceptionWhileRunningCommand catches CliError and displays a clean message without stack trace. Only add try-catch when the command needs to handle the error specially (e.g., login needs to continue without user info on failure, not crash). The Seer bot flagged whoami.ts for lacking try-catch around getCurrentUser() — this was a false positive because of this guarantee. - -* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: The resolve-target module (src/lib/resolve-target.ts) resolves org/project context through a strict 5-level priority cascade: 1. Explicit CLI flags (both org and project must be provided together) 2. SENTRY\_ORG / SENTRY\_PROJECT environment variables 3. Config defaults (SQLite defaults table) 4. DSN auto-detection (source code, .env files, SENTRY\_DSN env var) 5. Directory name inference (matches project slugs with word boundaries) SENTRY\_PROJECT supports combo notation: \`SENTRY\_PROJECT=org/project\` (slash presence auto-splits). When combo form is used, SENTRY\_ORG is ignored. If SENTRY\_PROJECT contains a slash but the combo parse fails (e.g. \`org/\` or \`/project\`), the entire SENTRY\_PROJECT value is discarded — it does NOT fall through to be used as a plain project slug alongside SENTRY\_ORG. Only SENTRY\_ORG (if set) provides the org in this case. The resolveFromEnvVars() helper is injected into all four resolution functions: resolveAllTargets, resolveOrgAndProject, resolveOrg, and resolveOrgsForListing. This matches the convention used by legacy sentry-cli and Sentry Webpack plugin. Added in PR #280. - -### Decision - - -* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\` → \`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing. - -* **Issue list global limit with fair per-project distribution and representation guarantees**: The \`issue list\` command's \`--limit\` flag specifies a global total across all detected projects, not per-project. The fetch strategy uses two phases in \`fetchWithBudget\`: Phase 1 divides the limit evenly (\`ceil(limit / numTargets)\`) and fetches in parallel. Phase 2 (\`runPhase2\`) redistributes surplus budget to targets that hit their quota and have more results (via cursor resume using \`startCursor\` param added to \`listIssuesAllPages\`). After fetching, \`trimWithProjectGuarantee\` ensures at least 1 issue per project is shown before filling remaining slots from the globally-sorted list. This prevents high-volume projects from completely hiding quiet ones. When more projects exist than the limit, the projects with the highest-ranked first issues get representation. JSON output for multi-target mode wraps in \`{ data: \[...], hasMore: bool }\` (with optional \`errors\` array) to align with org-all mode's existing \`data\` wrapper. A compound cursor is stored so \`-c last\` can resume multi-target pagination. - -* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Decision: Don't move the Sentry CLI config directory from ~/.sentry/ to ~/.config/sentry/ or ~/Library/Application Support/. The readonly database errors seen in telemetry (100% macOS) are caused by Unix permission issues from \`sudo brew install\` creating root-owned files, not by the directory location. Moving to any other path would have identical permission problems if created by root. The SENTRY\_CONFIG\_DIR env var already exists as an escape hatch. All three fixes have been implemented in PR #288: 1. Setup steps are non-fatal with bestEffort() try-catch wrapper 2. tryRepairReadonly() detects root-owned files and prints actionable \`sudo chown -R \ ~/.sentry\` message 3. \`sentry cli fix\` command handles ownership detection and repair (chown when run as root via sudo) - -### Gotcha - - -* **React useState async pitfall**: React useState setter is async — reading state immediately after setState returns stale value in dashboard components - -* **TypeScript strict mode caveat**: TypeScript strict null checks require explicit undefined handling - -* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install support for Sentry CLI was merged to main via PR #277. The implementation includes: 'brew' as a valid installation method in \`parseInstallationMethod()\` (src/lib/upgrade.ts), \`isHomebrewInstall()\` detection via Cellar realpath check (always checked first before stored install info), version pinning errors with dedicated 'unsupported\_operation' error reason, architecture validation, and post\_install setup that skips redundant work on \`brew upgrade\`. The upgrade command includes a 'brew' case that tells users to run \`brew upgrade getsentry/tools/sentry\`. Uses .gz compressed artifacts. The Homebrew formula lives in a tap at getsentry/tools. - -* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **Codecov patch coverage requires --coverage flag on ALL test invocations**: In the Sentry CLI, the test script runs \`bun run test:unit && bun run test:isolated\`. Only \`test:unit\` has \`--coverage --coverage-reporter=lcov\`. The \`test:isolated\` run (for tests using \`mock.module()\`) does NOT generate coverage. This means code paths exercised only in isolated tests won't count toward Codecov patch coverage. To boost patch coverage, either: (1) add \`--coverage\` to the isolated test script, or (2) write additional unit tests that call the real (non-mocked) functions where possible. For env var resolution, since env vars short-circuit at step 2 before any DB/API calls, unit tests can call the real resolve functions without mocking dependencies. - -* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses (e.g., switching getCurrentUser() from /users/me/ to /auth/), the mock route must be updated in BOTH test/mocks/routes.ts (single-region) AND test/mocks/multiregion.ts createControlSiloRoutes() (multi-region). Missing the multiregion mock causes 404s in multi-region test scenarios. The multiregion control silo mock serves auth, user info, and region discovery routes. Cursor Bugbot caught this gap when /api/0/auth/ was added to routes.ts but not multiregion.ts. - -* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens (including OAuth App tokens). The \`/auth/\` endpoint works with ALL token types (OAuth, API tokens, OAuth App tokens) and returns the authenticated user's information. \`/auth/\` lives on the control silo (sentry.io for SaaS, not regional endpoints). The sentry-mcp project uses this pattern: always route \`/auth/\` to the main sentry.io host for SaaS, bypassing regional endpoints. In the Sentry CLI, \`getControlSiloUrl()\` already handles this routing correctly. The \`getCurrentUser()\` function in \`src/lib/api-client.ts\` should use \`/auth/\` instead of \`/users/me/\`. The \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\` and makes \`email\`, \`username\`, \`name\` optional. - -* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks state across test files run in the same bun test process. If test file A mocks 'src/lib/api-client.js' to stub listOrganizations, ALL subsequent test files in that process see the mock instead of the real module. This caused ~100 test failures when test/isolated/resolve-target.test.ts ran alongside unit tests. Solution: tests using mock.module() must run in a separate bun test invocation (separate process). In package.json, the 'test' script uses 'bun run test:unit && bun run test:isolated' instead of 'bun test' to ensure process isolation. The test/isolated/ directory exists specifically for tests that use mock.module(). The file even documents this with a comment about Bun leaking mock.module() state (referencing getsentry/cli#258). - -* **Test suite has 131 pre-existing failures from DB schema drift and mock issues**: The Sentry CLI test suite had 131 pre-existing failures (1902 pass) caused by two root issues: (1) Bun's mock.module() in test/isolated/resolve-target.test.ts leaked globally, poisoning api-client.js (listOrganizations → undefined), db/defaults.js, db/project-cache.js, db/dsn-cache.js, and dsn/index.js for all subsequent test files — this caused ~80% of failures. (2) Minor issues: project root path resolution in temp dirs (11), DSN Detector module tests (17), E2E timeouts (2). Fix: changed package.json 'test' script from 'bun test' to 'bun run test:unit && bun run test:isolated' so isolated tests with mock.module() run in a separate Bun process. Result: 1931 tests, 0 failures. Key lesson: Bun's mock.module() replaces the ENTIRE barrel module globally and leaks across test files in the same process — tests using mock.module() must be isolated in separate bun test invocations. - -* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table could be created with a single-column PK (command\_key TEXT PRIMARY KEY) by earlier code versions, instead of the expected composite PK (PRIMARY KEY (command\_key, context)). This caused 'SQLiteError: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint' at runtime. Fixed by: (1) migration 5→6 that detects wrong PK via hasCompositePrimaryKey() and drops/recreates the table, (2) repairWrongPrimaryKeys() in repairSchema() for auto-repair, (3) isSchemaError() now catches 'on conflict clause does not match' to trigger auto-repair, (4) getSchemaIssues() reports 'wrong\_primary\_key' diagnostic. CURRENT\_SCHEMA\_VERSION bumped to 6. Data loss is acceptable since pagination cursors are ephemeral (5-min TTL). The hasCompositePrimaryKey() helper inspects sqlite\_master DDL for the expected PRIMARY KEY clause. - -* **resolveCursor must be called inside org-all closure, not before dispatch**: In list commands using dispatchOrgScopedList with cursor pagination (e.g., project/list.ts), resolveCursor() must be called inside the 'org-all' override closure, not before dispatchOrgScopedList. If called before, it throws a ContextError before dispatch can throw the correct ValidationError for --cursor being used in non-org-all modes. - -* **Bun.$ (shell tagged template) has no Node.js polyfill in Sentry CLI**: The Sentry CLI's node-polyfills.ts (used for the npm/Node.js distribution) provides shims for Bun.file(), Bun.write(), Bun.which(), Bun.spawn(), Bun.sleep(), Bun.Glob, Bun.randomUUIDv7(), Bun.semver.order(), and bun:sqlite — but NOT for Bun.$ (the tagged template shell). Any source code using Bun.$\`command\` will crash when running via the npm distribution (node dist/bin.cjs). Use execSync from node:child\_process instead for shell commands that need to work in both runtimes. The Bun.which polyfill already uses this pattern. As of PR #288, there are zero Bun.$ usages in the source code. AGENTS.md has been updated to document this: the Bun.$ row in the Quick Bun API Reference table has a ⚠️ warning, and a new exception block shows the execSync workaround. The phrasing 'Until a shim is added' signals that adding a Bun.$ polyfill is a desired future improvement. When someone adds the shim, remove the exception note and the ⚠️ from the table. - -* **handleProjectSearch ContextError resource must be "Project" not config.entityName**: In src/lib/org-list.ts handleProjectSearch, the first argument to ContextError is the resource name rendered as "${resource} is required.". Always pass "Project" (not config.entityName like "team" or "repository") since the error is about a missing project slug, not a missing entity of the command's type. A code comment documents the rationale inline. - -### Pattern - - -* **Kubernetes deployment pattern**: Use helm charts for Kubernetes deployments with resource limits - -* **Ownership check before permission check in CLI fix commands**: When a CLI fix/repair command checks filesystem health, ownership issues must be diagnosed BEFORE permission issues. If files are root-owned, chmod will fail with EPERM anyway — no point attempting it. The ordering in \`sentry cli fix\` is: (1) ownership check (stat().uid vs process.getuid()), (2) permission check (only if ownership is OK), (3) schema check (only if filesystem is accessible). When ownership repair succeeds (via sudo), skip the permission check since chown doesn't change mode bits — permissions may still need fixing on a subsequent non-sudo run. - -* **Sentry CLI Pattern A error: 133 events from missing context, mostly AI agents**: The #1 user error pattern in Sentry CLI (CLI-17, 110+ events, 50 users) is 'Organization and project is required' — users running commands without any org/project context. Breakdown by command: issue list (54), event view (41), issues alias (9), trace view (4). 24/110 events have --json flag (AI agent/CI usage). Many event view calls pass just an event ID with no org/project positional arg. Users are on CI (Ubuntu on Azure) or macOS with --json. The fix is multi-pronged: (1) SENTRY\_ORG/SENTRY\_PROJECT env var support (PR #280), (2) improved error messages mentioning env vars, (3) future: event view cross-project search. - -* **Store analysis/plan files in gitignored .plans/ directory**: For the Sentry CLI project, analysis and planning documents (like bug triage notes from automated review tools) are stored in .plans/ directory which is added to .gitignore. This keeps detailed analysis accessible locally without cluttering the repository. Previously such analysis was being added to AGENTS.md which is committed. - -* **Sentry CLI issue resolution wraps getIssue 404 in ContextError with ID hint**: In resolveIssue() (src/commands/issue/utils.ts), bare getIssue() calls for numeric and explicit-org-numeric cases should catch ApiError with status 404 and re-throw as ContextError that includes: (1) the ID the user provided, (2) a hint about access/deletion, (3) a suggestion to use short-ID format (\-\) if the user confused numeric group IDs with short-ID suffixes. Without this, the user gets a generic 'Issue not found' without knowing which ID failed or what to try instead. - -* **Sentry CLI setFlagContext redacts sensitive flags before telemetry**: The setFlagContext() function in src/lib/telemetry.ts must redact sensitive flag values (like --token) before setting Sentry tags. A SENSITIVE\_FLAGS set contains flag names that should have their values replaced with '\[REDACTED]' instead of the actual value. This prevents secrets from leaking into telemetry. The scrub happens at the source (in setFlagContext itself) rather than in beforeSend, so the sensitive value never reaches the Sentry SDK. - -* **Non-essential DB cache writes should be guarded with try-catch**: In the Sentry CLI, commands that write to the local SQLite cache as a side effect (e.g., setUserInfo() to update cached user identity) should wrap those writes in try-catch when the write is not essential to the command's primary purpose. If the DB is in a bad state (read-only filesystem, corrupted, schema mismatch), the cache write would throw and crash the command even though the primary operation (e.g., displaying user identity, completing login) already succeeded. Pattern: wrap non-essential setUserInfo() calls in try-catch, silently swallow errors. Applied in both whoami.ts and login.ts. Cursor Bugbot flagged the whoami.ts case — the cache update is a nice-to-have side effect that shouldn't prevent showing the fetched user data. - -* **Login --token flow: getCurrentUser failure must not block authentication**: In src/commands/auth/login.ts --token flow, the token is saved via setAuthToken() before fetching user info via getCurrentUser(). If getCurrentUser() fails after the token is saved, the user would be in an inconsistent state (isAuthenticated() true, getUserInfo() undefined). The fix: wrap getCurrentUser()+setUserInfo() in try-catch, log warning to stderr on failure but let login succeed. The 'Logged in as' line is conditional on user info being available. This differs from getUserRegions() failure which should clearAuth() and fail hard (indicates invalid token). Both Sentry Seer and Cursor Bugbot flagged this as a real bug and both suggested the same fix pattern. - -* **Sentry CLI api command: normalizeFields auto-corrects colon separators with stderr warning**: The \`sentry api\` command's \`--field\` flag requires \`key=value\` format with \`=\` separator. Users frequently confuse this with Sentry search syntax (\`key:value\`) or pass timestamps containing colons (e.g., \`since:2026-02-25T11:20:00\`). The \`normalizeFields()\` function in \`src/commands/api.ts\` auto-corrects \`:\` to \`=\` (splitting on first colon) and prints a warning to stderr, rather than crashing. This is safe because the correction only triggers when no \`=\` exists at all (the field would fail anyway). The normalization runs at the command level in \`func()\` before \`prepareRequestOptions()\`, keeping the parsing functions pure. Fields with \`=\` or ending in \`\[]\` pass through unchanged. The three downstream parsing functions (\`processField\`, \`buildQueryParams\`, \`buildRawQueryParams\`) use \`ValidationError\` instead of raw \`Error\` for truly uncorrectable fields, ensuring clean formatting through the central error handler. PR #302, fixes CLI-9H and CLI-93. - -* **Bun supports require() in ESM modules natively — ignore ESM-only linter warnings**: Bun natively supports \`require()\` in ESM modules (files with \`"type": "module"\` in package.json). This is different from Node.js where require() is not available in ESM. AI code reviewers (BugBot, Seer) may flag \`require()\` in ESM as high-severity bugs — these are false positives when the runtime is Bun. The Sentry CLI uses \`require()\` for lazy dynamic imports to break circular dependencies (e.g. \`require('../app.js')\` in list-command.ts to extract route subcommand names). For the npm distribution, this is also safe because esbuild bundles everything into a single CJS file (\`dist/bin.cjs\`) where \`require()\` is native — esbuild resolves and inlines all requires at bundle time. Bun docs: https://bun.sh/docs/runtime/modules#using-require - -### Preference - - -* **General coding preference**: Prefer explicit error handling over silent failures - -* **Code style**: User prefers no backwards-compat shims, fix callers directly - -* **Progress message format: 'N and counting (up to M)...' pattern**: User prefers progress messages that frame the limit as a ceiling rather than an expected total. Format: \`Fetching issues, 30 and counting (up to 50)...\` — not \`Fetching issues... 30/50\`. The 'up to' framing makes it clear the denominator is a max, not an expected count, avoiding confusion when fewer items exist than the limit. For multi-target fetches, include target count: \`Fetching issues from 10 projects, 30 and counting (up to 50)...\`. Initial message before any results: \`Fetching issues (up to 50)...\` or \`Fetching issues from 10 projects (up to 50)...\`. - From c46725fa5507c317cead81881ac287bdbffa153a Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 28 Feb 2026 13:43:39 +0000 Subject: [PATCH 3/3] docs(api): clarify why response cast in unwrapPaginatedResult is safe --- src/lib/api-client.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 03d40e09..5a8f615a 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -124,6 +124,11 @@ function unwrapResult( if (error instanceof AuthError || error instanceof ApiError) { throw error; } + // The @sentry/api SDK always includes `response` on the returned object in + // the default "fields" responseStyle (see createClient request() in the SDK + // source — it spreads `{ request, response }` into every return value). + // The cast is typed as optional only because the SDK's TypeScript types omit + // `response` from the return type, not because it can be absent at runtime. const response = (result as { response?: Response }).response; throwApiError(error, response, context); }