Refactor part 2: Electric Boogaloo. Standardize way of passing filter parameters.

This commit is contained in:
Mike Cao 2025-07-04 01:23:11 -07:00
parent f26f1b0581
commit cdf391d5c2
90 changed files with 867 additions and 709 deletions

View file

@ -3,17 +3,11 @@ import { useUsersQuery } from '@/components/hooks';
import { UsersTable } from './UsersTable'; import { UsersTable } from './UsersTable';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
export function UsersDataTable({ export function UsersDataTable({ showActions }: { showActions?: boolean; children?: ReactNode }) {
showActions,
children,
}: {
showActions?: boolean;
children?: ReactNode;
}) {
const queryResult = useUsersQuery(); const queryResult = useUsersQuery();
return ( return (
<DataGrid queryResult={queryResult} renderEmpty={() => children}> <DataGrid queryResult={queryResult} allowSearch={true}>
{({ data }) => <UsersTable data={data} showActions={showActions} />} {({ data }) => <UsersTable data={data} showActions={showActions} />}
</DataGrid> </DataGrid>
); );

View file

@ -19,7 +19,6 @@ import { TagsTable } from '@/components/metrics/TagsTable';
import { getCompareDate } from '@/lib/date'; import { getCompareDate } from '@/lib/date';
import { formatNumber } from '@/lib/format'; import { formatNumber } from '@/lib/format';
import { useState } from 'react'; import { useState } from 'react';
import { useWebsites } from '@/store/websites';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { DateDisplay } from '@/components/common/DateDisplay'; import { DateDisplay } from '@/components/common/DateDisplay';
@ -42,8 +41,7 @@ const views = {
export function WebsiteCompareTables({ websiteId }: { websiteId: string }) { export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const { dateRange } = useDateRange(websiteId); const { dateRange, dateCompare } = useDateRange(websiteId);
const dateCompare = useWebsites(state => state[websiteId]?.dateCompare);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { const {
updateParams, updateParams,

View file

@ -17,43 +17,43 @@ export function WebsiteMetricsBar({
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId); const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
const isAllTime = dateRange.value === 'all'; const isAllTime = dateRange.value === 'all';
const { pageviews, visitors, visits, bounces, totaltime, previous } = data || {}; const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};
const metrics = data const metrics = data
? [ ? [
{ {
value: visitors, value: visitors,
label: formatMessage(labels.visitors), label: formatMessage(labels.visitors),
change: visitors - previous.visitors, change: visitors - comparison.visitors,
formatValue: formatLongNumber, formatValue: formatLongNumber,
}, },
{ {
value: visits, value: visits,
label: formatMessage(labels.visits), label: formatMessage(labels.visits),
change: visits - previous.visits, change: visits - comparison.visits,
formatValue: formatLongNumber, formatValue: formatLongNumber,
}, },
{ {
value: pageviews, value: pageviews,
label: formatMessage(labels.views), label: formatMessage(labels.views),
change: pageviews - previous.pageviews, change: pageviews - comparison.pageviews,
formatValue: formatLongNumber, formatValue: formatLongNumber,
}, },
{ {
label: formatMessage(labels.bounceRate), label: formatMessage(labels.bounceRate),
value: (Math.min(visits, bounces) / visits) * 100, value: (Math.min(visits, bounces) / visits) * 100,
prev: (Math.min(previous.visits, previous.bounces) / previous.visits) * 100, prev: (Math.min(comparison.visits, comparison.bounces) / comparison.visits) * 100,
change: change:
(Math.min(visits, bounces) / visits) * 100 - (Math.min(visits, bounces) / visits) * 100 -
(Math.min(previous.visits, previous.bounces) / previous.visits) * 100, (Math.min(comparison.visits, comparison.bounces) / comparison.visits) * 100,
formatValue: n => Math.round(+n) + '%', formatValue: n => Math.round(+n) + '%',
reverseColors: true, reverseColors: true,
}, },
{ {
label: formatMessage(labels.visitDuration), label: formatMessage(labels.visitDuration),
value: totaltime / visits, value: totaltime / visits,
prev: previous.totaltime / previous.visits, prev: comparison.totaltime / comparison.visits,
change: totaltime / visits - previous.totaltime / previous.visits, change: totaltime / visits - comparison.totaltime / comparison.visits,
formatValue: n => formatValue: n =>
`${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`, `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`,
}, },

View file

@ -1,7 +1,9 @@
import { useWebsiteEventsQuery } from '@/components/hooks'; import { useState } from 'react';
import { useMessages, useWebsiteEventsQuery } from '@/components/hooks';
import { EventsTable } from './EventsTable'; import { EventsTable } from './EventsTable';
import { DataGrid } from '@/components/common/DataGrid'; import { DataGrid } from '@/components/common/DataGrid';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { FilterButtons } from '@/components/common/FilterButtons';
export function EventsDataTable({ export function EventsDataTable({
websiteId, websiteId,
@ -10,10 +12,37 @@ export function EventsDataTable({
teamId?: string; teamId?: string;
children?: ReactNode; children?: ReactNode;
}) { }) {
const { formatMessage, labels } = useMessages();
const queryResult = useWebsiteEventsQuery(websiteId); const queryResult = useWebsiteEventsQuery(websiteId);
const [view, setView] = useState('all');
const buttons = [
{
id: 'all',
label: formatMessage(labels.all),
},
{
id: 'page',
label: formatMessage(labels.page),
},
{
id: 'event',
label: formatMessage(labels.event),
},
];
const renderActions = () => {
return <FilterButtons items={buttons} value={view} onChange={setView} />;
};
return ( return (
<DataGrid queryResult={queryResult} allowSearch={true} autoFocus={false}> <DataGrid
queryResult={queryResult}
allowSearch={true}
autoFocus={false}
allowPaging={true}
renderActions={renderActions}
>
{({ data }) => <EventsTable data={data} />} {({ data }) => <EventsTable data={data} />}
</DataGrid> </DataGrid>
); );

View file

@ -5,6 +5,7 @@ import { Avatar } from '@/components/common/Avatar';
import Link from 'next/link'; import Link from 'next/link';
import { Bolt, Eye } from '@/components/icons'; import { Bolt, Eye } from '@/components/icons';
import { DateDistance } from '@/components/common/DateDistance'; import { DateDistance } from '@/components/common/DateDistance';
import { TypeIcon } from '@/components/common/TypeIcon';
export function EventsTable({ data = [] }) { export function EventsTable({ data = [] }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -16,13 +17,6 @@ export function EventsTable({ data = [] }) {
return ( return (
<DataTable data={data}> <DataTable data={data}>
<DataColumn id="session" label={formatMessage(labels.session)} width="100px">
{(row: any) => (
<Link href={renderUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}>
<Avatar seed={row.sessionId} size={64} />
</Link>
)}
</DataColumn>
<DataColumn id="event" label={formatMessage(labels.event)} width="2fr"> <DataColumn id="event" label={formatMessage(labels.event)} width="2fr">
{(row: any) => { {(row: any) => {
return ( return (
@ -34,8 +28,21 @@ export function EventsTable({ data = [] }) {
); );
}} }}
</DataColumn> </DataColumn>
<DataColumn id="created" label={formatMessage(labels.created)} width="200px"> <DataColumn id="created" width="1fr" align="end">
{(row: any) => <DateDistance date={new Date(row.createdAt)} />} {(row: any) => (
<Row alignItems="center" gap>
<DateDistance date={new Date(row.createdAt)} />
<Link href={renderUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}>
<Avatar seed={row.sessionId} size={32} />
</Link>
<Row alignItems="center" gap="1">
<TypeIcon type="country" value={row.country} />
<TypeIcon type="browser" value={row.browser} />
<TypeIcon type="os" value={row.os} />
<TypeIcon type="device" value={row.device} />
</Row>
</Row>
)}
</DataColumn> </DataColumn>
</DataTable> </DataTable>
); );

View file

@ -30,15 +30,11 @@ export function Attribution({
}: AttributionProps) { }: AttributionProps) {
const { data, error, isLoading } = useResultQuery<any>('attribution', { const { data, error, isLoading } = useResultQuery<any>('attribution', {
websiteId, websiteId,
dateRange: { startDate,
startDate, endDate,
endDate, model,
}, type,
parameters: { step,
model,
type,
step,
},
}); });
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();

View file

@ -7,12 +7,10 @@ export interface BreakdownProps {
websiteId: string; websiteId: string;
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
parameters: { selectedFields: string[];
fields: string[];
};
} }
export function Breakdown({ websiteId, parameters, startDate, endDate }: BreakdownProps) { export function Breakdown({ websiteId, selectedFields = [], startDate, endDate }: BreakdownProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat(); const { formatValue } = useFormat();
const { fields } = useFields(); const { fields } = useFields();
@ -20,19 +18,17 @@ export function Breakdown({ websiteId, parameters, startDate, endDate }: Breakdo
'breakdown', 'breakdown',
{ {
websiteId, websiteId,
dateRange: { startDate,
startDate, endDate,
endDate, fields: selectedFields,
},
parameters,
}, },
{ enabled: !!parameters.fields.length }, { enabled: !!selectedFields.length },
); );
return ( return (
<LoadingPanel data={data} isLoading={isLoading} error={error}> <LoadingPanel data={data} isLoading={isLoading} error={error}>
<DataTable data={data}> <DataTable data={data}>
{parameters?.fields.map(field => { {selectedFields.map(field => {
return ( return (
<DataColumn key={field} id={field} label={fields.find(f => f.name === field)?.label}> <DataColumn key={field} id={field} label={fields.find(f => f.name === field)?.label}>
{row => { {row => {

View file

@ -23,7 +23,7 @@ export function BreakdownPage({ websiteId }: { websiteId: string }) {
websiteId={websiteId} websiteId={websiteId}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
parameters={{ fields }} selectedFields={fields}
/> />
</Panel> </Panel>
</Column> </Column>

View file

@ -17,15 +17,11 @@ type FunnelResult = {
remaining: number; remaining: number;
}; };
export function Funnel({ id, name, type, parameters, websiteId, startDate, endDate }) { export function Funnel({ id, name, type, parameters, websiteId }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { data, error, isLoading } = useResultQuery<any>(type, { const { data, error, isLoading } = useResultQuery(type, {
websiteId, websiteId,
dateRange: { ...parameters,
startDate,
endDate,
},
parameters,
}); });
return ( return (

View file

@ -9,7 +9,7 @@ import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
export function FunnelsPage({ websiteId }: { websiteId: string }) { export function FunnelsPage({ websiteId }: { websiteId: string }) {
const { result, query } = useReportsQuery({ websiteId, type: 'funnel' }); const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'funnel' });
const { const {
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
} = useDateRange(websiteId); } = useDateRange(websiteId);
@ -20,14 +20,16 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) {
<SectionHeader> <SectionHeader>
<FunnelAddButton websiteId={websiteId} /> <FunnelAddButton websiteId={websiteId} />
</SectionHeader> </SectionHeader>
<LoadingPanel data={result?.data} isLoading={query?.isLoading} error={query?.error}> <LoadingPanel data={data} isLoading={isLoading} error={error}>
<Grid gap> {data && (
{result?.data?.map((report: any) => ( <Grid gap>
<Panel key={report.id}> {data['data']?.map((report: any) => (
<Funnel {...report} startDate={startDate} endDate={endDate} /> <Panel key={report.id}>
</Panel> <Funnel {...report} startDate={startDate} endDate={endDate} />
))} </Panel>
</Grid> ))}
</Grid>
)}
</LoadingPanel> </LoadingPanel>
</Column> </Column>
); );

View file

@ -26,11 +26,9 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, { const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, {
websiteId, websiteId,
dateRange: { startDate,
startDate, endDate,
endDate, ...parameters,
},
parameters,
}); });
const isPage = parameters?.type === 'page'; const isPage = parameters?.type === 'page';

View file

@ -22,28 +22,15 @@ export interface JourneyProps {
endStep?: string; endStep?: string;
} }
export function Journey({ export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps) {
websiteId,
startDate,
endDate,
steps,
startStep,
endStep,
}: JourneyProps) {
const [selectedNode, setSelectedNode] = useState(null); const [selectedNode, setSelectedNode] = useState(null);
const [activeNode, setActiveNode] = useState(null); const [activeNode, setActiveNode] = useState(null);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { data, error, isLoading } = useResultQuery<any>('journey', { const { data, error, isLoading } = useResultQuery<any>('journey', {
websiteId, websiteId,
dateRange: { steps,
startDate, startStep,
endDate, endStep,
},
parameters: {
steps,
startStep,
endStep,
},
}); });
useEscapeKey(() => setSelectedNode(null)); useEscapeKey(() => setSelectedNode(null));

View file

@ -1,7 +1,7 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Grid, Row, Column, Text, Icon } from '@umami/react-zen'; import { Grid, Row, Column, Text, Icon } from '@umami/react-zen';
import { Users } from '@/components/icons'; import { Users } from '@/components/icons';
import { useMessages, useLocale, useResultQuery, useTimezone } from '@/components/hooks'; import { useMessages, useLocale, useResultQuery } from '@/components/hooks';
import { formatDate } from '@/lib/date'; import { formatDate } from '@/lib/date';
import { formatLongNumber } from '@/lib/format'; import { formatLongNumber } from '@/lib/format';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
@ -19,14 +19,10 @@ export interface RetentionProps {
export function Retention({ websiteId, days = DAYS, startDate, endDate }: RetentionProps) { export function Retention({ websiteId, days = DAYS, startDate, endDate }: RetentionProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { locale } = useLocale(); const { locale } = useLocale();
const { timezone } = useTimezone(); const { data, error, isLoading } = useResultQuery('retention', {
const { data, error, isLoading } = useResultQuery<any>('retention', {
websiteId, websiteId,
dateRange: { startDate,
startDate, endDate,
endDate,
timezone,
},
}); });
const rows = const rows =

View file

@ -8,7 +8,7 @@ import { endOfMonth, startOfMonth } from 'date-fns';
export function RetentionPage({ websiteId }: { websiteId: string }) { export function RetentionPage({ websiteId }: { websiteId: string }) {
const { const {
dateRange: { startDate }, dateRange: { startDate },
} = useDateRange(websiteId); } = useDateRange(websiteId, { ignoreOffset: true });
const monthStartDate = startOfMonth(startDate); const monthStartDate = startOfMonth(startDate);
const monthEndDate = endOfMonth(startDate); const monthEndDate = endOfMonth(startDate);

View file

@ -32,13 +32,9 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
const unit = getMinimumUnit(startDate, endDate); const unit = getMinimumUnit(startDate, endDate);
const { data, error, isLoading } = useResultQuery<any>('revenue', { const { data, error, isLoading } = useResultQuery<any>('revenue', {
websiteId, websiteId,
dateRange: { startDate,
startDate, endDate,
endDate, currency,
},
parameters: {
currency,
},
}); });
const renderCountryName = useCallback( const renderCountryName = useCallback(

View file

@ -18,10 +18,8 @@ export function UTM({ websiteId, startDate, endDate }: UTMProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { data, error, isLoading } = useResultQuery<any>('utm', { const { data, error, isLoading } = useResultQuery<any>('utm', {
websiteId, websiteId,
dateRange: { startDate,
startDate, endDate,
endDate,
},
}); });
return ( return (

View file

@ -1,21 +1,15 @@
import { useWebsiteSessionsQuery } from '@/components/hooks'; import { useWebsiteSessionsQuery } from '@/components/hooks';
import { SessionsTable } from './SessionsTable'; import { SessionsTable } from './SessionsTable';
import { DataGrid } from '@/components/common/DataGrid'; import { DataGrid } from '@/components/common/DataGrid';
import { ReactNode } from 'react';
export function SessionsDataTable({ export function SessionsDataTable({ websiteId }: { websiteId?: string; teamId?: string }) {
websiteId,
children,
}: {
websiteId?: string;
teamId?: string;
children?: ReactNode;
}) {
const queryResult = useWebsiteSessionsQuery(websiteId); const queryResult = useWebsiteSessionsQuery(websiteId);
return ( return (
<DataGrid queryResult={queryResult} renderEmpty={() => children} allowPaging> <DataGrid queryResult={queryResult} allowPaging allowSearch>
{({ data }) => <SessionsTable data={data} showDomain={!websiteId} />} {({ data }) => {
return <SessionsTable data={data} showDomain={!websiteId} />;
}}
</DataGrid> </DataGrid>
); );
} }

View file

@ -14,7 +14,7 @@ export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean
<DataColumn id="id" label={formatMessage(labels.session)} width="100px"> <DataColumn id="id" label={formatMessage(labels.session)} width="100px">
{(row: any) => ( {(row: any) => (
<Link href={`sessions/${row.id}`}> <Link href={`sessions/${row.id}`}>
<Avatar seed={row.id} size={64} /> <Avatar seed={row.id} size={48} />
</Link> </Link>
)} )}
</DataColumn> </DataColumn>

View file

@ -1,8 +1,8 @@
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { json, unauthorized } from '@/lib/response'; import { json, unauthorized } from '@/lib/response';
import { reportResultSchema } from '@/lib/schema'; import { reportResultSchema } from '@/lib/schema';
import { getAttribution } from '@/queries/sql/reports/getAttribution'; import { AttributionParameters, getAttribution } from '@/queries/sql/reports/getAttribution';
export async function POST(request: Request) { export async function POST(request: Request) {
const { auth, body, error } = await parseRequest(request, reportResultSchema); const { auth, body, error } = await parseRequest(request, reportResultSchema);
@ -11,26 +11,16 @@ export async function POST(request: Request) {
return error(); return error();
} }
const { const { websiteId } = body;
websiteId,
dateRange: { startDate, endDate },
parameters: { model, type, step, currency },
...filters
} = body;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getAttribution(websiteId, { const parameters = await setWebsiteDate(websiteId, body.parameters);
...filters, const filters = getQueryFilters(body.filters);
startDate: new Date(startDate),
endDate: new Date(endDate), const data = await getAttribution(websiteId, parameters as AttributionParameters, filters);
model,
type,
step,
currency,
});
return json(data); return json(data);
} }

View file

@ -1,7 +1,7 @@
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { getBreakdown } from '@/queries'; import { BreakdownParameters, getBreakdown } from '@/queries';
import { reportResultSchema } from '@/lib/schema'; import { reportResultSchema } from '@/lib/schema';
export async function POST(request: Request) { export async function POST(request: Request) {
@ -11,22 +11,16 @@ export async function POST(request: Request) {
return error(); return error();
} }
const { const { websiteId } = body;
websiteId,
dateRange: { startDate, endDate },
parameters: { fields },
...filters
} = body;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getBreakdown(websiteId, fields, { const parameters = await setWebsiteDate(websiteId, body.parameters);
...filters, const filters = getQueryFilters(body.filters);
startDate: new Date(startDate),
endDate: new Date(endDate), const data = await getBreakdown(websiteId, parameters as BreakdownParameters, filters);
});
return json(data); return json(data);
} }

View file

@ -1,7 +1,7 @@
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { parseRequest } from '@/lib/request'; import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
import { getFunnel } from '@/queries'; import { FunnelParameters, getFunnel } from '@/queries';
import { reportResultSchema } from '@/lib/schema'; import { reportResultSchema } from '@/lib/schema';
export async function POST(request: Request) { export async function POST(request: Request) {
@ -11,24 +11,16 @@ export async function POST(request: Request) {
return error(); return error();
} }
const { const { websiteId } = body;
websiteId,
dateRange: { startDate, endDate },
parameters: { steps, window },
...filters
} = body;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getFunnel(websiteId, { const parameters = await setWebsiteDate(websiteId, body.parameters);
...filters, const filters = getQueryFilters(body.filters);
startDate: new Date(startDate),
endDate: new Date(endDate), const data = await getFunnel(websiteId, parameters as FunnelParameters, filters);
steps,
windowMinutes: +window,
});
return json(data); return json(data);
} }

View file

@ -1,7 +1,7 @@
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { getQueryFilters, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { getGoal } from '@/queries/sql/reports/getGoal'; import { getGoal, GoalParameters } from '@/queries/sql/reports/getGoal';
import { reportResultSchema } from '@/lib/schema'; import { reportResultSchema } from '@/lib/schema';
export async function POST(request: Request) { export async function POST(request: Request) {
@ -11,27 +11,16 @@ export async function POST(request: Request) {
return error(); return error();
} }
const { const { websiteId } = body;
websiteId,
dateRange: { startDate, endDate },
parameters: { type, value, property, operator },
} = body;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const filters = await getQueryFilters(body.filters); const parameters = await setWebsiteDate(websiteId, body.parameters);
const filters = getQueryFilters(body.filters);
const data = await getGoal(websiteId, { const data = await getGoal(websiteId, parameters as GoalParameters, filters);
startDate: new Date(startDate),
endDate: new Date(endDate),
type,
value,
property,
operator,
filters,
});
return json(data); return json(data);
} }

View file

@ -1,6 +1,6 @@
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { getJourney } from '@/queries'; import { getJourney } from '@/queries';
import { reportResultSchema } from '@/lib/schema'; import { reportResultSchema } from '@/lib/schema';
@ -11,25 +11,15 @@ export async function POST(request: Request) {
return error(); return error();
} }
const { const { websiteId, parameters, filters } = body;
websiteId,
dateRange: { startDate, endDate },
parameters: { steps, startStep, endStep },
...filters
} = body;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getJourney(websiteId, { const queryFilters = await setWebsiteDate(websiteId, getQueryFilters(filters));
...filters,
startDate: new Date(startDate), const data = await getJourney(websiteId, parameters, queryFilters);
endDate: new Date(endDate),
steps,
startStep,
endStep,
});
return json(data); return json(data);
} }

View file

@ -1,7 +1,7 @@
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { parseRequest } from '@/lib/request'; import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
import { getRetention } from '@/queries'; import { getRetention, RetentionParameters } from '@/queries';
import { reportResultSchema } from '@/lib/schema'; import { reportResultSchema } from '@/lib/schema';
export async function POST(request: Request) { export async function POST(request: Request) {
@ -11,22 +11,16 @@ export async function POST(request: Request) {
return error(); return error();
} }
const { const { websiteId } = body;
websiteId,
dateRange: { startDate, endDate, timezone },
...filters
} = body;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getRetention(websiteId, { const filters = getQueryFilters(body.filters);
...filters, const parameters = await setWebsiteDate(websiteId, body.parameters);
startDate: new Date(startDate),
endDate: new Date(endDate), const data = await getRetention(websiteId, parameters as RetentionParameters, filters);
timezone,
});
return json(data); return json(data);
} }

View file

@ -1,8 +1,8 @@
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { parseRequest } from '@/lib/request'; import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
import { reportResultSchema } from '@/lib/schema'; import { reportResultSchema } from '@/lib/schema';
import { getRevenue } from '@/queries/sql/reports/getRevenue'; import { getRevenue, RevenuParameters } from '@/queries/sql/reports/getRevenue';
export async function POST(request: Request) { export async function POST(request: Request) {
const { auth, body, error } = await parseRequest(request, reportResultSchema); const { auth, body, error } = await parseRequest(request, reportResultSchema);
@ -11,24 +11,16 @@ export async function POST(request: Request) {
return error(); return error();
} }
const { const { websiteId } = body;
websiteId,
dateRange: { startDate, endDate, unit },
parameters: { currency },
...filters
} = body;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getRevenue(websiteId, { const parameters = await setWebsiteDate(websiteId, body.parameters);
...filters, const filters = getQueryFilters(body.filters);
startDate: new Date(startDate),
endDate: new Date(endDate), const data = await getRevenue(websiteId, parameters as RevenuParameters, filters);
unit,
currency,
});
return json(data); return json(data);
} }

View file

@ -1,7 +1,7 @@
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { getUTM } from '@/queries'; import { getUTM, UTMParameters } from '@/queries';
import { reportResultSchema } from '@/lib/schema'; import { reportResultSchema } from '@/lib/schema';
export async function POST(request: Request) { export async function POST(request: Request) {
@ -11,21 +11,16 @@ export async function POST(request: Request) {
return error(); return error();
} }
const { const { websiteId } = body;
websiteId,
dateRange: { startDate, endDate },
...filters
} = body;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getUTM(websiteId, { const parameters = await setWebsiteDate(websiteId, body.parameters);
...filters, const filters = getQueryFilters(body.filters);
startDate: new Date(startDate),
endDate: new Date(endDate), const data = await getUTM(websiteId, parameters as UTMParameters, filters);
});
return json(data); return json(data);
} }

View file

@ -2,7 +2,7 @@ import { z } from 'zod';
import { getQueryFilters, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { pagingParams } from '@/lib/schema'; import { dateRangeParams, pagingParams, filterParams } from '@/lib/schema';
import { getWebsiteEvents } from '@/queries'; import { getWebsiteEvents } from '@/queries';
export async function GET( export async function GET(
@ -10,8 +10,8 @@ export async function GET(
{ params }: { params: Promise<{ websiteId: string }> }, { params }: { params: Promise<{ websiteId: string }> },
) { ) {
const schema = z.object({ const schema = z.object({
startAt: z.coerce.number().int(), ...dateRangeParams,
endAt: z.coerce.number().int(), ...filterParams,
...pagingParams, ...pagingParams,
}); });

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { parseRequest, getQueryFilters } from '@/lib/request'; import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; import { filterParams, timezoneParam, unitParam } from '@/lib/schema';
@ -29,7 +29,7 @@ export async function GET(
return unauthorized(); return unauthorized();
} }
const filters = await getQueryFilters({ ...query, websiteId }); const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const data = await getEventMetrics(websiteId, filters); const data = await getEventMetrics(websiteId, filters);

View file

@ -4,8 +4,6 @@ import { canViewWebsite } from '@/lib/auth';
import { import {
SESSION_COLUMNS, SESSION_COLUMNS,
EVENT_COLUMNS, EVENT_COLUMNS,
FILTER_COLUMNS,
OPERATORS,
SEARCH_DOMAINS, SEARCH_DOMAINS,
SOCIAL_DOMAINS, SOCIAL_DOMAINS,
EMAIL_DOMAINS, EMAIL_DOMAINS,
@ -13,7 +11,7 @@ import {
VIDEO_DOMAINS, VIDEO_DOMAINS,
PAID_AD_PARAMS, PAID_AD_PARAMS,
} from '@/lib/constants'; } from '@/lib/constants';
import { parseRequest, getQueryFilters } from '@/lib/request'; import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
import { json, unauthorized, badRequest } from '@/lib/response'; import { json, unauthorized, badRequest } from '@/lib/response';
import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries'; import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries';
import { filterParams } from '@/lib/schema'; import { filterParams } from '@/lib/schema';
@ -45,20 +43,14 @@ export async function GET(
return unauthorized(); return unauthorized();
} }
const column = FILTER_COLUMNS[type] || type; const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const filters = await getQueryFilters({ ...query, websiteId });
if (search) { if (search) {
filters[type] = { filters[type] = `c.${search}`;
name: type,
column,
operator: OPERATORS.contains,
value: search,
};
} }
if (SESSION_COLUMNS.includes(type)) { if (SESSION_COLUMNS.includes(type)) {
const data = await getSessionMetrics(websiteId, type, filters, limit, offset); const data = await getSessionMetrics(websiteId, { type, limit, offset }, filters);
if (type === 'language') { if (type === 'language') {
const combined = {}; const combined = {};
@ -80,7 +72,7 @@ export async function GET(
} }
if (EVENT_COLUMNS.includes(type)) { if (EVENT_COLUMNS.includes(type)) {
const data = await getPageviewMetrics(websiteId, type, filters, limit, offset); const data = await getPageviewMetrics(websiteId, { type, limit, offset }, filters);
return json(data); return json(data);
} }

View file

@ -1,6 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getQueryFilters, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { dateRangeParams, filterParams } from '@/lib/schema'; import { dateRangeParams, filterParams } from '@/lib/schema';
import { getCompareDate } from '@/lib/date'; import { getCompareDate } from '@/lib/date';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
@ -27,7 +27,7 @@ export async function GET(
return unauthorized(); return unauthorized();
} }
const filters = await getQueryFilters({ ...query, websiteId }); const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const [pageviews, sessions] = await Promise.all([ const [pageviews, sessions] = await Promise.all([
getPageviewStats(websiteId, filters), getPageviewStats(websiteId, filters),

View file

@ -1,5 +1,5 @@
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getQueryFilters, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { json, unauthorized } from '@/lib/response'; import { json, unauthorized } from '@/lib/response';
import { getSessionDataValues } from '@/queries'; import { getSessionDataValues } from '@/queries';
import { z } from 'zod'; import { z } from 'zod';
@ -22,7 +22,7 @@ export async function GET(
const { propertyName } = query; const { propertyName } = query;
const { websiteId } = await params; const { websiteId } = await params;
const filters = await getQueryFilters({ ...query, websiteId }); const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { getQueryFilters, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { dateRangeParams, filterParams, pagingParams } from '@/lib/schema'; import { dateRangeParams, filterParams, pagingParams } from '@/lib/schema';
@ -27,7 +27,7 @@ export async function GET(
return unauthorized(); return unauthorized();
} }
const filters = await getQueryFilters({ ...query, websiteId }); const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const data = await getWebsiteSessions(websiteId, filters); const data = await getWebsiteSessions(websiteId, filters);

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { parseRequest, getQueryFilters } from '@/lib/request'; import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { filterParams } from '@/lib/schema'; import { filterParams } from '@/lib/schema';
@ -27,7 +27,7 @@ export async function GET(
return unauthorized(); return unauthorized();
} }
const filters = await getQueryFilters(query); const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const metrics = await getWebsiteSessionStats(websiteId, filters); const metrics = await getWebsiteSessionStats(websiteId, filters);

View file

@ -1,9 +1,10 @@
import { z } from 'zod'; import { z } from 'zod';
import { parseRequest, getQueryFilters } from '@/lib/request'; import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { filterParams } from '@/lib/schema'; import { filterParams } from '@/lib/schema';
import { getWebsiteStats } from '@/queries'; import { getWebsiteStats } from '@/queries';
import { getCompareDate } from '@/lib/date';
export async function GET( export async function GET(
request: Request, request: Request,
@ -28,15 +29,17 @@ export async function GET(
return unauthorized(); return unauthorized();
} }
const filters = await getQueryFilters({ ...query, websiteId }); const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const data = await getWebsiteStats(websiteId, filters); const data = await getWebsiteStats(websiteId, filters);
const previous = await getWebsiteStats(websiteId, { const { startDate, endDate } = getCompareDate('prev', filters.startDate, filters.endDate);
const comparison = await getWebsiteStats(websiteId, {
...filters, ...filters,
startDate: filters.compareStartDate, startDate,
endDate: filters.compareEndDate, endDate,
}); });
return json({ ...data, previous }); return json({ ...data, comparison });
} }

View file

@ -6,13 +6,13 @@ import { LoadingPanel } from '@/components/common/LoadingPanel';
const DEFAULT_SEARCH_DELAY = 600; const DEFAULT_SEARCH_DELAY = 600;
export interface DataTableProps { export interface DataGridProps {
queryResult: any; queryResult: any;
searchDelay?: number; searchDelay?: number;
allowSearch?: boolean; allowSearch?: boolean;
allowPaging?: boolean; allowPaging?: boolean;
autoFocus?: boolean; autoFocus?: boolean;
renderEmpty?: () => ReactNode; renderActions?: () => ReactNode;
children: ReactNode | ((data: any) => ReactNode); children: ReactNode | ((data: any) => ReactNode);
} }
@ -20,12 +20,13 @@ export function DataGrid({
queryResult, queryResult,
searchDelay = 600, searchDelay = 600,
allowSearch, allowSearch,
allowPaging, allowPaging = true,
autoFocus, autoFocus,
renderActions,
children, children,
}: DataTableProps) { }: DataGridProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { data, error, isLoading, isFetching, setParams } = queryResult || {}; const { data, error, isLoading, isFetching, setParams } = queryResult;
const { router, updateParams } = useNavigation(); const { router, updateParams } = useNavigation();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@ -43,29 +44,33 @@ export function DataGrid({
return ( return (
<Column gap="4" minHeight="300px"> <Column gap="4" minHeight="300px">
{allowSearch && (data || search) && ( {allowSearch && (data || search) && (
<Row width="280px" alignItems="center"> <Row alignItems="center" justifyContent="space-between">
<SearchField <SearchField
value={search} value={search}
onSearch={handleSearch} onSearch={handleSearch}
delay={searchDelay || DEFAULT_SEARCH_DELAY} delay={searchDelay || DEFAULT_SEARCH_DELAY}
autoFocus={autoFocus} autoFocus={autoFocus}
placeholder={formatMessage(labels.search)} placeholder={formatMessage(labels.search)}
style={{ width: '280px' }}
/> />
{renderActions?.()}
</Row> </Row>
)} )}
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}> <LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}>
<Column> {data && (
{data ? (typeof children === 'function' ? children(data) : children) : null} <>
</Column> <Column>{typeof children === 'function' ? children(data) : children}</Column>
{allowPaging && data && ( {allowPaging && data && (
<Row marginTop="6"> <Row marginTop="6">
<Pager <Pager
page={data.page} page={data.page}
pageSize={data.pageSize} pageSize={data.pageSize}
count={data.count} count={data.count}
onPageChange={handlePageChange} onPageChange={handlePageChange}
/> />
</Row> </Row>
)}
</>
)} )}
</LoadingPanel> </LoadingPanel>
</Column> </Column>

View file

@ -1,14 +1,16 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
export function useEventDataEventsQuery(websiteId: string, options?: ReactQueryOptions<any>) { export function useEventDataEventsQuery(websiteId: string, options?: ReactQueryOptions<any>) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const params = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery({ return useQuery({
queryKey: ['websites:event-data:events', { websiteId, ...params }], queryKey: ['websites:event-data:events', { websiteId, ...date, ...filters }],
queryFn: () => get(`/websites/${websiteId}/event-data/events`, { ...params }), queryFn: () => get(`/websites/${websiteId}/event-data/events`, { ...date, ...filters }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View file

@ -1,14 +1,16 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
export function useEventDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions<any>) { export function useEventDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions<any>) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const params = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery<any>({ return useQuery<any>({
queryKey: ['websites:event-data:properties', { websiteId, ...params }], queryKey: ['websites:event-data:properties', { websiteId, ...date, ...filters }],
queryFn: () => get(`/websites/${websiteId}/event-data/properties`, { ...params }), queryFn: () => get(`/websites/${websiteId}/event-data/properties`, { ...date, ...filters }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View file

@ -1,5 +1,6 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
export function useEventDataQuery( export function useEventDataQuery(
@ -8,11 +9,12 @@ export function useEventDataQuery(
options?: ReactQueryOptions<any>, options?: ReactQueryOptions<any>,
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const params = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const params = useFilterParameters();
return useQuery({ return useQuery({
queryKey: ['websites:event-data', { websiteId, eventId, ...params }], queryKey: ['websites:event-data', { websiteId, eventId, ...date, ...params }],
queryFn: () => get(`/websites/${websiteId}/event-data/${eventId}`, { ...params }), queryFn: () => get(`/websites/${websiteId}/event-data/${eventId}`, { ...date, ...params }),
enabled: !!(websiteId && eventId), enabled: !!(websiteId && eventId),
...options, ...options,
}); });

View file

@ -1,5 +1,6 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
export function useEventDataValuesQuery( export function useEventDataValuesQuery(
@ -9,12 +10,21 @@ export function useEventDataValuesQuery(
options?: ReactQueryOptions<any>, options?: ReactQueryOptions<any>,
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const params = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery<any>({ return useQuery<any>({
queryKey: ['websites:event-data:values', { websiteId, eventName, propertyName, ...params }], queryKey: [
'websites:event-data:values',
{ websiteId, eventName, propertyName, ...date, ...filters },
],
queryFn: () => queryFn: () =>
get(`/websites/${websiteId}/event-data/values`, { ...params, eventName, propertyName }), get(`/websites/${websiteId}/event-data/values`, {
...date,
...filters,
eventName,
propertyName,
}),
enabled: !!(websiteId && propertyName), enabled: !!(websiteId && propertyName),
...options, ...options,
}); });

View file

@ -1,15 +1,17 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
import { useDateParameters } from '@/components/hooks/useDateParameters';
export function useResultQuery<T = any>( export function useResultQuery<T = any>(
type: string, type: string,
params?: Record<string, any>, params?: Record<string, any>,
options?: ReactQueryOptions<T>, options?: ReactQueryOptions<T>,
) { ) {
const { websiteId } = params; const { websiteId, ...parameters } = params;
const { post, useQuery } = useApi(); const { post, useQuery } = useApi();
const filters = useFilterParams(websiteId); const { startDate, endDate, timezone } = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery<T>({ return useQuery<T>({
queryKey: [ queryKey: [
@ -17,11 +19,25 @@ export function useResultQuery<T = any>(
{ {
type, type,
websiteId, websiteId,
...filters, startDate,
endDate,
timezone,
...params, ...params,
...filters,
}, },
], ],
queryFn: () => post(`/reports/${type}`, { type, filters, ...params }), queryFn: () =>
post(`/reports/${type}`, {
websiteId,
type,
filters,
parameters: {
startDate,
endDate,
timezone,
...parameters,
},
}),
enabled: !!type, enabled: !!type,
...options, ...options,
}); });

View file

@ -1,14 +1,16 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
export function useSessionDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions<any>) { export function useSessionDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions<any>) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const params = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery<any>({ return useQuery<any>({
queryKey: ['websites:session-data:properties', { websiteId, ...params }], queryKey: ['websites:session-data:properties', { websiteId, ...date, ...filters }],
queryFn: () => get(`/websites/${websiteId}/session-data/properties`, { ...params }), queryFn: () => get(`/websites/${websiteId}/session-data/properties`, { ...date, ...filters }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View file

@ -1,5 +1,6 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
export function useSessionDataValuesQuery( export function useSessionDataValuesQuery(
@ -8,11 +9,13 @@ export function useSessionDataValuesQuery(
options?: ReactQueryOptions<any>, options?: ReactQueryOptions<any>,
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const params = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery<any>({ return useQuery<any>({
queryKey: ['websites:session-data:values', { websiteId, propertyName, ...params }], queryKey: ['websites:session-data:values', { websiteId, propertyName, ...date, ...filters }],
queryFn: () => get(`/websites/${websiteId}/session-data/values`, { ...params, propertyName }), queryFn: () =>
get(`/websites/${websiteId}/session-data/values`, { ...date, ...filters, propertyName }),
enabled: !!(websiteId && propertyName), enabled: !!(websiteId && propertyName),
...options, ...options,
}); });

View file

@ -1,15 +1,17 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
import { usePagedQuery } from '../usePagedQuery'; import { usePagedQuery } from '../usePagedQuery';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
export function useWebsiteEventsQuery(websiteId: string, options?: ReactQueryOptions<any>) { export function useWebsiteEventsQuery(websiteId: string, options?: ReactQueryOptions<any>) {
const { get } = useApi(); const { get } = useApi();
const queryParams = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return usePagedQuery({ return usePagedQuery({
queryKey: ['websites:events', { websiteId, ...queryParams }], queryKey: ['websites:events', { websiteId, ...date, ...filters }],
queryFn: () => get(`/websites/${websiteId}/events`, { ...queryParams, pageSize: 20 }), queryFn: () => get(`/websites/${websiteId}/events`, { ...date, ...filters, pageSize: 20 }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View file

@ -1,14 +1,16 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
export function useWebsiteEventsSeriesQuery(websiteId: string, options?: ReactQueryOptions<any>) { export function useWebsiteEventsSeriesQuery(websiteId: string, options?: ReactQueryOptions<any>) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const params = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery({ return useQuery({
queryKey: ['websites:events:series', { websiteId, ...params }], queryKey: ['websites:events:series', { websiteId, ...date, ...filters }],
queryFn: () => get(`/websites/${websiteId}/events/series`, { ...params }), queryFn: () => get(`/websites/${websiteId}/events/series`, { ...date, ...filters }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View file

@ -1,6 +1,7 @@
import { keepPreviousData } from '@tanstack/react-query'; import { keepPreviousData } from '@tanstack/react-query';
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
@ -11,11 +12,12 @@ export type WebsiteMetricsData = {
export function useWebsiteMetricsQuery( export function useWebsiteMetricsQuery(
websiteId: string, websiteId: string,
params: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number }, params: { type: string; limit?: number; search?: string },
options?: ReactQueryOptions<WebsiteMetricsData>, options?: ReactQueryOptions<WebsiteMetricsData>,
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const queryParams = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
return useQuery<WebsiteMetricsData>({ return useQuery<WebsiteMetricsData>({
@ -23,13 +25,15 @@ export function useWebsiteMetricsQuery(
'websites:metrics', 'websites:metrics',
{ {
websiteId, websiteId,
...queryParams, ...date,
...filters,
...params, ...params,
}, },
], ],
queryFn: async () => queryFn: async () =>
get(`/websites/${websiteId}/metrics`, { get(`/websites/${websiteId}/metrics`, {
...queryParams, ...date,
...filters,
[searchParams.get('view')]: undefined, [searchParams.get('view')]: undefined,
...params, ...params,
}), }),

View file

@ -1,5 +1,6 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
export interface WebsitePageviewsData { export interface WebsitePageviewsData {
@ -12,11 +13,12 @@ export function useWebsitePageviewsQuery(
options?: ReactQueryOptions<WebsitePageviewsData>, options?: ReactQueryOptions<WebsitePageviewsData>,
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const queryParams = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const queryParams = useFilterParameters();
return useQuery<WebsitePageviewsData>({ return useQuery<WebsitePageviewsData>({
queryKey: ['websites:pageviews', { websiteId, compare, ...queryParams }], queryKey: ['websites:pageviews', { websiteId, compare, ...date, ...queryParams }],
queryFn: () => get(`/websites/${websiteId}/pageviews`, { compare, ...queryParams }), queryFn: () => get(`/websites/${websiteId}/pageviews`, { compare, ...date, ...queryParams }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View file

@ -1,13 +1,15 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
export function useWebsiteSessionStatsQuery(websiteId: string, options?: Record<string, string>) { export function useWebsiteSessionStatsQuery(websiteId: string, options?: Record<string, string>) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const params = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery({ return useQuery({
queryKey: ['sessions:stats', { websiteId, ...params }], queryKey: ['sessions:stats', { websiteId, ...date, ...filters }],
queryFn: () => get(`/websites/${websiteId}/sessions/stats`, { ...params }), queryFn: () => get(`/websites/${websiteId}/sessions/stats`, { ...date, ...filters }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View file

@ -1,7 +1,8 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { usePagedQuery } from '../usePagedQuery'; import { usePagedQuery } from '../usePagedQuery';
import { useModified } from '../useModified'; import { useModified } from '../useModified';
import { useFilterParams } from '@/components/hooks/useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '../useDateParameters';
export function useWebsiteSessionsQuery( export function useWebsiteSessionsQuery(
websiteId: string, websiteId: string,
@ -9,14 +10,16 @@ export function useWebsiteSessionsQuery(
) { ) {
const { get } = useApi(); const { get } = useApi();
const { modified } = useModified(`sessions`); const { modified } = useModified(`sessions`);
const filters = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return usePagedQuery({ return usePagedQuery({
queryKey: ['sessions', { websiteId, modified, ...params, ...filters }], queryKey: ['sessions', { websiteId, modified, ...params, ...date, ...filters }],
queryFn: () => { queryFn: () => {
return get(`/websites/${websiteId}/sessions`, { return get(`/websites/${websiteId}/sessions`, {
...params, ...params,
...filters, ...filters,
...date,
pageSize: 20, pageSize: 20,
}); });
}, },

View file

@ -1,6 +1,7 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useModified } from '../useModified'; import { useModified } from '../useModified';
import { useFilterParams } from '@/components/hooks/useFilterParams'; import { useDateParameters } from '../useDateParameters';
import { useFilterParameters } from '@/components/hooks/useFilterParameters';
export function useWebsiteSessionsWeeklyQuery( export function useWebsiteSessionsWeeklyQuery(
websiteId: string, websiteId: string,
@ -8,13 +9,15 @@ export function useWebsiteSessionsWeeklyQuery(
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { modified } = useModified(`sessions`); const { modified } = useModified(`sessions`);
const filters = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery({ return useQuery({
queryKey: ['sessions', { websiteId, modified, ...params, ...filters }], queryKey: ['sessions', { websiteId, modified, ...params, ...date, ...filters }],
queryFn: () => { queryFn: () => {
return get(`/websites/${websiteId}/sessions/weekly`, { return get(`/websites/${websiteId}/sessions/weekly`, {
...params, ...params,
...date,
...filters, ...filters,
}); });
}, },

View file

@ -1,6 +1,7 @@
import { UseQueryOptions } from '@tanstack/react-query'; import { UseQueryOptions } from '@tanstack/react-query';
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '@/components/hooks/useDateParameters';
export interface WebsiteStatsData { export interface WebsiteStatsData {
pageviews: number; pageviews: number;
@ -8,7 +9,7 @@ export interface WebsiteStatsData {
visits: number; visits: number;
bounces: number; bounces: number;
totaltime: number; totaltime: number;
previous: { comparison: {
pageviews: number; pageviews: number;
visitors: number; visitors: number;
visits: number; visits: number;
@ -22,11 +23,12 @@ export function useWebsiteStatsQuery(
options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>, options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>,
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const filterParams = useFilterParams(websiteId); const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery<WebsiteStatsData>({ return useQuery<WebsiteStatsData>({
queryKey: ['websites:stats', { websiteId, ...filterParams }], queryKey: ['websites:stats', { websiteId, ...date, ...filters }],
queryFn: () => get(`/websites/${websiteId}/stats`, { ...filterParams }), queryFn: () => get(`/websites/${websiteId}/stats`, { ...date, ...filters }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View file

@ -0,0 +1,16 @@
import { useMemo } from 'react';
import { useNavigation } from './useNavigation';
export function usePageParameters() {
const {
query: { page, pageSize, search },
} = useNavigation();
return useMemo(() => {
return {
page,
pageSize,
search,
};
}, [page, pageSize, search]);
}

View file

@ -0,0 +1,18 @@
import { useDateRange } from './useDateRange';
import { useTimezone } from './useTimezone';
export function useDateParameters(websiteId: string) {
const {
dateRange: { startDate, endDate, unit },
} = useDateRange(websiteId);
const { timezone, toUtc } = useTimezone();
return {
startAt: +toUtc(startDate),
endAt: +toUtc(endDate),
startDate: toUtc(startDate).toISOString(),
endDate: toUtc(endDate).toISOString(),
unit,
timezone,
};
}

View file

@ -8,7 +8,11 @@ import { useApi } from './useApi';
import { useNavigation } from './useNavigation'; import { useNavigation } from './useNavigation';
import { useMemo } from 'react'; import { useMemo } from 'react';
export function useDateRange(websiteId?: string) { export interface UseDateRangeOptions {
ignoreOffset?: boolean;
}
export function useDateRange(websiteId?: string, options: UseDateRangeOptions = {}) {
const { get } = useApi(); const { get } = useApi();
const { locale } = useLocale(); const { locale } = useLocale();
const { const {
@ -16,14 +20,16 @@ export function useDateRange(websiteId?: string) {
} = useNavigation(); } = useNavigation();
const websiteConfig = useWebsites(state => state[websiteId]?.dateRange); const websiteConfig = useWebsites(state => state[websiteId]?.dateRange);
const globalConfig = useApp(state => state.dateRangeValue); const globalConfig = useApp(state => state.dateRangeValue);
const dateRangeObject = parseDateRange( const dateValue = date || websiteConfig?.value || globalConfig || DEFAULT_DATE_RANGE_VALUE;
date || websiteConfig?.value || globalConfig || DEFAULT_DATE_RANGE_VALUE,
locale, const dateRange = useMemo(() => {
); const dateRangeObject = parseDateRange(dateValue, locale);
const dateRange = useMemo(
() => (offset ? getOffsetDateRange(dateRangeObject, +offset) : dateRangeObject), return !options.ignoreOffset && offset
[date, offset, websiteConfig], ? getOffsetDateRange(dateRangeObject, +offset)
); : dateRangeObject;
}, [date, offset, dateValue, options]);
const dateCompare = useWebsites(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE); const dateCompare = useWebsites(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE);
const saveDateRange = async (value: string) => { const saveDateRange = async (value: string) => {

View file

@ -1,11 +1,7 @@
import { useMemo } from 'react';
import { useNavigation } from './useNavigation'; import { useNavigation } from './useNavigation';
import { useDateRange } from './useDateRange';
import { useTimezone } from './useTimezone';
export function useFilterParams(websiteId: string) { export function useFilterParameters() {
const { dateRange } = useDateRange(websiteId);
const { startDate, endDate, unit } = dateRange;
const { timezone, toUtc } = useTimezone();
const { const {
query: { query: {
path, path,
@ -28,13 +24,25 @@ export function useFilterParams(websiteId: string) {
}, },
} = useNavigation(); } = useNavigation();
return { return useMemo(() => {
// Date range return {
startAt: +toUtc(startDate), path,
endAt: +toUtc(endDate), referrer,
unit, title,
timezone, query,
// Filters host,
os,
browser,
device,
country,
region,
city,
event,
tag,
hostname,
search,
};
}, [
path, path,
referrer, referrer,
title, title,
@ -49,9 +57,8 @@ export function useFilterParams(websiteId: string) {
event, event,
tag, tag,
hostname, hostname,
// Paging
page, page,
pageSize, pageSize,
search, search,
}; ]);
} }

View file

@ -46,7 +46,12 @@ export function MetricsTable({
const { data, isLoading, isFetching, error } = useWebsiteMetricsQuery( const { data, isLoading, isFetching, error } = useWebsiteMetricsQuery(
websiteId, websiteId,
{ type, limit, search: searchFormattedValues ? undefined : search, ...params }, {
type,
limit,
search: searchFormattedValues ? undefined : search,
...params,
},
{ {
retryDelay: delay || DEFAULT_ANIMATION_DURATION, retryDelay: delay || DEFAULT_ANIMATION_DURATION,
}, },

View file

@ -136,7 +136,7 @@ function getQueryParams(filters: Record<string, any>) {
}; };
} }
async function parseFilters(filters: Record<string, any>, options?: QueryOptions) { function parseFilters(filters: Record<string, any>, options?: QueryOptions) {
return { return {
filterQuery: getFilterQuery(filters, options), filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters), dateQuery: getDateQuery(filters),

View file

@ -16,7 +16,8 @@ const enabled = Boolean(process.env.KAFKA_URL && process.env.KAFKA_BROKER);
function getClient() { function getClient() {
const { username, password } = new URL(process.env.KAFKA_URL); const { username, password } = new URL(process.env.KAFKA_URL);
const brokers = process.env.KAFKA_BROKER.split(','); const brokers = process.env.KAFKA_BROKER.split(',');
const mechanism = process.env.KAFKA_SASL_MECHANISM as 'plain' | 'scram-sha-256' | 'scram-sha-512'; const mechanism =
(process.env.KAFKA_SASL_MECHANISM as 'plain' | 'scram-sha-256' | 'scram-sha-512') || 'plain';
const ssl: { ssl?: tls.ConnectionOptions | boolean; sasl?: SASLOptions } = const ssl: { ssl?: tls.ConnectionOptions | boolean; sasl?: SASLOptions } =
username && password username && password

View file

@ -37,6 +37,7 @@ export function filtersToArray(filters: QueryFilters, options: QueryOptions = {}
column: options?.columns?.[key] ?? FILTER_COLUMNS[key], column: options?.columns?.[key] ?? FILTER_COLUMNS[key],
operator, operator,
value, value,
prefix: options?.prefix,
}); });
}, []); }, []);
} }

View file

@ -84,15 +84,10 @@ function getTimestampDiffSQL(field1: string, field2: string): string {
} }
function getSearchSQL(column: string, param: string = 'search'): string { function getSearchSQL(column: string, param: string = 'search'): string {
const db = getDatabaseType(); return `and ${column} ilike {{${param}}}`;
const like = db === POSTGRESQL ? 'ilike' : 'like';
return `and ${column} ${like} {{${param}}}`;
} }
function mapFilter(column: string, operator: string, name: string, type: string = '') { function mapFilter(column: string, operator: string, name: string, type: string = '') {
const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like';
const value = `{{${name}${type ? `::${type}` : ''}}}`; const value = `{{${name}${type ? `::${type}` : ''}}}`;
switch (operator) { switch (operator) {
@ -101,28 +96,31 @@ function mapFilter(column: string, operator: string, name: string, type: string
case OPERATORS.notEquals: case OPERATORS.notEquals:
return `${column} != ${value}`; return `${column} != ${value}`;
case OPERATORS.contains: case OPERATORS.contains:
return `${column} ${like} ${value}`; return `${column} ilike ${value}`;
case OPERATORS.doesNotContain: case OPERATORS.doesNotContain:
return `${column} not ${like} ${value}`; return `${column} not ilike ${value}`;
default: default:
return ''; return '';
} }
} }
function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}): string { function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}): string {
const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => { const query = filtersToArray(filters, options).reduce(
if (column) { (arr, { name, column, operator, prefix = '' }) => {
arr.push(`and ${mapFilter(column, operator, name)}`); if (column) {
arr.push(`and ${mapFilter(`${prefix}${column}`, operator, name)}`);
if (name === 'referrer') { if (name === 'referrer') {
arr.push( arr.push(
`and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`, `and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`,
); );
}
} }
}
return arr; return arr;
}, []); },
[],
);
return query.join('\n'); return query.join('\n');
} }
@ -154,7 +152,7 @@ function getQueryParams(filters: Record<string, any>) {
}; };
} }
async function parseFilters(filters: Record<string, any>, options?: QueryOptions) { function parseFilters(filters: Record<string, any>, options?: QueryOptions) {
const joinSession = Object.keys(filters).find(key => const joinSession = Object.keys(filters).find(key =>
['referrer', ...SESSION_COLUMNS].includes(key), ['referrer', ...SESSION_COLUMNS].includes(key),
); );

View file

@ -1,7 +1,7 @@
import { z } from 'zod/v4'; import { z } from 'zod/v4';
import { FILTER_COLUMNS, DEFAULT_PAGE_SIZE } from '@/lib/constants'; import { FILTER_COLUMNS, DEFAULT_PAGE_SIZE } from '@/lib/constants';
import { badRequest, unauthorized } from '@/lib/response'; import { badRequest, unauthorized } from '@/lib/response';
import { getAllowedUnits, getCompareDate, getMinimumUnit, maxDate } from '@/lib/date'; import { getAllowedUnits, getMinimumUnit, maxDate } from '@/lib/date';
import { checkAuth } from '@/lib/auth'; import { checkAuth } from '@/lib/auth';
import { fetchWebsite } from '@/lib/load'; import { fetchWebsite } from '@/lib/load';
import { QueryFilters } from '@/lib/types'; import { QueryFilters } from '@/lib/types';
@ -50,23 +50,14 @@ export async function getJsonBody(request: Request) {
} }
export function getRequestDateRange(query: Record<string, string>) { export function getRequestDateRange(query: Record<string, string>) {
const { startAt, endAt, unit, compare, timezone } = query; const { startAt, endAt, unit, timezone } = query;
const startDate = new Date(+startAt); const startDate = new Date(+startAt);
const endDate = new Date(+endAt); const endDate = new Date(+endAt);
const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
compare,
startDate,
endDate,
);
return { return {
startDate, startDate,
endDate, endDate,
compare,
compareStartDate,
compareEndDate,
timezone, timezone,
unit: getAllowedUnits(startDate, endDate).includes(unit) unit: getAllowedUnits(startDate, endDate).includes(unit)
? unit ? unit
@ -86,11 +77,21 @@ export function getRequestFilters(query: Record<string, any>) {
}, {}); }, {});
} }
export async function getQueryFilters(params: Record<string, any>): Promise<QueryFilters> { export async function setWebsiteDate(websiteId: string, data: Record<string, any>) {
const website = await fetchWebsite(websiteId);
if (website) {
data.startDate = maxDate(data.startDate, new Date(website?.resetAt));
}
return data;
}
export function getQueryFilters(params: Record<string, any>): QueryFilters {
const dateRange = getRequestDateRange(params); const dateRange = getRequestDateRange(params);
const filters = getRequestFilters(params); const filters = getRequestFilters(params);
const data = { return {
...dateRange, ...dateRange,
...filters, ...filters,
page: params?.page, page: params?.page,
@ -98,17 +99,5 @@ export async function getQueryFilters(params: Record<string, any>): Promise<Quer
orderBy: params?.orderBy, orderBy: params?.orderBy,
sortDescending: params?.sortDescending, sortDescending: params?.sortDescending,
search: params?.search, search: params?.search,
websiteId: undefined,
}; };
const { websiteId } = params;
if (websiteId) {
const website = await fetchWebsite(websiteId);
data.websiteId = websiteId;
data.startDate = maxDate(data.startDate, new Date(website?.resetAt));
}
return data;
} }

View file

@ -11,8 +11,10 @@ export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value),
}); });
export const dateRangeParams = { export const dateRangeParams = {
startAt: z.coerce.number(), startAt: z.coerce.number().optional(),
endAt: z.coerce.number(), endAt: z.coerce.number().optional(),
startDate: z.coerce.date().optional(),
endDate: z.coerce.date().optional(),
timezone: timezoneParam.optional(), timezone: timezoneParam.optional(),
unit: unitParam.optional(), unit: unitParam.optional(),
compare: z.string().optional(), compare: z.string().optional(),
@ -33,12 +35,12 @@ export const filterParams = {
hostname: z.string().optional(), hostname: z.string().optional(),
language: z.string().optional(), language: z.string().optional(),
event: z.string().optional(), event: z.string().optional(),
search: z.string().optional(),
}; };
export const pagingParams = { export const pagingParams = {
page: z.coerce.number().int().positive().optional(), page: z.coerce.number().int().positive().optional(),
pageSize: z.coerce.number().int().positive().optional(), pageSize: z.coerce.number().int().positive().optional(),
search: z.string().optional(),
}; };
export const sortingParams = { export const sortingParams = {
@ -93,23 +95,63 @@ export const reportTypeParam = z.enum([
'utm', 'utm',
]); ]);
export const reportParms = { export const dateRangeSchema = z.object({ ...dateRangeParams }).superRefine((data, ctx) => {
websiteId: z.string().uuid(), const hasTimestamps = data.startAt !== undefined && data.endAt !== undefined;
dateRange: z.object({ const hasDates = data.startDate !== undefined && data.endDate !== undefined;
startDate: z.coerce.date(),
endDate: z.coerce.date(), if (!hasTimestamps && !hasDates) {
timezone: timezoneParam.optional(), ctx.addIssue({
unit: unitParam.optional(), code: z.ZodIssueCode.custom,
compare: z.string().optional(), message: 'You must provide either startAt & endAt or startDate & endDate.',
compareStartDate: z.coerce.date().optional(), });
compareEndDate: z.coerce.date().optional(), }
}),
}; if (hasTimestamps && hasDates) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Provide either startAt & endAt or startDate & endDate, not both.',
});
}
if (data.startAt !== undefined && data.endAt === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'If you provide startAt, you must also provide endAt.',
path: ['endAt'],
});
}
if (data.endAt !== undefined && data.startAt === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'If you provide endAt, you must also provide startAt.',
path: ['startAt'],
});
}
if (data.startDate !== undefined && data.endDate === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'If you provide startDate, you must also provide endDate.',
path: ['endDate'],
});
}
if (data.endDate !== undefined && data.startDate === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'If you provide endDate, you must also provide startDate.',
path: ['startDate'],
});
}
});
export const goalReportSchema = z.object({ export const goalReportSchema = z.object({
type: z.literal('goal'), type: z.literal('goal'),
parameters: z parameters: z
.object({ .object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
type: z.string(), type: z.string(),
value: z.string(), value: z.string(),
operator: z.enum(['count', 'sum', 'average']).optional(), operator: z.enum(['count', 'sum', 'average']).optional(),
@ -126,6 +168,8 @@ export const goalReportSchema = z.object({
export const funnelReportSchema = z.object({ export const funnelReportSchema = z.object({
type: z.literal('funnel'), type: z.literal('funnel'),
parameters: z.object({ parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
window: z.coerce.number().positive(), window: z.coerce.number().positive(),
steps: z steps: z
.array( .array(
@ -142,6 +186,8 @@ export const funnelReportSchema = z.object({
export const journeyReportSchema = z.object({ export const journeyReportSchema = z.object({
type: z.literal('journey'), type: z.literal('journey'),
parameters: z.object({ parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
steps: z.coerce.number().min(2).max(7), steps: z.coerce.number().min(2).max(7),
startStep: z.string().optional(), startStep: z.string().optional(),
endStep: z.string().optional(), endStep: z.string().optional(),
@ -150,15 +196,27 @@ export const journeyReportSchema = z.object({
export const retentionReportSchema = z.object({ export const retentionReportSchema = z.object({
type: z.literal('retention'), type: z.literal('retention'),
parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
timezone: z.string().optional(),
}),
}); });
export const utmReportSchema = z.object({ export const utmReportSchema = z.object({
type: z.literal('utm'), type: z.literal('utm'),
parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
}),
}); });
export const revenueReportSchema = z.object({ export const revenueReportSchema = z.object({
type: z.literal('revenue'), type: z.literal('revenue'),
parameters: z.object({ parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
timezone: z.string().optional(),
currency: z.string(), currency: z.string(),
}), }),
}); });
@ -166,6 +224,8 @@ export const revenueReportSchema = z.object({
export const attributionReportSchema = z.object({ export const attributionReportSchema = z.object({
type: z.literal('attribution'), type: z.literal('attribution'),
parameters: z.object({ parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
model: z.enum(['first-click', 'last-click']), model: z.enum(['first-click', 'last-click']),
type: z.enum(['page', 'event']), type: z.enum(['page', 'event']),
step: z.string(), step: z.string(),
@ -176,6 +236,8 @@ export const attributionReportSchema = z.object({
export const breakdownReportSchema = z.object({ export const breakdownReportSchema = z.object({
type: z.literal('breakdown'), type: z.literal('breakdown'),
parameters: z.object({ parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
fields: z.array(fieldsParam), fields: z.array(fieldsParam),
}), }),
}); });
@ -202,7 +264,7 @@ export const reportSchema = z.intersection(reportBaseSchema, reportTypeSchema);
export const reportResultSchema = z.intersection( export const reportResultSchema = z.intersection(
z.object({ z.object({
...reportParms, websiteId: z.string().uuid(),
filters: z.object({ ...filterParams }), filters: z.object({ ...filterParams }),
}), }),
reportTypeSchema, reportTypeSchema,

View file

@ -41,19 +41,20 @@ export interface QueryOptions {
joinSession?: boolean; joinSession?: boolean;
columns?: Record<string, string>; columns?: Record<string, string>;
limit?: number; limit?: number;
prefix?: string;
} }
export interface QueryFilters { export interface QueryFilters extends DateParams, FilterParams, SortParams, PageParams {}
websiteId?: string;
// Date range export interface DateParams {
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
compareStartDate?: Date;
compareEndDate?: Date;
compare?: string;
unit?: string; unit?: string;
timezone?: string; timezone?: string;
// Filters compareDate?: Date;
}
export interface FilterParams {
path?: string; path?: string;
referrer?: string; referrer?: string;
title?: string; title?: string;
@ -70,20 +71,16 @@ export interface QueryFilters {
search?: string; search?: string;
tag?: string; tag?: string;
eventType?: number; eventType?: number;
// Paging }
page?: number;
pageSize?: number; export interface SortParams {
// Sorting
orderBy?: string; orderBy?: string;
sortDescending?: boolean; sortDescending?: boolean;
} }
export interface PageParams { export interface PageParams {
page: number; page?: number;
pageSize: number; pageSize?: number;
orderBy?: string;
sortDescending?: boolean;
search?: string;
} }
export interface PageResult<T> { export interface PageResult<T> {

View file

@ -23,7 +23,7 @@ export async function getEventDataEvents(
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { event } = filters; const { event } = filters;
const { queryParams } = await parseFilters(filters); const { queryParams } = parseFilters(filters);
if (event) { if (event) {
return rawQuery( return rawQuery(
@ -73,7 +73,7 @@ async function clickhouseQuery(
): Promise<{ eventName: string; propertyName: string; dataType: number; total: number }[]> { ): Promise<{ eventName: string; propertyName: string; dataType: number; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { event } = filters; const { event } = filters;
const { queryParams } = await parseFilters(filters); const { queryParams } = parseFilters(filters);
if (event) { if (event) {
return rawQuery( return rawQuery(

View file

@ -12,7 +12,7 @@ export async function getEventDataFields(...args: [websiteId: string, filters: Q
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters, getDateSQL } = prisma; const { rawQuery, parseFilters, getDateSQL } = prisma;
const { filterQuery, queryParams } = await parseFilters(filters); const { filterQuery, queryParams } = parseFilters(filters);
return rawQuery( return rawQuery(
` `
@ -43,7 +43,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> { ): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters(filters); const { filterQuery, queryParams } = parseFilters(filters);
return rawQuery( return rawQuery(
` `

View file

@ -17,7 +17,7 @@ async function relationalQuery(
filters: QueryFilters & { propertyName?: string }, filters: QueryFilters & { propertyName?: string },
) { ) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterQuery, queryParams } = await parseFilters(filters, { const { filterQuery, queryParams } = parseFilters(filters, {
columns: { propertyName: 'data_key' }, columns: { propertyName: 'data_key' },
}); });
@ -45,7 +45,7 @@ async function clickhouseQuery(
filters: QueryFilters & { propertyName?: string }, filters: QueryFilters & { propertyName?: string },
): Promise<{ eventName: string; propertyName: string; total: number }[]> { ): Promise<{ eventName: string; propertyName: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters(filters, { const { filterQuery, queryParams } = parseFilters(filters, {
columns: { propertyName: 'data_key' }, columns: { propertyName: 'data_key' },
}); });

View file

@ -18,7 +18,7 @@ export async function getEventDataStats(
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId }); const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId });
return rawQuery( return rawQuery(
` `
@ -47,7 +47,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ events: number; properties: number; records: number }[]> { ): Promise<{ events: number; properties: number; records: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId }); const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId });
return rawQuery( return rawQuery(
` `

View file

@ -25,7 +25,7 @@ async function relationalQuery(
filters: QueryFilters & { eventName?: string; propertyName?: string }, filters: QueryFilters & { eventName?: string; propertyName?: string },
) { ) {
const { rawQuery, parseFilters, getDateSQL } = prisma; const { rawQuery, parseFilters, getDateSQL } = prisma;
const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId }); const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId });
return rawQuery( return rawQuery(
` `
@ -56,7 +56,7 @@ async function clickhouseQuery(
filters: QueryFilters & { eventName?: string; propertyName?: string }, filters: QueryFilters & { eventName?: string; propertyName?: string },
): Promise<{ value: string; total: number }[]> { ): Promise<{ value: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId }); const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId });
return rawQuery( return rawQuery(
` `

View file

@ -22,8 +22,9 @@ export async function getEventMetrics(
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = prisma; const { rawQuery, getDateSQL, parseFilters } = prisma;
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({ const { filterQuery, joinSessionQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId,
eventType: EVENT_TYPE.customEvent, eventType: EVENT_TYPE.customEvent,
}); });
@ -49,11 +50,12 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
filters: QueryFilters, filters: QueryFilters,
): Promise<{ x: string; t: string; y: number }[]> { ): Promise<WebsiteEventMetricData[]> {
const { timezone = 'UTC', unit = 'day' } = filters; const { timezone = 'UTC', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = clickhouse; const { rawQuery, getDateSQL, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({ const { filterQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId,
eventType: EVENT_TYPE.customEvent, eventType: EVENT_TYPE.customEvent,
}); });

View file

@ -13,7 +13,7 @@ export function getWebsiteEvents(...args: [websiteId: string, filters: QueryFilt
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters } = prisma; const { pagedRawQuery, parseFilters } = prisma;
const { search } = filters; const { search } = filters;
const { filterQuery, queryParams } = await parseFilters({ const { filterQuery, dateQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId, websiteId,
}); });
@ -40,7 +40,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
event_name as "eventName" event_name as "eventName"
from website_event from website_event
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}} ${dateQuery}
${filterQuery} ${filterQuery}
${searchQuery} ${searchQuery}
order by created_at desc order by created_at desc
@ -52,7 +52,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters } = clickhouse; const { pagedRawQuery, parseFilters } = clickhouse;
const { queryParams, dateQuery, filterQuery } = await parseFilters({ const { queryParams, dateQuery, filterQuery } = parseFilters({
...filters, ...filters,
websiteId, websiteId,
}); });
@ -74,6 +74,10 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
referrer_path as referrerPath, referrer_path as referrerPath,
referrer_query as referrerQuery, referrer_query as referrerQuery,
referrer_domain as referrerDomain, referrer_domain as referrerDomain,
country as country,
device as device,
os as os,
browser as browser,
page_title as pageTitle, page_title as pageTitle,
event_type as eventType, event_type as eventType,
event_name as eventName event_name as eventName

View file

@ -12,7 +12,7 @@ export async function getChannelMetrics(...args: [websiteId: string, filters?: Q
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { queryParams, filterQuery, dateQuery } = await parseFilters(filters); const { queryParams, filterQuery, dateQuery } = parseFilters(filters);
return rawQuery( return rawQuery(
` `
@ -36,7 +36,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { queryParams, filterQuery, dateQuery } = await parseFilters(filters); const { queryParams, filterQuery, dateQuery } = parseFilters(filters);
const sql = ` const sql = `
select select

View file

@ -12,7 +12,7 @@ export async function getRealtimeActivity(...args: [websiteId: string, filters:
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { queryParams, filterQuery, dateQuery } = await parseFilters(filters); const { queryParams, filterQuery, dateQuery } = parseFilters(filters);
return rawQuery( return rawQuery(
` `
@ -41,7 +41,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promise<{ x: number }> { async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promise<{ x: number }> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { queryParams, filterQuery, dateQuery } = await parseFilters(filters); const { queryParams, filterQuery, dateQuery } = parseFilters(filters);
return rawQuery( return rawQuery(
` `

View file

@ -12,7 +12,7 @@ export async function getWebsiteDateRange(...args: [websiteId: string]) {
async function relationalQuery(websiteId: string) { async function relationalQuery(websiteId: string) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { queryParams } = await parseFilters({ const { queryParams } = parseFilters({
startDate: new Date(DEFAULT_RESET_DATE), startDate: new Date(DEFAULT_RESET_DATE),
websiteId, websiteId,
}); });
@ -34,7 +34,7 @@ async function relationalQuery(websiteId: string) {
async function clickhouseQuery(websiteId: string) { async function clickhouseQuery(websiteId: string) {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { queryParams } = await parseFilters({ const { queryParams } = parseFilters({
startDate: new Date(DEFAULT_RESET_DATE), startDate: new Date(DEFAULT_RESET_DATE),
websiteId, websiteId,
}); });

View file

@ -27,7 +27,7 @@ async function relationalQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<WebsiteStatsData[]> { ): Promise<WebsiteStatsData[]> {
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma; const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({ const { filterQuery, joinSessionQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId, websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -66,7 +66,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<WebsiteStatsData[]> { ): Promise<WebsiteStatsData[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({ const { filterQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId, websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,

View file

@ -4,14 +4,19 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types'; import { QueryFilters } from '@/lib/types';
export interface PageviewMetricsParameters {
type: string;
limit?: number | string;
offset?: number | string;
}
export interface PageviewMetricsData {
x: string;
y: number;
}
export async function getPageviewMetrics( export async function getPageviewMetrics(
...args: [ ...args: [websiteId: string, parameters: PageviewMetricsParameters, filters: QueryFilters]
websiteId: string,
type: string,
filters: QueryFilters,
limit?: number | string,
offset?: number | string,
]
) { ) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
@ -21,16 +26,16 @@ export async function getPageviewMetrics(
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
type: string, parameters: PageviewMetricsParameters,
filters: QueryFilters, filters: QueryFilters,
limit: number | string = 500, ): Promise<PageviewMetricsData[]> {
offset: number | string = 0, const { type, limit = 500, offset = 0 } = parameters;
) {
const column = FILTER_COLUMNS[type] || type; const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters( const { filterQuery, joinSessionQuery, queryParams } = parseFilters(
{ {
...filters, ...filters,
websiteId,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
}, },
{ {
@ -87,15 +92,15 @@ async function relationalQuery(
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
type: string, parameters: PageviewMetricsParameters,
filters: QueryFilters, filters: QueryFilters,
limit: number | string = 500, ): Promise<PageviewMetricsData[]> {
offset: number | string = 0, const { type, limit = 500, offset = 0 } = parameters;
): Promise<{ x: string; y: number }[]> {
const column = FILTER_COLUMNS[type] || type; const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({ const { filterQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
}); });

View file

@ -14,7 +14,7 @@ export async function getPageviewStats(...args: [websiteId: string, filters: Que
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { getDateSQL, parseFilters, rawQuery } = prisma; const { getDateSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({ const { filterQuery, joinSessionQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId, websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -44,7 +44,7 @@ async function clickhouseQuery(
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateSQL } = clickhouse; const { parseFilters, rawQuery, getDateSQL } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({ const { filterQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId, websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,

View file

@ -2,8 +2,9 @@ import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE } from '@/lib/constants'; import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface AttributionCriteria { export interface AttributionParameters {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
model: string; model: string;
@ -23,7 +24,9 @@ export interface AttributionResult {
total: { pageviews: number; visitors: number; visits: number }; total: { pageviews: number; visitors: number; visits: number };
} }
export async function getAttribution(...args: [websiteId: string, criteria: AttributionCriteria]) { export async function getAttribution(
...args: [websiteId: string, parameters: AttributionParameters, filters: QueryFilters]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
@ -32,12 +35,18 @@ export async function getAttribution(...args: [websiteId: string, criteria: Attr
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
criteria: AttributionCriteria, parameters: AttributionParameters,
filters: QueryFilters,
): Promise<AttributionResult> { ): Promise<AttributionResult> {
const { startDate, endDate, model, type, step, currency } = criteria; const { model, type, currency } = parameters;
const { rawQuery } = prisma; const { rawQuery, parseFilters } = prisma;
const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent; const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = type === 'page' ? 'url_path' : 'event_name'; const column = type === 'page' ? 'url_path' : 'event_name';
const { filterQuery, queryParams } = parseFilters({
...filters,
...parameters,
eventType,
});
function getUTMQuery(utmColumn: string) { function getUTMQuery(utmColumn: string) {
return ` return `
@ -64,8 +73,9 @@ async function relationalQuery(
from website_event from website_event
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}} and created_at between {{startDate}} and {{endDate}}
and ${column} = {{conversionStep}} and ${column} = {{step}}
and event_type = {{eventType}} and event_type = {{eventType}}
${filterQuery}
group by 1),`; group by 1),`;
const revenueEventQuery = `WITH events AS ( const revenueEventQuery = `WITH events AS (
@ -76,8 +86,9 @@ async function relationalQuery(
from revenue from revenue
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}} and created_at between {{startDate}} and {{endDate}}
and ${column} = {{conversionStep}} and ${column} = {{step}}
and currency = {{currency}} and currency = {{currency}}
${filterQuery}
group by 1),`; group by 1),`;
function getModelQuery(model: string) { function getModelQuery(model: string) {
@ -128,7 +139,7 @@ async function relationalQuery(
order by 2 desc order by 2 desc
limit 20 limit 20
`, `,
{ websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const paidAdsres = await rawQuery( const paidAdsres = await rawQuery(
@ -161,7 +172,7 @@ async function relationalQuery(
FROM results FROM results
${currency ? '' : `WHERE name != ''`} ${currency ? '' : `WHERE name != ''`}
`, `,
{ websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const sourceRes = await rawQuery( const sourceRes = await rawQuery(
@ -170,7 +181,7 @@ async function relationalQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_source')} ${getUTMQuery('utm_source')}
`, `,
{ websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const mediumRes = await rawQuery( const mediumRes = await rawQuery(
@ -179,7 +190,7 @@ async function relationalQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_medium')} ${getUTMQuery('utm_medium')}
`, `,
{ websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const campaignRes = await rawQuery( const campaignRes = await rawQuery(
@ -188,7 +199,7 @@ async function relationalQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_campaign')} ${getUTMQuery('utm_campaign')}
`, `,
{ websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const contentRes = await rawQuery( const contentRes = await rawQuery(
@ -197,7 +208,7 @@ async function relationalQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_content')} ${getUTMQuery('utm_content')}
`, `,
{ websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const termRes = await rawQuery( const termRes = await rawQuery(
@ -206,7 +217,7 @@ async function relationalQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_term')} ${getUTMQuery('utm_term')}
`, `,
{ websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const totalRes = await rawQuery( const totalRes = await rawQuery(
@ -218,10 +229,11 @@ async function relationalQuery(
from website_event from website_event
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}} and created_at between {{startDate}} and {{endDate}}
and ${column} = {{conversionStep}} and ${column} = {{step}}
and event_type = {{eventType}} and event_type = {{eventType}}
${filterQuery}
`, `,
{ websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
).then(result => result?.[0]); ).then(result => result?.[0]);
return { return {
@ -238,13 +250,19 @@ async function relationalQuery(
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
criteria: AttributionCriteria, parameters: AttributionParameters,
filters: QueryFilters,
): Promise<AttributionResult> { ): Promise<AttributionResult> {
const { startDate, endDate, model, type, step, currency } = criteria; const { model, type, currency } = parameters;
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent; const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = type === 'page' ? 'url_path' : 'event_name'; const column = type === 'page' ? 'url_path' : 'event_name';
const { filterQuery, queryParams } = await parseFilters(criteria); const { filterQuery, queryParams } = parseFilters({
...filters,
...parameters,
websiteId,
eventType,
});
function getUTMQuery(utmColumn: string) { function getUTMQuery(utmColumn: string) {
return ` return `
@ -301,7 +319,7 @@ async function clickhouseQuery(
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64} and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and ${column} = {conversionStep:String} and ${column} = {step:String}
and event_type = {eventType:UInt32} and event_type = {eventType:UInt32}
group by 1),`; group by 1),`;
@ -313,7 +331,7 @@ async function clickhouseQuery(
from website_revenue from website_revenue
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64} and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and ${column} = {conversionStep:String} and ${column} = {step:String}
and currency = {currency:String} and currency = {currency:String}
group by 1),`; group by 1),`;
@ -345,7 +363,7 @@ async function clickhouseQuery(
order by 2 desc order by 2 desc
limit 20 limit 20
`, `,
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const paidAdsres = await rawQuery< const paidAdsres = await rawQuery<
@ -376,7 +394,7 @@ async function clickhouseQuery(
order by 2 desc order by 2 desc
limit 20 limit 20
`, `,
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const sourceRes = await rawQuery< const sourceRes = await rawQuery<
@ -390,7 +408,7 @@ async function clickhouseQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_source')} ${getUTMQuery('utm_source')}
`, `,
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const mediumRes = await rawQuery< const mediumRes = await rawQuery<
@ -404,7 +422,7 @@ async function clickhouseQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_medium')} ${getUTMQuery('utm_medium')}
`, `,
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const campaignRes = await rawQuery< const campaignRes = await rawQuery<
@ -418,7 +436,7 @@ async function clickhouseQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_campaign')} ${getUTMQuery('utm_campaign')}
`, `,
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const contentRes = await rawQuery< const contentRes = await rawQuery<
@ -432,7 +450,7 @@ async function clickhouseQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_content')} ${getUTMQuery('utm_content')}
`, `,
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const termRes = await rawQuery< const termRes = await rawQuery<
@ -446,7 +464,7 @@ async function clickhouseQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_term')} ${getUTMQuery('utm_term')}
`, `,
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
); );
const totalRes = await rawQuery<{ pageviews: number; visitors: number; visits: number }>( const totalRes = await rawQuery<{ pageviews: number; visitors: number; visits: number }>(
@ -458,11 +476,11 @@ async function clickhouseQuery(
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64} and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and ${column} = {conversionStep:String} and ${column} = {step:String}
and event_type = {eventType:UInt32} and event_type = {eventType:UInt32}
${filterQuery} ${filterQuery}
`, `,
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, queryParams,
).then(result => result?.[0]); ).then(result => result?.[0]);
return { return {

View file

@ -4,8 +4,19 @@ import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { QueryFilters } from '@/lib/types'; import { QueryFilters } from '@/lib/types';
export interface BreakdownParameters {
startDate: Date;
endDate: Date;
fields: string[];
}
export interface BreakdownData {
x: string;
y: number;
}
export async function getBreakdown( export async function getBreakdown(
...args: [websiteId: string, fields: string[], filters: QueryFilters] ...args: [websiteId: string, parameters: BreakdownParameters, filters: QueryFilters]
) { ) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
@ -15,22 +26,21 @@ export async function getBreakdown(
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
fields: string[], parameters: BreakdownParameters,
filters: QueryFilters, filters: QueryFilters,
): Promise< ): Promise<BreakdownData[]> {
{
x: string;
y: number;
}[]
> {
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma; const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters( const { startDate, endDate, fields } = parameters;
const { filterQuery, joinSessionQuery, queryParams } = parseFilters(
{ {
...filters, ...filters,
websiteId,
startDate,
endDate,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}, },
{ {
joinSession: !!fields.find(name => SESSION_COLUMNS.includes(name)), joinSession: !!fields.find((name: string) => SESSION_COLUMNS.includes(name)),
}, },
); );
@ -70,17 +80,16 @@ async function relationalQuery(
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
fields: string[], parameters: BreakdownParameters,
filters: QueryFilters, filters: QueryFilters,
): Promise< ): Promise<BreakdownData[]> {
{
x: string;
y: number;
}[]
> {
const { parseFilters, rawQuery } = clickhouse; const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({ const { startDate, endDate, fields } = parameters;
const { filterQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId,
startDate,
endDate,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });

View file

@ -1,15 +1,24 @@
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface FunnelCriteria { export interface FunnelParameters {
windowMinutes: number;
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
window: number;
steps: { type: string; value: string }[]; steps: { type: string; value: string }[];
} }
export async function getFunnel(...args: [websiteId: string, criteria: FunnelCriteria]) { export interface FunnelResult {
value: string;
visitors: number;
dropoff: number;
}
export async function getFunnel(
...args: [websiteId: string, parameters: FunnelParameters, filters: QueryFilters]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
@ -18,21 +27,18 @@ export async function getFunnel(...args: [websiteId: string, criteria: FunnelCri
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
criteria: FunnelCriteria, parameters: FunnelParameters,
): Promise< filters: QueryFilters,
{ ): Promise<FunnelResult[]> {
value: string; const { startDate, endDate, window, steps } = parameters;
visitors: number; const { rawQuery, getAddIntervalQuery, parseFilters } = prisma;
dropoff: number; const { levelOneQuery, levelQuery, sumQuery, params } = getFunnelQuery(steps, window);
}[]
> { const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate });
const { windowMinutes, startDate, endDate, steps } = criteria;
const { rawQuery, getAddIntervalQuery } = prisma;
const { levelOneQuery, levelQuery, sumQuery, params } = getFunnelQuery(steps, windowMinutes);
function getFunnelQuery( function getFunnelQuery(
steps: { type: string; value: string }[], steps: { type: string; value: string }[],
windowMinutes: number, window: number,
): { ): {
levelOneQuery: string; levelOneQuery: string;
levelQuery: string; levelQuery: string;
@ -62,6 +68,7 @@ async function relationalQuery(
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}} and created_at between {{startDate}} and {{endDate}}
and ${column} ${operator} {{${i}}} and ${column} ${operator} {{${i}}}
${filterQuery}
)`; )`;
} else { } else {
pv.levelQuery += ` pv.levelQuery += `
@ -73,7 +80,7 @@ async function relationalQuery(
where we.website_id = {{websiteId::uuid}} where we.website_id = {{websiteId::uuid}}
and we.created_at between l.created_at and ${getAddIntervalQuery( and we.created_at between l.created_at and ${getAddIntervalQuery(
`l.created_at `, `l.created_at `,
`${windowMinutes} minute`, `${window} minute`,
)} )}
and we.${column} ${operator} {{${i}}} and we.${column} ${operator} {{${i}}}
and we.created_at <= {{endDate}} and we.created_at <= {{endDate}}
@ -102,17 +109,16 @@ async function relationalQuery(
ORDER BY level; ORDER BY level;
`, `,
{ {
websiteId,
startDate,
endDate,
...params, ...params,
...queryParams,
}, },
).then(formatResults(steps)); ).then(formatResults(steps));
} }
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
criteria: FunnelCriteria, parameters: FunnelParameters,
filters: QueryFilters,
): Promise< ): Promise<
{ {
value: string; value: string;
@ -120,17 +126,17 @@ async function clickhouseQuery(
dropoff: number; dropoff: number;
}[] }[]
> { > {
const { windowMinutes, startDate, endDate, steps } = criteria; const { startDate, endDate, window, steps } = parameters;
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { levelOneQuery, levelQuery, sumQuery, stepFilterQuery, params } = getFunnelQuery( const { levelOneQuery, levelQuery, sumQuery, stepFilterQuery, params } = getFunnelQuery(
steps, steps,
windowMinutes, window,
); );
const { filterQuery, queryParams } = await parseFilters(criteria); const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate });
function getFunnelQuery( function getFunnelQuery(
steps: { type: string; value: string }[], steps: { type: string; value: string }[],
windowMinutes: number, window: number,
): { ): {
levelOneQuery: string; levelOneQuery: string;
levelQuery: string; levelQuery: string;
@ -172,7 +178,7 @@ async function clickhouseQuery(
from level${i} x from level${i} x
join level0 y join level0 y
on x.session_id = y.session_id on x.session_id = y.session_id
where y.created_at between x.created_at and x.created_at + interval ${windowMinutes} minute where y.created_at between x.created_at and x.created_at + interval ${window} minute
and y.${column} ${operator} {param${i}:String} and y.${column} ${operator} {param${i}:String}
)`; )`;
} }
@ -211,9 +217,6 @@ async function clickhouseQuery(
) ORDER BY level; ) ORDER BY level;
`, `,
{ {
websiteId,
startDate,
endDate,
...params, ...params,
...queryParams, ...queryParams,
}, },

View file

@ -3,30 +3,37 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types'; import { QueryFilters } from '@/lib/types';
export interface GoalCriteria { export interface GoalParameters {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
type: string; type: string;
value: string; value: string;
operator?: string; operator?: string;
property?: string; property?: string;
filters: QueryFilters;
} }
export async function getGoal(...args: [websiteId: string, criteria: GoalCriteria]) { export async function getGoal(
...args: [websiteId: string, params: GoalParameters, filters: QueryFilters]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
}); });
} }
async function relationalQuery(websiteId: string, criteria: GoalCriteria) { async function relationalQuery(
const { type, value, filters } = criteria; websiteId: string,
parameters: GoalParameters,
filters: QueryFilters,
) {
const { startDate, endDate, type, value } = parameters;
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterQuery, dateQuery, queryParams } = await parseFilters({ const { filterQuery, dateQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId, websiteId,
value, value,
startDate,
endDate,
}); });
const isPage = type === 'page'; const isPage = type === 'page';
const column = isPage ? 'url_path' : 'event_name'; const column = isPage ? 'url_path' : 'event_name';
@ -53,13 +60,19 @@ async function relationalQuery(websiteId: string, criteria: GoalCriteria) {
); );
} }
async function clickhouseQuery(websiteId: string, criteria: GoalCriteria) { async function clickhouseQuery(
const { type, value, filters } = criteria; websiteId: string,
parameters: GoalParameters,
filters: QueryFilters,
) {
const { startDate, endDate, type, value } = parameters;
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, dateQuery, queryParams } = await parseFilters({ const { filterQuery, dateQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId, websiteId,
value, value,
startDate,
endDate,
}); });
const isPage = type === 'page'; const isPage = type === 'page';
const column = isPage ? 'url_path' : 'event_name'; const column = isPage ? 'url_path' : 'event_name';

View file

@ -1,8 +1,17 @@
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
interface JourneyResult { export interface JourneyParameters {
startDate: Date;
endDate: Date;
steps: number;
startStep?: string;
endStep?: string;
}
export interface JourneyResult {
e1: string; e1: string;
e2: string; e2: string;
e3: string; e3: string;
@ -14,16 +23,7 @@ interface JourneyResult {
} }
export async function getJourney( export async function getJourney(
...args: [ ...args: [websiteId: string, parameters: JourneyParameters, filters: QueryFilters]
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
steps: number;
startStep?: string;
endStep?: string;
},
]
) { ) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
@ -33,21 +33,17 @@ export async function getJourney(
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
filters: { parameters: JourneyParameters,
startDate: Date; filters: QueryFilters,
endDate: Date;
steps: number;
startStep?: string;
endStep?: string;
},
): Promise<JourneyResult[]> { ): Promise<JourneyResult[]> {
const { startDate, endDate, steps, startStep, endStep } = filters; const { startDate, endDate, steps, startStep, endStep } = parameters;
const { rawQuery } = prisma; const { rawQuery, parseFilters } = prisma;
const { sequenceQuery, startStepQuery, endStepQuery, params } = getJourneyQuery( const { sequenceQuery, startStepQuery, endStepQuery, params } = getJourneyQuery(
steps, steps,
startStep, startStep,
endStep, endStep,
); );
const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate });
function getJourneyQuery( function getJourneyQuery(
steps: number, steps: number,
@ -123,6 +119,7 @@ async function relationalQuery(
from website_event from website_event
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}), and created_at between {{startDate}} and {{endDate}}),
${filterQuery}
${sequenceQuery} ${sequenceQuery}
select * select *
from sequences from sequences
@ -133,31 +130,25 @@ async function relationalQuery(
limit 100 limit 100
`, `,
{ {
websiteId,
startDate,
endDate,
...params, ...params,
...queryParams,
}, },
).then(parseResult); ).then(parseResult);
} }
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
filters: { parameters: JourneyParameters,
startDate: Date; filters: QueryFilters,
endDate: Date;
steps: number;
startStep?: string;
endStep?: string;
},
): Promise<JourneyResult[]> { ): Promise<JourneyResult[]> {
const { startDate, endDate, steps, startStep, endStep } = filters; const { startDate, endDate, steps, startStep, endStep } = parameters;
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { sequenceQuery, startStepQuery, endStepQuery, params } = getJourneyQuery( const { sequenceQuery, startStepQuery, endStepQuery, params } = getJourneyQuery(
steps, steps,
startStep, startStep,
endStep, endStep,
); );
const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate });
function getJourneyQuery( function getJourneyQuery(
steps: number, steps: number,
@ -222,8 +213,6 @@ async function clickhouseQuery(
}; };
} }
const { filterQuery, queryParams } = await parseFilters(filters);
return rawQuery( return rawQuery(
` `
WITH events AS ( WITH events AS (
@ -245,9 +234,6 @@ async function clickhouseQuery(
limit 100 limit 100
`, `,
{ {
websiteId,
startDate,
endDate,
...params, ...params,
...queryParams, ...queryParams,
}, },

View file

@ -1,8 +1,9 @@
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface RetentionCriteria { export interface RetentionParameters {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
timezone?: string; timezone?: string;
@ -16,7 +17,9 @@ export interface RetentionResult {
percentage: number; percentage: number;
} }
export async function getRetention(...args: [websiteId: string, criteria: RetentionCriteria]) { export async function getRetention(
...args: [websiteId: string, parameters: RetentionParameters, filters: QueryFilters]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
@ -25,13 +28,20 @@ export async function getRetention(...args: [websiteId: string, criteria: Retent
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
criteria: RetentionCriteria, parameters: RetentionParameters,
filters: QueryFilters,
): Promise<RetentionResult[]> { ): Promise<RetentionResult[]> {
const { startDate, endDate, timezone } = criteria; const { startDate, endDate, timezone } = parameters;
const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery, parseFilters } = prisma; const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery, parseFilters } = prisma;
const unit = 'day'; const unit = 'day';
const { filterQuery, queryParams } = await parseFilters(criteria); const { filterQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
timezone,
});
return rawQuery( return rawQuery(
` `
@ -81,24 +91,26 @@ async function relationalQuery(
on c.cohort_date = s.cohort_date on c.cohort_date = s.cohort_date
where c.day_number <= 31 where c.day_number <= 31
order by 1, 2`, order by 1, 2`,
{ queryParams,
websiteId,
startDate,
endDate,
...queryParams,
},
); );
} }
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
criteria: RetentionCriteria, parameters: RetentionParameters,
filters: QueryFilters,
): Promise<RetentionResult[]> { ): Promise<RetentionResult[]> {
const { startDate, endDate, timezone } = criteria; const { startDate, endDate, timezone } = parameters;
const { getDateSQL, rawQuery, parseFilters } = clickhouse; const { getDateSQL, rawQuery, parseFilters } = clickhouse;
const unit = 'day'; const unit = 'day';
const { filterQuery, queryParams } = await parseFilters(criteria); const { filterQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
timezone,
});
return rawQuery( return rawQuery(
` `
@ -150,11 +162,6 @@ async function clickhouseQuery(
on c.cohort_date = s.cohort_date on c.cohort_date = s.cohort_date
where c.day_number <= 31 where c.day_number <= 31
order by 1, 2`, order by 1, 2`,
{ queryParams,
websiteId,
startDate,
endDate,
...queryParams,
},
); );
} }

View file

@ -1,8 +1,9 @@
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface RevenueCriteria { export interface RevenuParameters {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
unit: string; unit: string;
@ -21,7 +22,9 @@ export interface RevenueResult {
}[]; }[];
} }
export async function getRevenue(...args: [websiteId: string, criteria: RevenueCriteria]) { export async function getRevenue(
...args: [websiteId: string, parameters: RevenuParameters, filters: QueryFilters]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
@ -30,12 +33,12 @@ export async function getRevenue(...args: [websiteId: string, criteria: RevenueC
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
criteria: RevenueCriteria, parameters: RevenuParameters,
filters: QueryFilters,
): Promise<RevenueResult> { ): Promise<RevenueResult> {
const { startDate, endDate, unit = 'day', currency } = criteria; const { startDate, endDate, currency, unit = 'day' } = parameters;
const { getDateSQL, rawQuery } = prisma; const { getDateSQL, rawQuery, parseFilters } = prisma;
const db = getDatabaseType(); const { queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate, currency });
const like = db === POSTGRESQL ? 'ilike' : 'like';
const chart = await rawQuery( const chart = await rawQuery(
` `
@ -50,7 +53,7 @@ async function relationalQuery(
group by x, t group by x, t
order by t order by t
`, `,
{ websiteId, startDate, endDate, unit, currency }, queryParams,
); );
const country = await rawQuery( const country = await rawQuery(
@ -63,10 +66,10 @@ async function relationalQuery(
on s.session_id = r.session_id on s.session_id = r.session_id
where r.website_id = {{websiteId::uuid}} where r.website_id = {{websiteId::uuid}}
and r.created_at between {{startDate}} and {{endDate}} and r.created_at between {{startDate}} and {{endDate}}
and r.currency ${like} {{currency}} and r.currency = {{currency}}
group by s.country group by s.country
`, `,
{ websiteId, startDate, endDate, currency }, queryParams,
); );
const total = await rawQuery( const total = await rawQuery(
@ -78,9 +81,9 @@ async function relationalQuery(
from revenue r from revenue r
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}} and created_at between {{startDate}} and {{endDate}}
and currency ${like} {{currency}} and currency = {{currency}}
`, `,
{ websiteId, startDate, endDate, currency }, queryParams,
).then(result => result?.[0]); ).then(result => result?.[0]);
total.average = total.count > 0 ? total.sum / total.count : 0; total.average = total.count > 0 ? total.sum / total.count : 0;
@ -98,7 +101,7 @@ async function relationalQuery(
group by currency group by currency
order by sum desc order by sum desc
`, `,
{ websiteId, startDate, endDate, unit, currency }, queryParams,
); );
return { chart, country, table, total }; return { chart, country, table, total };
@ -106,10 +109,18 @@ async function relationalQuery(
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
criteria: RevenueCriteria, parameters: RevenuParameters,
filters: QueryFilters,
): Promise<RevenueResult> { ): Promise<RevenueResult> {
const { startDate, endDate, unit = 'day', currency } = criteria; const { startDate, endDate, unit = 'day', currency } = parameters;
const { getDateSQL, rawQuery } = clickhouse; const { getDateSQL, rawQuery, parseFilters } = clickhouse;
const { queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
currency,
});
const chart = await rawQuery< const chart = await rawQuery<
{ {
@ -130,7 +141,7 @@ async function clickhouseQuery(
group by x, t group by x, t
order by t order by t
`, `,
{ websiteId, startDate, endDate, unit, currency }, queryParams,
); );
const country = await rawQuery< const country = await rawQuery<
@ -144,9 +155,11 @@ async function clickhouseQuery(
s.country as name, s.country as name,
sum(w.revenue) as value sum(w.revenue) as value
from website_revenue w from website_revenue w
join (select distinct website_id, session_id, country join (
from website_event select distinct website_id, session_id, country
where website_id = {websiteId:UUID}) s from website_event
where website_id = {websiteId:UUID}
) s
on w.website_id = s.website_id on w.website_id = s.website_id
and w.session_id = s.session_id and w.session_id = s.session_id
where w.website_id = {websiteId:UUID} where w.website_id = {websiteId:UUID}
@ -154,7 +167,7 @@ async function clickhouseQuery(
and w.currency = {currency:String} and w.currency = {currency:String}
group by s.country group by s.country
`, `,
{ websiteId, startDate, endDate, currency }, queryParams,
); );
const total = await rawQuery<{ const total = await rawQuery<{
@ -172,7 +185,7 @@ async function clickhouseQuery(
and created_at between {startDate:DateTime64} and {endDate:DateTime64} and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and currency = {currency:String} and currency = {currency:String}
`, `,
{ websiteId, startDate, endDate, currency }, queryParams,
).then(result => result?.[0]); ).then(result => result?.[0]);
total.average = total.count > 0 ? total.sum / total.count : 0; total.average = total.count > 0 ? total.sum / total.count : 0;
@ -197,7 +210,7 @@ async function clickhouseQuery(
group by currency group by currency
order by sum desc order by sum desc
`, `,
{ websiteId, startDate, endDate, unit, currency }, queryParams,
); );
return { chart, country, table, total }; return { chart, country, table, total };

View file

@ -1,23 +1,30 @@
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface UTMCriteria { export interface UTMParameters {
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
} }
export async function getUTM(...args: [websiteId: string, criteria: UTMCriteria]) { export async function getUTM(
...args: [websiteId: string, parameters: UTMParameters, filters: QueryFilters]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
}); });
} }
async function relationalQuery(websiteId: string, criteria: UTMCriteria) { async function relationalQuery(
const { startDate, endDate } = criteria; websiteId: string,
parameters: UTMParameters,
filters: QueryFilters,
) {
const { startDate, endDate } = parameters;
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterQuery, queryParams } = await parseFilters(criteria); const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate });
return rawQuery( return rawQuery(
` `
@ -30,19 +37,18 @@ async function relationalQuery(websiteId: string, criteria: UTMCriteria) {
${filterQuery} ${filterQuery}
group by 1 group by 1
`, `,
{ queryParams,
...queryParams,
websiteId,
startDate,
endDate,
},
).then(result => parseParameters(result as any[])); ).then(result => parseParameters(result as any[]));
} }
async function clickhouseQuery(websiteId: string, criteria: UTMCriteria) { async function clickhouseQuery(
const { startDate, endDate } = criteria; websiteId: string,
parameters: UTMParameters,
filters: QueryFilters,
) {
const { startDate, endDate } = parameters;
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters(criteria); const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId, startDate, endDate });
return rawQuery( return rawQuery(
` `
@ -55,12 +61,7 @@ async function clickhouseQuery(websiteId: string, criteria: UTMCriteria) {
${filterQuery} ${filterQuery}
group by 1 group by 1
`, `,
{ queryParams,
...queryParams,
websiteId,
startDate,
endDate,
},
).then(result => parseParameters(result as any[])); ).then(result => parseParameters(result as any[]));
} }

View file

@ -17,7 +17,7 @@ async function relationalQuery(
filters: QueryFilters & { propertyName?: string }, filters: QueryFilters & { propertyName?: string },
) { ) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterQuery, queryParams } = await parseFilters(filters, { const { filterQuery, queryParams } = parseFilters(filters, {
columns: { propertyName: 'data_key' }, columns: { propertyName: 'data_key' },
}); });
@ -45,7 +45,7 @@ async function clickhouseQuery(
filters: QueryFilters & { propertyName?: string }, filters: QueryFilters & { propertyName?: string },
): Promise<{ propertyName: string; total: number }[]> { ): Promise<{ propertyName: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters(filters, { const { filterQuery, queryParams } = parseFilters(filters, {
columns: { propertyName: 'data_key' }, columns: { propertyName: 'data_key' },
}); });

View file

@ -17,7 +17,7 @@ async function relationalQuery(
filters: QueryFilters & { propertyName?: string }, filters: QueryFilters & { propertyName?: string },
) { ) {
const { rawQuery, parseFilters, getDateSQL } = prisma; const { rawQuery, parseFilters, getDateSQL } = prisma;
const { filterQuery, queryParams } = await parseFilters(filters); const { filterQuery, queryParams } = parseFilters(filters);
return rawQuery( return rawQuery(
` `
@ -48,7 +48,7 @@ async function clickhouseQuery(
filters: QueryFilters & { propertyName?: string }, filters: QueryFilters & { propertyName?: string },
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> { ): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters(filters); const { filterQuery, queryParams } = parseFilters(filters);
return rawQuery( return rawQuery(
` `

View file

@ -4,14 +4,14 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types'; import { QueryFilters } from '@/lib/types';
export interface SessionMetricsParameters {
type: string;
limit: number | string;
offset: number | string;
}
export async function getSessionMetrics( export async function getSessionMetrics(
...args: [ ...args: [websiteId: string, parameters: SessionMetricsParameters, filters: QueryFilters]
websiteId: string,
type: string,
filters: QueryFilters,
limit?: number | string,
offset?: number | string,
]
) { ) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
@ -21,16 +21,16 @@ export async function getSessionMetrics(
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
type: string, parameters: SessionMetricsParameters,
filters: QueryFilters, filters: QueryFilters,
limit: number | string = 500,
offset: number | string = 0,
) { ) {
const { type, limit = 500, offset = 0 } = parameters;
const column = FILTER_COLUMNS[type] || type; const column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = prisma; const { parseFilters, rawQuery } = prisma;
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters( const { filterQuery, joinSessionQuery, queryParams } = parseFilters(
{ {
...filters, ...filters,
websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}, },
{ {
@ -57,21 +57,21 @@ async function relationalQuery(
limit ${limit} limit ${limit}
offset ${offset} offset ${offset}
`, `,
queryParams, { ...queryParams, ...parameters },
); );
} }
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
type: string, parameters: SessionMetricsParameters,
filters: QueryFilters, filters: QueryFilters,
limit: number | string = 500,
offset: number | string = 0,
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const { type, limit = 500, offset = 0 } = parameters;
const column = FILTER_COLUMNS[type] || type; const column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = clickhouse; const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({ const { filterQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });
const includeCountry = column === 'city' || column === 'region'; const includeCountry = column === 'city' || column === 'region';
@ -114,5 +114,5 @@ async function clickhouseQuery(
`; `;
} }
return rawQuery(sql, queryParams); return rawQuery(sql, { ...queryParams, ...parameters });
} }

View file

@ -14,8 +14,9 @@ export async function getSessionStats(...args: [websiteId: string, filters: Quer
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { getDateSQL, parseFilters, rawQuery } = prisma; const { getDateSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({ const { filterQuery, joinSessionQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });
@ -43,8 +44,9 @@ async function clickhouseQuery(
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateSQL } = clickhouse; const { parseFilters, rawQuery, getDateSQL } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({ const { filterQuery, queryParams } = parseFilters({
...filters, ...filters,
websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });

View file

@ -25,7 +25,7 @@ async function relationalQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<WebsiteSessionStatsData[]> { ): Promise<WebsiteSessionStatsData[]> {
const { parseFilters, rawQuery } = prisma; const { parseFilters, rawQuery } = prisma;
const { filterQuery, queryParams } = await parseFilters(filters); const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId });
return rawQuery( return rawQuery(
` `
@ -50,7 +50,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<WebsiteSessionStatsData[]> { ): Promise<WebsiteSessionStatsData[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters(filters); const { filterQuery, queryParams } = parseFilters({ ...filters, websiteId });
return rawQuery( return rawQuery(
` `

View file

@ -12,7 +12,14 @@ export async function getWebsiteSessions(...args: [websiteId: string, filters: Q
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters } = prisma; const { pagedRawQuery, parseFilters } = prisma;
const { filterQuery, dateQuery, queryParams } = await parseFilters(filters); const { search } = filters;
const { filterQuery, dateQuery, queryParams } = parseFilters({
...filters,
websiteId,
search: search ? `%${search}%` : undefined,
});
const searchQuery = search ? `and session.distinct_id ilike {{search}}` : '';
return pagedRawQuery( return pagedRawQuery(
` `
@ -38,6 +45,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
where website_event.website_id = {{websiteId::uuid}} where website_event.website_id = {{websiteId::uuid}}
${dateQuery} ${dateQuery}
${filterQuery} ${filterQuery}
${searchQuery}
group by session.session_id, group by session.session_id,
session.website_id, session.website_id,
website_event.hostname, website_event.hostname,
@ -58,7 +66,13 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters, getDateStringSQL } = clickhouse; const { pagedRawQuery, parseFilters, getDateStringSQL } = clickhouse;
const { filterQuery, dateQuery, queryParams } = await parseFilters(filters); const { search } = filters;
const { filterQuery, dateQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
const searchQuery = search ? `and positionCaseInsensitive(distinct_id, {search:String}) > 0` : '';
return pagedRawQuery( return pagedRawQuery(
` `
@ -83,6 +97,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
${dateQuery} ${dateQuery}
${filterQuery} ${filterQuery}
${searchQuery}
group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city
order by lastAt desc order by lastAt desc
`, `,

View file

@ -15,7 +15,7 @@ export async function getWebsiteSessionsWeekly(
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc' } = filters; const { timezone = 'utc' } = filters;
const { rawQuery, getDateWeeklySQL, parseFilters } = prisma; const { rawQuery, getDateWeeklySQL, parseFilters } = prisma;
const { queryParams } = await parseFilters(filters); const { queryParams } = parseFilters(filters);
return rawQuery( return rawQuery(
` `
@ -35,7 +35,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc' } = filters; const { timezone = 'utc' } = filters;
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { queryParams } = await parseFilters(filters); const { queryParams } = parseFilters(filters);
return rawQuery( return rawQuery(
` `