fix(status): scope the freshness indicator to the viewed instance; unify status query keys - #1477
Conversation
…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
There was a problem hiding this comment.
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.
kriszyp
left a comment
There was a problem hiding this comment.
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
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
useAnalyticsFreshnessmatched cache entries by key prefix only (get_analytics_rawat 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 instanceentityIdand matches it at key position 1 (wheregetAnalytics.tsputs it).TimeRangePickerthreads it in fromAnalyticsContext.Same fix applied to the auto-refresh in-flight guard in
StatusTabs—queryClient.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.tsused['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: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.ts—get_statusinvalidation afterset_statusinstallUsageLicense.ts,useApplyLicensesClick.tsx—get_usage_licensesuseRollingConfigUpdate.tsx—get_configurationBroad 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 refetchget_status, sorestartRequiredwould linger). They now use a new predicate-based helper,invalidateEntityQueries(src/react-query/invalidateEntityQueries.ts), which matchesentityIdat 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
useAnalyticsFreshness.test.ts: cache entries forinst-Aandinst-B; the hook keyed toinst-Aignores B's in-flight fetch and success timestamp, and still reports A's own fetch/timestamp. Fails against the old unscoped implementation.tsc -b,dprint, andoxlintclean.Cross-model review
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).