Improve website events query performance and fix activity search persistence

This commit is contained in:
Francis Cao committed 2026-08-03 16:30:19 -07:00
1 parent b6b500ea4f
commit 079cfdde84
7 files changed
+85 -44

No files matched your search

@@ -7,6 +7,10 @@ export function WebsiteTabs() {
const website = useWebsite();
const { pathname, renderUrl } = useNavigation();
const { t, labels } = useMessages();
const resetParams = {
search: undefined,
page: undefined,
};
const links = [
{
@@ -49,7 +53,7 @@ export function WebsiteTabs() {
<TabList>
{links.map(({ id, label, icon, path }) => {
return (
<Tab key={id} id={id} href={renderUrl(`/websites/${website.id}${path}`)}>
<Tab key={id} id={id} href={renderUrl(`/websites/${website.id}${path}`, resetParams)}>
<Row alignItems="center" gap>
<Icon>{icon}</Icon>
<Text>{label}</Text>
@@ -1,5 +1,6 @@
import {
Button,
Column,
DataColumn,
DataTable,
type DataTableProps,
@@ -43,7 +44,7 @@ export function EventsTable(props: DataTableProps) {
<DataColumn id="event" label={t(labels.event)} width="2fr">
{(row: any) => {
return (
<Row alignItems="center" wrap="wrap" gap>
<Column gap="2">
<Row>
<IconLabel
icon={row.eventName ? <Lightning /> : <Eye />}
@@ -51,6 +52,7 @@ export function EventsTable(props: DataTableProps) {
labelProps={{ wrap: 'nowrap' }}
/>
</Row>
<Row alignItems="center" gap>
<Text
weight="bold"
style={{ maxWidth: '300px' }}
@@ -61,6 +63,7 @@ export function EventsTable(props: DataTableProps) {
</Text>
{row.hasData > 0 && <PropertiesButton websiteId={row.websiteId} eventId={row.id} />}
</Row>
</Column>
);
}}
</DataColumn>
+20 -2
View File
@@ -16,6 +16,7 @@ import {
type ReactElement,
type ReactNode,
useCallback,
useEffect,
useState,
} from 'react';
import { Empty } from '@/components/common/Empty';
@@ -73,7 +74,6 @@ export function DataGrid({
const handleSearch = (value: string) => {
if (value !== search) {
setSearch(value);
router.push(updateParams({ search: value, page: 1 }));
}
};
@@ -84,6 +84,24 @@ export function DataGrid({
[router, updateParams, search],
);
useEffect(() => {
setSearch(queryParams?.search || '');
}, [queryParams?.search]);
useEffect(() => {
const currentSearch = queryParams?.search || '';
if (search === currentSearch) {
return;
}
const timer = window.setTimeout(() => {
router.push(updateParams({ search: search || undefined, page: 1 }));
}, searchDelay || DEFAULT_SEARCH_DELAY);
return () => window.clearTimeout(timer);
}, [queryParams?.search, router, search, searchDelay, updateParams]);
const child = data ? (typeof children === 'function' ? children(data) : children) : null;
const viewToggleButton = (
@@ -106,7 +124,7 @@ export function DataGrid({
<SearchField
value={search}
onSearch={handleSearch}
delay={searchDelay || DEFAULT_SEARCH_DELAY}
delay={0}
autoFocus={autoFocus}
placeholder={t(labels.search)}
className="w-full max-w-md"
+3 -6
View File
@@ -1,5 +1,5 @@
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useMemo } from 'react';
import { buildPath } from '@/lib/url';
export function useNavigation() {
@@ -11,7 +11,8 @@ export function useNavigation() {
const [, linkId] = pathname.match(/\/links\/([a-f0-9-]+)/) || [];
const [, pixelId] = pathname.match(/\/pixels\/([a-f0-9-]+)/) || [];
const [, boardId] = pathname.match(/\/boards\/([a-f0-9-]+)/) || [];
const [queryParams, setQueryParams] = useState(Object.fromEntries(searchParams));
const searchParamsString = searchParams.toString();
const queryParams = useMemo(() => Object.fromEntries(searchParams), [searchParamsString]);
const updateParams = useCallback(
(params?: Record<string, string | number>) => {
@@ -37,10 +38,6 @@ export function useNavigation() {
[teamId, queryParams],
);
useEffect(() => {
setQueryParams(Object.fromEntries(searchParams));
}, [searchParams.toString()]);
return {
router,
pathname,
@@ -17,9 +17,14 @@ import { useNavigation } from './useNavigation';
export function useWebsiteNavItems(websiteId: string) {
const { t, labels } = useMessages();
const { pathname, renderUrl } = useNavigation();
const resetParams = {
search: undefined,
page: undefined,
};
const renderPath = (path: string) =>
renderUrl(`/websites/${websiteId}${path}`, {
...resetParams,
event: undefined,
compare: undefined,
view: undefined,
@@ -88,7 +88,6 @@ describe('getWebsiteEvents', () => {
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}}');
@@ -122,15 +121,19 @@ describe('getWebsiteEvents', () => {
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('website_id as websiteId');
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(dataQuery).toContain('inner join paged_events on paged_events.id = event_data.event_id');
expect(dataQuery).toContain(
'and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}',
);
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('toUInt8(1) as has_data');
expect(dataQuery).toContain('ifNull(paged_event_data.has_data, 0) as hasData');
expect(result).toEqual({
data: [{ id: 'event-1' }],
count: '25',
@@ -182,7 +185,7 @@ describe('getWebsiteEvents', () => {
const [dataQuery] = clickhouseRawQuery.mock.calls[1];
expect(dataQuery).toContain(
'and created_at between toTimezone({startDate:DateTime64},{timezone:String}) and toTimezone({endDate:DateTime64},{timezone:String})',
'and event_data.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}',
+37 -26
View File
@@ -114,6 +114,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
...filters,
websiteId,
});
const hasDataDateQuery = dateQuery.replaceAll('created_at', 'event_data.created_at');
const searchQuery = search
? `and ((positionCaseInsensitive(event_name, {search:String}) > 0 and event_type = ${EVENT_TYPE.customEvent})
@@ -142,41 +143,51 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const data = await rawQuery(
`
with paged_events as (
${eventQuery}
select
event_id as id,
website_id as websiteId,
session_id as sessionId,
created_at as createdAt,
hostname,
url_path as urlPath,
url_query as urlQuery,
referrer_path as referrerPath,
referrer_query as referrerQuery,
referrer_domain as referrerDomain,
country,
city,
device,
os,
browser,
page_title as pageTitle,
event_type as eventType,
event_name as eventName
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and event_type != ${EVENT_TYPE.performance}
${dateQuery}
${filterQuery}
${searchQuery}
order by created_at desc
limit ${size} offset ${offset}
),
paged_event_data as (
select distinct event_id
select
event_id,
toUInt8(1) as has_data
from event_data
inner join paged_events on paged_events.event_id = event_data.event_id
inner join paged_events on paged_events.id = event_data.event_id
where website_id = {websiteId:UUID}
${dateQuery}
${hasDataDateQuery}
group by event_id
)
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 as country,
website_event.city as city,
website_event.device as device,
website_event.os as os,
website_event.browser as browser,
website_event.page_title as pageTitle,
website_event.event_type as eventType,
website_event.event_name as eventName,
paged_event_data.event_id is not null as hasData
paged_events.*,
ifNull(paged_event_data.has_data, 0) as hasData
from paged_events
inner join website_event on website_event.event_id = paged_events.event_id
left join paged_event_data on paged_event_data.event_id = website_event.event_id
order by paged_events.created_at desc
left join paged_event_data on paged_event_data.event_id = paged_events.id
order by paged_events.createdAt desc
`,
queryParams,
FUNCTION_NAME,