Skip to content

Centralize mutation cache invalidation via a command-level helper #792

@BYK

Description

@BYK

Follow-up to #788, which wired identity-scoped response caching and per-mutation
`invalidateCachedResponse` / `invalidateCachedResponsesMatching` calls into
individual API domain modules (`api/issues.ts`, `api/projects.ts`,
`api/dashboards.ts`).

That per-site approach works but is spread across many files, easy to forget,
and couples cache concerns with API-call logic. A reviewer noted on #788:

I'd say at this point we should have a centralized mechanism for this.
Maybe another command helper for mutation endpoints where we pass the API
URL(s) we use so it automatically invalidates the caches upon success.

This issue tracks building that helper.

Current state (post-#788)

Mutations that currently invalidate cache entries manually:

Command Location Invalidates
`project create` `src/lib/api/projects.ts` `createProject` Org projects list prefix
`project delete` `src/lib/api/projects.ts` `deleteProject` Project detail + org projects list prefix
`issue resolve` / `unresolve` `src/lib/api/issues.ts` `updateIssueStatus` Issue detail (both org-scoped + legacy) + org issues list prefix
`issue merge` `src/lib/api/issues.ts` `mergeIssues` Per-ID issue detail + org issues list prefix
`dashboard update` `src/lib/api/dashboards.ts` `updateDashboard` Dashboard detail

Mutations that DO NOT yet invalidate (will need to be covered as we centralize):

  • `release` CRUD (`createRelease`, `updateRelease`, `deleteRelease`, `setCommits*`, `createReleaseDeploy`)
  • `team create` (`createTeam`, `addMemberToTeam`)
  • `dashboard create` (`createDashboard`)
  • `seer` trigger endpoints (transient state, may want to `no-cache` classify instead)
  • `trials` `startProductTrial`
  • Sourcemap `assemble` / chunk uploads
  • Anything users exercise via `sentry api -X POST/PUT/DELETE` (see below)

Design options

Option A — declarative helper at the command layer (preferred)

Introduce `buildMutateCommand` (sibling of the existing `buildDeleteCommand`)
or extend `buildCommand` with an `invalidates` option:

```ts
buildMutateCommand({
// ...same as buildCommand...
invalidates: (result, ctx) => [
invalidationPrefixForOrgProjects(ctx.orgSlug),
invalidationPrefixForProjectDetail(ctx.orgSlug, ctx.projectSlug),
],
});
```

The wrapper calls the `invalidates` callback after `func()` resolves
successfully (and skips it on `--dry-run`), passing the result so prefixes
that depend on newly-created IDs work out of the box. `invalidates` can
also be a static array for the common case.

Pros: explicit, reviewable, colocated with the command definition.

Cons: needs wiring into every mutation command. Requires every mutation
command to use the new builder — easy to miss for `sentry api -X DELETE ...`.

Option B — automatic invalidation at the HTTP layer

Intercept non-GET responses in `sentry-client.ts` `authenticatedFetch` and
invalidate any cached GET responses whose URL `startsWith` the mutation URL.
Add a URL-prefix-mapping table for related list endpoints (e.g., mutating
`/api/0/projects/<org>/<proj>/` also invalidates
`/api/0/organizations/<org>/projects/`).

Pros: zero-touch for command authors. Covers `sentry api -X POST` too.

Cons: mapping table is another source of truth; cross-endpoint
invalidations (project detail → org project list) need explicit rules.

Option C — hybrid (recommended)

Combine A and B:

  1. HTTP-layer default: auto-invalidate the exact URL family of any non-GET.
    Covers the common self-URL case and `sentry api` for free.
  2. Command-layer override: `invalidates` callback for cross-endpoint
    relationships the HTTP layer can't know (e.g., `POST /projects/` also
    invalidates `/organizations//projects/`).

The domain API modules keep no invalidation calls.

Proposed approach

  1. Add a URL-prefix builder module (`src/lib/cache-keys.ts` or similar) that
    encodes the canonical list-vs-detail relationships for each entity. This
    keeps the identity-scoped prefix construction out of command code.
  2. Implement the HTTP-layer default in `sentry-client.ts`: on successful
    non-GET, compute the invalidation URLs via the prefix builder and call
    `invalidateCachedResponse` / `invalidateCachedResponsesMatching` with
    them. Wrap in try/catch — invalidation failures must never fail a
    mutation that already succeeded.
  3. Add `invalidates?: InvalidateSpec` to `buildCommand` for the
    cross-endpoint cases. Run after `func()` success, before return-value
    rendering, skipped on `--dry-run`.
  4. Delete the per-site `invalidateCachedResponse` calls added in feat(cache): scope response cache per active identity (#785) #788
    and the domain-specific `invalidate*Caches` helpers in
    `api/issues.ts` / `api/projects.ts`.
  5. Audit every mutation in `src/lib/api/` against the prefix builder and
    verify the tests covering cache freshness after mutations still pass.

Non-goals

Tests

  • Unit: per-entity tests that a mutation followed by a GET reads fresh data,
    with the HTTP-layer + command-layer hooks exercised in isolation.
  • Model-based: extend `response-cache.model-based.test.ts` with mutation
    commands that verify GET cache staleness bounds.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions