Skip to content

Commit 2cee148

Browse files
committed
fix: address review comments — refactor cache, export FRESH_ALIASES, parallelize cleanup
Review Round 3 — 15 human review comments addressed: #1: login.ts — Replace await .catch() with proper try/await/catch blocks #2: whoami.ts + all commands — Export FRESH_ALIASES constant from list-command.ts to reduce boilerplate; update 15 command files to use it #3: response-cache.ts — Bump immutable TTL from 1hr to 24hr (events/traces never change once created) #4–6: response-cache.ts — Restructure URL_TIER_PATTERNS as Record<TtlTier, RegExp[]>, combine duplicate regex patterns into single alternations #7: response-cache.ts — Replace localeCompare with simple < comparison for ASCII URL query param sorting #8: response-cache.ts — Remove try-catch in normalizeUrl (URLs reaching the cache already came from a fetch, always valid) #9: response-cache.ts — Link immutableMinTimeToLive to FALLBACK_TTL_MS.immutable instead of hardcoded magic number #10: response-cache.ts — Use Object.fromEntries(headers.entries()) instead of manual forEach loop #11: response-cache.ts — Remove unnecessary await on fire-and-forget unlink in catch block #12: response-cache.ts — Add expiresAt field to CacheEntry for O(1) expiry checks during cleanup (no CachePolicy deserialization needed) #13–15: response-cache.ts — Parallelize cache I/O (collectEntryMetadata, deleteExpiredEntries, evictExcessEntries) using p-limit-style concurrency limiter (max 8 concurrent)
1 parent 85a119d commit 2cee148

File tree

19 files changed

+226
-113
lines changed

19 files changed

+226
-113
lines changed

AGENTS.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ mock.module("./some-module", () => ({
635635
* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry API scoping: Events require org+project in URL path (\`/projects/{org}/{project}/events/{id}/\`). Issues use legacy global endpoint (\`/api/0/issues/{id}/\`) without org context. Traces need only org (\`/organizations/{org}/trace/{traceId}/\`). Two-step lookup for events: fetch issue → extract org/project from response → fetch event. Cross-project event search possible via Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\`.
636636
637637
<!-- lore:019cb6ab-ab98-7a9c-a25f-e154a5adbbe1 -->
638-
* **Sentry CLI authenticated fetch architecture with response caching**: \`createAuthenticatedFetch()\` in \`sentry-client.ts\` wraps native \`fetch()\` with auth injection, 30s timeout, retry with backoff (max 2), 401 refresh, and HTTP span tracing. Response caching (src/lib/response-cache.ts) integrates BEFORE auth/retry. Uses \`http-cache-semantics\` (RFC 7234) with filesystem storage at \`~/.sentry/cache/responses/\`. URL-based fallback TTL tiers (Sentry sends NO cache headers): immutable (1hr: events/traces by ID), stable (5min: orgs/projects), volatile (60s: issue lists), no-cache (0: autofix/polling — prevents stale polling responses). Only GET 2xx cached (\`response.ok\` guard needed since library caches 404s per RFC 7231). \`--refresh\` flag and \`SENTRY\_NO\_CACHE=1\` bypass cache. Cache cleared on login/logout.
638+
* **Sentry CLI authenticated fetch architecture with response caching**: \`createAuthenticatedFetch()\` in \`sentry-client.ts\` wraps native \`fetch()\` with auth injection, 30s timeout, retry with backoff (max 2), 401 refresh, and HTTP span tracing. Response caching (src/lib/response-cache.ts) integrates BEFORE auth/retry via \`http-cache-semantics\` (RFC 7234) with filesystem storage at \`~/.sentry/cache/responses/\`. URL-based fallback TTL tiers: immutable (1hr), stable (5min), volatile (60s), no-cache (0). Only GET 2xx cached. \`--fresh\` flag and \`SENTRY\_NO\_CACHE=1\` bypass cache. Cache cleared on login/logout via \`clearAuth()\`\`clearResponseCache()\`. Corrupted \`CachePolicy\` entries trigger cache miss with best-effort cleanup.
639639
640640
<!-- lore:019c8c72-b871-7d5e-a1a4-5214359a5a77 -->
641641
* **Sentry CLI has two distribution channels with different runtimes**: Sentry CLI ships two ways: (1) Standalone binary via \`Bun.build()\` with \`compile: true\`. (2) npm package via esbuild producing CJS \`dist/bin.cjs\` for Node 22+, with Bun API polyfills from \`script/node-polyfills.ts\`. \`Bun.$\` has NO polyfill — use \`execSync\` instead. \`require()\` in ESM is safe (Bun native, esbuild resolves at bundle time).
@@ -659,15 +659,15 @@ mock.module("./some-module", () => ({
659659
<!-- lore:70319dc2-556d-4e30-9562-e51d1b68cf45 -->
660660
* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces modules globally and leaks across test files in the same process. Solution: tests using mock.module() must run in a separate \`bun test\` invocation. In package.json, use \`bun run test:unit && bun run test:isolated\` instead of \`bun test\`. The \`test/isolated/\` directory exists for these tests. This was the root cause of ~100 test failures (getsentry/cli#258).
661661
662+
<!-- lore:019cb8cc-bfa8-7dd8-8ec7-77c974fd7985 -->
663+
* **Making clearAuth() async breaks model-based tests — use non-async Promise\<void> return instead**: Making \`clearAuth()\` \`async\` breaks fast-check model-based tests. Any real async yield (macrotask like \`Bun.sleep(1)\` or \`rm()\`) during \`asyncModelRun\` causes \`createIsolatedDbContext\` cleanup to interleave, switching the DB. Microtasks (\`Promise.resolve()\`) are fine. Fix: keep \`clearAuth()\` non-async, return \`clearResponseCache().catch(...)\` directly — DB ops execute synchronously, callers can optionally \`await\` the returned promise. Model-based tests should NOT await it. Related: model-based tests are inherently slow (5-20s with shrinking) and need explicit timeouts (e.g., \`30\_000\`) to avoid false failures from Bun's default 5s timeout. Don't chase 'data corruption' bugs that are actually timeout-interrupted shrinking runs.
664+
662665
<!-- lore:a28c4f2a-e2b6-4f24-9663-a85461bc6412 -->
663666
* **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.
664667
665668
<!-- lore:8c0fabbb-590c-4a7b-812b-15accf978748 -->
666669
* **pagination\_cursors table schema mismatch requires repair migration**: The pagination\_cursors SQLite table may have wrong PK (single-column instead of composite). Migration 5→6 detects via \`hasCompositePrimaryKey()\` and drops/recreates. \`repairWrongPrimaryKeys()\` in \`repairSchema()\` handles auto-repair. \`isSchemaError()\` catches 'on conflict clause does not match' to trigger repair. Data loss acceptable since cursors are ephemeral (5-min TTL).
667670
668-
<!-- lore:019cb3e6-da66-7534-a573-30d2ecadfd53 -->
669-
* **Returning bare promises loses async function from error stack traces**: When an \`async\` function returns another promise without \`await\`, the calling function disappears from error stack traces if the inner promise rejects. A function that drops \`async\` and does \`return someAsyncCall()\` loses its frame entirely. Fix: keep the function \`async\` and use \`return await someAsyncCall()\`. This matters for debugging — the intermediate function name in the stack trace helps locate which code path triggered the failure. ESLint rule \`no-return-await\` is outdated; modern engines optimize \`return await\` in async functions.
670-
671671
<!-- lore:ce43057f-2eff-461f-b49b-fb9ebaadff9d -->
672672
* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens. Use \`/auth/\` instead — it works with ALL token types and lives on the control silo. In the CLI, \`getControlSiloUrl()\` handles routing correctly. \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\`.
673673
@@ -692,7 +692,4 @@ mock.module("./some-module", () => ({
692692
693693
<!-- lore:019cb3e6-da61-7dfe-83c2-17fe3257bece -->
694694
* **PR workflow: address review comments, resolve threads, wait for CI**: User's PR workflow after creation: (1) Wait for CI checks to pass, (2) Check for unresolved review comments via \`gh api\` for PR review comments, (3) Fix issues in follow-up commits (not amends), (4) Reply to the comment thread explaining the fix, (5) Resolve the thread programmatically via \`gh api graphql\` with \`resolveReviewThread\` mutation, (6) Push and wait for CI again, (7) Final sweep for any remaining unresolved comments. Use \`git notes add\` to attach implementation plans to commits. Branch naming: \`fix/descriptive-slug\` or \`feat/descriptive-slug\`.
695-
696-
<!-- lore:019c9f9c-40f3-7b3e-99ba-a3af2e56e519 -->
697-
* **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)...\`.
698695
<!-- End lore-managed section -->

src/commands/auth/login.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,11 @@ export const loginCommand = buildCommand({
5454
// Token-based authentication
5555
if (flags.token) {
5656
// Clear stale cached responses from a previous session
57-
await clearResponseCache().catch(() => {
57+
try {
58+
await clearResponseCache();
59+
} catch {
5860
// Non-fatal: cache directory may not exist
59-
});
61+
}
6062

6163
// Save token first, then validate by fetching user regions
6264
await setAuthToken(flags.token);
@@ -97,9 +99,11 @@ export const loginCommand = buildCommand({
9799
}
98100

99101
// Clear stale cached responses from a previous session
100-
await clearResponseCache().catch(() => {
102+
try {
103+
await clearResponseCache();
104+
} catch {
101105
// Non-fatal: cache directory may not exist
102-
});
106+
}
103107

104108
// Device Flow OAuth
105109
const loginSuccess = await runInteractiveLogin(

src/commands/auth/status.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ import {
2525
formatUserIdentity,
2626
maskToken,
2727
} from "../../lib/formatters/human.js";
28-
import { applyFreshFlag, FRESH_FLAG } from "../../lib/list-command.js";
28+
import {
29+
applyFreshFlag,
30+
FRESH_ALIASES,
31+
FRESH_FLAG,
32+
} from "../../lib/list-command.js";
2933
import type { Writer } from "../../types/index.js";
3034

3135
type StatusFlags = {
@@ -135,7 +139,7 @@ export const statusCommand = buildCommand({
135139
},
136140
fresh: FRESH_FLAG,
137141
},
138-
aliases: { f: "fresh" },
142+
aliases: FRESH_ALIASES,
139143
},
140144
async func(this: SentryContext, flags: StatusFlags): Promise<void> {
141145
applyFreshFlag(flags);

src/commands/auth/whoami.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import { isAuthenticated } from "../../lib/db/auth.js";
1313
import { setUserInfo } from "../../lib/db/user.js";
1414
import { AuthError } from "../../lib/errors.js";
1515
import { formatUserIdentity, writeJson } from "../../lib/formatters/index.js";
16-
import { applyFreshFlag, FRESH_FLAG } from "../../lib/list-command.js";
16+
import {
17+
applyFreshFlag,
18+
FRESH_ALIASES,
19+
FRESH_FLAG,
20+
} from "../../lib/list-command.js";
1721

1822
type WhoamiFlags = {
1923
readonly json: boolean;
@@ -37,7 +41,7 @@ export const whoamiCommand = buildCommand({
3741
},
3842
fresh: FRESH_FLAG,
3943
},
40-
aliases: { f: "fresh" },
44+
aliases: FRESH_ALIASES,
4145
},
4246
async func(this: SentryContext, flags: WhoamiFlags): Promise<void> {
4347
applyFreshFlag(flags);

src/commands/event/view.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import { openInBrowser } from "../../lib/browser.js";
2121
import { buildCommand } from "../../lib/command.js";
2222
import { ContextError, ResolutionError } from "../../lib/errors.js";
2323
import { formatEventDetails, writeJson } from "../../lib/formatters/index.js";
24-
import { applyFreshFlag, FRESH_FLAG } from "../../lib/list-command.js";
24+
import {
25+
applyFreshFlag,
26+
FRESH_ALIASES,
27+
FRESH_FLAG,
28+
} from "../../lib/list-command.js";
2529
import { resolveEffectiveOrg } from "../../lib/region.js";
2630
import {
2731
resolveOrgAndProject,
@@ -305,7 +309,7 @@ export const viewCommand = buildCommand({
305309
...spansFlag,
306310
fresh: FRESH_FLAG,
307311
},
308-
aliases: { f: "fresh", w: "web" },
312+
aliases: { ...FRESH_ALIASES, w: "web" },
309313
},
310314
async func(
311315
this: SentryContext,

src/commands/issue/explain.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
formatRootCauseList,
1313
handleSeerApiError,
1414
} from "../../lib/formatters/seer.js";
15-
import { applyFreshFlag, FRESH_FLAG } from "../../lib/list-command.js";
15+
import {
16+
applyFreshFlag,
17+
FRESH_ALIASES,
18+
FRESH_FLAG,
19+
} from "../../lib/list-command.js";
1620
import { extractRootCauses } from "../../types/seer.js";
1721
import {
1822
ensureRootCauseAnalysis,
@@ -65,7 +69,7 @@ export const explainCommand = buildCommand({
6569
},
6670
fresh: FRESH_FLAG,
6771
},
68-
aliases: { f: "fresh" },
72+
aliases: FRESH_ALIASES,
6973
},
7074
async func(
7175
this: SentryContext,

src/commands/issue/list.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
applyFreshFlag,
4747
buildListCommand,
4848
buildListLimitFlag,
49+
FRESH_ALIASES,
4950
FRESH_FLAG,
5051
LIST_BASE_ALIASES,
5152
LIST_JSON_FLAG,
@@ -1184,7 +1185,7 @@ export const listCommand = buildListCommand("issue", {
11841185
},
11851186
aliases: {
11861187
...LIST_BASE_ALIASES,
1187-
f: "fresh",
1188+
...FRESH_ALIASES,
11881189
q: "query",
11891190
s: "sort",
11901191
t: "period",

src/commands/issue/plan.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import {
1515
formatSolution,
1616
handleSeerApiError,
1717
} from "../../lib/formatters/seer.js";
18-
import { applyFreshFlag, FRESH_FLAG } from "../../lib/list-command.js";
18+
import {
19+
applyFreshFlag,
20+
FRESH_ALIASES,
21+
FRESH_FLAG,
22+
} from "../../lib/list-command.js";
1923
import type { Writer } from "../../types/index.js";
2024
import {
2125
type AutofixState,
@@ -178,7 +182,7 @@ export const planCommand = buildCommand({
178182
},
179183
fresh: FRESH_FLAG,
180184
},
181-
aliases: { f: "fresh" },
185+
aliases: FRESH_ALIASES,
182186
},
183187
async func(
184188
this: SentryContext,

src/commands/issue/view.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import {
1616
writeFooter,
1717
writeJson,
1818
} from "../../lib/formatters/index.js";
19-
import { applyFreshFlag, FRESH_FLAG } from "../../lib/list-command.js";
19+
import {
20+
applyFreshFlag,
21+
FRESH_ALIASES,
22+
FRESH_FLAG,
23+
} from "../../lib/list-command.js";
2024
import { getSpanTreeLines } from "../../lib/span-tree.js";
2125
import type { SentryEvent, SentryIssue, Writer } from "../../types/index.js";
2226
import { issueIdPositional, resolveIssue } from "./utils.js";
@@ -104,7 +108,7 @@ export const viewCommand = buildCommand({
104108
...spansFlag,
105109
fresh: FRESH_FLAG,
106110
},
107-
aliases: { f: "fresh", w: "web" },
111+
aliases: { ...FRESH_ALIASES, w: "web" },
108112
},
109113
async func(
110114
this: SentryContext,

src/commands/log/view.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import { openInBrowser } from "../../lib/browser.js";
1414
import { buildCommand } from "../../lib/command.js";
1515
import { ContextError, ValidationError } from "../../lib/errors.js";
1616
import { formatLogDetails, writeJson } from "../../lib/formatters/index.js";
17-
import { applyFreshFlag, FRESH_FLAG } from "../../lib/list-command.js";
17+
import {
18+
applyFreshFlag,
19+
FRESH_ALIASES,
20+
FRESH_FLAG,
21+
} from "../../lib/list-command.js";
1822
import {
1923
resolveOrgAndProject,
2024
resolveProjectBySlug,
@@ -135,7 +139,7 @@ export const viewCommand = buildCommand({
135139
},
136140
fresh: FRESH_FLAG,
137141
},
138-
aliases: { f: "fresh", w: "web" },
142+
aliases: { ...FRESH_ALIASES, w: "web" },
139143
},
140144
async func(
141145
this: SentryContext,

0 commit comments

Comments
 (0)