diff --git a/apps/web/src/app/admin/cloud-billing-skus/UsageRecordsContent.tsx b/apps/web/src/app/admin/cloud-billing-skus/UsageRecordsContent.tsx
index 04ddbbb909c..a70dae3b8f9 100644
--- a/apps/web/src/app/admin/cloud-billing-skus/UsageRecordsContent.tsx
+++ b/apps/web/src/app/admin/cloud-billing-skus/UsageRecordsContent.tsx
@@ -68,6 +68,7 @@ type UsageSummaryRequest = {
start: string;
end: string;
};
+type UsageReconciliationRequest = UsageSummaryRequest & { intervalIds: string[] };
function formatTimestamp(value: string | null): string {
return value ? new Date(value).toLocaleString() : '—';
@@ -85,6 +86,8 @@ function assertNever(value: never): never {
function reconciliationStatusLabel(status: ReconciliationStatus): string {
switch (status) {
+ case 'compared':
+ return 'Compared';
case 'missing_from_cloudflare':
return 'Missing from Cloudflare';
case 'ambiguous_application':
@@ -99,6 +102,8 @@ function reconciliationStatusLabel(status: ReconciliationStatus): string {
function reconciliationStatusVariant(status: ReconciliationStatus) {
switch (status) {
+ case 'compared':
+ return 'new' as const;
case 'missing_from_cloudflare':
return 'destructive' as const;
case 'ambiguous_application':
@@ -111,9 +116,9 @@ function reconciliationStatusVariant(status: ReconciliationStatus) {
return assertNever(status);
}
-function sameSummaryRequest(
- first: UsageSummaryRequest | null | undefined,
- second: UsageSummaryRequest | null | undefined
+function sameReconciliationRequest(
+ first: UsageReconciliationRequest | null | undefined,
+ second: UsageReconciliationRequest | null | undefined
): boolean {
return (
first !== null &&
@@ -123,7 +128,9 @@ function sameSummaryRequest(
first.subjectType === second.subjectType &&
first.subjectId === second.subjectId &&
first.start === second.start &&
- first.end === second.end
+ first.end === second.end &&
+ first.intervalIds.length === second.intervalIds.length &&
+ first.intervalIds.every((intervalId, index) => intervalId === second.intervalIds[index])
);
}
@@ -132,6 +139,15 @@ function formatProvisionedCapacity(memoryBytes: number | null, diskBytes: number
return `${formatProviderNumber(memoryBytes / 1024 ** 3)} GiB memory · ${formatProviderNumber(diskBytes / 1_000_000_000)} GB disk`;
}
+function formatVariance(seconds: number | null, percent: number | null): string {
+ if (seconds === null) return 'Variance unavailable';
+ const secondsSign = seconds > 0 ? '+' : '';
+ if (percent === null)
+ return `${secondsSign}${formatProviderNumber(seconds)}s (no meter baseline)`;
+ const percentSign = percent > 0 ? '+' : '';
+ return `${secondsSign}${formatProviderNumber(seconds)}s (${percentSign}${formatProviderNumber(percent)}%)`;
+}
+
function toDateTimeLocalValue(value: Date): string {
const offset = value.getTimezoneOffset() * 60_000;
return new Date(value.getTime() - offset).toISOString().slice(0, 16);
@@ -307,9 +323,10 @@ export default function UsageRecordsContent() {
closeReason: submitted.closeReason,
skuId: submitted.skuId,
cursor,
- limit: submitted.kind === 'recent' ? 10 : 25,
+ limit: submitted.kind === 'recent' ? 10 : 15,
};
const results = useQuery(trpc.admin.cloudBillingSkus.searchUsageIntervals.queryOptions(input));
+ const rows = results.data?.items ?? [];
const summary = useQuery({
...trpc.admin.cloudBillingSkus.getUsageSummary.queryOptions(
summaryRequest ?? {
@@ -326,11 +343,17 @@ export default function UsageRecordsContent() {
trpc.admin.cloudBillingSkus.reconcileUsageWithCloudflare.mutationOptions()
);
const resetReconciliation = reconciliation.reset;
- const reconciliationMatchesSummary = sameSummaryRequest(reconciliation.variables, summaryRequest);
+ const reconciliationRequest =
+ summaryRequest && results.isSuccess && rows.length > 0
+ ? { ...summaryRequest, intervalIds: rows.map(row => row.id) }
+ : null;
+ const reconciliationMatchesScope = sameReconciliationRequest(
+ reconciliation.variables,
+ reconciliationRequest
+ );
const reconciliationErrorCode = reconciliation.error?.data?.code;
const reconciliationCanRetry =
reconciliationErrorCode !== 'BAD_REQUEST' && reconciliationErrorCode !== 'PRECONDITION_FAILED';
- const rows = results.data?.items ?? [];
const resetResultNavigation = () => {
setCursor(undefined);
@@ -598,7 +621,8 @@ export default function UsageRecordsContent() {
aria-describedby={
summaryInputError ? 'usage-summary-window-error' : undefined
}
- className="w-full sm:w-[9.5rem]"
+ className="w-full cursor-pointer sm:w-52 [&::-webkit-calendar-picker-indicator]:cursor-pointer"
+ onClick={event => event.currentTarget.showPicker?.()}
onChange={event => setSummaryStart(event.target.value)}
/>
event.currentTarget.showPicker?.()}
onChange={event => setSummaryEnd(event.target.value)}
/>
@@ -740,25 +765,27 @@ export default function UsageRecordsContent() {
Cloudflare reconciliation
- Query Cloudflare only for physical instance IDs recorded for this applied
- subject and window. This is shadow validation and does not change usage or
+ Query Cloudflare only for physical instance IDs in the usage records currently
+ displayed below. This is shadow validation and does not change usage or
billing.
reconciliation.mutate(summaryRequest)}
+ disabled={reconciliation.isPending || !reconciliationRequest}
+ onClick={() => {
+ if (reconciliationRequest) reconciliation.mutate(reconciliationRequest);
+ }}
>
{reconciliation.isPending
? 'Reconciling with Cloudflare...'
- : 'Reconcile with Cloudflare'}
+ : 'Reconcile displayed records'}
- {reconciliationMatchesSummary && reconciliation.isError && (
+ {reconciliationMatchesScope && reconciliation.isError && (
Cloudflare reconciliation could not be completed
@@ -769,7 +796,9 @@ export default function UsageRecordsContent() {
variant="outline"
size="sm"
disabled={reconciliation.isPending}
- onClick={() => reconciliation.mutate(summaryRequest)}
+ onClick={() => {
+ if (reconciliationRequest) reconciliation.mutate(reconciliationRequest);
+ }}
>
Retry reconciliation
@@ -778,7 +807,7 @@ export default function UsageRecordsContent() {
)}
- {reconciliationMatchesSummary && reconciliation.isSuccess && (
+ {reconciliationMatchesScope && reconciliation.isSuccess && (
{reconciliation.data.rows.length === 0 ? (
@@ -788,8 +817,7 @@ export default function UsageRecordsContent() {
) : (
<>
- {reconciliation.data.comparison.description} Provider CPU is shown only as
- a diagnostic.
+ {reconciliation.data.comparison.description}
@@ -802,14 +830,15 @@ export default function UsageRecordsContent() {
-
Instances queried
+ Runs queried
- {reconciliation.data.totals.queriedCloudflareInstances.toLocaleString()}
+ {reconciliation.data.totals.queriedCloudflareRuns.toLocaleString()}
+ {reconciliation.data.counts.compared} compared ·{' '}
{reconciliation.data.counts.missing} missing ·{' '}
{reconciliation.data.counts.ambiguous} ambiguous ·{' '}
{reconciliation.data.counts.partial} partial ·{' '}
@@ -840,17 +869,15 @@ export default function UsageRecordsContent() {
Instance
Application / service
SKU(s)
- Meter accepted
- Provider allocation equivalents
- Provider CPU
+ Meter run
+ Cloudflare memory / disk
+ Cloudflare CPU (diagnostic)
Status
{reconciliation.data.rows.map(row => (
-
+
{row.instanceId}
@@ -883,17 +910,37 @@ export default function UsageRecordsContent() {
- {row.meterAcceptedSeconds.toLocaleString()}s
+
+ {row.meterAcceptedSeconds.toLocaleString()}s accepted
+
+
+ {formatTimestamp(row.meterStartedAt)}
+
+
+ to {formatTimestamp(row.meterEndedAt)}
+
Memory {formatProviderNumber(row.providerMemorySeconds)}
{row.providerMemorySeconds === null ? '' : 's'}
-
+
+ {formatVariance(
+ row.providerMemoryDifferenceSeconds,
+ row.providerMemoryDifferencePercent
+ )}
+
+
Disk {formatProviderNumber(row.providerDiskSeconds)}
{row.providerDiskSeconds === null ? '' : 's'}
+
+ {formatVariance(
+ row.providerDiskDifferenceSeconds,
+ row.providerDiskDifferencePercent
+ )}
+
{formatProviderNumber(row.providerCpuTimeSec)}
@@ -969,14 +1016,13 @@ export default function UsageRecordsContent() {
{reconciliation.data.provider.rawResponses.map((raw, index) => (
- {raw.dataset} · window {raw.windowIndex + 1} · batch{' '}
- {raw.batchIndex + 1}
- {raw.window
- ? ` · ${formatTimestamp(raw.window.start)} to ${formatTimestamp(raw.window.end)}`
+ {raw.dataset}
+ {raw.queries.length > 0
+ ? ` · batch ${raw.batchIndex} · ${raw.queries.length} window${raw.queries.length === 1 ? '' : 's'}`
: ''}
diff --git a/apps/web/src/lib/cloudflare/container-usage-analytics.test.ts b/apps/web/src/lib/cloudflare/container-usage-analytics.test.ts
index 943bf09e379..77413b165cd 100644
--- a/apps/web/src/lib/cloudflare/container-usage-analytics.test.ts
+++ b/apps/web/src/lib/cloudflare/container-usage-analytics.test.ts
@@ -21,9 +21,14 @@ const USAGE_FIELDS = [
'sum_txBytes',
];
const input = {
- instanceIds: ['instance-1'],
- start: '2026-07-01T00:00:00.000Z',
- end: '2026-07-01T01:00:00.000Z',
+ runs: [
+ {
+ key: 'run-1',
+ instanceId: 'instance-1',
+ start: '2026-07-01T00:00:00.000Z',
+ end: '2026-07-01T01:00:00.000Z',
+ },
+ ],
};
const options: ContainerUsageAnalyticsOptions = {
accountId: ACCOUNT_ID,
@@ -67,21 +72,30 @@ function settingsBody(
};
}
+type UsageGroup = {
+ dimensions: { applicationId: string; instanceId: string };
+ sum: { cpuTimeSec: number; allocatedMemory: number; allocatedDisk: number; txBytes: number };
+};
+
function usageBody(
- groups: Array<{
- dimensions: { applicationId: string; instanceId: string };
- sum: { cpuTimeSec: number; allocatedMemory: number; allocatedDisk: number; txBytes: number };
- }> = [],
+ groups: UsageGroup[] = [],
errors: Array<{ message: string }> | null = null,
extra: Record = {}
) {
return {
- data: { viewer: { accounts: [{ containersUsageAdaptiveGroups: groups }] } },
+ data: { viewer: { accounts: [{ u0: groups }] } },
errors,
...extra,
};
}
+function usageBatchBody(
+ aliases: Record,
+ errors: Array<{ message: string; path?: Array }> | null = null
+) {
+ return { data: { viewer: { accounts: [aliases] } }, errors };
+}
+
function response(body: unknown, init: ResponseInit = {}) {
return new Response(typeof body === 'string' ? body : JSON.stringify(body), {
status: 200,
@@ -120,16 +134,18 @@ describe('queryContainerUsageAnalytics', () => {
expect(fetch).toHaveBeenCalledTimes(2);
expect(requests[0]?.body.variables).toEqual({ accountTag: ACCOUNT_ID });
expect(requests[1]?.headers.get('authorization')).toBe(`Bearer ${API_TOKEN}`);
- expect(requests[1]?.body.query).toContain('datetime_lt: $datetimeEnd');
- expect(requests[1]?.body.query).toContain('instanceId_in: $instanceIds');
+ expect(requests[1]?.body.query).toContain('u0: containersUsageAdaptiveGroups');
+ expect(requests[1]?.body.query).toContain('datetime_lt: $datetimeEnd0');
+ expect(requests[1]?.body.query).toContain('instanceId_in: $instanceIds0');
expect(requests[1]?.body.variables).toEqual({
accountTag: ACCOUNT_ID,
- datetimeStart: input.start,
- datetimeEnd: input.end,
- instanceIds: ['instance-1'],
+ datetimeStart0: input.runs[0]?.start,
+ datetimeEnd0: input.runs[0]?.end,
+ instanceIds0: ['instance-1'],
});
expect(result.rows).toEqual([
{
+ runKey: 'run-1',
applicationId: 'observed-app',
instanceId: 'instance-1',
usage: providerRow.sum,
@@ -143,6 +159,93 @@ describe('queryContainerUsageAnalytics', () => {
expect(JSON.stringify(result)).not.toContain('authorization');
});
+ it('keeps reused instance IDs isolated by alias and marks only the errored run partial', async () => {
+ const runs = [
+ input.runs[0]!,
+ {
+ key: 'run-2',
+ instanceId: 'instance-1',
+ start: '2026-07-01T02:00:00.000Z',
+ end: '2026-07-01T03:00:00.000Z',
+ },
+ ];
+ const { fetch } = requestCapture((_request, index) =>
+ response(
+ index === 0
+ ? settingsBody()
+ : usageBatchBody(
+ {
+ u0: [
+ {
+ dimensions: { applicationId: 'app', instanceId: 'instance-1' },
+ sum: { cpuTimeSec: 1, allocatedMemory: 10, allocatedDisk: 20, txBytes: 30 },
+ },
+ ],
+ u1: [
+ {
+ dimensions: { applicationId: 'app', instanceId: 'instance-1' },
+ sum: { cpuTimeSec: 2, allocatedMemory: 100, allocatedDisk: 200, txBytes: 300 },
+ },
+ ],
+ },
+ [{ message: 'run 2 partial', path: ['ContainerUsage', 'u1'] }]
+ )
+ )
+ );
+
+ const result = await queryContainerUsageAnalytics({ runs }, { ...options, fetch });
+
+ expect(result.rows).toEqual([
+ expect.objectContaining({
+ runKey: 'run-1',
+ usage: expect.objectContaining({ allocatedMemory: 10 }),
+ }),
+ expect.objectContaining({
+ runKey: 'run-2',
+ usage: expect.objectContaining({ allocatedMemory: 100 }),
+ }),
+ ]);
+ expect(result.usagePartialRunKeys).toEqual(['run-2']);
+ expect(result.issues).toEqual([
+ expect.stringContaining('run run-2 window 0 returned partial data: run 2 partial'),
+ ]);
+ expect(result.issues.join(' ')).not.toContain('run run-1');
+ });
+
+ it('fails an unscoped batched GraphQL error without retry fan-out', async () => {
+ const runs = [
+ input.runs[0]!,
+ {
+ key: 'run-2',
+ instanceId: 'instance-2',
+ start: '2026-07-01T02:00:00.000Z',
+ end: '2026-07-01T03:00:00.000Z',
+ },
+ ];
+ const providerRows: UsageGroup[] = runs.map((run, index) => ({
+ dimensions: { applicationId: 'app', instanceId: run.instanceId },
+ sum: {
+ cpuTimeSec: index + 1,
+ allocatedMemory: (index + 1) * 10,
+ allocatedDisk: (index + 1) * 20,
+ txBytes: (index + 1) * 30,
+ },
+ }));
+ const { fetch } = requestCapture((_request, index) => {
+ if (index === 0) return response(settingsBody());
+ return response(
+ usageBatchBody({ u0: [providerRows[0]!], u1: [providerRows[1]!] }, [
+ { message: 'batch partial' },
+ ])
+ );
+ });
+
+ await expect(
+ queryContainerUsageAnalytics({ runs }, { ...options, fetch })
+ ).rejects.toMatchObject({ code: 'graphql_error' });
+ expect(fetch).toHaveBeenCalledTimes(2);
+ });
+
it.each([
['HTTP failure', () => response({ error: 'denied' }, { status: 403 }), 'http_error'],
['invalid JSON', () => response('bad gateway'), 'invalid_json'],
@@ -178,7 +281,7 @@ describe('queryContainerUsageAnalytics', () => {
);
const result = await queryContainerUsageAnalytics(input, { ...options, fetch: partial.fetch });
expect(result.partial).toBe(true);
- expect(result.usagePartialInstanceIds).toEqual(['instance-1']);
+ expect(result.usagePartialRunKeys).toEqual(['run-1']);
expect(result.rows).toHaveLength(1);
});
@@ -207,25 +310,38 @@ describe('queryContainerUsageAnalytics', () => {
}
});
- it('chunks by live duration and batches exact IDs below the page limit', async () => {
- const ids = Array.from({ length: 51 }, (_, index) => `instance-${index}`);
+ it('batches up to 15 independently filtered run windows per usage request', async () => {
+ const runs = Array.from({ length: 16 }, (_, index) => ({
+ key: index === 0 ? 'interval:with:colons' : `run-${index}`,
+ instanceId: `instance-${index}`,
+ start: '2026-07-01T00:00:00.000Z',
+ end: '2026-07-01T01:00:00.000Z',
+ }));
const { fetch, requests } = requestCapture((_request, index) =>
- response(index === 0 ? settingsBody({ maxDuration: 3_600, maxPageSize: 100 }) : usageBody())
+ response(
+ index === 0
+ ? settingsBody({ maxDuration: 3_600, maxPageSize: 100 })
+ : usageBatchBody(
+ Object.fromEntries(
+ Array.from({ length: index === 1 ? 15 : 1 }, (_, aliasIndex) => [
+ `u${aliasIndex}`,
+ [],
+ ])
+ )
+ )
+ )
);
- await queryContainerUsageAnalytics(
- { instanceIds: ids, start: input.start, end: '2026-07-01T02:00:00.000Z' },
- { ...options, fetch }
- );
+ const result = await queryContainerUsageAnalytics({ runs }, { ...options, fetch });
- expect(fetch).toHaveBeenCalledTimes(5);
+ expect(fetch).toHaveBeenCalledTimes(3);
const queries = requests.slice(1);
- expect(new Set(queries.map(item => item.body.variables.instanceIds.length))).toEqual(
- new Set([1, 50])
- );
- expect(new Set(queries.map(item => item.body.variables.datetimeStart))).toEqual(
- new Set(['2026-07-01T00:00:00.000Z', '2026-07-01T01:00:00.000Z'])
- );
+ expect(queries[0]?.body.variables.instanceIds14).toEqual(['instance-14']);
+ expect(queries[1]?.body.variables.instanceIds0).toEqual(['instance-15']);
+ expect(queries[0]?.body.query).not.toContain('interval:with:colons');
+ expect(result.rawResponses).toHaveLength(3);
+ expect(result.rawResponses[1]?.queries).toHaveLength(15);
+ expect(result.rawResponses[1]?.queries[0]?.runKey).toBe('interval:with:colons');
});
it('marks a full page partial and rejects request plans above the cap', async () => {
@@ -249,7 +365,7 @@ describe('queryContainerUsageAnalytics', () => {
...options,
fetch: fullPage.fetch,
});
- expect(partial.usagePartialInstanceIds).toEqual(['instance-1']);
+ expect(partial.usagePartialRunKeys).toEqual(['run-1']);
const requestCap = requestCapture((_request, index) =>
response(index === 0 ? settingsBody({ maxPageSize: 2, maxDuration: 3_600 }) : usageBody())
@@ -257,9 +373,12 @@ describe('queryContainerUsageAnalytics', () => {
await expect(
queryContainerUsageAnalytics(
{
- instanceIds: Array.from({ length: 50 }, (_, index) => `instance-${index}`),
- start: input.start,
- end: '2026-07-02T00:00:00.000Z',
+ runs: Array.from({ length: 5 }, (_, index) => ({
+ key: `run-${index}`,
+ instanceId: `instance-${index}`,
+ start: '2026-07-01T00:00:00.000Z',
+ end: '2026-07-02T00:00:00.000Z',
+ })),
},
{ ...options, fetch: requestCap.fetch }
)
@@ -296,7 +415,7 @@ describe('queryContainerUsageAnalytics', () => {
viewer: {
accounts: [
{
- containersUsageAdaptiveGroups: [
+ u0: [
{
dimensions: { applicationId: 'app', instanceId: 'instance-1' },
sum: { cpuTimeSec: 1, allocatedMemory: 2, txBytes: 3 },
@@ -315,7 +434,7 @@ describe('queryContainerUsageAnalytics', () => {
});
});
- it('marks retention clipping partial and rejects fully expired windows', async () => {
+ it('marks retention clipping partial and skips fully expired runs without blocking valid runs', async () => {
const retained = requestCapture((_request, index) =>
response(index === 0 ? settingsBody({ notOlderThan: 84_600 }) : usageBody())
);
@@ -324,11 +443,33 @@ describe('queryContainerUsageAnalytics', () => {
fetch: retained.fetch,
});
expect(partial.partial).toBe(true);
- expect(partial.usagePartialInstanceIds).toEqual(['instance-1']);
+ expect(partial.usagePartialRunKeys).toEqual(['run-1']);
- const expired = requestCapture(() => response(settingsBody({ notOlderThan: 3_600 })));
- await expect(
- queryContainerUsageAnalytics(input, { ...options, fetch: expired.fetch })
- ).rejects.toMatchObject({ code: 'outside_retention' });
+ const validProviderRow = {
+ dimensions: { applicationId: 'app', instanceId: 'instance-2' },
+ sum: { cpuTimeSec: 1, allocatedMemory: 2, allocatedDisk: 3, txBytes: 4 },
+ };
+ const expired = requestCapture((_request, index) =>
+ response(index === 0 ? settingsBody({ notOlderThan: 3_600 }) : usageBody([validProviderRow]))
+ );
+ const result = await queryContainerUsageAnalytics(
+ {
+ runs: [
+ input.runs[0]!,
+ {
+ key: 'valid-run',
+ instanceId: 'instance-2',
+ start: '2026-07-01T23:30:00.000Z',
+ end: '2026-07-01T23:45:00.000Z',
+ },
+ ],
+ },
+ { ...options, fetch: expired.fetch }
+ );
+ expect(expired.fetch).toHaveBeenCalledTimes(2);
+ expect(result.usageUnavailableRuns).toEqual([{ runKey: 'run-1', reason: 'outside_retention' }]);
+ expect(result.rows).toEqual([
+ expect.objectContaining({ runKey: 'valid-run', instanceId: 'instance-2' }),
+ ]);
});
});
diff --git a/apps/web/src/lib/cloudflare/container-usage-analytics.ts b/apps/web/src/lib/cloudflare/container-usage-analytics.ts
index d8d24a1df18..ab948c4ea74 100644
--- a/apps/web/src/lib/cloudflare/container-usage-analytics.ts
+++ b/apps/web/src/lib/cloudflare/container-usage-analytics.ts
@@ -9,13 +9,17 @@ const DATASET = 'containersUsageAdaptiveGroups' as const;
const DEFAULT_TIMEOUT_MS = 10_000;
const MAX_RAW_RESPONSE_BYTES = 1024 * 1024;
const MAX_RETAINED_RAW_BYTES = 4 * 1024 * 1024;
-const MAX_INSTANCE_BATCH_SIZE = 50;
-const MAX_PROVIDER_REQUESTS = 100;
+const MAX_PROVIDER_QUERY_PLANS = 100;
+// Cloudflare applies maxNumberOfFields per aliased dataset selection, not across aliases.
+const MAX_RUN_WINDOWS_PER_REQUEST = 15;
export type ContainerUsageAnalyticsInput = {
- instanceIds: string[];
- start: string;
- end: string;
+ runs: Array<{
+ key: string;
+ instanceId: string;
+ start: string;
+ end: string;
+ }>;
};
export type ContainerUsageAnalyticsOptions = {
@@ -37,13 +41,18 @@ export type ContainerDatasetSettings = {
export type ContainerUsageAnalyticsRawResponse = {
dataset: typeof DATASET | 'settings';
- windowIndex: number;
batchIndex: number;
- window: { start: string; end: string } | null;
+ queries: Array<{
+ alias: string;
+ runKey: string;
+ windowIndex: number;
+ window: { start: string; end: string };
+ }>;
body: unknown;
};
export type ContainerUsageAnalyticsRow = {
+ runKey: string;
applicationId: string;
instanceId: string;
usage: {
@@ -57,7 +66,8 @@ export type ContainerUsageAnalyticsRow = {
export type ContainerUsageAnalyticsResult = {
rows: ContainerUsageAnalyticsRow[];
partial: boolean;
- usagePartialInstanceIds: string[];
+ usagePartialRunKeys: string[];
+ usageUnavailableRuns: Array<{ runKey: string; reason: 'outside_retention' }>;
issues: string[];
settings: { containersUsageAdaptiveGroups: ContainerDatasetSettings };
rawResponses: ContainerUsageAnalyticsRawResponse[];
@@ -83,7 +93,14 @@ const DatasetSettingsSchema = z.object({
});
const GraphqlErrorsSchema = z
- .array(z.object({ message: z.string().optional() }).passthrough())
+ .array(
+ z
+ .object({
+ message: z.string().optional(),
+ path: z.array(z.union([z.string(), z.number()])).optional(),
+ })
+ .passthrough()
+ )
.nullish();
const SettingsResponseSchema = z.object({
@@ -123,9 +140,7 @@ const UsageResponseSchema = z.object({
viewer: z
.object({
accounts: z
- .array(
- z.object({ containersUsageAdaptiveGroups: z.array(UsageGroupSchema).optional() })
- )
+ .array(z.record(z.string(), z.array(UsageGroupSchema).nullable()).nullable())
.optional(),
})
.optional(),
@@ -152,27 +167,43 @@ query ContainerUsageAnalyticsSettings($accountTag: String!) {
}
}`;
-function usageQuery(limit: number): string {
- return `
-query ContainerUsage(
- $accountTag: String!
- $datetimeStart: Time!
- $datetimeEnd: Time!
- $instanceIds: [String!]
-) {
- viewer {
- accounts(filter: { accountTag: $accountTag }) {
- containersUsageAdaptiveGroups(
+type QueryPlan = {
+ run: { key: string; instanceId: string };
+ window: { start: string; end: string };
+ windowIndex: number;
+};
+
+function usageQuery(limit: number, plans: QueryPlan[]): string {
+ const variableDefinitions = plans
+ .map(
+ (_plan, index) =>
+ `$datetimeStart${index}: Time!\n $datetimeEnd${index}: Time!\n $instanceIds${index}: [String!]`
+ )
+ .join('\n ');
+ const fields = plans
+ .map(
+ (_plan, index) => `
+ u${index}: containersUsageAdaptiveGroups(
limit: ${limit}
filter: {
- datetime_geq: $datetimeStart
- datetime_lt: $datetimeEnd
- instanceId_in: $instanceIds
+ datetime_geq: $datetimeStart${index}
+ datetime_lt: $datetimeEnd${index}
+ instanceId_in: $instanceIds${index}
}
) {
dimensions { applicationId instanceId }
sum { cpuTimeSec allocatedMemory allocatedDisk txBytes }
- }
+ }`
+ )
+ .join('\n');
+ return `
+query ContainerUsage(
+ $accountTag: String!
+ ${variableDefinitions}
+) {
+ viewer {
+ accounts(filter: { accountTag: $accountTag }) {
+ ${fields}
}
}
}`;
@@ -333,18 +364,35 @@ export async function queryContainerUsageAnalytics(
);
}
- const startMs = parseTime(input.start, 'start');
- const endMs = parseTime(input.end, 'end');
- if (endMs <= startMs) {
- throw new ContainerUsageAnalyticsError('invalid_input', 'end must be after start.');
- }
- const instanceIds = [...new Set(input.instanceIds.filter(id => id.length > 0))];
+ const runKeys = new Set();
+ const runs = input.runs.map(run => {
+ if (!run.key || !run.instanceId) {
+ throw new ContainerUsageAnalyticsError(
+ 'invalid_input',
+ 'Each run must include a key and instance ID.'
+ );
+ }
+ if (runKeys.has(run.key)) {
+ throw new ContainerUsageAnalyticsError('invalid_input', `Duplicate run key: ${run.key}`);
+ }
+ runKeys.add(run.key);
+ const startMs = parseTime(run.start, `start for ${run.key}`);
+ const endMs = parseTime(run.end, `end for ${run.key}`);
+ if (endMs <= startMs) {
+ throw new ContainerUsageAnalyticsError(
+ 'invalid_input',
+ `end must be after start for ${run.key}.`
+ );
+ }
+ return { ...run, startMs, endMs };
+ });
const fetchImpl = options.fetch ?? fetch;
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const retainedBytes = { value: 0 };
const rawResponses: ContainerUsageAnalyticsRawResponse[] = [];
const issues: string[] = [];
const partialIds = new Set();
+ const unavailableRuns: ContainerUsageAnalyticsResult['usageUnavailableRuns'] = [];
const settingsBody = await postGraphql({
fetchImpl,
@@ -363,9 +411,8 @@ export async function queryContainerUsageAnalytics(
}
rawResponses.push({
dataset: 'settings',
- windowIndex: 0,
batchIndex: 0,
- window: null,
+ queries: [],
body: settingsBody,
});
const settingsErrors = graphqlErrors(settingsResponse.data.errors);
@@ -381,99 +428,146 @@ export async function queryContainerUsageAnalytics(
const settings = validateSettings(account.settings?.containersUsageAdaptiveGroups);
if (settingsErrors.length > 0) {
issues.push(`Settings query reported GraphQL errors: ${settingsErrors.join('; ')}`);
- for (const id of instanceIds) partialIds.add(id);
+ for (const run of runs) partialIds.add(run.key);
}
const nowMs = (options.now ?? (() => new Date()))().getTime();
const oldestAllowedMs = nowMs - settings.notOlderThan * 1000;
- if (endMs <= oldestAllowedMs) {
- throw new ContainerUsageAnalyticsError(
- 'outside_retention',
- 'The requested window is outside Cloudflare Analytics retention. Choose a more recent window.'
- );
- }
- const effectiveStartMs = Math.max(startMs, oldestAllowedMs);
- if (effectiveStartMs !== startMs) {
- issues.push('Results cover only the portion of the window retained by Cloudflare Analytics.');
- for (const id of instanceIds) partialIds.add(id);
- }
-
const settingsSummary = { containersUsageAdaptiveGroups: settings };
- if (instanceIds.length === 0) {
+ if (runs.length === 0) {
return {
rows: [],
partial: issues.length > 0,
- usagePartialInstanceIds: [],
+ usagePartialRunKeys: [],
+ usageUnavailableRuns: [],
issues,
settings: settingsSummary,
rawResponses,
};
}
- const timeWindows = windows(effectiveStartMs, endMs, settings.maxDuration);
- // Leave one row of headroom: one row per requested ID is complete, while a full
- // page means an extra application row may have truncated the response. A complete
- // multi-application ID can therefore be conservatively marked partial.
- const idBatches = batches(
- instanceIds,
- Math.min(MAX_INSTANCE_BATCH_SIZE, settings.maxPageSize - 1)
- );
- const requestCount = 1 + timeWindows.length * idBatches.length;
- if (requestCount > MAX_PROVIDER_REQUESTS) {
+ const queryPlans = runs.flatMap(run => {
+ if (run.endMs <= oldestAllowedMs) {
+ unavailableRuns.push({ runKey: run.key, reason: 'outside_retention' });
+ issues.push(`Run ${run.key} is outside Cloudflare Analytics retention.`);
+ return [];
+ }
+ const effectiveStartMs = Math.max(run.startMs, oldestAllowedMs);
+ if (effectiveStartMs !== run.startMs) {
+ issues.push(`Run ${run.key} covers only retained Cloudflare Analytics data.`);
+ partialIds.add(run.key);
+ }
+ return windows(effectiveStartMs, run.endMs, settings.maxDuration).map(
+ (window, windowIndex) => ({
+ run,
+ window,
+ windowIndex,
+ })
+ );
+ });
+ if (queryPlans.length > MAX_PROVIDER_QUERY_PLANS) {
throw new ContainerUsageAnalyticsError(
'request_limit_exceeded',
- `Cloudflare Analytics would require ${requestCount} requests (max ${MAX_PROVIDER_REQUESTS}). Narrow the window and retry.`
+ `The selected runs require ${queryPlans.length} Cloudflare Analytics query plans (max ${MAX_PROVIDER_QUERY_PLANS}). Select fewer or shorter runs and retry.`
);
}
const rows = new Map();
- for (let windowIndex = 0; windowIndex < timeWindows.length; windowIndex += 1) {
- const window = timeWindows[windowIndex];
- if (!window) continue;
- for (let batchIndex = 0; batchIndex < idBatches.length; batchIndex += 1) {
- const batch = idBatches[batchIndex];
- if (!batch) continue;
- const body = await postGraphql({
- fetchImpl,
- token,
- timeoutMs,
- query: usageQuery(settings.maxPageSize),
- variables: {
- accountTag: accountId,
- datetimeStart: window.start,
- datetimeEnd: window.end,
- instanceIds: batch,
- },
- retainedBytes,
- });
- rawResponses.push({ dataset: DATASET, windowIndex, batchIndex, window, body });
- const response = UsageResponseSchema.safeParse(body);
- if (!response.success) {
- throw new ContainerUsageAnalyticsError(
- 'invalid_response_shape',
- 'Cloudflare Analytics usage response had an unexpected shape.'
+ const queryBatches = batches(queryPlans, MAX_RUN_WINDOWS_PER_REQUEST);
+ let responseIndex = 1;
+ const queryBatch = async (batch: QueryPlan[]): Promise => {
+ const variables: Record = { accountTag: accountId };
+ for (let index = 0; index < batch.length; index += 1) {
+ const plan = batch[index];
+ if (!plan) continue;
+ variables[`datetimeStart${index}`] = plan.window.start;
+ variables[`datetimeEnd${index}`] = plan.window.end;
+ variables[`instanceIds${index}`] = [plan.run.instanceId];
+ }
+ const body = await postGraphql({
+ fetchImpl,
+ token,
+ timeoutMs,
+ query: usageQuery(settings.maxPageSize, batch),
+ variables,
+ retainedBytes,
+ });
+ const batchIndex = responseIndex;
+ responseIndex += 1;
+ rawResponses.push({
+ dataset: DATASET,
+ batchIndex,
+ queries: batch.map((plan, index) => ({
+ alias: `u${index}`,
+ runKey: plan.run.key,
+ windowIndex: plan.windowIndex,
+ window: plan.window,
+ })),
+ body,
+ });
+ const response = UsageResponseSchema.safeParse(body);
+ if (!response.success) {
+ throw new ContainerUsageAnalyticsError(
+ 'invalid_response_shape',
+ 'Cloudflare Analytics usage response had an unexpected shape.'
+ );
+ }
+ const errorDetails = response.data.errors ?? [];
+ const errors = graphqlErrors(errorDetails);
+ const account = response.data.data?.viewer?.accounts?.[0];
+ const unscopedErrors = errorDetails.filter(
+ error => !error.path?.some(part => typeof part === 'string' && /^u\d+$/.test(part))
+ );
+ if (unscopedErrors.length > 0 && batch.length > 1) {
+ throw new ContainerUsageAnalyticsError(
+ 'graphql_error',
+ `Cloudflare Analytics returned an unscoped batch error: ${graphqlErrors(unscopedErrors).join('; ')}`
+ );
+ }
+ if (!account) {
+ throw new ContainerUsageAnalyticsError(
+ 'graphql_error',
+ errors.length > 0
+ ? `Cloudflare Analytics usage query failed: ${errors.join('; ')}`
+ : 'Cloudflare Analytics usage query returned no account data.'
+ );
+ }
+ for (let index = 0; index < batch.length; index += 1) {
+ const plan = batch[index];
+ if (!plan) continue;
+ const alias = `u${index}`;
+ const groups = account[alias];
+ const aliasErrors = errorDetails.filter(error => {
+ const errorAlias = error.path?.find(
+ part => typeof part === 'string' && /^u\d+$/.test(part)
);
- }
- const errors = graphqlErrors(response.data.errors);
- const groups = response.data.data?.viewer?.accounts?.[0]?.containersUsageAdaptiveGroups;
- if (!groups) {
- throw new ContainerUsageAnalyticsError(
- 'graphql_error',
- errors.length > 0
- ? `Cloudflare Analytics usage query failed: ${errors.join('; ')}`
- : 'Cloudflare Analytics usage query returned no account data.'
+ return errorAlias === alias || errorAlias === undefined;
+ });
+ const aliasErrorMessages = graphqlErrors(aliasErrors);
+ const aliasPartial = aliasErrors.length > 0;
+ if (groups === null || groups === undefined) {
+ if (!aliasPartial) {
+ throw new ContainerUsageAnalyticsError(
+ 'invalid_response_shape',
+ `Cloudflare Analytics usage response omitted ${alias}.`
+ );
+ }
+ partialIds.add(plan.run.key);
+ issues.push(
+ `${DATASET} run ${plan.run.key} window ${plan.windowIndex} returned no usable data: ${aliasErrorMessages.join('; ')}`
);
+ continue;
}
- if (errors.length > 0 || groups.length >= settings.maxPageSize) {
- for (const id of batch) partialIds.add(id);
+ if (aliasPartial || groups.length >= settings.maxPageSize) {
+ partialIds.add(plan.run.key);
issues.push(
- errors.length > 0
- ? `${DATASET} window ${windowIndex} batch ${batchIndex} returned partial data: ${errors.join('; ')}`
- : `${DATASET} window ${windowIndex} batch ${batchIndex} reached the page limit.`
+ aliasPartial
+ ? `${DATASET} run ${plan.run.key} window ${plan.windowIndex} returned partial data: ${aliasErrorMessages.join('; ')}`
+ : `${DATASET} run ${plan.run.key} window ${plan.windowIndex} reached the page limit.`
);
}
for (const group of groups) {
- const key = `${group.dimensions.applicationId}\0${group.dimensions.instanceId}`;
+ const key = `${plan.run.key}\0${group.dimensions.applicationId}\0${group.dimensions.instanceId}`;
const existing = rows.get(key);
if (existing) {
existing.usage.cpuTimeSec += group.sum.cpuTimeSec;
@@ -482,6 +576,7 @@ export async function queryContainerUsageAnalytics(
existing.usage.txBytes += group.sum.txBytes;
} else {
rows.set(key, {
+ runKey: plan.run.key,
applicationId: group.dimensions.applicationId,
instanceId: group.dimensions.instanceId,
usage: { ...group.sum },
@@ -489,15 +584,19 @@ export async function queryContainerUsageAnalytics(
}
}
}
- }
+ };
+ for (const batch of queryBatches) await queryBatch(batch);
return {
rows: [...rows.values()].sort(
(a, b) =>
- a.applicationId.localeCompare(b.applicationId) || a.instanceId.localeCompare(b.instanceId)
+ a.runKey.localeCompare(b.runKey) ||
+ a.applicationId.localeCompare(b.applicationId) ||
+ a.instanceId.localeCompare(b.instanceId)
),
- partial: partialIds.size > 0,
- usagePartialInstanceIds: [...partialIds].sort(),
+ partial: partialIds.size > 0 || unavailableRuns.length > 0,
+ usagePartialRunKeys: [...partialIds].sort(),
+ usageUnavailableRuns: unavailableRuns.sort((a, b) => a.runKey.localeCompare(b.runKey)),
issues,
settings: settingsSummary,
rawResponses,
diff --git a/apps/web/src/routers/admin/cloud-billing-skus-router.test.ts b/apps/web/src/routers/admin/cloud-billing-skus-router.test.ts
index 7ea403312f4..ab04168888a 100644
--- a/apps/web/src/routers/admin/cloud-billing-skus-router.test.ts
+++ b/apps/web/src/routers/admin/cloud-billing-skus-router.test.ts
@@ -1,6 +1,9 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import { cleanupDbForTest, db } from '@/lib/drizzle';
-import type { ContainerUsageAnalyticsResult } from '@/lib/cloudflare/container-usage-analytics';
+import type {
+ ContainerUsageAnalyticsInput,
+ ContainerUsageAnalyticsResult,
+} from '@/lib/cloudflare/container-usage-analytics';
import { createCallerForUser } from '@/routers/test-utils';
import { insertTestUser } from '@/tests/helpers/user.helper';
import {
@@ -18,13 +21,7 @@ import {
} from './cloud-billing-skus-router';
const mockQueryContainerUsageAnalytics =
- jest.fn<
- (input: {
- instanceIds: string[];
- start: string;
- end: string;
- }) => Promise
- >();
+ jest.fn<(input: ContainerUsageAnalyticsInput) => Promise>();
let admin: User;
let nonAdmin: User;
@@ -50,6 +47,7 @@ async function insertUsageInterval(params: {
subjectType?: 'user' | 'org';
subjectId?: string;
startedAt?: string;
+ lastSeenAt?: string;
service?: string;
instanceId?: string;
skuId?: string;
@@ -69,7 +67,7 @@ async function insertUsageInterval(params: {
actor_type: 'user',
actor_id: subjectType === 'user' ? subjectId : admin.id,
started_at: params.startedAt ?? '2026-07-22T10:00:00.000Z',
- last_seen_at: params.startedAt ?? '2026-07-22T10:00:00.000Z',
+ last_seen_at: params.lastSeenAt ?? params.startedAt ?? '2026-07-22T10:00:00.000Z',
metadata: params.metadata,
});
}
@@ -87,12 +85,18 @@ function providerSettings() {
function providerResult(
rows: ContainerUsageAnalyticsResult['rows'],
- options: { partial?: boolean; issues?: string[]; usagePartialInstanceIds?: string[] } = {}
+ options: {
+ partial?: boolean;
+ issues?: string[];
+ usagePartialRunKeys?: string[];
+ usageUnavailableRuns?: ContainerUsageAnalyticsResult['usageUnavailableRuns'];
+ } = {}
) {
return {
rows,
partial: options.partial ?? false,
- usagePartialInstanceIds: options.usagePartialInstanceIds ?? [],
+ usagePartialRunKeys: options.usagePartialRunKeys ?? [],
+ usageUnavailableRuns: options.usageUnavailableRuns ?? [],
issues: options.issues ?? [],
settings: {
containersUsageAdaptiveGroups: providerSettings(),
@@ -100,12 +104,8 @@ function providerResult(
rawResponses: [
{
dataset: 'containersUsageAdaptiveGroups' as const,
- windowIndex: 0,
batchIndex: 0,
- window: {
- start: '2026-07-22T09:00:00.000Z',
- end: '2026-07-22T11:00:00.000Z',
- },
+ queries: [],
body: { data: { viewer: { accounts: [] } }, errors: null },
},
],
@@ -438,7 +438,7 @@ describe('admin.cloudBillingSkus usage records', () => {
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
});
- it('reconciles only exact subject activity, resolves provider identities, and deduplicates generations', async () => {
+ it('reconciles each selected meter generation in its exact run window', async () => {
await db.insert(cloud_billing_sku).values({
...validInput('usage-reconciliation-sku'),
created_by_user_id: admin.id,
@@ -465,14 +465,25 @@ describe('admin.cloudBillingSkus usage records', () => {
service: 'cloud-agent-next-sandbox',
instanceId: 'sandbox-name',
skuId: 'usage-reconciliation-sku',
+ startedAt: '2026-07-22T10:30:00.000Z',
metadata: { durable_object_id: 'cloud-physical-id' },
});
+ await db
+ .update(container_usage_interval)
+ .set({
+ status: 'closed',
+ close_reason: 'exit',
+ stopped_at: '2026-07-22T11:00:00.000Z',
+ last_seen_at: '2026-07-22T11:00:00.000Z',
+ })
+ .where(eq(container_usage_interval.id, 'cloud-generation-2'));
await insertUsageInterval({
id: 'gastown-generation',
subjectType: 'org',
subjectId: 'reconcile-org',
service: 'gastown',
instanceId: 'gastown-physical-id',
+ lastSeenAt: '2026-07-22T11:00:00.000Z',
});
await insertUsageInterval({
id: 'other-subject-generation',
@@ -480,6 +491,12 @@ describe('admin.cloudBillingSkus usage records', () => {
service: 'gastown',
instanceId: 'unrelated-physical-id',
});
+ await insertUsageInterval({
+ id: 'unselected-generation',
+ subjectId: 'reconcile-subject',
+ service: 'gastown',
+ instanceId: 'unselected-physical-id',
+ });
await db.insert(container_usage_segment).values([
{
interval_id: 'cloud-generation-1',
@@ -487,7 +504,7 @@ describe('admin.cloudBillingSkus usage records', () => {
idempotency_key: 'cloud-generation-1-start',
reported_seconds: 10,
usage_seconds: 10,
- received_at: '2026-07-22T09:00:00.000Z',
+ received_at: '2026-07-22T10:10:00.000Z',
},
{
interval_id: 'cloud-generation-2',
@@ -495,7 +512,7 @@ describe('admin.cloudBillingSkus usage records', () => {
idempotency_key: 'cloud-generation-2-middle',
reported_seconds: 20,
usage_seconds: 20,
- received_at: '2026-07-22T10:00:00.000Z',
+ received_at: '2026-07-22T10:40:00.000Z',
},
{
interval_id: 'cloud-generation-2',
@@ -503,7 +520,7 @@ describe('admin.cloudBillingSkus usage records', () => {
idempotency_key: 'cloud-generation-2-end',
reported_seconds: 200,
usage_seconds: 200,
- received_at: '2026-07-22T11:00:00.000Z',
+ received_at: '2026-07-22T10:59:00.000Z',
},
{
interval_id: 'gastown-generation',
@@ -521,20 +538,38 @@ describe('admin.cloudBillingSkus usage records', () => {
usage_seconds: 999,
received_at: '2026-07-22T10:00:00.000Z',
},
+ {
+ interval_id: 'unselected-generation',
+ seq: 1,
+ idempotency_key: 'unselected-middle',
+ reported_seconds: 500,
+ usage_seconds: 500,
+ received_at: '2026-07-22T10:00:00.000Z',
+ },
]);
mockQueryContainerUsageAnalytics.mockImplementation(async input =>
providerResult(
- input.instanceIds.map(instanceId => ({
- applicationId:
- instanceId === 'cloud-physical-id' ? 'observed-cloud-app' : 'observed-gastown-app',
- instanceId,
- usage: {
- cpuTimeSec: instanceId === 'cloud-physical-id' ? 3 : 4,
- allocatedMemory: 12 * 1024 ** 3 * 30,
- allocatedDisk: 20_000_000_000 * 30,
- txBytes: 3_000,
- },
- }))
+ input.runs.map(run => {
+ const seconds = run.key.endsWith('cloud-generation-1')
+ ? 10
+ : run.key.endsWith('cloud-generation-2')
+ ? 220
+ : 30;
+ return {
+ runKey: run.key,
+ applicationId:
+ run.instanceId === 'cloud-physical-id'
+ ? 'observed-cloud-app'
+ : 'observed-gastown-app',
+ instanceId: run.instanceId,
+ usage: {
+ cpuTimeSec: run.instanceId === 'cloud-physical-id' ? 3 : 4,
+ allocatedMemory: 12 * 1024 ** 3 * seconds,
+ allocatedDisk: 20_000_000_000 * seconds,
+ txBytes: 3_000,
+ },
+ };
+ })
)
);
@@ -544,33 +579,56 @@ describe('admin.cloudBillingSkus usage records', () => {
subjectId: 'reconcile-subject',
start: '2026-07-22T09:00:00.000Z',
end: '2026-07-22T11:00:00.000Z',
+ intervalIds: ['cloud-generation-1', 'cloud-generation-2'],
},
mockQueryContainerUsageAnalytics
);
expect(mockQueryContainerUsageAnalytics).toHaveBeenLastCalledWith({
- instanceIds: ['cloud-physical-id'],
- start: '2026-07-22T09:00:00.000Z',
- end: '2026-07-22T11:00:00.000Z',
+ runs: [
+ {
+ key: 'cloud-generation-1',
+ instanceId: 'cloud-physical-id',
+ start: '2026-07-22T09:59:55.000Z',
+ end: '2026-07-22T10:30:05.000Z',
+ },
+ {
+ key: 'cloud-generation-2',
+ instanceId: 'cloud-physical-id',
+ start: '2026-07-22T10:29:55.000Z',
+ end: '2026-07-22T11:00:05.000Z',
+ },
+ ],
});
expect(userResult.totals).toMatchObject({
- meterAcceptedSeconds: 30,
+ meterAcceptedSeconds: 230,
intervalCount: 2,
- uniqueMeterInstances: 1,
- queriedCloudflareInstances: 1,
+ meterRuns: 2,
+ queriedCloudflareRuns: 2,
});
expect(userResult.rows).toEqual([
expect.objectContaining({
providerInstanceId: 'cloud-physical-id',
- meterAcceptedSeconds: 30,
- intervalCount: 2,
+ meterAcceptedSeconds: 10,
+ intervalCount: 1,
services: ['cloud-agent-next-sandbox'],
- skuIds: ['usage-reconciliation-sku', 'usage-search-sku'],
+ skuIds: ['usage-search-sku'],
providerCpuTimeSec: 3,
- providerMemorySeconds: 30,
- providerDiskSeconds: 30,
+ providerMemorySeconds: 10,
+ providerDiskSeconds: 10,
+ providerMemoryDifferencePercent: 0,
+ providerDiskDifferencePercent: 0,
provisionedMemoryBytes: 12 * 1024 ** 3,
provisionedDiskBytes: 20_000_000_000,
- status: 'comparison_unavailable',
+ status: 'compared',
+ }),
+ expect.objectContaining({
+ providerInstanceId: 'cloud-physical-id',
+ meterAcceptedSeconds: 220,
+ intervalCount: 1,
+ skuIds: ['usage-reconciliation-sku'],
+ providerMemorySeconds: 220,
+ providerDiskSeconds: 220,
+ status: 'compared',
}),
]);
expect(userResult.provider.rawResponses).toHaveLength(1);
@@ -581,13 +639,19 @@ describe('admin.cloudBillingSkus usage records', () => {
subjectId: 'reconcile-org',
start: '2026-07-22T09:00:00.000Z',
end: '2026-07-22T11:00:00.000Z',
+ intervalIds: ['gastown-generation'],
},
mockQueryContainerUsageAnalytics
);
expect(mockQueryContainerUsageAnalytics).toHaveBeenLastCalledWith({
- instanceIds: ['gastown-physical-id'],
- start: '2026-07-22T09:00:00.000Z',
- end: '2026-07-22T11:00:00.000Z',
+ runs: [
+ {
+ key: 'gastown-generation',
+ instanceId: 'gastown-physical-id',
+ start: '2026-07-22T09:59:55.000Z',
+ end: '2026-07-22T11:00:05.000Z',
+ },
+ ],
});
expect(orgResult.rows[0]).toMatchObject({
providerInstanceId: 'gastown-physical-id',
@@ -595,7 +659,7 @@ describe('admin.cloudBillingSkus usage records', () => {
meterAcceptedSeconds: 30,
providerMemorySeconds: 30,
providerDiskSeconds: 30,
- status: 'comparison_unavailable',
+ status: 'compared',
});
});
@@ -619,6 +683,12 @@ describe('admin.cloudBillingSkus usage records', () => {
service: 'gastown',
instanceId: 'partial-id',
}),
+ insertUsageInterval({
+ id: 'expired-provider',
+ subjectId: 'status-subject',
+ service: 'gastown',
+ instanceId: 'expired-id',
+ }),
insertUsageInterval({
id: 'cloud-without-provider-id',
subjectId: 'status-subject',
@@ -631,6 +701,7 @@ describe('admin.cloudBillingSkus usage records', () => {
'missing-provider',
'ambiguous-provider',
'partial-provider',
+ 'expired-provider',
'cloud-without-provider-id',
].map((intervalId, index) => ({
interval_id: intervalId,
@@ -645,16 +716,19 @@ describe('admin.cloudBillingSkus usage records', () => {
providerResult(
[
{
+ runKey: 'ambiguous-provider',
applicationId: 'app-gastown',
instanceId: 'ambiguous-id',
usage: { cpuTimeSec: 1, allocatedMemory: 2, allocatedDisk: 3, txBytes: 4 },
},
{
+ runKey: 'ambiguous-provider',
applicationId: 'app-other',
instanceId: 'ambiguous-id',
usage: { cpuTimeSec: 10, allocatedMemory: 20, allocatedDisk: 30, txBytes: 40 },
},
{
+ runKey: 'partial-provider',
applicationId: 'app-gastown',
instanceId: 'partial-id',
usage: { cpuTimeSec: 0, allocatedMemory: 0, allocatedDisk: 0, txBytes: 0 },
@@ -662,8 +736,12 @@ describe('admin.cloudBillingSkus usage records', () => {
],
{
partial: true,
- issues: ['Provider usage response was partial.'],
- usagePartialInstanceIds: ['partial-id'],
+ issues: [
+ 'Provider usage response was partial.',
+ 'Run expired-provider is outside Cloudflare Analytics retention.',
+ ],
+ usagePartialRunKeys: ['partial-provider'],
+ usageUnavailableRuns: [{ runKey: 'expired-provider', reason: 'outside_retention' }],
}
)
);
@@ -674,20 +752,31 @@ describe('admin.cloudBillingSkus usage records', () => {
subjectId: 'status-subject',
start: '2026-07-22T09:00:00.000Z',
end: '2026-07-22T11:00:00.000Z',
+ intervalIds: [
+ 'missing-provider',
+ 'ambiguous-provider',
+ 'partial-provider',
+ 'expired-provider',
+ 'cloud-without-provider-id',
+ ],
},
mockQueryContainerUsageAnalytics
);
expect(mockQueryContainerUsageAnalytics).toHaveBeenCalledWith({
- instanceIds: expect.arrayContaining(['missing-id', 'ambiguous-id', 'partial-id']),
- start: '2026-07-22T09:00:00.000Z',
- end: '2026-07-22T11:00:00.000Z',
+ runs: expect.arrayContaining([
+ expect.objectContaining({ key: 'missing-provider', instanceId: 'missing-id' }),
+ expect.objectContaining({ key: 'ambiguous-provider', instanceId: 'ambiguous-id' }),
+ expect.objectContaining({ key: 'partial-provider', instanceId: 'partial-id' }),
+ expect.objectContaining({ key: 'expired-provider', instanceId: 'expired-id' }),
+ ]),
});
- expect(mockQueryContainerUsageAnalytics.mock.calls[0]?.[0].instanceIds).toHaveLength(3);
+ expect(mockQueryContainerUsageAnalytics.mock.calls[0]?.[0].runs).toHaveLength(4);
expect(result.counts).toEqual({
+ compared: 0,
missing: 1,
ambiguous: 1,
partial: 1,
- comparisonUnavailable: 1,
+ comparisonUnavailable: 2,
});
expect(result.rows.find(row => row.instanceId === 'missing-id')).toMatchObject({
status: 'missing_from_cloudflare',
@@ -702,6 +791,11 @@ describe('admin.cloudBillingSkus usage records', () => {
status: 'provider_partial',
providerCpuTimeSec: null,
});
+ expect(result.rows.find(row => row.instanceId === 'expired-id')).toMatchObject({
+ status: 'comparison_unavailable',
+ statusDetail: 'This run is outside Cloudflare Analytics retention.',
+ providerCpuTimeSec: null,
+ });
expect(result.rows.find(row => row.instanceId === 'sandbox-without-metadata')).toMatchObject({
status: 'comparison_unavailable',
providerInstanceId: null,
@@ -715,6 +809,7 @@ describe('admin.cloudBillingSkus usage records', () => {
subjectId: 'no-usage-subject',
start: '2026-07-22T09:00:00.000Z',
end: '2026-07-22T11:00:00.000Z',
+ intervalIds: ['not-present'],
});
expect(result.rows).toEqual([]);
expect(result.provider).toEqual({
@@ -732,11 +827,69 @@ describe('admin.cloudBillingSkus usage records', () => {
subjectId: 'no-usage-subject',
start: '2026-07-22T09:00:00.000Z',
end: '2026-07-22T11:00:00.000Z',
+ intervalIds: ['not-present'],
})
).rejects.toMatchObject({ code: 'FORBIDDEN' });
});
- it('does not normalize provider seconds when memory and disk durations disagree', async () => {
+ it('uses an open run last-seen boundary and retains absolute variance without a meter baseline', async () => {
+ await insertUsageInterval({
+ id: 'open-zero-meter-run',
+ subjectId: 'open-run-subject',
+ service: 'gastown',
+ instanceId: 'open-run-instance',
+ startedAt: '2026-07-22T10:00:00.000Z',
+ lastSeenAt: '2026-07-22T12:00:00.000Z',
+ });
+ mockQueryContainerUsageAnalytics.mockResolvedValue(
+ providerResult([
+ {
+ runKey: 'open-zero-meter-run',
+ applicationId: 'app-gastown',
+ instanceId: 'open-run-instance',
+ usage: {
+ cpuTimeSec: 1,
+ allocatedMemory: 12 * 1024 ** 3 * 10,
+ allocatedDisk: 20_000_000_000 * 10,
+ txBytes: 0,
+ },
+ },
+ ])
+ );
+
+ const result = await reconcileUsageWithCloudflare(
+ {
+ subjectType: 'user',
+ subjectId: 'open-run-subject',
+ start: '2026-07-22T09:00:00.000Z',
+ end: '2026-07-22T11:00:00.000Z',
+ intervalIds: ['open-zero-meter-run'],
+ },
+ mockQueryContainerUsageAnalytics
+ );
+
+ expect(mockQueryContainerUsageAnalytics).toHaveBeenCalledWith({
+ runs: [
+ {
+ key: 'open-zero-meter-run',
+ instanceId: 'open-run-instance',
+ start: '2026-07-22T09:59:55.000Z',
+ end: '2026-07-22T12:00:05.000Z',
+ },
+ ],
+ });
+ expect(result.rows[0]).toMatchObject({
+ meterAcceptedSeconds: 0,
+ meterEndedAt: '2026-07-22T12:00:00.000Z',
+ providerMemoryDifferenceSeconds: 10,
+ providerMemoryDifferencePercent: null,
+ providerDiskDifferenceSeconds: 10,
+ providerDiskDifferencePercent: null,
+ status: 'compared',
+ });
+ });
+
+ it('reports both normalized variances when memory and disk equivalents disagree', async () => {
await insertUsageInterval({
id: 'capacity-mismatch',
subjectId: 'capacity-subject',
@@ -754,6 +907,7 @@ describe('admin.cloudBillingSkus usage records', () => {
mockQueryContainerUsageAnalytics.mockResolvedValue(
providerResult([
{
+ runKey: 'capacity-mismatch',
applicationId: 'app-gastown',
instanceId: 'capacity-instance',
usage: {
@@ -772,6 +926,7 @@ describe('admin.cloudBillingSkus usage records', () => {
subjectId: 'capacity-subject',
start: '2026-07-22T09:00:00.000Z',
end: '2026-07-22T11:00:00.000Z',
+ intervalIds: ['capacity-mismatch'],
},
mockQueryContainerUsageAnalytics
);
@@ -779,13 +934,15 @@ describe('admin.cloudBillingSkus usage records', () => {
expect(result.rows[0]).toMatchObject({
providerMemorySeconds: 10,
providerDiskSeconds: 11,
- status: 'comparison_unavailable',
- statusDetail: expect.stringContaining('imply different active durations'),
+ providerMemoryDifferencePercent: 0,
+ providerDiskDifferencePercent: 10,
+ status: 'compared',
+ statusDetail: expect.stringContaining('differ'),
});
});
- it('rejects reconciliation above the unique physical instance cap', async () => {
- const intervalIds = Array.from({ length: 101 }, (_, index) => `capped-instance-${index}`);
+ it('rejects reconciliation above the selected run cap', async () => {
+ const intervalIds = Array.from({ length: 16 }, (_, index) => `capped-instance-${index}`);
await db.insert(container_usage_interval).values(
intervalIds.map((id, index) => ({
id,
@@ -820,12 +977,13 @@ describe('admin.cloudBillingSkus usage records', () => {
subjectId: 'capped-subject',
start: '2026-07-22T09:00:00.000Z',
end: '2026-07-22T11:00:00.000Z',
+ intervalIds,
},
mockQueryContainerUsageAnalytics
)
).rejects.toMatchObject({
code: 'BAD_REQUEST',
- message: expect.stringContaining('maximum 100'),
+ message: expect.stringContaining('maximum 15'),
});
expect(mockQueryContainerUsageAnalytics).not.toHaveBeenCalled();
});
diff --git a/apps/web/src/routers/admin/cloud-billing-skus-router.ts b/apps/web/src/routers/admin/cloud-billing-skus-router.ts
index 5c603ee4306..69dda541f78 100644
--- a/apps/web/src/routers/admin/cloud-billing-skus-router.ts
+++ b/apps/web/src/routers/admin/cloud-billing-skus-router.ts
@@ -19,7 +19,7 @@ import {
type ContainerUsageSegment,
} from '@kilocode/db/schema';
import { TRPCError } from '@trpc/server';
-import { and, desc, eq, gt, lt, or, sql, type SQL } from 'drizzle-orm';
+import { and, asc, desc, eq, gt, inArray, lt, or, sql, type SQL } from 'drizzle-orm';
import * as z from 'zod';
export type SerializedCloudBillingSku = Omit & {
@@ -79,6 +79,8 @@ const segmentSearchSchema = z
})
.strict();
+const MAX_RECONCILIATION_RUNS = 15;
+
const usageSummarySchema = z
.object({
subjectType: z.enum(['user', 'org']),
@@ -91,14 +93,27 @@ const usageSummarySchema = z
validateUsageWindow(input.start, input.end, context);
});
+const usageReconciliationSchema = z
+ .object({
+ subjectType: z.enum(['user', 'org']),
+ subjectId: z.string().trim().min(1).max(256),
+ start: z.iso.datetime(),
+ end: z.iso.datetime(),
+ intervalIds: z.array(z.string().trim().min(1).max(512)).min(1).max(MAX_RECONCILIATION_RUNS),
+ })
+ .strict()
+ .superRefine((input, context) => {
+ validateUsageWindow(input.start, input.end, context);
+ });
+
const BILLING_HEALTH_WINDOW_MS = 24 * 60 * 60 * 1_000;
const STALE_OPEN_INTERVAL_MS = 15 * 60 * 1_000;
-const MAX_RECONCILIATION_INSTANCES = 100;
+const PROVIDER_BOUNDARY_PADDING_MS = 5_000;
const MEBIBYTE_BYTES = 1024 ** 2;
const MEGABYTE_BYTES = 1_000_000;
const CAPACITY_CROSS_CHECK_RELATIVE_TOLERANCE = 1e-6;
const COMPARISON_METHOD =
- 'Cloudflare allocated resource sums include overlapping container and micro-VM records, so they cannot be normalized to awake wall-clock seconds. Capacity-equivalent memory and disk seconds are diagnostic only.';
+ 'Cloudflare memory and disk byte-seconds are queried with five seconds of boundary tolerance, normalized by configured instance capacity, and compared with accepted meter seconds for each exact meter run. CPU time is included as a secondary usage diagnostic.';
const usageMetadataSchema = z
.record(z.string().min(1).max(64), z.string().max(512))
.refine(
@@ -204,6 +219,7 @@ function postgresErrorCode(error: unknown): string | undefined {
}
export type ReconciliationStatus =
+ | 'compared'
| 'missing_from_cloudflare'
| 'ambiguous_application'
| 'provider_partial'
@@ -217,6 +233,8 @@ type MeterReconciliationRow = {
skuIds: Set;
intervalIds: Set;
acceptedSeconds: number;
+ startedAt: string;
+ endedAt: string;
identityIssue: string | null;
};
@@ -298,12 +316,12 @@ function addMeterReconciliationRow(
skuId: string;
metadata: unknown;
acceptedSeconds: number;
+ startedAt: string;
+ endedAt: string;
}
): void {
const providerIdentity = providerIdentityForMeterRow(meter);
- const key = providerIdentity.instanceId
- ? `provider\0${providerIdentity.instanceId}`
- : `meter\0${meter.service}\0${meter.instanceId}`;
+ const key = meter.intervalId;
let row = rows.get(key);
if (!row) {
row = {
@@ -314,6 +332,8 @@ function addMeterReconciliationRow(
skuIds: new Set(),
intervalIds: new Set(),
acceptedSeconds: 0,
+ startedAt: meter.startedAt,
+ endedAt: meter.endedAt,
identityIssue: providerIdentity.issue,
};
rows.set(key, row);
@@ -343,17 +363,22 @@ function normalizedReconciliationRows(
meterRows: MeterReconciliationRow[],
provider: ContainerUsageAnalyticsResult | null
) {
- const providerByInstance = new Map();
- const partialUsageInstanceIds = new Set(provider?.usagePartialInstanceIds ?? []);
+ const providerByRun = new Map();
+ const partialUsageRunKeys = new Set(provider?.usagePartialRunKeys ?? []);
+ const unavailableRuns = new Map(
+ provider?.usageUnavailableRuns.map(run => [run.runKey, run.reason] as const) ?? []
+ );
for (const row of provider?.rows ?? []) {
- const existing = providerByInstance.get(row.instanceId);
+ const existing = providerByRun.get(row.runKey);
if (existing) existing.push(row);
- else providerByInstance.set(row.instanceId, [row]);
+ else providerByRun.set(row.runKey, [row]);
}
return meterRows.map(meter => {
const candidates = meter.providerInstanceId
- ? (providerByInstance.get(meter.providerInstanceId) ?? [])
+ ? (providerByRun.get(meter.key) ?? []).filter(
+ candidate => candidate.instanceId === meter.providerInstanceId
+ )
: [];
const providerApplicationIds = candidates.map(candidate => candidate.applicationId).sort();
let status: ReconciliationStatus;
@@ -361,14 +386,20 @@ function normalizedReconciliationRows(
let matchedProvider: ContainerUsageAnalyticsResult['rows'][number] | null = null;
let providerMemorySeconds: number | null = null;
let providerDiskSeconds: number | null = null;
+ let providerMemoryDifferenceSeconds: number | null = null;
+ let providerMemoryDifferencePercent: number | null = null;
+ let providerDiskDifferenceSeconds: number | null = null;
+ let providerDiskDifferencePercent: number | null = null;
let provisionedMemoryBytes: number | null = null;
let provisionedDiskBytes: number | null = null;
if (meter.identityIssue) {
status = 'comparison_unavailable';
statusDetail = meter.identityIssue;
+ } else if (unavailableRuns.get(meter.key) === 'outside_retention') {
+ status = 'comparison_unavailable';
+ statusDetail = 'This run is outside Cloudflare Analytics retention.';
} else if (candidates.length === 0) {
- const usagePartial =
- meter.providerInstanceId !== null && partialUsageInstanceIds.has(meter.providerInstanceId);
+ const usagePartial = partialUsageRunKeys.has(meter.key);
status = usagePartial ? 'provider_partial' : 'missing_from_cloudflare';
statusDetail = usagePartial
? 'Cloudflare returned partial data, so this instance cannot be classified as missing.'
@@ -376,10 +407,7 @@ function normalizedReconciliationRows(
} else if (candidates.length !== 1) {
status = 'ambiguous_application';
statusDetail = 'Multiple Cloudflare applications returned this physical instance ID.';
- } else if (
- meter.providerInstanceId !== null &&
- partialUsageInstanceIds.has(meter.providerInstanceId)
- ) {
+ } else if (partialUsageRunKeys.has(meter.key)) {
status = 'provider_partial';
statusDetail = 'Cloudflare returned only part of the required provider data.';
} else {
@@ -394,19 +422,26 @@ function normalizedReconciliationRows(
providerMemorySeconds =
matchedProvider.usage.allocatedMemory / provisionedCapacity.memoryBytes;
providerDiskSeconds = matchedProvider.usage.allocatedDisk / provisionedCapacity.diskBytes;
+ providerMemoryDifferenceSeconds = providerMemorySeconds - meter.acceptedSeconds;
+ providerDiskDifferenceSeconds = providerDiskSeconds - meter.acceptedSeconds;
+ if (meter.acceptedSeconds > 0) {
+ providerMemoryDifferencePercent =
+ (providerMemoryDifferenceSeconds / meter.acceptedSeconds) * 100;
+ providerDiskDifferencePercent =
+ (providerDiskDifferenceSeconds / meter.acceptedSeconds) * 100;
+ }
const capacityCrossCheckDifference = Math.abs(providerMemorySeconds - providerDiskSeconds);
const capacityCrossCheckScale = Math.max(1, providerMemorySeconds, providerDiskSeconds);
if (
capacityCrossCheckDifference / capacityCrossCheckScale >
CAPACITY_CROSS_CHECK_RELATIVE_TOLERANCE
) {
- status = 'comparison_unavailable';
+ status = 'compared';
statusDetail =
- 'Provider memory and disk byte-seconds imply different active durations for this service capacity.';
+ 'Provider memory and disk allocation equivalents differ; review both variances.';
} else {
- status = 'comparison_unavailable';
- statusDetail =
- 'Cloudflare memory and disk allocation equivalents agree, but overlapping billing records prevent an awake-time comparison.';
+ status = 'compared';
+ statusDetail = 'Provider memory and disk allocation equivalents agree.';
}
}
}
@@ -418,10 +453,17 @@ function normalizedReconciliationRows(
services: [...meter.services].sort(),
providerApplicationIds,
skuIds: [...meter.skuIds].sort(),
+ intervalIds: [...meter.intervalIds].sort(),
intervalCount: meter.intervalIds.size,
meterAcceptedSeconds: meter.acceptedSeconds,
+ meterStartedAt: meter.startedAt,
+ meterEndedAt: meter.endedAt,
providerMemorySeconds,
providerDiskSeconds,
+ providerMemoryDifferenceSeconds,
+ providerMemoryDifferencePercent,
+ providerDiskDifferenceSeconds,
+ providerDiskDifferencePercent,
provisionedMemoryBytes,
provisionedDiskBytes,
providerCpuTimeSec: matchedProvider?.usage.cpuTimeSec ?? null,
@@ -434,11 +476,11 @@ function normalizedReconciliationRows(
});
}
-type UsageSummaryInput = z.infer;
+type UsageReconciliationInput = z.infer;
type QueryContainerUsageAnalytics = typeof queryContainerUsageAnalytics;
export async function reconcileUsageWithCloudflare(
- input: UsageSummaryInput,
+ input: UsageReconciliationInput,
queryProvider: QueryContainerUsageAnalytics = queryContainerUsageAnalytics
) {
const meterSegments = await db
@@ -448,20 +490,24 @@ export async function reconcileUsageWithCloudflare(
instanceId: container_usage_interval.instance_id,
skuId: container_usage_interval.cloud_billing_sku_id,
metadata: container_usage_interval.metadata,
+ startedAt: container_usage_interval.started_at,
+ lastSeenAt: container_usage_interval.last_seen_at,
+ stoppedAt: container_usage_interval.stopped_at,
acceptedSeconds:
sql`coalesce(sum(${container_usage_segment.usage_seconds}), 0)`.mapWith(Number),
})
- .from(container_usage_segment)
- .innerJoin(
- container_usage_interval,
- eq(container_usage_segment.interval_id, container_usage_interval.id)
+ .from(container_usage_interval)
+ .leftJoin(
+ container_usage_segment,
+ eq(container_usage_interval.id, container_usage_segment.interval_id)
)
.where(
and(
eq(container_usage_interval.subject_type, input.subjectType),
eq(container_usage_interval.subject_id, input.subjectId),
- sql`${container_usage_segment.received_at} >= ${input.start}`,
- lt(container_usage_segment.received_at, input.end)
+ inArray(container_usage_interval.id, input.intervalIds),
+ lt(container_usage_interval.started_at, input.end),
+ gt(container_usage_interval.last_seen_at, input.start)
)
)
.groupBy(
@@ -469,30 +515,45 @@ export async function reconcileUsageWithCloudflare(
container_usage_interval.service,
container_usage_interval.instance_id,
container_usage_interval.cloud_billing_sku_id,
- container_usage_interval.metadata
- );
+ container_usage_interval.metadata,
+ container_usage_interval.started_at,
+ container_usage_interval.last_seen_at,
+ container_usage_interval.stopped_at
+ )
+ .orderBy(asc(container_usage_interval.started_at), asc(container_usage_interval.id));
const grouped = new Map();
- for (const meterSegment of meterSegments) addMeterReconciliationRow(grouped, meterSegment);
+ for (const meterSegment of meterSegments) {
+ addMeterReconciliationRow(grouped, {
+ ...meterSegment,
+ startedAt: new Date(meterSegment.startedAt).toISOString(),
+ endedAt: new Date(meterSegment.stoppedAt ?? meterSegment.lastSeenAt).toISOString(),
+ });
+ }
const meterRows = [...grouped.values()];
- const providerInstanceIds = [
- ...new Set(meterRows.flatMap(row => (row.providerInstanceId ? [row.providerInstanceId] : []))),
- ];
- if (providerInstanceIds.length > MAX_RECONCILIATION_INSTANCES) {
+ const providerRuns = meterRows.flatMap(row =>
+ row.providerInstanceId
+ ? [
+ {
+ key: row.key,
+ instanceId: row.providerInstanceId,
+ start: new Date(Date.parse(row.startedAt) - PROVIDER_BOUNDARY_PADDING_MS).toISOString(),
+ end: new Date(Date.parse(row.endedAt) + PROVIDER_BOUNDARY_PADDING_MS).toISOString(),
+ },
+ ]
+ : []
+ );
+ if (providerRuns.length > MAX_RECONCILIATION_RUNS) {
throw new TRPCError({
code: 'BAD_REQUEST',
- message: `This window contains ${providerInstanceIds.length} unique Cloudflare instances (maximum ${MAX_RECONCILIATION_INSTANCES}). Narrow the window and retry.`,
+ message: `The selected records contain ${providerRuns.length} Cloudflare runs (maximum ${MAX_RECONCILIATION_RUNS}).`,
});
}
let provider: ContainerUsageAnalyticsResult | null = null;
- if (providerInstanceIds.length > 0) {
+ if (providerRuns.length > 0) {
try {
- provider = await queryProvider({
- instanceIds: providerInstanceIds,
- start: input.start,
- end: input.end,
- });
+ provider = await queryProvider({ runs: providerRuns });
} catch (error) {
if (error instanceof ContainerUsageAnalyticsError) {
throw new TRPCError({
@@ -516,17 +577,18 @@ export async function reconcileUsageWithCloudflare(
end: input.end,
generatedAt: new Date().toISOString(),
comparison: {
- available: false as const,
- method: 'unavailable' as const,
+ available: true as const,
+ method: 'capacity_equivalent' as const,
description: COMPARISON_METHOD,
},
totals: {
meterAcceptedSeconds: totalMeterSeconds,
intervalCount: new Set(meterSegments.map(row => row.intervalId)).size,
- uniqueMeterInstances: rows.length,
- queriedCloudflareInstances: providerInstanceIds.length,
+ meterRuns: rows.length,
+ queriedCloudflareRuns: providerRuns.length,
},
counts: {
+ compared: countStatus('compared'),
missing: countStatus('missing_from_cloudflare'),
ambiguous: countStatus('ambiguous_application'),
partial: countStatus('provider_partial'),
@@ -732,7 +794,7 @@ export const cloudBillingSkusRouter = createTRPCRouter({
}),
reconcileUsageWithCloudflare: adminProcedure
- .input(usageSummarySchema)
+ .input(usageReconciliationSchema)
.mutation(async ({ input }) => reconcileUsageWithCloudflare(input)),
usageHealth: adminProcedure.query(async () => {