From bbd5134dc5b12a0cd1b9b3a8c1ffa04c8d24b60c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 28 Aug 2026 10:47:54 -0700 Subject: [PATCH 1/2] improvement(usage): add a period-labelled chart to the workspace drill-down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Workspaces drill-down showed two ranked lists and no chart, and stated its window nowhere — the period picker lives on the list behind it, so the carried-over window was invisible once you were inside. - Draw the summary's headline, delta, and trend chart at the top of the drill-down, narrowed to that workspace - Label its section with the selected period, which is now the only place the drill-down states its window - Carry workspaceId through the summary contract, route, use case, query key, and hook so the chart reads one workspace - Move the workspace narrowing onto buildUsageAnalyticsScope, so the chart, the headline, and both lists derive it from one definition instead of the breakdown query owning a second copy The comparison window takes the same narrowing, or the delta would measure one workspace against the whole organization. No allowance figure is shown, unlike the Overview: the limit is pooled across the organization and would read as that workspace's own cap. --- .../organizations/[id]/usage/summary/route.ts | 1 + .../components/usage-monitoring.tsx | 29 +++++++++++++++++++ apps/sim/hooks/queries/organization-usage.ts | 17 ++++++++--- .../queries/utils/organization-usage-keys.ts | 9 ++++-- .../lib/api/contracts/organization-usage.ts | 17 +++++++++-- .../get-organization-usage-breakdown.ts | 4 +-- .../get-organization-usage-summary.ts | 10 +++++-- .../organization-usage-use-cases.test.ts | 26 +++++++++++++++++ .../billing/core/usage-analytics-queries.ts | 3 -- .../lib/billing/core/usage-analytics.test.ts | 27 +++++++++++++++++ apps/sim/lib/billing/core/usage-analytics.ts | 18 +++++++++++- 11 files changed, 144 insertions(+), 17 deletions(-) diff --git a/apps/sim/app/api/organizations/[id]/usage/summary/route.ts b/apps/sim/app/api/organizations/[id]/usage/summary/route.ts index ba7abb7372a..4d1111e1962 100644 --- a/apps/sim/app/api/organizations/[id]/usage/summary/route.ts +++ b/apps/sim/app/api/organizations/[id]/usage/summary/route.ts @@ -26,6 +26,7 @@ export const GET = defineInternalJsonRoute({ errorPolicy: organizationUsageErrorPolicy, mapInput: ({ params, query }) => ({ organizationId: params.id, + workspaceId: query.workspaceId, preset: query.preset, startDate: query.startDate ? new Date(query.startDate) : undefined, endDate: query.endDate ? new Date(query.endDate) : undefined, diff --git a/apps/sim/ee/organization-usage/components/usage-monitoring.tsx b/apps/sim/ee/organization-usage/components/usage-monitoring.tsx index 6a5bde4ff99..bb40073c660 100644 --- a/apps/sim/ee/organization-usage/components/usage-monitoring.tsx +++ b/apps/sim/ee/organization-usage/components/usage-monitoring.tsx @@ -183,6 +183,18 @@ export function UsageMonitoring({ limit: rowLimitFor('source'), ...(workspace ? { workspaceId: workspace } : {}), }) + /** + * The same headline and trend the Overview draws, narrowed to this workspace. + * + * A second summary rather than a figure derived from the lists below it: they carry + * totals but no time series, and the shape of the period is the question the chart + * answers. It is also the only place the drill-down states its window, which is why + * its section is labelled with the period rather than with the word "Usage". + */ + const workspaceSummary = useOrganizationUsageSummary(organizationId, window, { + enabled: isWorkspaceDetail, + ...(workspace ? { workspaceId: workspace } : {}), + }) // Already cached by Members and Billing, so the meter costs nothing extra and // cannot report a different allowance than they do. const billing = useOrganizationBilling(organizationId) @@ -321,6 +333,23 @@ export function UsageMonitoring({ : [] } > + {/* + Labelled with the period, not "Usage": the picker lives on the list behind + this view, so once you are in here the window is carried but invisible — and + a total with no stated period is a number people read as all-time. The + heading the chart already needs is where that belongs. + + No allowance passed, unlike the Overview: the limit is pooled across the + whole organization, and printing it under one workspace's figure would read + as that workspace's own cap. + */} + + + {/* Sources first, because in most workspaces the majority of usage is Chat rather than workflow runs — and a workflow list alone hid that behind a diff --git a/apps/sim/hooks/queries/organization-usage.ts b/apps/sim/hooks/queries/organization-usage.ts index 6eb65523ae5..ee7c7053bf4 100644 --- a/apps/sim/hooks/queries/organization-usage.ts +++ b/apps/sim/hooks/queries/organization-usage.ts @@ -26,19 +26,28 @@ export const ORGANIZATION_USAGE_EVENTS_STALE_TIME = 30 * 1000 const EVENTS_PAGE_SIZE = 50 +interface UseSummaryOptions { + /** The panel fetches the drill-down's chart only while that view is open. */ + enabled?: boolean + /** Narrows to one workspace, for the Workspaces drill-down. */ + workspaceId?: string +} + export function useOrganizationUsageSummary( organizationId: string | undefined, - window: OrganizationUsageWindowKey + window: OrganizationUsageWindowKey, + options: UseSummaryOptions = {} ) { + const { workspaceId } = options return useQuery({ - queryKey: organizationUsageKeys.summary(organizationId ?? '', window), + queryKey: organizationUsageKeys.summary(organizationId ?? '', window, workspaceId), queryFn: ({ signal }): Promise => requestJson(getOrganizationUsageSummaryContract, { params: { id: organizationId as string }, - query: { ...window }, + query: { ...window, ...(workspaceId ? { workspaceId } : {}) }, signal, }), - enabled: Boolean(organizationId), + enabled: Boolean(organizationId) && (options.enabled ?? true), staleTime: ORGANIZATION_USAGE_SUMMARY_STALE_TIME, // Changing the period should dim the current figures rather than blank them. placeholderData: keepPreviousData, diff --git a/apps/sim/hooks/queries/utils/organization-usage-keys.ts b/apps/sim/hooks/queries/utils/organization-usage-keys.ts index 3c147137bc0..cc5012e98cd 100644 --- a/apps/sim/hooks/queries/utils/organization-usage-keys.ts +++ b/apps/sim/hooks/queries/utils/organization-usage-keys.ts @@ -23,8 +23,13 @@ export interface OrganizationUsageWindowKey { export const organizationUsageKeys = { all: (organizationId: string) => ['organizations', 'detail', organizationId, 'usage'] as const, - summary: (organizationId: string, window: OrganizationUsageWindowKey) => - [...organizationUsageKeys.all(organizationId), 'summary', window] as const, + summary: ( + organizationId: string, + window: OrganizationUsageWindowKey, + /** Set only inside the Workspaces drill-down, whose chart reads one workspace. */ + workspaceId?: string + ) => + [...organizationUsageKeys.all(organizationId), 'summary', window, workspaceId ?? ''] as const, breakdowns: (organizationId: string, window: OrganizationUsageWindowKey) => [...organizationUsageKeys.all(organizationId), 'breakdown', window] as const, breakdown: ( diff --git a/apps/sim/lib/api/contracts/organization-usage.ts b/apps/sim/lib/api/contracts/organization-usage.ts index 658a9f040c1..5686c1ca61f 100644 --- a/apps/sim/lib/api/contracts/organization-usage.ts +++ b/apps/sim/lib/api/contracts/organization-usage.ts @@ -126,13 +126,24 @@ const organizationUsageWindowQuerySchema = z.object({ .default('UTC'), }) -export const organizationUsageSummaryQuerySchema = organizationUsageWindowQuerySchema +/** + * Narrows a read to one workspace, for the Workspaces drill-down. + * + * Declared once and spread into both query schemas: the drill-down draws its chart + * from the summary and its lists from the breakdown, so a workspace filter either + * surface could express alone is one the two could disagree about. + */ +const usageWorkspaceScopeShape = { + workspaceId: workspaceIdSchema.optional(), +} as const + +export const organizationUsageSummaryQuerySchema = + organizationUsageWindowQuerySchema.extend(usageWorkspaceScopeShape) export type OrganizationUsageSummaryQuery = z.input export const organizationUsageBreakdownQuerySchema = organizationUsageWindowQuerySchema.extend({ + ...usageWorkspaceScopeShape, dimension: usageBreakdownDimensionSchema, - /** Narrows the breakdown to one workspace, for the Workspaces drill-down. */ - workspaceId: workspaceIdSchema.optional(), limit: usageLimitSchema(50, 10), }) export type OrganizationUsageBreakdownQuery = z.input diff --git a/apps/sim/lib/billing/application/organization-usage/get-organization-usage-breakdown.ts b/apps/sim/lib/billing/application/organization-usage/get-organization-usage-breakdown.ts index 8efa0811cfd..4e5ea54fea3 100644 --- a/apps/sim/lib/billing/application/organization-usage/get-organization-usage-breakdown.ts +++ b/apps/sim/lib/billing/application/organization-usage/get-organization-usage-breakdown.ts @@ -78,8 +78,8 @@ export const getOrganizationUsageBreakdown = defineAuthorizedOrganizationUsageUs customEnd: input.endDate, timezone: input.timezone, }) - const scope = buildUsageAnalyticsScope(context.billingEntity, window) - const raw = await readUsageBreakdown(scope, input.dimension, input.workspaceId) + const scope = buildUsageAnalyticsScope(context.billingEntity, window, input.workspaceId) + const raw = await readUsageBreakdown(scope, input.dimension) /** * Re-key onto what the panel actually displays before ranking. diff --git a/apps/sim/lib/billing/application/organization-usage/get-organization-usage-summary.ts b/apps/sim/lib/billing/application/organization-usage/get-organization-usage-summary.ts index 48616c6f1a9..611f229be8e 100644 --- a/apps/sim/lib/billing/application/organization-usage/get-organization-usage-summary.ts +++ b/apps/sim/lib/billing/application/organization-usage/get-organization-usage-summary.ts @@ -20,6 +20,8 @@ export interface OrganizationUsageSummaryInput { startDate?: Date endDate?: Date timezone: string + /** Narrows to one workspace, for the Workspaces drill-down. */ + workspaceId?: string } export interface OrganizationUsageSummaryResult { @@ -47,7 +49,7 @@ export const getOrganizationUsageSummary = defineAuthorizedOrganizationUsageUseC timezone: input.timezone, }) const bucket = resolveUsageBucket(window) - const scope = buildUsageAnalyticsScope(context.billingEntity, window) + const scope = buildUsageAnalyticsScope(context.billingEntity, window, input.workspaceId) /** * The comparison window only exists when it is exactly derivable. @@ -68,7 +70,11 @@ export const getOrganizationUsageSummary = defineAuthorizedOrganizationUsageUseC readUsageTotals(scope), readUsageTimeSeries(scope, bucket, input.timezone), comparison - ? readUsageTotals(buildUsageAnalyticsScope(context.billingEntity, comparison)) + ? readUsageTotals( + // Same narrowing as the current window, or the delta would compare one + // workspace against the whole organization. + buildUsageAnalyticsScope(context.billingEntity, comparison, input.workspaceId) + ) : Promise.resolve(null), ]) diff --git a/apps/sim/lib/billing/application/organization-usage/organization-usage-use-cases.test.ts b/apps/sim/lib/billing/application/organization-usage/organization-usage-use-cases.test.ts index 173e15826b5..22120ef90ff 100644 --- a/apps/sim/lib/billing/application/organization-usage/organization-usage-use-cases.test.ts +++ b/apps/sim/lib/billing/application/organization-usage/organization-usage-use-cases.test.ts @@ -125,4 +125,30 @@ describe('organization usage authorization', () => { expect(JSON.stringify(totalsScope)).toContain(ORG) expect(JSON.stringify(seriesScope)).toBe(JSON.stringify(totalsScope)) }) + + it('narrows the drill-down’s chart and its comparison window to the same workspace', async () => { + /* + A reporting period, so the delta's read actually happens: `resolvePreviousPeriod` + returns null for a stripe period, and against the default subscription above this + test would assert the narrowing of a query that was never issued. + */ + mocks.getOrganizationSubscription.mockResolvedValue({ + plan: 'enterprise', + metadata: { reportingPeriodAnchorDate: '2026-01-01', reportingPeriodInterval: 'month' }, + }) + + await getOrganizationUsageSummary.execute({ + principal: session, + input: { ...input, workspaceId: 'ws-1' }, + }) + + // The current window and the previous one, both narrowed. Narrowing only the + // current window measures one workspace against the whole organization and + // renders the difference as that workspace's own trend. + expect(mocks.readUsageTotals).toHaveBeenCalledTimes(2) + for (const [scope] of mocks.readUsageTotals.mock.calls) { + expect(JSON.stringify(scope)).toContain('ws-1') + } + expect(JSON.stringify(mocks.readUsageTimeSeries.mock.calls[0][0])).toContain('ws-1') + }) }) diff --git a/apps/sim/lib/billing/core/usage-analytics-queries.ts b/apps/sim/lib/billing/core/usage-analytics-queries.ts index fec2b8e0480..f804a83ab66 100644 --- a/apps/sim/lib/billing/core/usage-analytics-queries.ts +++ b/apps/sim/lib/billing/core/usage-analytics-queries.ts @@ -120,13 +120,10 @@ function breakdownColumn(dimension: UsageBreakdownDimension) { export async function readUsageBreakdown( scope: SQL[], dimension: UsageBreakdownDimension, - /** Narrows to one workspace, for the Workspaces drill-down. */ - workspaceId: string | undefined, executor: DbClient = dbReplica ): Promise { const column = breakdownColumn(dimension) const conditions = [...scope] - if (workspaceId) conditions.push(eq(usageLog.workspaceId, workspaceId)) /** * `description` holds a model name only for the model categories; a tool or fixed * row would otherwise appear as a phantom "model". The two model dimensions split diff --git a/apps/sim/lib/billing/core/usage-analytics.test.ts b/apps/sim/lib/billing/core/usage-analytics.test.ts index c288c8e62ec..3fe079040e6 100644 --- a/apps/sim/lib/billing/core/usage-analytics.test.ts +++ b/apps/sim/lib/billing/core/usage-analytics.test.ts @@ -68,6 +68,33 @@ describe('buildUsageAnalyticsScope', () => { expect(shape).toContain('usageLog.billingEntityType') expect(shape).toContain('usageLog.billingEntityId') }) + + it('narrows to a workspace in every window shape', () => { + // Each branch returns its own array, so a narrowing added to only one of them is a + // drill-down that quietly reports the whole organization under the other two. + const windows = [ + { kind: 'period', period: period({ source: 'stripe' }) }, + { + kind: 'period', + period: period({ source: 'reporting', anchorDate: '2026-08-01', interval: 'month' }), + }, + { + kind: 'range', + from: new Date('2026-08-01T00:00:00.000Z'), + to: new Date('2026-08-08T00:00:00.000Z'), + }, + ] as const + + for (const window of windows) { + expect(JSON.stringify(buildUsageAnalyticsScope(ENTITY, window, 'ws-1'))).toContain( + 'usageLog.workspaceId' + ) + } + }) + + it('leaves the scope organization-wide when no workspace is given', () => { + expect(scopeShape({ kind: 'period', period: period() })).not.toContain('usageLog.workspaceId') + }) }) describe('usageWindowLedgerFilter', () => { diff --git a/apps/sim/lib/billing/core/usage-analytics.ts b/apps/sim/lib/billing/core/usage-analytics.ts index 07533fe0ac6..d50a71dbafa 100644 --- a/apps/sim/lib/billing/core/usage-analytics.ts +++ b/apps/sim/lib/billing/core/usage-analytics.ts @@ -65,12 +65,28 @@ export type UsageAnalyticsWindow = */ export function buildUsageAnalyticsScope( entity: BillingEntity, - window: UsageAnalyticsWindow + window: UsageAnalyticsWindow, + /** + * Narrows every read built from this scope to one workspace, for the Workspaces + * drill-down. + * + * On the scope rather than on each query: the drill-down draws a chart, a headline, + * and two ranked lists from separate reads, and a narrowing each one applied for + * itself is one they could apply differently. Not an authorization boundary — the + * entity predicates above are — so a workspace belonging to another organization + * narrows to nothing rather than disclosing anything. + * + * `workspace_id` is not in `usage_log_billing_entity_created_at_cost_idx`, so a + * narrowed read heap-fetches per row in the window. That is the cost the Workspaces + * tab already pays to rank its list, and it is only ever paid inside a drill-down. + */ + workspaceId?: string ): SQL[] { const conditions: SQL[] = [ eq(usageLog.billingEntityType, entity.type), eq(usageLog.billingEntityId, entity.id), ] + if (workspaceId) conditions.push(eq(usageLog.workspaceId, workspaceId)) if (window.kind === 'range') { conditions.push(gte(usageLog.createdAt, window.from), lt(usageLog.createdAt, window.to)) From 2880ff8f1adc97f22715d7c683c0c2cb1b11177f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 28 Aug 2026 11:02:48 -0700 Subject: [PATCH 2/2] fix(usage): stop a retained summary from crossing workspace scopes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The summary key now carries a workspaceId, and keepPreviousData retains across any key change — so moving between two drill-downs drew one workspace's headline, delta, and chart under the other's name until the fetch landed. - Narrow placeholderData to a period change only, matching the breakdown's existing scoped predicate, and share the one key-identity helper - Order the summary key so window is the trailing segment, making the scope a plain prefix as it already is on the breakdown - Give UsageSummary the isPlaceholderData signal UsageConsumers already takes, so retained figures dim instead of reading as fresh ones --- .../components/usage-monitoring.tsx | 2 + .../components/usage-summary.tsx | 20 +++++++-- apps/sim/hooks/queries/organization-usage.ts | 44 ++++++++++++++----- .../queries/utils/organization-usage-keys.ts | 14 +++++- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/apps/sim/ee/organization-usage/components/usage-monitoring.tsx b/apps/sim/ee/organization-usage/components/usage-monitoring.tsx index bb40073c660..6a060b66d4b 100644 --- a/apps/sim/ee/organization-usage/components/usage-monitoring.tsx +++ b/apps/sim/ee/organization-usage/components/usage-monitoring.tsx @@ -348,6 +348,7 @@ export function UsageMonitoring({ summary={workspaceSummary.data} isLoading={workspaceSummary.isLoading} isError={workspaceSummary.isError} + isPlaceholderData={workspaceSummary.isPlaceholderData} /> {/* @@ -481,6 +482,7 @@ export function UsageMonitoring({ } isLoading={summary.isLoading} isError={summary.isError} + isPlaceholderData={summary.isPlaceholderData} /> {/* diff --git a/apps/sim/ee/organization-usage/components/usage-summary.tsx b/apps/sim/ee/organization-usage/components/usage-summary.tsx index 0acdeec924a..41e5d5643dd 100644 --- a/apps/sim/ee/organization-usage/components/usage-summary.tsx +++ b/apps/sim/ee/organization-usage/components/usage-summary.tsx @@ -1,7 +1,7 @@ 'use client' import { useMemo } from 'react' -import { Badge } from '@sim/emcn' +import { Badge, cn } from '@sim/emcn' import { BarChart } from '@/components/charts' import type { OrganizationUsageSummary } from '@/lib/api/contracts/organization-usage' import { formatCreditsLabel } from '@/lib/billing/credits/conversion' @@ -16,6 +16,12 @@ interface UsageSummaryProps { limitCredits?: number | null isLoading: boolean isError: boolean + /** + * Dims the figures while a re-keyed fetch resolves, rather than blanking them — the + * same treatment `UsageConsumers` gives a retained list. Without it the headline and + * chart present the previous period's numbers as though they were the new period's. + */ + isPlaceholderData?: boolean } function percentDelta(current: number, previous: number): number | null { @@ -23,7 +29,13 @@ function percentDelta(current: number, previous: number): number | null { return ((current - previous) / previous) * 100 } -export function UsageSummary({ summary, limitCredits, isLoading, isError }: UsageSummaryProps) { +export function UsageSummary({ + summary, + limitCredits, + isLoading, + isError, + isPlaceholderData, +}: UsageSummaryProps) { /* Stabilized so `BarChart`'s `memo()` can actually pass. Built inline it was a new array on every render of the panel — a date-picker toggle or an export click @@ -52,7 +64,9 @@ export function UsageSummary({ summary, limitCredits, isLoading, isError }: Usag const isOverLimit = hasLimit && used > limitCredits return ( -
+
{/* One line, and the allowance sits beside the figure rather than under it — restating "4,958 credits used" below a "4,958 credits" headline said the same diff --git a/apps/sim/hooks/queries/organization-usage.ts b/apps/sim/hooks/queries/organization-usage.ts index ee7c7053bf4..1f514c9e568 100644 --- a/apps/sim/hooks/queries/organization-usage.ts +++ b/apps/sim/hooks/queries/organization-usage.ts @@ -26,6 +26,19 @@ export const ORGANIZATION_USAGE_EVENTS_STALE_TIME = 30 * 1000 const EVENTS_PAGE_SIZE = 50 +/** + * A usage key with its trailing segment dropped — the identity of the question being + * asked, which is what `placeholderData` has to compare on. + * + * Both keys put the one segment their placeholder may legitimately cross last: the + * summary's window ("the same scope, a different period") and the breakdown's row limit + * ("the same list, more rows"). Everything a retained answer must never cross — + * organization, workspace, dimension — sits in the prefix. + */ +function usageKeyIdentity(key: readonly unknown[]): string { + return hashKey(key.slice(0, -1)) +} + interface UseSummaryOptions { /** The panel fetches the drill-down's chart only while that view is open. */ enabled?: boolean @@ -39,8 +52,9 @@ export function useOrganizationUsageSummary( options: UseSummaryOptions = {} ) { const { workspaceId } = options + const queryKey = organizationUsageKeys.summary(organizationId ?? '', window, workspaceId) return useQuery({ - queryKey: organizationUsageKeys.summary(organizationId ?? '', window, workspaceId), + queryKey, queryFn: ({ signal }): Promise => requestJson(getOrganizationUsageSummaryContract, { params: { id: organizationId as string }, @@ -49,8 +63,22 @@ export function useOrganizationUsageSummary( }), enabled: Boolean(organizationId) && (options.enabled ?? true), staleTime: ORGANIZATION_USAGE_SUMMARY_STALE_TIME, - // Changing the period should dim the current figures rather than blank them. - placeholderData: keepPreviousData, + /** + * Kept only across a period change — the same scope asked about a different window, + * where dimming the figures beats blanking them. + * + * Not `keepPreviousData`, which retains across *any* key change: once the key + * carries a workspace, moving between two drill-downs would draw one workspace's + * headline, delta, and chart under the other's name until the fetch landed. A + * figure attributed to the wrong workspace is worse than a brief skeleton, and + * unlike a ranked list it carries nothing that would look out of place. + */ + placeholderData: (previous, previousQuery) => + previous && + previousQuery && + usageKeyIdentity(previousQuery.queryKey) === usageKeyIdentity(queryKey) + ? previous + : undefined, }) } @@ -62,14 +90,6 @@ interface UseBreakdownOptions { workspaceId?: string } -/** - * A breakdown key with its trailing row limit removed — the identity of the list, - * which is what "the same list, more rows" has to compare on. - */ -function breakdownListIdentity(key: readonly unknown[]): string { - return hashKey(key.slice(0, -1)) -} - export function useOrganizationUsageBreakdown( organizationId: string | undefined, window: OrganizationUsageWindowKey, @@ -110,7 +130,7 @@ export function useOrganizationUsageBreakdown( placeholderData: (previous, previousQuery) => previous && previousQuery && - breakdownListIdentity(previousQuery.queryKey) === breakdownListIdentity(queryKey) + usageKeyIdentity(previousQuery.queryKey) === usageKeyIdentity(queryKey) ? previous : undefined, }) diff --git a/apps/sim/hooks/queries/utils/organization-usage-keys.ts b/apps/sim/hooks/queries/utils/organization-usage-keys.ts index cc5012e98cd..0b0f4063f85 100644 --- a/apps/sim/hooks/queries/utils/organization-usage-keys.ts +++ b/apps/sim/hooks/queries/utils/organization-usage-keys.ts @@ -29,7 +29,19 @@ export const organizationUsageKeys = { /** Set only inside the Workspaces drill-down, whose chart reads one workspace. */ workspaceId?: string ) => - [...organizationUsageKeys.all(organizationId), 'summary', window, workspaceId ?? ''] as const, + [ + ...organizationUsageKeys.all(organizationId), + 'summary', + workspaceId ?? '', + /* + Last deliberately, as on `breakdown`: it is the one segment the summary's + `placeholderData` may cross, so the scope's identity is a plain prefix rather + than an index-based filter that would silently drop `workspaceId` if a segment + were ever appended. Ordering it the other way is what let a retained summary + cross workspaces. + */ + window, + ] as const, breakdowns: (organizationId: string, window: OrganizationUsageWindowKey) => [...organizationUsageKeys.all(organizationId), 'breakdown', window] as const, breakdown: (