feat(cache): centralize mutation invalidation at the HTTP layer (#792)#801
feat(cache): centralize mutation invalidation at the HTTP layer (#792)#801
Conversation
Follow-up to #788. The per-site `invalidate*` helpers in `api/issues.ts`, `api/projects.ts`, and `api/dashboards.ts` are replaced by a single post-mutation hook in `authenticatedFetch` that auto-invalidates the cache for every successful non-GET. ## Layer 1: HTTP hook (new) `invalidateAfterMutation` fires in `sentry-client.ts` after `fetchWithRetry` returns a 2xx non-GET response. Prefix computation is delegated to a new `src/lib/cache-keys.ts` module: - **Hierarchy walk.** For a mutation on `/api/0/organizations/{org}/releases/1.0.0/deploys/`, sweep the URL path plus every ancestor: `releases/1.0.0/`, `releases/`, `organizations/{org}/`, `organizations/`. Catches the corresponding GET caches in a single pass. - **Cross-endpoint rules.** Tiny table for the 2 cases where a mutation affects a different URL tree: - `POST /teams/{org}/{team}/projects/` invalidates `/organizations/{org}/projects/` - `DELETE /projects/{org}/{project}/` invalidates `/organizations/{org}/projects/` The hook is awaited so a subsequent read in the same command sees fresh data. Identity-gated via the existing sweep primitive, so a mutation by one account can't evict another account's cache. `classifyUrl === "no-cache"` paths (autofix/root-cause) skip naturally because reads to those URLs aren't cached either. ## Layer 2: Command-level override (deferred) Issue #792 proposed an optional `invalidates` callback on `buildCommand` for cross-endpoint fan-outs. Turned out the 2 hardcoded rules above cover every current case; deferring the callback API to when a use case actually emerges. ## Coverage Every mutation in `src/lib/api/` is now covered for free, including mutations that had no invalidation before (`releases`, `teams`, `dashboards` create, `trials`). `sentry api -X POST/PUT/DELETE ...` also gets auto-invalidation — users of the raw escape hatch no longer need `--fresh` on follow-up reads. ## Removed - `api/issues.ts`: `invalidateIssueCaches`, `invalidateIssueDetailCaches`, `invalidateOrgIssueList` plus their call sites in `updateIssueStatus` and `mergeIssues`. - `api/projects.ts`: `invalidateProjectCaches`, `invalidateOrgProjectCache` plus call sites in `createProject` and `deleteProject`. - `api/dashboards.ts`: inline `invalidateCachedResponse` in `updateDashboard`. Net: -156 lines source + new `cache-keys.ts` (~130 lines). ## Tests - `test/lib/cache-keys.test.ts` — 13 tests covering the hierarchy walk, cross-endpoint rules, query-string stripping, unparseable URLs, self-hosted bases, and dedup. - `test/lib/sentry-client.invalidation.test.ts` — 5 integration tests: successful mutation clears self + list, failed mutation leaves cache alone, GET doesn't invalidate, cross-endpoint rule fires, identity isolation holds. Full unit suite: 5545 passing. Closes #792.
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨Cache
Other
Bug Fixes 🐛Init
Other
Documentation 📚
Internal Changes 🔧
🤖 This preview updates automatically when you update the PR. |
|
Codecov Results 📊✅ 138 passed | Total: 138 | Pass Rate: 100% | Execution Time: 0ms 📊 Comparison with Base Branch
✨ No test changes detected All tests are passing successfully. ✅ Patch coverage is 98.53%. Project has 1721 uncovered lines. Files with missing lines (1)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
+ Coverage 95.64% 95.72% +0.08%
==========================================
Files 280 281 +1
Lines 40248 40232 -16
Branches 0 0 —
==========================================
+ Hits 38494 38511 +17
- Misses 1754 1721 -33
- Partials 0 0 —Generated by Codecov Action |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit d51ba3a. Configure here.
…atch Two review-feedback fixes on #801: 1. **Cap the hierarchy walk.** Before: every mutation swept the bare top-level prefix (`/api/0/organizations/`, `/api/0/teams/`, etc.), which would evict cross-org caches on any single-org mutation. Now the walk stops at 2 segments — owner level (`organizations/{org}/`). Paths that are already ≤ 2 segments still walk to their root (a mutation targeting the root itself should clear its cache). Added a new test for the two-segment case. 2. **Drop the defensive try/catch in `invalidateAfterMutation`.** `invalidateCachedResponsesMatching` is already contractually no-throw; wrapping its `Promise.all` in another try/catch was dead code. Matches BYK's brevity preference in the lore. No behavior change beyond the walk cap. Tests updated accordingly.

Closes #792. Follow-up to #788.
Summary
The per-site
invalidate*helpers inapi/issues.ts,api/projects.ts, andapi/dashboards.tsare replaced by a single post-mutation hook inauthenticatedFetchthat auto-invalidates the cache for every successful non-GET. Prefix computation lives in a newsrc/lib/cache-keys.tsmodule.Layer 1: HTTP-layer hook (new)
invalidateAfterMutationfires afterfetchWithRetryreturns a 2xx non-GET response:/api/0/organizations/{org}/releases/1.0.0/deploys/, sweeps the URL path plus every ancestor down to the owner level (releases/1.0.0/,releases/,organizations/{org}/). The bare top-levelorganizations/root is deliberately not swept — sweeping it on every mutation would evict unrelated cross-org caches.POST /api/0/teams/{org}/{team}/projects/invalidates/api/0/organizations/{org}/projects/DELETE /api/0/projects/{org}/{project}/invalidates/api/0/organizations/{org}/projects/The hook is awaited before returning the response, so a subsequent read in the same command sees fresh data. Identity-gated via the existing sweep primitive (no cross-account eviction).
classifyUrl === "no-cache"paths (autofix / root-cause) are never cached to begin with, so sweeping them is a no-op.Layer 2: Command-level override (deferred)
Issue #792 proposed an optional
invalidatescallback onbuildCommandfor cross-endpoint fan-outs the HTTP layer can't know about. Turned out the 2 hardcoded rules above cover every current case — deferred the callback API to when a real use case emerges.Coverage
Every mutation in
src/lib/api/is now covered for free, including ones that had no invalidation before:release create/update/delete,release deploy,set-commits*team create,member adddashboard createtrial startsentry api -X POST/PUT/DELETE ...also gets auto-invalidation — users of the raw escape hatch no longer need--freshon follow-up reads.Removed
api/issues.ts:invalidateIssueCaches,invalidateIssueDetailCaches,invalidateOrgIssueList+ their call sites inupdateIssueStatus/mergeIssues.api/projects.ts:invalidateProjectCaches,invalidateOrgProjectCache+ call sites increateProject/deleteProject.api/dashboards.ts: inlineinvalidateCachedResponseinupdateDashboard.Net: -156 lines of source, +130 lines for
cache-keys.ts.Tests
test/lib/cache-keys.test.ts(14 tests) — hierarchy walk (with the owner-level cap), cross-endpoint rules, query-string stripping, unparseable URLs, self-hosted bases, dedup.test/lib/sentry-client.invalidation.test.ts(5 integration tests) — successful mutation clears self + list, failed mutation leaves cache alone, GET doesn't invalidate, cross-endpoint rule fires, identity isolation holds.Full unit suite: 5546 passing.
bun run typecheck,bun run lintclean.Follow-ups
One minor optimization deferred: when the hook computes multiple prefixes,
Promise.allkicks off independentinvalidateCachedResponsesMatchingcalls, each doing its ownreaddir+ per-file parse. For typical cache sizes (few hundred entries) this is negligible, but the natural shape is "read the dir once, match every entry against ALL prefixes, unlink if any match." A futureinvalidateCachedResponsesMatchingAny(prefixes: string[])API would eliminate the redundant I/O. Not done here to keep the PR scoped.