Fix getWebsiteEvents

This commit is contained in:
Francis Cao committed 2026-08-05 10:41:15 -07:00
1 parent 311190f44a
commit ff08111ef1
2 files changed
+124 -60

No files matched your search

+76 -23
View File
@@ -32,6 +32,7 @@ async function loadModule({
const clickhouseRawQuery = vi
.fn()
.mockResolvedValueOnce([{ num: '25' }])
.mockResolvedValueOnce([{ event_id: 'event-1', created_at: '2026-08-05 10:00:00' }])
.mockResolvedValueOnce([{ id: 'event-1' }]);
const clickhouseParseFilters = vi.fn().mockReturnValue(clickhouseParseFiltersResult);
@@ -116,29 +117,34 @@ describe('getWebsiteEvents', () => {
} as any);
const [countQuery] = clickhouseRawQuery.mock.calls[0];
const [dataQuery] = clickhouseRawQuery.mock.calls[1];
const [pagedEventsQuery] = clickhouseRawQuery.mock.calls[1];
const [dataQuery, dataParams] = clickhouseRawQuery.mock.calls[2];
expect(countQuery).toContain('select count(*) as num from (select 1 from (');
expect(countQuery).not.toContain('event_data');
expect(dataQuery).toContain('with candidate_events as (');
expect(dataQuery).toContain('and url_path = {path:String}');
expect(dataQuery).toContain('positionCaseInsensitive');
expect(dataQuery).toContain(
'inner join website_event on website_event.event_id = candidate_events.event_id',
);
expect(pagedEventsQuery).toContain('select');
expect(pagedEventsQuery).toContain('event_id,');
expect(pagedEventsQuery).toContain('created_at');
expect(pagedEventsQuery).toContain('from website_event');
expect(pagedEventsQuery).toContain('and url_path = {path:String}');
expect(pagedEventsQuery).toContain('positionCaseInsensitive');
expect(pagedEventsQuery).toContain('order by created_at desc');
expect(pagedEventsQuery).toContain('limit 20 offset 0');
expect(dataQuery).toContain('paged_event_data as (');
expect(dataQuery).toContain('where website_id = {websiteId:UUID}');
expect(dataQuery).toContain('from umami.event_data_pivot');
expect(dataQuery).toContain('inner join paged_events on paged_events.id = event_data_pivot.event_id');
expect(dataQuery).toContain('and event_id in {eventIds:Array(UUID)}');
expect(dataQuery).toContain(
'and event_data_pivot.created_at between {startDate:DateTime64} and {endDate:DateTime64}',
);
expect(dataQuery).toContain('from candidate_events');
expect(dataQuery).toContain('from paged_events');
expect(dataQuery).toContain('left join paged_event_data on paged_event_data.event_id = paged_events.id');
expect(dataQuery).toContain('order by paged_events.createdAt desc');
expect(dataQuery).toContain('from website_event');
expect(dataQuery).toContain(
'left join paged_event_data on paged_event_data.event_id = website_event.event_id',
);
expect(dataQuery).toContain('order by indexOf({eventIds:Array(UUID)}, website_event.event_id)');
expect(dataQuery).toContain('toUInt8(1) as has_data');
expect(dataQuery).toContain('ifNull(paged_event_data.has_data, 0) as hasData');
expect(dataParams).toMatchObject({ eventIds: ['event-1'] });
expect(result).toEqual({
data: [{ id: 'event-1' }],
count: '25',
@@ -156,6 +162,7 @@ describe('getWebsiteEvents', () => {
const clickhouseRawQuery = vi
.fn()
.mockResolvedValueOnce([{ num: '25' }])
.mockResolvedValueOnce([{ event_id: 'event-1', created_at: '2026-08-05 10:00:00' }])
.mockResolvedValueOnce([{ id: 'event-1' }]);
vi.doMock('@/lib/db', () => ({
@@ -187,7 +194,7 @@ describe('getWebsiteEvents', () => {
timezone: 'America/Los_Angeles',
} as any);
const [dataQuery] = clickhouseRawQuery.mock.calls[1];
const [dataQuery] = clickhouseRawQuery.mock.calls[2];
expect(dataQuery).toContain(
'and event_data_pivot.created_at between toTimezone({startDate:DateTime64},{timezone:String}) and toTimezone({endDate:DateTime64},{timezone:String})',
@@ -203,6 +210,7 @@ describe('getWebsiteEvents', () => {
const clickhouseRawQuery = vi
.fn()
.mockResolvedValueOnce([{ num: '10000' }])
.mockResolvedValueOnce([{ event_id: 'event-1', created_at: '2026-08-05 10:00:00' }])
.mockResolvedValueOnce([{ id: 'event-1' }]);
vi.doMock('@/lib/db', () => ({
@@ -235,20 +243,65 @@ describe('getWebsiteEvents', () => {
} as any);
const [countQuery] = clickhouseRawQuery.mock.calls[0];
const [dataQuery] = clickhouseRawQuery.mock.calls[1];
const [pagedEventsQuery] = clickhouseRawQuery.mock.calls[1];
const [dataQuery, dataParams] = clickhouseRawQuery.mock.calls[2];
expect(countQuery).toContain('select count(*) as num from (select 1 from (');
expect(countQuery).toContain('from website_event');
expect(countQuery).not.toContain('from website_event_stats_hourly');
expect(dataQuery).toContain('with candidate_events as (');
expect(dataQuery).toContain('select');
expect(dataQuery).toContain('event_id,');
expect(dataQuery).toContain('created_at');
expect(pagedEventsQuery).toContain('select');
expect(pagedEventsQuery).toContain('event_id,');
expect(pagedEventsQuery).toContain('created_at');
expect(pagedEventsQuery).toContain('from website_event');
expect(pagedEventsQuery).toContain('limit 20 offset 0');
expect(pagedEventsQuery).not.toContain('positionCaseInsensitive');
expect(dataQuery).toContain('and event_id in {eventIds:Array(UUID)}');
expect(dataQuery).toContain('from website_event');
expect(dataQuery).toContain('limit 20');
expect(dataQuery).toContain(
'inner join website_event on website_event.event_id = candidate_events.event_id',
);
expect(dataQuery).not.toContain('positionCaseInsensitive');
expect(dataQuery).toContain('order by indexOf({eventIds:Array(UUID)}, website_event.event_id)');
expect(dataParams).toMatchObject({ eventIds: ['event-1'] });
});
test('clickhouse returns an empty page without running the wide hydration query when no ids match', async () => {
vi.resetModules();
const clickhouseRawQuery = vi
.fn()
.mockResolvedValueOnce([{ num: '0' }])
.mockResolvedValueOnce([]);
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),
},
}));
const { getWebsiteEvents } = await import('./getWebsiteEvents');
const result = await getWebsiteEvents('website-1', {
page: 1,
pageSize: 20,
} as any);
expect(clickhouseRawQuery).toHaveBeenCalledTimes(2);
expect(result).toEqual({
data: [],
count: '0',
page: 1,
pageSize: 20,
orderBy: undefined,
search: undefined,
isCapped: false,
});
});
});
+48 -37
View File
@@ -120,7 +120,6 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
? `and ((positionCaseInsensitive(event_name, {search:String}) > 0 and event_type = ${EVENT_TYPE.customEvent})
or (positionCaseInsensitive(url_path, {search:String}) > 0 and event_type = ${EVENT_TYPE.pageView}))`
: '';
const candidateLimit = maxResults ? Math.min(+maxResults, offset + size) : offset + size;
const eventQuery = `
select
@@ -140,57 +139,69 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
: `select count(*) as num from (${eventQuery}) t`;
const count = await rawQuery(countQuery, queryParams).then(res => res[0].num);
const pagedEventRows = await rawQuery<{ event_id: string; created_at: string }[]>(
`
${eventQuery}
order by created_at desc
limit ${size} offset ${offset}
`,
queryParams,
FUNCTION_NAME,
);
const eventIds = pagedEventRows.map(({ event_id }) => event_id);
if (!eventIds.length) {
return {
data: [],
count,
page: +page,
pageSize: size,
orderBy,
search,
isCapped: !!maxResults && +count >= +maxResults,
};
}
const dataQuery = `
with candidate_events as (
${eventQuery}
order by created_at desc
limit ${candidateLimit}
),
paged_events as (
select
website_event.event_id as id,
website_event.website_id as websiteId,
website_event.session_id as sessionId,
website_event.created_at as createdAt,
website_event.hostname,
website_event.url_path as urlPath,
website_event.url_query as urlQuery,
website_event.referrer_path as referrerPath,
website_event.referrer_query as referrerQuery,
website_event.referrer_domain as referrerDomain,
website_event.country,
website_event.city,
website_event.device,
website_event.os,
website_event.browser,
website_event.page_title as pageTitle,
website_event.event_type as eventType,
website_event.event_name as eventName
from candidate_events
inner join website_event on website_event.event_id = candidate_events.event_id
order by candidate_events.created_at desc
limit ${size} offset ${offset}
),
paged_event_data as (
select
event_id,
toUInt8(1) as has_data
from umami.event_data_pivot
inner join paged_events on paged_events.id = event_data_pivot.event_id
where website_id = {websiteId:UUID}
and event_id in {eventIds:Array(UUID)}
${hasDataDateQuery}
group by event_id
)
select
paged_events.*,
website_event.event_id as id,
website_event.website_id as websiteId,
website_event.session_id as sessionId,
website_event.created_at as createdAt,
website_event.hostname,
website_event.url_path as urlPath,
website_event.url_query as urlQuery,
website_event.referrer_path as referrerPath,
website_event.referrer_query as referrerQuery,
website_event.referrer_domain as referrerDomain,
website_event.country,
website_event.city,
website_event.device,
website_event.os,
website_event.browser,
website_event.page_title as pageTitle,
website_event.event_type as eventType,
website_event.event_name as eventName,
ifNull(paged_event_data.has_data, 0) as hasData
from paged_events
left join paged_event_data on paged_event_data.event_id = paged_events.id
order by paged_events.createdAt desc
from website_event
left join paged_event_data on paged_event_data.event_id = website_event.event_id
where website_event.website_id = {websiteId:UUID}
and website_event.event_id in {eventIds:Array(UUID)}
order by indexOf({eventIds:Array(UUID)}, website_event.event_id)
`;
const data = await rawQuery(dataQuery, queryParams, FUNCTION_NAME);
const data = await rawQuery(dataQuery, { ...queryParams, eventIds }, FUNCTION_NAME);
return {
data,