Files
umami/src/queries/sql/events/getWebsiteEvents.test.ts
T
2026-08-03 16:02:22 -07:00

192 lines
6.8 KiB
TypeScript

import { afterEach, describe, expect, test, vi } from 'vitest';
const prismaParseFiltersResult = {
queryParams: { websiteId: 'website-1' },
filterQuery: 'and website_event.url_path = {{path}}',
dateQuery: 'and website_event.created_at between {{startDate}} and {{endDate}}',
cohortQuery: 'join cohort on cohort.session_id = website_event.session_id',
joinSessionQuery: 'inner join session on website_event.session_id = session.session_id and website_event.website_id = session.website_id',
};
const clickhouseParseFiltersResult = {
queryParams: { websiteId: 'website-1' },
filterQuery: 'and url_path = {path:String}',
dateQuery: 'and created_at between {startDate:DateTime64} and {endDate:DateTime64}',
cohortQuery: 'join cohort on cohort.session_id = website_event.session_id',
joinSessionQuery: '',
};
async function loadModule({
mode,
}: {
mode: 'prisma' | 'clickhouse';
}) {
vi.resetModules();
const state = { mode };
const prismaRawQuery = vi
.fn()
.mockResolvedValueOnce([{ num: '25' }])
.mockResolvedValueOnce([{ id: 'event-1' }]);
const prismaParseFilters = vi.fn().mockReturnValue(prismaParseFiltersResult);
const clickhouseRawQuery = vi
.fn()
.mockResolvedValueOnce([{ num: '25' }])
.mockResolvedValueOnce([{ id: 'event-1' }]);
const clickhouseParseFilters = vi.fn().mockReturnValue(clickhouseParseFiltersResult);
vi.doMock('@/lib/db', () => ({
CLICKHOUSE: 'clickhouse',
PRISMA: 'prisma',
runQuery: vi.fn((queries: Record<string, () => unknown>) => queries[state.mode]()),
}));
vi.doMock('@/lib/prisma', () => ({
default: {
rawQuery: prismaRawQuery,
parseFilters: prismaParseFilters,
},
}));
vi.doMock('@/lib/clickhouse', () => ({
default: {
rawQuery: clickhouseRawQuery,
parseFilters: clickhouseParseFilters,
},
}));
const mod = await import('./getWebsiteEvents');
return {
getWebsiteEvents: mod.getWebsiteEvents,
prismaRawQuery,
clickhouseRawQuery,
};
}
afterEach(() => {
vi.resetModules();
vi.clearAllMocks();
});
describe('getWebsiteEvents', () => {
test('postgres counts from the lightweight filtered event query and enriches only the paged rows', async () => {
const { getWebsiteEvents, prismaRawQuery } = await loadModule({ mode: 'prisma' });
const result = await getWebsiteEvents('website-1', {
page: 1,
pageSize: 20,
maxResults: 10000,
orderBy: 'createdAt',
search: 'signup',
} as any);
const [countQuery] = prismaRawQuery.mock.calls[0];
const [dataQuery] = prismaRawQuery.mock.calls[1];
expect(countQuery).toContain('select count(*) as num from (select 1 from (');
expect(countQuery).not.toContain('exists(');
expect(dataQuery).toContain('with paged_events as (');
expect(dataQuery).toContain('paged_event_data as (');
expect(dataQuery).toContain('join website_event on website_event.event_id = paged_events.event_id');
expect(dataQuery).toContain('join paged_events on paged_events.event_id = event_data.website_event_id');
expect(dataQuery).toContain('where event_data.website_id = {{websiteId::uuid}}');
expect(dataQuery).toContain('and event_data.created_at between {{startDate}} and {{endDate}}');
expect(dataQuery).toContain('left join paged_event_data on paged_event_data.event_id = website_event.event_id');
expect(dataQuery).toContain('order by paged_events.created_at desc');
expect(dataQuery).toContain('(paged_event_data.event_id is not null) as "hasData"');
expect(result).toEqual({
data: [{ id: 'event-1' }],
count: 25,
page: 1,
pageSize: 20,
orderBy: 'createdAt',
isCapped: false,
});
});
test('clickhouse counts from the lightweight filtered event query and resolves hasData from paged ids', async () => {
const { getWebsiteEvents, clickhouseRawQuery } = await loadModule({ mode: 'clickhouse' });
const result = await getWebsiteEvents('website-1', {
page: 1,
pageSize: 20,
maxResults: 10000,
orderBy: 'createdAt',
search: 'signup',
} as any);
const [countQuery] = clickhouseRawQuery.mock.calls[0];
const [dataQuery] = clickhouseRawQuery.mock.calls[1];
expect(countQuery).toContain('select count(*) as num from (select 1 from (');
expect(countQuery).not.toContain('event_data');
expect(dataQuery).toContain('with paged_events as (');
expect(dataQuery).toContain('paged_event_data as (');
expect(dataQuery).toContain('inner join website_event on website_event.event_id = paged_events.event_id');
expect(dataQuery).toContain('where website_id = {websiteId:UUID}');
expect(dataQuery).toContain('from event_data');
expect(dataQuery).toContain('inner join paged_events on paged_events.event_id = event_data.event_id');
expect(dataQuery).toContain('and created_at between {startDate:DateTime64} and {endDate:DateTime64}');
expect(dataQuery).toContain('left join paged_event_data on paged_event_data.event_id = website_event.event_id');
expect(dataQuery).toContain('order by paged_events.created_at desc');
expect(dataQuery).toContain('paged_event_data.event_id is not null as hasData');
expect(result).toEqual({
data: [{ id: 'event-1' }],
count: '25',
page: 1,
pageSize: 20,
orderBy: 'createdAt',
search: 'signup',
isCapped: false,
});
});
test('clickhouse reuses the parsed date filter for hasData so timezone-aware ranges stay aligned', async () => {
vi.resetModules();
const clickhouseRawQuery = vi
.fn()
.mockResolvedValueOnce([{ num: '25' }])
.mockResolvedValueOnce([{ id: 'event-1' }]);
vi.doMock('@/lib/db', () => ({
CLICKHOUSE: 'clickhouse',
PRISMA: 'prisma',
runQuery: vi.fn((queries: Record<string, () => unknown>) => queries.clickhouse()),
}));
vi.doMock('@/lib/prisma', () => ({
default: {},
}));
vi.doMock('@/lib/clickhouse', () => ({
default: {
rawQuery: clickhouseRawQuery,
parseFilters: vi.fn().mockReturnValue({
...clickhouseParseFiltersResult,
dateQuery:
'and created_at between toTimezone({startDate:DateTime64},{timezone:String}) and toTimezone({endDate:DateTime64},{timezone:String})',
}),
},
}));
const { getWebsiteEvents } = await import('./getWebsiteEvents');
await getWebsiteEvents('website-1', {
page: 1,
pageSize: 20,
timezone: 'America/Los_Angeles',
} as any);
const [dataQuery] = clickhouseRawQuery.mock.calls[1];
expect(dataQuery).toContain(
'and created_at between toTimezone({startDate:DateTime64},{timezone:String}) and toTimezone({endDate:DateTime64},{timezone:String})',
);
expect(dataQuery).not.toContain(
'and created_at between {startDate:DateTime64} and {endDate:DateTime64}',
);
});
});