Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ When `VERCEL_TARGET_ENV` is absent in local development or a script process, tra
- `IMPACT_ADVOCATE_TENANT_ALIAS` - Impact.com Advocate tenant alias. [SERVER]
- `IMPACT_ADVOCATE_WIDGET_ID` - Impact.com Advocate widget ID. [SERVER]
- `IMPACT_CAMPAIGN_ID` - Impact.com campaign ID for event tracking. [SERVER]
### Cloudflare Analytics

- `CLOUDFLARE_ACCOUNT_ID` - Cloudflare account ID used as the GraphQL `accountTag` for Containers Analytics queries and admin dashboard deep links; used in `apps/web/src/lib/config.server.ts` and `apps/web/src/lib/cloudflare/container-usage-analytics.ts`. [SERVER]
- `CLOUDFLARE_ANALYTICS_API_TOKEN` - Cloudflare API token with **Account Analytics: Read** only, used by the web app to query `containersUsageAdaptiveGroups` for on-demand admin reconciliation; used in `apps/web/src/lib/config.server.ts` and `apps/web/src/lib/cloudflare/container-usage-analytics.ts`. Optional at process start — missing values surface as actionable errors at point of use. `[SECRET]`

### R2 / Object Storage

Expand Down
348 changes: 346 additions & 2 deletions apps/web/src/app/admin/cloud-billing-skus/UsageRecordsContent.tsx

Large diffs are not rendered by default.

334 changes: 334 additions & 0 deletions apps/web/src/lib/cloudflare/container-usage-analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
import { describe, expect, it, jest } from '@jest/globals';

jest.mock('@/lib/config.server', () => ({
CLOUDFLARE_ACCOUNT_ID: '',
CLOUDFLARE_ANALYTICS_API_TOKEN: '',
}));

import {
queryContainerUsageAnalytics,
type ContainerUsageAnalyticsOptions,
} from './container-usage-analytics';

const ACCOUNT_ID = 'account-id';
const API_TOKEN = 'analytics-token';
const USAGE_FIELDS = [
'dimensions_applicationId',
'dimensions_instanceId',
'sum_cpuTimeSec',
'sum_allocatedMemory',
'sum_allocatedDisk',
'sum_txBytes',
];
const input = {
instanceIds: ['instance-1'],
start: '2026-07-01T00:00:00.000Z',
end: '2026-07-01T01:00:00.000Z',
};
const options: ContainerUsageAnalyticsOptions = {
accountId: ACCOUNT_ID,
apiToken: API_TOKEN,
now: () => new Date('2026-07-02T00:00:00.000Z'),
};

function settingsBody(
overrides: Partial<{
enabled: boolean;
availableFields: string[];
maxPageSize: number;
maxNumberOfFields: number;
notOlderThan: number;
maxDuration: number;
}> = {},
extra: Record<string, unknown> = {}
) {
return {
data: {
viewer: {
accounts: [
{
settings: {
containersUsageAdaptiveGroups: {
enabled: true,
availableFields: USAGE_FIELDS,
maxPageSize: 100,
maxNumberOfFields: 30,
notOlderThan: 2_678_400,
maxDuration: 86_400,
...overrides,
},
},
},
],
},
},
errors: null,
...extra,
};
}

function usageBody(
groups: Array<{
dimensions: { applicationId: string; instanceId: string };
sum: { cpuTimeSec: number; allocatedMemory: number; allocatedDisk: number; txBytes: number };
}> = [],
errors: Array<{ message: string }> | null = null,
extra: Record<string, unknown> = {}
) {
return {
data: { viewer: { accounts: [{ containersUsageAdaptiveGroups: groups }] } },
errors,
...extra,
};
}

function response(body: unknown, init: ResponseInit = {}) {
return new Response(typeof body === 'string' ? body : JSON.stringify(body), {
status: 200,
headers: { 'content-type': 'application/json' },
...init,
});
}

function requestCapture(
handler: (request: { body: Record<string, any>; headers: Headers }, index: number) => Response
) {
const requests: Array<{ body: Record<string, any>; headers: Headers }> = [];
const fetch = jest.fn(async (_url: string | URL | Request, init?: RequestInit) => {
const request = {
body: JSON.parse(String(init?.body)) as Record<string, any>,
headers: new Headers(init?.headers),
};
requests.push(request);
return handler(request, requests.length - 1);
}) as typeof globalThis.fetch;
return { fetch, requests };
}

describe('queryContainerUsageAnalytics', () => {
it('sends the scoped billing-usage query and returns labeled raw data without auth material', async () => {
const providerRow = {
dimensions: { applicationId: 'observed-app', instanceId: 'instance-1' },
sum: { cpuTimeSec: 2, allocatedMemory: 3, allocatedDisk: 4, txBytes: 5 },
};
const { fetch, requests } = requestCapture((_request, index) =>
response(index === 0 ? settingsBody() : usageBody([providerRow]))
);

const result = await queryContainerUsageAnalytics(input, { ...options, fetch });

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.variables).toEqual({
accountTag: ACCOUNT_ID,
datetimeStart: input.start,
datetimeEnd: input.end,
instanceIds: ['instance-1'],
});
expect(result.rows).toEqual([
{
applicationId: 'observed-app',
instanceId: 'instance-1',
usage: providerRow.sum,
},
]);
expect(result.rawResponses.map(item => item.dataset)).toEqual([
'settings',
'containersUsageAdaptiveGroups',
]);
expect(JSON.stringify(result)).not.toContain(API_TOKEN);
expect(JSON.stringify(result)).not.toContain('authorization');
});

it.each([
['HTTP failure', () => response({ error: 'denied' }, { status: 403 }), 'http_error'],
['invalid JSON', () => response('<html>bad gateway</html>'), 'invalid_json'],
])('rejects %s safely', async (_name, failingResponse, code) => {
const fetch = jest.fn(async () => failingResponse()) as typeof globalThis.fetch;
await expect(queryContainerUsageAnalytics(input, { ...options, fetch })).rejects.toMatchObject({
code,
});
});

it('distinguishes GraphQL failure from usable partial data', async () => {
const hardFailure = requestCapture((_request, index) =>
response(index === 0 ? settingsBody() : { data: null, errors: [{ message: 'denied' }] })
);
await expect(
queryContainerUsageAnalytics(input, { ...options, fetch: hardFailure.fetch })
).rejects.toMatchObject({ code: 'graphql_error' });

const partial = requestCapture((_request, index) =>
response(
index === 0
? settingsBody()
: usageBody(
[
{
dimensions: { applicationId: 'app', instanceId: 'instance-1' },
sum: { cpuTimeSec: 1, allocatedMemory: 2, allocatedDisk: 3, txBytes: 4 },
},
],
[{ message: 'partial failure' }]
)
)
);
const result = await queryContainerUsageAnalytics(input, { ...options, fetch: partial.fetch });
expect(result.partial).toBe(true);
expect(result.usagePartialInstanceIds).toEqual(['instance-1']);
expect(result.rows).toHaveLength(1);
});

it('fails safely for missing configuration, account, dataset, and fields', async () => {
await expect(
queryContainerUsageAnalytics(input, { accountId: '', apiToken: '' })
).rejects.toMatchObject({
code: 'missing_config',
});
await expect(
queryContainerUsageAnalytics(input, { accountId: ACCOUNT_ID, apiToken: '' })
).rejects.toMatchObject({ code: 'missing_config' });

const cases: Array<[unknown, string]> = [
[{ data: { viewer: { accounts: [] } }, errors: null }, 'account_not_found'],
[{ data: { viewer: { accounts: [{ settings: {} }] } }, errors: null }, 'dataset_unavailable'],
[settingsBody({ enabled: false }), 'dataset_disabled'],
[settingsBody({ availableFields: USAGE_FIELDS.slice(0, -1) }), 'fields_unavailable'],
[settingsBody({ maxPageSize: 1 }), 'dataset_unavailable'],
];
for (const [body, code] of cases) {
const fetch = jest.fn(async () => response(body)) as typeof globalThis.fetch;
await expect(
queryContainerUsageAnalytics(input, { ...options, fetch })
).rejects.toMatchObject({ code });
}
});

it('chunks by live duration and batches exact IDs below the page limit', async () => {
const ids = Array.from({ length: 51 }, (_, index) => `instance-${index}`);
const { fetch, requests } = requestCapture((_request, index) =>
response(index === 0 ? settingsBody({ maxDuration: 3_600, maxPageSize: 100 }) : usageBody())
);

await queryContainerUsageAnalytics(
{ instanceIds: ids, start: input.start, end: '2026-07-01T02:00:00.000Z' },
{ ...options, fetch }
);

expect(fetch).toHaveBeenCalledTimes(5);
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'])
);
});

it('marks a full page partial and rejects request plans above the cap', async () => {
const fullPage = requestCapture((_request, index) =>
response(
index === 0
? settingsBody({ maxPageSize: 2 })
: usageBody([
{
dimensions: { applicationId: 'app-a', instanceId: 'instance-1' },
sum: { cpuTimeSec: 1, allocatedMemory: 2, allocatedDisk: 3, txBytes: 4 },
},
{
dimensions: { applicationId: 'app-b', instanceId: 'instance-1' },
sum: { cpuTimeSec: 1, allocatedMemory: 2, allocatedDisk: 3, txBytes: 4 },
},
])
)
);
const partial = await queryContainerUsageAnalytics(input, {
...options,
fetch: fullPage.fetch,
});
expect(partial.usagePartialInstanceIds).toEqual(['instance-1']);

const requestCap = requestCapture((_request, index) =>
response(index === 0 ? settingsBody({ maxPageSize: 2, maxDuration: 3_600 }) : usageBody())
);
await expect(
queryContainerUsageAnalytics(
{
instanceIds: Array.from({ length: 50 }, (_, index) => `instance-${index}`),
start: input.start,
end: '2026-07-02T00:00:00.000Z',
},
{ ...options, fetch: requestCap.fetch }
)
).rejects.toMatchObject({ code: 'request_limit_exceeded' });
});

it('enforces timeout and raw response caps', async () => {
const timeoutFetch = jest.fn(async () => {
const error = new Error('timeout');
error.name = 'TimeoutError';
throw error;
}) as typeof globalThis.fetch;
await expect(
queryContainerUsageAnalytics(input, { ...options, fetch: timeoutFetch, timeoutMs: 5 })
).rejects.toMatchObject({ code: 'timeout' });

const oversized = jest.fn(async () =>
response('x'.repeat(1024 * 1024 + 1))
) as typeof globalThis.fetch;
await expect(
queryContainerUsageAnalytics(input, { ...options, fetch: oversized })
).rejects.toMatchObject({
code: 'raw_response_too_large',
});
});

it('rejects missing billing sums instead of converting them to zero', async () => {
const { fetch } = requestCapture((_request, index) =>
response(
index === 0
? settingsBody()
: {
data: {
viewer: {
accounts: [
{
containersUsageAdaptiveGroups: [
{
dimensions: { applicationId: 'app', instanceId: 'instance-1' },
sum: { cpuTimeSec: 1, allocatedMemory: 2, txBytes: 3 },
},
],
},
],
},
},
errors: null,
}
)
);
await expect(queryContainerUsageAnalytics(input, { ...options, fetch })).rejects.toMatchObject({
code: 'invalid_response_shape',
});
});

it('marks retention clipping partial and rejects fully expired windows', async () => {
const retained = requestCapture((_request, index) =>
response(index === 0 ? settingsBody({ notOlderThan: 84_600 }) : usageBody())
);
const partial = await queryContainerUsageAnalytics(input, {
...options,
fetch: retained.fetch,
});
expect(partial.partial).toBe(true);
expect(partial.usagePartialInstanceIds).toEqual(['instance-1']);

const expired = requestCapture(() => response(settingsBody({ notOlderThan: 3_600 })));
await expect(
queryContainerUsageAnalytics(input, { ...options, fetch: expired.fetch })
).rejects.toMatchObject({ code: 'outside_retention' });
});
});
Loading
Loading