Skip to content

feat: add response caching for read-only API calls#330

Open
BYK wants to merge 11 commits intomainfrom
feat/response-cache
Open

feat: add response caching for read-only API calls#330
BYK wants to merge 11 commits intomainfrom
feat/response-cache

Conversation

@BYK
Copy link
Member

@BYK BYK commented Mar 4, 2026

Summary

Implements RFC 7234/9111-aware HTTP response caching for GET requests to the Sentry API, integrated transparently at the fetch level inside createAuthenticatedFetch().

Closes #318

Cache Design

  • Filesystem-based at ~/.sentry/cache/responses/ with JSON files keyed by SHA-256 hash of normalized URL + Authorization header
  • Standards-compliant via http-cache-semantics for freshness evaluation
  • Tiered TTL system based on URL pattern matching:
    Tier TTL Examples
    Immutable 1 hour /events/{id}/, /issues/{id}/hashes/
    Stable 5 min /organizations/, /projects/, /teams/
    Volatile 60 sec /issues/ (lists), issue details
    No-cache 0 sec /autofix/, /root-cause/ (polling)
  • Probabilistic cleanup (10% chance per write) with 500-file LRU cap
  • Only successful (2xx) responses are cached

New Flags & Env Vars

  • --refresh / -r flag on all read commands — bypasses cache for that request
  • SENTRY_NO_CACHE=1 env var — disables caching globally
  • Cache is automatically cleared on login and logout for security

Commands Updated

All read-only commands now support --refresh: issue list/view/explain/plan, project list/view, org list/view, event view, trace list/view/logs, log list/view, auth whoami/status, team list, repo list.

Testing

  • 18 unit tests for cache operations, TTL tiers, cleanup, and edge cases
  • 15 property-based tests for URL classification, key normalization, TTL bounds, and round-trip invariants
  • 4 integration-style tests for end-to-end cache behavior
  • All existing tests pass (2604 unit + 100 isolated + 101 e2e)

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

Trace

Other

  • (api) Add --data/-d flag and auto-detect JSON body in fields by BYK in #320
  • (formatters) Render all terminal output as markdown by BYK in #297
  • (install) Add Sentry error telemetry to install script by BYK in #334
  • (issue-list) Global limit with fair distribution, compound cursor, and richer progress by BYK in #306
  • (log-list) Add --trace flag to filter logs by trace ID by BYK in #329
  • (project) Add project create command by betegon in #237
  • (upgrade) Add binary delta patching via TRDIFF10/bsdiff by BYK in #327
  • Add response caching for read-only API calls by BYK in #330

Bug Fixes 🐛

Api

  • Use numeric project ID to avoid "not actively selected" error by betegon in #312
  • Use limit param for issues endpoint page size by BYK in #309
  • Auto-correct ':' to '=' in --field values with a warning by BYK in #302

Formatters

  • Expand streaming table to fill terminal width by betegon in #314
  • Fix HTML entities and escaped underscores in table output by betegon in #313

Setup

  • Suppress agent skills and welcome messages on upgrade by BYK in #328
  • Suppress shell completion messages on upgrade by BYK in #326

Other

  • (ci) Generate JUnit XML to silence codecov-action warnings by BYK in #300
  • (install) Fix nightly digest extraction on macOS by BYK in #331
  • (nightly) Push to GHCR from artifacts dir so layer titles are bare filenames by BYK in #301
  • (project create) Auto-correct dot-separated platform to hyphens by BYK in #336
  • (region) Resolve DSN org prefix at resolution layer by BYK in #316
  • (test) Handle 0/-0 in getComparator anti-symmetry property test by BYK in #308
  • (trace-logs) Timestamp_precise is a number, not a string by BYK in #323
  • (upgrade) Use MAP_PRIVATE mmap to prevent macOS SIGKILL during delta upgrade by BYK in #339

Documentation 📚

  • Document SENTRY_URL and self-hosted setup by BYK in #337

Internal Changes 🔧

Api

  • Upgrade @sentry/api to 0.21.0, remove raw HTTP pagination workarounds by BYK in #321
  • Wire listIssuesPaginated through @sentry/api SDK for type safety by BYK in #310

Other

  • (craft) Add sentry-release-registry target by BYK in #325

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

Codecov Results 📊

2668 passed | Total: 2668 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests 📈 +37
Passed Tests 📈 +37
Failed Tests
Skipped Tests

All tests are passing successfully.

✅ Patch coverage is 93.36%. Project has 3156 uncovered lines.
✅ Project coverage is 82.69%. Comparing base (base) to head (head).

Files with missing lines (21)
File Patch % Lines
plan.ts 20.62% ⚠️ 154 Missing
list.ts 87.11% ⚠️ 96 Missing
view.ts 27.64% ⚠️ 89 Missing
view.ts 42.00% ⚠️ 87 Missing
status.ts 25.89% ⚠️ 83 Missing
list.ts 25.00% ⚠️ 72 Missing
view.ts 68.72% ⚠️ 71 Missing
list.ts 85.51% ⚠️ 60 Missing
explain.ts 35.63% ⚠️ 56 Missing
telemetry.ts 90.91% ⚠️ 38 Missing
view.ts 61.76% ⚠️ 26 Missing
view.ts 89.00% ⚠️ 23 Missing
response-cache.ts 93.69% ⚠️ 19 Missing
list.ts 94.19% ⚠️ 18 Missing
sentry-client.ts 93.64% ⚠️ 14 Missing
view.ts 95.04% ⚠️ 7 Missing
auth.ts 95.68% ⚠️ 6 Missing
logs.ts 97.89% ⚠️ 3 Missing
login.ts 98.82% ⚠️ 1 Missing
list.ts 99.30% ⚠️ 1 Missing
list-command.ts 99.33% ⚠️ 1 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    82.43%    82.69%    +0.26%
==========================================
  Files          126       127        +1
  Lines        17792     18234      +442
  Branches         0         0         —
==========================================
+ Hits         14666     15078      +412
- Misses        3126      3156       +30
- Partials         0         0         —

Generated by Codecov Action

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

BugBot #9 (corrupted cache entry) — fixed in 85a119d.

Wrapped CachePolicy.fromObject(), isEntryFresh(), buildResponseHeaders(), and the Response construction in a try-catch block inside getCachedResponse(). Corrupted or version-incompatible policy objects now:

  1. Return undefined (cache miss) — the request proceeds normally via fetch
  2. Best-effort delete the broken cache entry via unlink() so it doesn't keep failing

BugBot #10 (async clearAuth not awaited) — fixed in 85a119d.

Added await to both clearAuth() call sites in test/commands/project/list.test.ts (lines 1080 and 1362).

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

Re: login.ts — don't mix await with .catch() (thread PRRT_kwDOQm6jAs5yDJFz)

Fixed in 2cee148. Both occurrences replaced with try { await clearResponseCache(); } catch { ... } pattern.

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

Re: whoami.ts — factory/wrapper for fresh flag boilerplate (thread PRRT_kwDOQm6jAs5yDJvW)

Fixed in 2cee148. Exported FRESH_ALIASES = { f: "fresh" } as const from list-command.ts with JSDoc documenting the log list exception. Updated 15 command files to use ...FRESH_ALIASES instead of inline { f: "fresh" }. The per-command pattern is now:

import { applyFreshFlag, FRESH_ALIASES, FRESH_FLAG } from "../../lib/list-command.js";
// parameters:
flags: { ..., fresh: FRESH_FLAG },
aliases: { ..., ...FRESH_ALIASES },
// func():
applyFreshFlag(flags);

buildOrgListCommand also uses ...FRESH_ALIASES internally now.

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

Re: response-cache.ts — immutable TTL too short (thread PRRT_kwDOQm6jAs5yDLi1)

Fixed in 2cee148. Bumped immutable tier from 1 hour → 24 hours. Events and traces are write-once, so caching them for a day is safe. The --fresh flag and SENTRY_NO_CACHE=1 still bypass when needed.

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

Re: response-cache.ts — URL tier patterns restructure (threads PRRT_kwDOQm6jAs5yDMZM, PRRT_kwDOQm6jAs5yDMoU, PRRT_kwDOQm6jAs5yDMyc)

All three addressed in 2cee148:

  • Restructured from Array<{ pattern, tier }>Record<TtlTier, readonly RegExp[]> (URL_TIER_REGEXPS) with a TIER_CHECK_ORDER array for iteration order
  • Combined no-cache patterns: /\/autofix\// + /\/root-cause\///\/(?:autofix|root-cause)\//
  • Combined dataset patterns: /[?&]dataset=logs/ + /[?&]dataset=transactions//[?&]dataset=(?:logs|transactions)/
  • Removed redundant /\/issues\/?$/ (already covered by /\/issues\//)

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

Re: response-cache.ts — assorted cleanup (threads #7#11)

All addressed in 2cee148:

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

Re: response-cache.ts — calculate expiresAt for cheaper cleanup (thread PRRT_kwDOQm6jAs5yDSK3)

Fixed in 2cee148. Added expiresAt?: number to CacheEntry, computed at write time from policy.timeToLive() with URL-based fallback. Cleanup now does a simple now >= expiresAt check without deserializing CachePolicy at all.

Kept the policy field since getCachedResponse() still needs it for RFC 7234 satisfiesWithoutRevalidation() and responseHeaders(). Backwards compatible — old entries without expiresAt fall back to URL-based TTL classification.

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

Re: response-cache.ts — parallelize I/O with concurrency limiter (threads #13#15)

All three addressed in 2cee148. Added a parallel<T>() helper that limits concurrent async operations (default 8):

  • collectEntryMetadata: file reads run in parallel batches
  • deleteExpiredEntries: unlinks run in parallel
  • evictExcessEntries: unlinks run in parallel

Used a lightweight inline p-limit-style implementation to avoid adding a new dependency — creates a pool of concurrency slots using Promise.race() to wait for a free slot before starting the next operation.

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

Re: issue/list.ts — Does FRESH_ALIASES override f: "follow"? (thread PRRT_kwDOQm6jAs5yD7C7)

No conflict. Verified that only log/list.ts has f: "follow" (line 322), and it does NOT use FRESH_ALIASES. The issue list command has no --follow flag, so ...FRESH_ALIASES ({ f: "fresh" }) is safe here.

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

Re: response-cache.ts — cacheHeuristic vs CLEANUP_PROBABILITY (thread PRRT_kwDOQm6jAs5yD94G)

These are completely unrelated despite having the same numeric value (0.1):

  • cacheHeuristic: 0.1 is an http-cache-semantics option — it means "use 10% of the resource's age as fallback TTL" per the RFC 7234 heuristic freshness calculation
  • CLEANUP_PROBABILITY = 0.1 means "10% chance of triggering cache cleanup after each write"

Same number, different domain — no shared constant makes sense.

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

Re: response-cache.ts — parallel() simplification & eviction await (threads PRRT_kwDOQm6jAs5yD_wj, PRRT_kwDOQm6jAs5yEAVl)

Both addressed in c3c622a:

Simplified parallel(): Dropped the custom parallel<T>() wrapper entirely. Now using p-limit's built-in .map() method directly:

const cacheIO = pLimit(CACHE_IO_CONCURRENCY);
// At each call site:
await cacheIO.map(items, fn);

Best-effort eviction: Both deleteExpiredEntries and evictExcessEntries now run in parallel via Promise.all() since they operate on disjoint file sets (expired vs non-expired). The internal unlinks are already wrapped in .catch().

@BYK
Copy link
Member Author

BYK commented Mar 4, 2026

Re: response-cache.ts — Sentry instrumentation (thread PRRT_kwDOQm6jAs5yEBHK)

Added in c3c622a:

  • New withCacheSpan() helper in telemetry.ts — creates child spans with op: "cache" and onlyIfParent: true
  • getCachedResponse() wrapped with cache.lookup span (includes cache.url attribute)
  • storeCachedResponse() wrapped with cache.store span (includes cache.url attribute)

This lets us see cache hit/miss rates and latency impact in the Sentry performance dashboard.

@BYK BYK force-pushed the feat/response-cache branch 2 times, most recently from 8d13fd1 to 21db140 Compare March 4, 2026 14:44
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

BYK and others added 4 commits March 4, 2026 16:44
Implement RFC 7234/9111-aware HTTP response caching for GET requests to
the Sentry API. Caching is transparent and integrated at the fetch level
inside createAuthenticatedFetch().

Cache design:
- Filesystem-based at ~/.sentry/cache/responses/ with JSON files keyed
  by SHA-256 hash of normalized URL + Authorization header
- Uses http-cache-semantics for standards-compliant freshness evaluation
- Tiered TTL system: immutable (1hr), stable (5min), volatile (60sec),
  no-cache (0sec) based on URL pattern matching
- Probabilistic cleanup (10% chance per write) with 500-file LRU cap
- Only successful (2xx) responses are cached

Integration:
- --refresh / -r flag added to all read commands (issue list/view/explain/
  plan, project list/view, org list/view, event view, trace list/view/logs,
  log list/view, auth whoami/status, team list, repo list)
- SENTRY_NO_CACHE=1 env var for global cache bypass
- Cache cleared on login and logout for security
- Autofix/root-cause polling endpoints excluded from caching

Testing:
- 18 unit tests for cache operations, TTL tiers, cleanup, and edge cases
- 15 property-based tests for URL classification, key normalization,
  TTL bounds, and round-trip invariants
- 4 additional integration-style tests

Closes #318
…lassification and auth headers

- Fix issue detail URLs (/issues/12345/) misclassified as 'stable' (5min)
  instead of 'volatile' (60sec). Broadened pattern to match all /issues/ URLs.
- Fix cache ignoring Authorization headers for Vary-correctness. Thread
  current auth token into tryCacheHit() and refreshed token into
  cacheResponse() via new authHeaders() helper.
- Extract fetchWithRetry() to keep authenticatedFetch complexity under 15.
- Rename --refresh flag to --fresh with -f alias across all 16 commands.
- Create FRESH_FLAG constant and applyFreshFlag() helper in list-command.ts
  as a common primitive, removing direct disableResponseCache imports from
  all command files.
- Add -f alias to buildOrgListCommand and all individual command aliases.
  Exception: log list uses -f for --follow (no conflict).
- Verify logout already clears cache via clearAuth() → clearResponseCache().
BYK added 7 commits March 4, 2026 16:44
- Fix fallback TTL overriding explicitly expired server cache headers.
  Changed isEntryFresh() to use serverTtl !== 0 (catches both positive
  and negative values) instead of serverTtl > 0 (missed negative/expired).
  Same fix in collectEntryMetadata() with explicit < 0 branch.
- Fix cache storing stale token after 401 refresh. Use getAuthToken()
  (reads current DB value) instead of the captured token variable when
  building cache request headers, so post-refresh tokens are stored.
Make clearAuth() properly async so clearResponseCache() is awaited
rather than fire-and-forget. This ensures the filesystem cache is
fully removed before the process exits.

Fix pre-existing bug in model-based tests: fcAssert(asyncProperty(...))
returns a Promise that was never awaited. When all commands were
synchronous this was invisible, but with the async clearAuth() the
yield point caused createIsolatedDbContext() cleanups to interleave
with subsequent iterations, corrupting the SENTRY_CONFIG_DIR env var.

Changes:
- src/lib/db/auth.ts: clearAuth() → async, try/await/catch for
  clearResponseCache() (was .catch() fire-and-forget)
- test/lib/db/model-based.test.ts: add async/await to all test
  functions using asyncProperty; await clearAuth() calls
- test/lib/db/pagination.model-based.test.ts: same async/await fix
- test/commands/project/list.test.ts: await clearAuth() calls,
  add required fresh:false to ListFlags objects
BugBot #9: Wrap CachePolicy.fromObject() and related calls in try-catch
inside getCachedResponse(). A corrupted or version-incompatible policy
object now triggers a cache miss (and best-effort cleanup of the broken
entry) instead of crashing the API request.

BugBot #10: Add missing `await` to clearAuth() calls in project/list
tests at lines 1080 and 1362 to prevent floating promises.
…parallelize cleanup

Review Round 3 — 15 human review comments addressed:

    to reduce boilerplate; update 15 command files to use it
    never change once created)
    combine duplicate regex patterns into single alternations
    ASCII URL query param sorting
    cache already came from a fetch, always valid)
    instead of hardcoded magic number
    manual forEach loop
    catch block
    checks during cleanup (no CachePolicy deserialization needed)
    deleteExpiredEntries, evictExcessEntries) using p-limit-style concurrency
    limiter (max 8 concurrent)
…pans

Review comments addressed:

    has f: 'follow' and it doesn't use FRESH_ALIASES
    not shared (RFC heuristic vs probabilistic cleanup trigger)
    built-in .map() method (cacheIO.map(items, fn)) at all 3 call sites
    Promise.all() since they operate on disjoint file sets
    cache.lookup and cache.store spans in response-cache.ts with URL attrs
@BYK BYK force-pushed the feat/response-cache branch from 56f46c1 to 2a22df6 Compare March 4, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add response caching for read-only API calls

1 participant