Skip to content

fix(status): scope the freshness indicator to the viewed instance; unify status query keys - #1477

Merged
kylebernhardy merged 1 commit into
stagefrom
kyle/freshness-instance-scope
Jul 13, 2026
Merged

fix(status): scope the freshness indicator to the viewed instance; unify status query keys#1477
kylebernhardy merged 1 commit into
stagefrom
kyle/freshness-instance-scope

Conversation

@kylebernhardy

@kylebernhardy kylebernhardy commented Jul 12, 2026

Copy link
Copy Markdown
Member

Fixes #1445 — Analytics freshness indicator doesn't filter by instance; query-key shapes are inconsistent.

What changed

1. Freshness indicator is now scoped to the viewed instance

useAnalyticsFreshness matched cache entries by key prefix only (get_analytics_raw at position 0), so with two instance Status pages open, the "Updated Xs ago" pill and the refresh spinner reflected whichever instance fetched last. The hook now takes the instance entityId and matches it at key position 1 (where getAnalytics.ts puts it). TimeRangePicker threads it in from AnalyticsContext.

Same fix applied to the auto-refresh in-flight guard in StatusTabsqueryClient.isFetching({ queryKey: [ANALYTICS_QUERY_KEY_PREFIX] }) was also unscoped, so another instance's in-flight analytics batch could starve this instance's refresh cadence.

2. Query-key convention unified on operation-first

src/integrations/api/instance/status/ mixed two shapes: getAnalytics.ts used ['get_analytics_raw', entityId, ...] while everything else used [entityId, 'operation']. Prefix matching (the freshness hook) and targeted invalidation depend on a single convention, so the whole directory is now operation-first:

File Key (before → after)
getStatus.ts [entityId, 'get_status']['get_status', entityId]
getConfiguration.ts [entityId, 'get_configuration']['get_configuration', entityId]
getSystemInformation.ts [entityId, 'system_information']['system_information', entityId]
getInstanceHealth.ts [entityId, 'health']['health', entityId]
getOpenAPI.ts [entityId, 'OpenAPI']['OpenAPI', entityId]
getRegistrationInfo.ts [entityId, 'registration_info']['registration_info', entityId]
getUsageLicenses.ts [entityId, 'get_usage_licenses']['get_usage_licenses', entityId]
getReadLog.ts [entityId, 'read_log', ...filters]['read_log', entityId, ...filters]
getAnalytics.ts ['get_analytics_raw', entityId, ...] (unchanged — already the target shape)

Post-change symmetry audit: every query-key definition in the directory is ['operation', entityId, ...].

Blast radius of the key flip

Every repo consumer of the flipped keys was located (grep for invalidateQueries / setQueryData / getQueryData / removeQueries / isFetching / key literals) and updated:

Targeted invalidations (key literal flipped in place):

  • setStatus.tsget_status invalidation after set_status
  • installUsageLicense.ts, useApplyLicensesClick.tsxget_usage_licenses
  • useRollingConfigUpdate.tsxget_configuration

Broad entity-prefix invalidations — the subtle part. Several flows invalidated everything for an entity via invalidateQueries({ queryKey: [entityId] }), which only works while keys are entity-first. Those would have silently stopped matching the status-directory queries (e.g. restart would no longer refetch get_status, so restartRequired would linger). They now use a new predicate-based helper, invalidateEntityQueries (src/react-query/invalidateEntityQueries.ts), which matches entityId at key position 0 or 1, covering both conventions:

  • useRestartInstanceClick.ts (instance + cluster restart)
  • useRestartClusterClick.tsx (rolling cluster restart)
  • ClusterForm.tsx (post-create/update [clusterId] invalidation)
  • useImportApplication.ts (post-import instance invalidation)

Org-scoped broad invalidations ([organizationId]) were left alone — no status-directory key is ever keyed by an organization id.

Tests

  • New regression in useAnalyticsFreshness.test.ts: cache entries for inst-A and inst-B; the hook keyed to inst-A ignores B's in-flight fetch and success timestamp, and still reports A's own fetch/timestamp. Fails against the old unscoped implementation.
  • Full suite: 197 files / 1311 passed (11 skipped). tsc -b, dprint, and oxlint clean.

Cross-model review

  • Gemini 3.1 Pro (read-only, asked specifically to verify every consumer of the changed keys): clean — confirmed all consumers updated, the predicate helper preserves prior invalidation reach, the hook scoping/effect deps are correct, and the regression test fails against the old implementation. No findings at any severity.
  • Codex (codex exec review): clean — "no discrete correctness issues in the reviewed changes. The query-key updates and scoped analytics freshness logic appear consistently applied with matching invalidation updates."

Cross-model reviews: both complete and clean (Codex finished after opening: "no discrete correctness issues... consistently applied with matching invalidation updates").

Generated by kAIle (Claude Fable 5).

…ify status query keys

The analytics freshness hook (and the StatusTabs auto-refresh in-flight
guard) matched React Query cache entries by the get_analytics_raw prefix
only, so two open instance Status pages reflected each other's fetches.
Both now match the instance entityId at key position 1.

Every query key in src/integrations/api/instance/status/ is flipped from
[entityId, 'operation'] to the operation-first ['operation', entityId]
shape that prefix matching and targeted invalidation rely on. All key
consumers were updated, and the broad invalidate-everything-for-an-entity
call sites (restart instance/cluster, app import, cluster form) now use a
predicate-based invalidateEntityQueries helper that matches entityId at
key position 0 or 1, preserving their reach across both conventions.

Fixes #1445

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 47.51% 4489 / 9447
🔵 Statements 47.88% 4790 / 10003
🔵 Functions 39.49% 1070 / 2709
🔵 Branches 39.83% 2912 / 7311
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/clusters/upsert/ClusterForm.tsx 0% 0% 0% 0% 71-564
src/features/instance/status/analytics/StatusTabs.tsx 90.74% 80% 83.33% 90% 46-49, 54-61, 130, 138, 190
src/features/instance/status/analytics/components/TimeRangePicker.tsx 87.5% 33.33% 80% 87.5% 50
src/features/instance/status/analytics/hooks/useAnalyticsFreshness.ts 98.27% 81.57% 100% 100% 69
src/hooks/useApplyLicensesClick.tsx 0% 0% 0% 0% 22-146
src/hooks/useRestartClusterClick.tsx 0% 0% 0% 0% 29-167
src/hooks/useRestartInstanceClick.ts 0% 0% 0% 0% 25-83
src/hooks/useRollingConfigUpdate.tsx 0% 0% 0% 0% 30-176
src/integrations/api/instance/auth/installUsageLicense.ts 0% 100% 0% 0% 10-21
src/integrations/api/instance/status/getConfiguration.ts 0% 100% 0% 0% 18-24
src/integrations/api/instance/status/getInstanceHealth.ts 0% 100% 0% 0% 8-15
src/integrations/api/instance/status/getReadLog.ts 61.53% 72.22% 40% 61.53% 66-84
src/integrations/api/instance/status/getRegistrationInfo.ts 0% 100% 0% 0% 9-17
src/integrations/api/instance/status/getStatus.ts 50% 100% 50% 40% 41-52
src/integrations/api/instance/status/getSystemInformation.ts 0% 100% 0% 0% 100-107
src/integrations/api/instance/status/getUsageLicenses.ts 0% 100% 0% 0% 31-42
src/integrations/api/instance/status/setStatus.ts 0% 100% 0% 0% 15-26
src/react-query/invalidateEntityQueries.ts 0% 0% 0% 0% 14-16
Generated in workflow #1447 for commit 6463c78 by the Vitest Coverage Report Action

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request standardizes React Query keys to use an operation-first convention (e.g., ['operation', entityId]) and introduces a helper function, invalidateEntityQueries, to reliably invalidate cached queries for a given entity across both key conventions. It also scopes analytics freshness tracking to specific entity IDs to prevent interference when multiple status pages are open simultaneously, backed by new unit tests. Feedback suggests improving the flexibility of the invalidateEntityQueries helper by typing its options parameter with Omit instead of Pick to allow other standard React Query invalidation filters.

Comment thread src/react-query/invalidateEntityQueries.ts
@kylebernhardy
kylebernhardy marked this pull request as ready for review July 12, 2026 03:01
@kylebernhardy
kylebernhardy requested a review from a team as a code owner July 12, 2026 03:01

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving — this cleanly fixes #1445 and satisfies the studio#1464-class "unscoped across instances" concern on both surfaces.

Verified per-instance scoping and switch-safety:

  • Freshness hook (useAnalyticsFreshness.ts): isOurs now matches entityId at key position 1 — exactly where getAnalytics.ts places instanceId — and entityId is in the effect dep array, so an instance switch tears down the old subscription and re-subscribes scoped to the new id with sync() recomputing immediately. No stale-instance state survives the switch.
  • Auto-refresh in-flight guard (StatusTabs.tsx): isFetching is now scoped to instanceParams.entityId with entityId added to the effect deps, so the interval re-arms per instance — precisely the #1464 starvation mode, closed.

The operation-first key-convention flip's blast radius is fully covered: all four broad-invalidation call sites converted, the new invalidateEntityQueries predicate matches entityId at position 0 OR 1 (a strict superset of the old prefix match plus the new keys), org-scoped [organizationId] invalidations correctly left untouched, and zero remaining entity-first [entityId,'op'] consumers. Nice touch making the helper's reach a superset so restart/refetch flows can't silently stop matching.

Solid regression test — keying the hook to inst-A while inst-B is in-flight directly encodes the two-Status-pages bug and fails against the old position-0-only isOurs.

One non-blocking nit: on an instance switch, if the picker subtree isn't remounted by the router, the hook could render a single frame with the previous instance's lastFetchedAt before the re-subscribed sync() corrects it. In practice the route change remounts StatusTabs so state resets anyway — noting for completeness, no action needed.

— Claude (Sonnet 5), reviewed via review-queue

@kylebernhardy
kylebernhardy added this pull request to the merge queue Jul 13, 2026
Merged via the queue into stage with commit 6195fdf Jul 13, 2026
6 checks passed
@kylebernhardy
kylebernhardy deleted the kyle/freshness-instance-scope branch July 13, 2026 13:28
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.

Analytics freshness indicator doesn't filter by instance; query-key shapes are inconsistent

2 participants