Files
twenty/packages/twenty-server/test/integration/audit/suites/clickHouse-usage-event-writer.integration-spec.ts
T
Etienne fbaea0639a Billing - optimize usageEvent CH table (#20019)
- Update usageEvent clickhouse table, partitioning, indexing and
projection (auto materialized view) to optimize credit usage queries
- Add caching for available credits and billing subscription


To do in next PR: deprecate enforceCapUsage cron. Bonus : real-time on
billingSubscription
2026-04-28 14:06:49 +00:00

139 lines
4.1 KiB
TypeScript

import process from 'process';
import {
type ClickHouseClient,
ClickHouseLogLevel,
createClient,
} from '@clickhouse/client';
import { formatDateTimeForClickHouse } from 'src/database/clickHouse/clickHouse.util';
import { UsageOperationType } from 'src/engine/core-modules/usage/enums/usage-operation-type.enum';
import { UsageResourceType } from 'src/engine/core-modules/usage/enums/usage-resource-type.enum';
import { UsageUnit } from 'src/engine/core-modules/usage/enums/usage-unit.enum';
const buildUsageEventRow = (
workspaceId: string,
overrides: Partial<{
userWorkspaceId: string;
resourceType: UsageResourceType;
operationType: UsageOperationType;
quantity: number;
unit: UsageUnit;
creditsUsedMicro: number;
resourceId: string;
resourceContext: string;
}> = {},
) => ({
timestamp: formatDateTimeForClickHouse(new Date()),
workspaceId,
userWorkspaceId: overrides.userWorkspaceId ?? '',
resourceType: overrides.resourceType ?? UsageResourceType.AI,
operationType: overrides.operationType ?? UsageOperationType.AI_CHAT_TOKEN,
quantity: overrides.quantity ?? 0,
unit: overrides.unit ?? UsageUnit.TOKEN,
creditsUsedMicro: overrides.creditsUsedMicro ?? 0,
resourceId: overrides.resourceId ?? '',
resourceContext: overrides.resourceContext ?? '',
metadata: {},
});
describe('ClickHouse Usage Event Writer (integration)', () => {
let clickHouseClient: ClickHouseClient;
beforeAll(async () => {
jest.useRealTimers();
clickHouseClient = createClient({
url: process.env.CLICKHOUSE_URL,
log: { level: ClickHouseLogLevel.OFF },
});
await clickHouseClient.query({
query: 'TRUNCATE TABLE usageEvent',
format: 'JSONEachRow',
});
});
afterAll(async () => {
if (clickHouseClient) {
await clickHouseClient.close();
}
});
it('should insert a usage event row matching the schema produced by UsageEventWriterService', async () => {
const workspaceId = '00000000-0000-0000-0000-000000000001';
const row = buildUsageEventRow(workspaceId, {
userWorkspaceId: '00000000-0000-0000-0000-000000000002',
resourceType: UsageResourceType.AI,
operationType: UsageOperationType.AI_CHAT_TOKEN,
quantity: 1500,
unit: UsageUnit.TOKEN,
creditsUsedMicro: 7500,
resourceId: 'agent-123',
resourceContext: 'gpt-4o',
});
await clickHouseClient.insert({
table: 'usageEvent',
values: [row],
format: 'JSONEachRow',
});
const queryResult = await clickHouseClient.query({
query: `
SELECT *
FROM usageEvent
WHERE workspaceId = '${workspaceId}'
`,
format: 'JSONEachRow',
});
const rows = await queryResult.json<Record<string, unknown>>();
expect(rows).toHaveLength(1);
expect(rows[0].workspaceId).toBe(workspaceId);
expect(rows[0].resourceType).toBe(UsageResourceType.AI);
expect(rows[0].operationType).toBe(UsageOperationType.AI_CHAT_TOKEN);
expect(rows[0].quantity).toBe(1500);
expect(rows[0].unit).toBe(UsageUnit.TOKEN);
expect(rows[0].creditsUsedMicro).toBe(7500);
expect(rows[0].resourceId).toBe('agent-123');
expect(rows[0].resourceContext).toBe('gpt-4o');
});
it('should insert a usage event with empty optional fields', async () => {
const workspaceId = '00000000-0000-0000-0000-000000000003';
const row = buildUsageEventRow(workspaceId, {
resourceType: UsageResourceType.WORKFLOW,
operationType: UsageOperationType.WORKFLOW_EXECUTION,
quantity: 1,
unit: UsageUnit.INVOCATION,
creditsUsedMicro: 1,
});
await clickHouseClient.insert({
table: 'usageEvent',
values: [row],
format: 'JSONEachRow',
});
const queryResult = await clickHouseClient.query({
query: `
SELECT *
FROM usageEvent
WHERE workspaceId = '${workspaceId}'
`,
format: 'JSONEachRow',
});
const rows = await queryResult.json<Record<string, unknown>>();
expect(rows).toHaveLength(1);
expect(rows[0].userWorkspaceId).toBe('');
expect(rows[0].resourceId).toBe('');
expect(rows[0].resourceContext).toBe('');
});
});