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 { ReactNode } from 'react';
export function UsersDataTable({
showActions,
children,
}: {
showActions?: boolean;
children?: ReactNode;
}) {
export function UsersDataTable({ showActions }: { showActions?: boolean; children?: ReactNode }) {
const queryResult = useUsersQuery();
return (
<DataGrid queryResult={queryResult} renderEmpty={() => children}>
<DataGrid queryResult={queryResult} allowSearch={true}>
{({ data }) => <UsersTable data={data} showActions={showActions} />}
</DataGrid>
);

View file

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

View file

@ -17,43 +17,43 @@ export function WebsiteMetricsBar({
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
const isAllTime = dateRange.value === 'all';
const { pageviews, visitors, visits, bounces, totaltime, previous } = data || {};
const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};
const metrics = data
? [
{
value: visitors,
label: formatMessage(labels.visitors),
change: visitors - previous.visitors,
change: visitors - comparison.visitors,
formatValue: formatLongNumber,
},
{
value: visits,
label: formatMessage(labels.visits),
change: visits - previous.visits,
change: visits - comparison.visits,
formatValue: formatLongNumber,
},
{
value: pageviews,
label: formatMessage(labels.views),
change: pageviews - previous.pageviews,
change: pageviews - comparison.pageviews,
formatValue: formatLongNumber,
},
{
label: formatMessage(labels.bounceRate),
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:
(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) + '%',
reverseColors: true,
},
{
label: formatMessage(labels.visitDuration),
value: totaltime / visits,
prev: previous.totaltime / previous.visits,
change: totaltime / visits - previous.totaltime / previous.visits,
prev: comparison.totaltime / comparison.visits,
change: totaltime / visits - comparison.totaltime / comparison.visits,
formatValue: n =>
`${+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 { DataGrid } from '@/components/common/DataGrid';
import { ReactNode } from 'react';
import { FilterButtons } from '@/components/common/FilterButtons';
export function EventsDataTable({
websiteId,
@ -10,10 +12,37 @@ export function EventsDataTable({
teamId?: string;
children?: ReactNode;
}) {
const { formatMessage, labels } = useMessages();
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 (
<DataGrid queryResult={queryResult} allowSearch={true} autoFocus={false}>
<DataGrid
queryResult={queryResult}
allowSearch={true}
autoFocus={false}
allowPaging={true}
renderActions={renderActions}
>
{({ data }) => <EventsTable data={data} />}
</DataGrid>
);

View file

@ -5,6 +5,7 @@ import { Avatar } from '@/components/common/Avatar';
import Link from 'next/link';
import { Bolt, Eye } from '@/components/icons';
import { DateDistance } from '@/components/common/DateDistance';
import { TypeIcon } from '@/components/common/TypeIcon';
export function EventsTable({ data = [] }) {
const { formatMessage, labels } = useMessages();
@ -16,13 +17,6 @@ export function EventsTable({ data = [] }) {
return (
<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">
{(row: any) => {
return (
@ -34,8 +28,21 @@ export function EventsTable({ data = [] }) {
);
}}
</DataColumn>
<DataColumn id="created" label={formatMessage(labels.created)} width="200px">
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
<DataColumn id="created" width="1fr" align="end">
{(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>
</DataTable>
);

View file

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

View file

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

View file

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

View file

@ -17,15 +17,11 @@ type FunnelResult = {
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 { data, error, isLoading } = useResultQuery<any>(type, {
const { data, error, isLoading } = useResultQuery(type, {
websiteId,
dateRange: {
startDate,
endDate,
},
parameters,
...parameters,
});
return (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { z } from 'zod';
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 { getCompareDate } from '@/lib/date';
import { unauthorized, json } from '@/lib/response';
@ -27,7 +27,7 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters({ ...query, websiteId });
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
const [pageviews, sessions] = await Promise.all([
getPageviewStats(websiteId, filters),

View file

@ -1,5 +1,5 @@
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 { getSessionDataValues } from '@/queries';
import { z } from 'zod';
@ -22,7 +22,7 @@ export async function GET(
const { propertyName } = query;
const { websiteId } = await params;
const filters = await getQueryFilters({ ...query, websiteId });
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();

View file

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

View file

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

View file

@ -1,9 +1,10 @@
import { z } from 'zod';
import { parseRequest, getQueryFilters } from '@/lib/request';
import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { filterParams } from '@/lib/schema';
import { getWebsiteStats } from '@/queries';
import { getCompareDate } from '@/lib/date';
export async function GET(
request: Request,
@ -28,15 +29,17 @@ export async function GET(
return unauthorized();
}
const filters = await getQueryFilters({ ...query, websiteId });
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
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,
startDate: filters.compareStartDate,
endDate: filters.compareEndDate,
startDate,
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;
export interface DataTableProps {
export interface DataGridProps {
queryResult: any;
searchDelay?: number;
allowSearch?: boolean;
allowPaging?: boolean;
autoFocus?: boolean;
renderEmpty?: () => ReactNode;
renderActions?: () => ReactNode;
children: ReactNode | ((data: any) => ReactNode);
}
@ -20,12 +20,13 @@ export function DataGrid({
queryResult,
searchDelay = 600,
allowSearch,
allowPaging,
allowPaging = true,
autoFocus,
renderActions,
children,
}: DataTableProps) {
}: DataGridProps) {
const { formatMessage, labels } = useMessages();
const { data, error, isLoading, isFetching, setParams } = queryResult || {};
const { data, error, isLoading, isFetching, setParams } = queryResult;
const { router, updateParams } = useNavigation();
const [search, setSearch] = useState('');
@ -43,29 +44,33 @@ export function DataGrid({
return (
<Column gap="4" minHeight="300px">
{allowSearch && (data || search) && (
<Row width="280px" alignItems="center">
<Row alignItems="center" justifyContent="space-between">
<SearchField
value={search}
onSearch={handleSearch}
delay={searchDelay || DEFAULT_SEARCH_DELAY}
autoFocus={autoFocus}
placeholder={formatMessage(labels.search)}
style={{ width: '280px' }}
/>
{renderActions?.()}
</Row>
)}
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}>
<Column>
{data ? (typeof children === 'function' ? children(data) : children) : null}
</Column>
{allowPaging && data && (
<Row marginTop="6">
<Pager
page={data.page}
pageSize={data.pageSize}
count={data.count}
onPageChange={handlePageChange}
/>
</Row>
{data && (
<>
<Column>{typeof children === 'function' ? children(data) : children}</Column>
{allowPaging && data && (
<Row marginTop="6">
<Pager
page={data.page}
pageSize={data.pageSize}
count={data.count}
onPageChange={handlePageChange}
/>
</Row>
)}
</>
)}
</LoadingPanel>
</Column>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,15 @@
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>) {
const { get, useQuery } = useApi();
const params = useFilterParams(websiteId);
const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery({
queryKey: ['sessions:stats', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/sessions/stats`, { ...params }),
queryKey: ['sessions:stats', { websiteId, ...date, ...filters }],
queryFn: () => get(`/websites/${websiteId}/sessions/stats`, { ...date, ...filters }),
enabled: !!websiteId,
...options,
});

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import { UseQueryOptions } from '@tanstack/react-query';
import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams';
import { useFilterParameters } from '../useFilterParameters';
import { useDateParameters } from '@/components/hooks/useDateParameters';
export interface WebsiteStatsData {
pageviews: number;
@ -8,7 +9,7 @@ export interface WebsiteStatsData {
visits: number;
bounces: number;
totaltime: number;
previous: {
comparison: {
pageviews: number;
visitors: number;
visits: number;
@ -22,11 +23,12 @@ export function useWebsiteStatsQuery(
options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>,
) {
const { get, useQuery } = useApi();
const filterParams = useFilterParams(websiteId);
const date = useDateParameters(websiteId);
const filters = useFilterParameters();
return useQuery<WebsiteStatsData>({
queryKey: ['websites:stats', { websiteId, ...filterParams }],
queryFn: () => get(`/websites/${websiteId}/stats`, { ...filterParams }),
queryKey: ['websites:stats', { websiteId, ...date, ...filters }],
queryFn: () => get(`/websites/${websiteId}/stats`, { ...date, ...filters }),
enabled: !!websiteId,
...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 { useMemo } from 'react';
export function useDateRange(websiteId?: string) {
export interface UseDateRangeOptions {
ignoreOffset?: boolean;
}
export function useDateRange(websiteId?: string, options: UseDateRangeOptions = {}) {
const { get } = useApi();
const { locale } = useLocale();
const {
@ -16,14 +20,16 @@ export function useDateRange(websiteId?: string) {
} = useNavigation();
const websiteConfig = useWebsites(state => state[websiteId]?.dateRange);
const globalConfig = useApp(state => state.dateRangeValue);
const dateRangeObject = parseDateRange(
date || websiteConfig?.value || globalConfig || DEFAULT_DATE_RANGE_VALUE,
locale,
);
const dateRange = useMemo(
() => (offset ? getOffsetDateRange(dateRangeObject, +offset) : dateRangeObject),
[date, offset, websiteConfig],
);
const dateValue = date || websiteConfig?.value || globalConfig || DEFAULT_DATE_RANGE_VALUE;
const dateRange = useMemo(() => {
const dateRangeObject = parseDateRange(dateValue, locale);
return !options.ignoreOffset && offset
? getOffsetDateRange(dateRangeObject, +offset)
: dateRangeObject;
}, [date, offset, dateValue, options]);
const dateCompare = useWebsites(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE);
const saveDateRange = async (value: string) => {

View file

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

View file

@ -46,7 +46,12 @@ export function MetricsTable({
const { data, isLoading, isFetching, error } = useWebsiteMetricsQuery(
websiteId,
{ type, limit, search: searchFormattedValues ? undefined : search, ...params },
{
type,
limit,
search: searchFormattedValues ? undefined : search,
...params,
},
{
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 {
filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters),

View file

@ -16,7 +16,8 @@ const enabled = Boolean(process.env.KAFKA_URL && process.env.KAFKA_BROKER);
function getClient() {
const { username, password } = new URL(process.env.KAFKA_URL);
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 } =
username && password

View file

@ -37,6 +37,7 @@ export function filtersToArray(filters: QueryFilters, options: QueryOptions = {}
column: options?.columns?.[key] ?? FILTER_COLUMNS[key],
operator,
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 {
const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like';
return `and ${column} ${like} {{${param}}}`;
return `and ${column} ilike {{${param}}}`;
}
function mapFilter(column: string, operator: string, name: string, type: string = '') {
const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like';
const value = `{{${name}${type ? `::${type}` : ''}}}`;
switch (operator) {
@ -101,28 +96,31 @@ function mapFilter(column: string, operator: string, name: string, type: string
case OPERATORS.notEquals:
return `${column} != ${value}`;
case OPERATORS.contains:
return `${column} ${like} ${value}`;
return `${column} ilike ${value}`;
case OPERATORS.doesNotContain:
return `${column} not ${like} ${value}`;
return `${column} not ilike ${value}`;
default:
return '';
}
}
function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}): string {
const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => {
if (column) {
arr.push(`and ${mapFilter(column, operator, name)}`);
const query = filtersToArray(filters, options).reduce(
(arr, { name, column, operator, prefix = '' }) => {
if (column) {
arr.push(`and ${mapFilter(`${prefix}${column}`, operator, name)}`);
if (name === 'referrer') {
arr.push(
`and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`,
);
if (name === 'referrer') {
arr.push(
`and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`,
);
}
}
}
return arr;
}, []);
return arr;
},
[],
);
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 =>
['referrer', ...SESSION_COLUMNS].includes(key),
);

View file

@ -1,7 +1,7 @@
import { z } from 'zod/v4';
import { FILTER_COLUMNS, DEFAULT_PAGE_SIZE } from '@/lib/constants';
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 { fetchWebsite } from '@/lib/load';
import { QueryFilters } from '@/lib/types';
@ -50,23 +50,14 @@ export async function getJsonBody(request: Request) {
}
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 endDate = new Date(+endAt);
const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
compare,
startDate,
endDate,
);
return {
startDate,
endDate,
compare,
compareStartDate,
compareEndDate,
timezone,
unit: getAllowedUnits(startDate, endDate).includes(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 filters = getRequestFilters(params);
const data = {
return {
...dateRange,
...filters,
page: params?.page,
@ -98,17 +99,5 @@ export async function getQueryFilters(params: Record<string, any>): Promise<Quer
orderBy: params?.orderBy,
sortDescending: params?.sortDescending,
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 = {
startAt: z.coerce.number(),
endAt: z.coerce.number(),
startAt: z.coerce.number().optional(),
endAt: z.coerce.number().optional(),
startDate: z.coerce.date().optional(),
endDate: z.coerce.date().optional(),
timezone: timezoneParam.optional(),
unit: unitParam.optional(),
compare: z.string().optional(),
@ -33,12 +35,12 @@ export const filterParams = {
hostname: z.string().optional(),
language: z.string().optional(),
event: z.string().optional(),
search: z.string().optional(),
};
export const pagingParams = {
page: z.coerce.number().int().positive().optional(),
pageSize: z.coerce.number().int().positive().optional(),
search: z.string().optional(),
};
export const sortingParams = {
@ -93,23 +95,63 @@ export const reportTypeParam = z.enum([
'utm',
]);
export const reportParms = {
websiteId: z.string().uuid(),
dateRange: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
timezone: timezoneParam.optional(),
unit: unitParam.optional(),
compare: z.string().optional(),
compareStartDate: z.coerce.date().optional(),
compareEndDate: z.coerce.date().optional(),
}),
};
export const dateRangeSchema = z.object({ ...dateRangeParams }).superRefine((data, ctx) => {
const hasTimestamps = data.startAt !== undefined && data.endAt !== undefined;
const hasDates = data.startDate !== undefined && data.endDate !== undefined;
if (!hasTimestamps && !hasDates) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'You must provide either startAt & endAt or startDate & endDate.',
});
}
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({
type: z.literal('goal'),
parameters: z
.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
type: z.string(),
value: z.string(),
operator: z.enum(['count', 'sum', 'average']).optional(),
@ -126,6 +168,8 @@ export const goalReportSchema = z.object({
export const funnelReportSchema = z.object({
type: z.literal('funnel'),
parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
window: z.coerce.number().positive(),
steps: z
.array(
@ -142,6 +186,8 @@ export const funnelReportSchema = z.object({
export const journeyReportSchema = z.object({
type: z.literal('journey'),
parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
steps: z.coerce.number().min(2).max(7),
startStep: z.string().optional(),
endStep: z.string().optional(),
@ -150,15 +196,27 @@ export const journeyReportSchema = z.object({
export const retentionReportSchema = z.object({
type: z.literal('retention'),
parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
timezone: z.string().optional(),
}),
});
export const utmReportSchema = z.object({
type: z.literal('utm'),
parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
}),
});
export const revenueReportSchema = z.object({
type: z.literal('revenue'),
parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
timezone: z.string().optional(),
currency: z.string(),
}),
});
@ -166,6 +224,8 @@ export const revenueReportSchema = z.object({
export const attributionReportSchema = z.object({
type: z.literal('attribution'),
parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
model: z.enum(['first-click', 'last-click']),
type: z.enum(['page', 'event']),
step: z.string(),
@ -176,6 +236,8 @@ export const attributionReportSchema = z.object({
export const breakdownReportSchema = z.object({
type: z.literal('breakdown'),
parameters: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
fields: z.array(fieldsParam),
}),
});
@ -202,7 +264,7 @@ export const reportSchema = z.intersection(reportBaseSchema, reportTypeSchema);
export const reportResultSchema = z.intersection(
z.object({
...reportParms,
websiteId: z.string().uuid(),
filters: z.object({ ...filterParams }),
}),
reportTypeSchema,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,8 +22,9 @@ export async function getEventMetrics(
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = prisma;
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({
const { filterQuery, joinSessionQuery, queryParams } = parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.customEvent,
});
@ -49,11 +50,12 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(
websiteId: string,
filters: QueryFilters,
): Promise<{ x: string; t: string; y: number }[]> {
): Promise<WebsiteEventMetricData[]> {
const { timezone = 'UTC', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({
const { filterQuery, queryParams } = parseFilters({
...filters,
websiteId,
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) {
const { pagedRawQuery, parseFilters } = prisma;
const { search } = filters;
const { filterQuery, queryParams } = await parseFilters({
const { filterQuery, dateQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
@ -40,7 +40,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
event_name as "eventName"
from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
${dateQuery}
${filterQuery}
${searchQuery}
order by created_at desc
@ -52,7 +52,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters } = clickhouse;
const { queryParams, dateQuery, filterQuery } = await parseFilters({
const { queryParams, dateQuery, filterQuery } = parseFilters({
...filters,
websiteId,
});
@ -74,6 +74,10 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
referrer_path as referrerPath,
referrer_query as referrerQuery,
referrer_domain as referrerDomain,
country as country,
device as device,
os as os,
browser as browser,
page_title as pageTitle,
event_type as eventType,
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) {
const { rawQuery, parseFilters } = prisma;
const { queryParams, filterQuery, dateQuery } = await parseFilters(filters);
const { queryParams, filterQuery, dateQuery } = parseFilters(filters);
return rawQuery(
`
@ -36,7 +36,7 @@ async function clickhouseQuery(
filters: QueryFilters,
): Promise<{ x: string; y: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { queryParams, filterQuery, dateQuery } = await parseFilters(filters);
const { queryParams, filterQuery, dateQuery } = parseFilters(filters);
const sql = `
select

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,19 @@ import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
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(
...args: [websiteId: string, fields: string[], filters: QueryFilters]
...args: [websiteId: string, parameters: BreakdownParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@ -15,22 +26,21 @@ export async function getBreakdown(
async function relationalQuery(
websiteId: string,
fields: string[],
parameters: BreakdownParameters,
filters: QueryFilters,
): Promise<
{
x: string;
y: number;
}[]
> {
): Promise<BreakdownData[]> {
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters(
const { startDate, endDate, fields } = parameters;
const { filterQuery, joinSessionQuery, queryParams } = parseFilters(
{
...filters,
websiteId,
startDate,
endDate,
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(
websiteId: string,
fields: string[],
parameters: BreakdownParameters,
filters: QueryFilters,
): Promise<
{
x: string;
y: number;
}[]
> {
): Promise<BreakdownData[]> {
const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({
const { startDate, endDate, fields } = parameters;
const { filterQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
eventType: EVENT_TYPE.pageView,
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,14 +4,14 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface SessionMetricsParameters {
type: string;
limit: number | string;
offset: number | string;
}
export async function getSessionMetrics(
...args: [
websiteId: string,
type: string,
filters: QueryFilters,
limit?: number | string,
offset?: number | string,
]
...args: [websiteId: string, parameters: SessionMetricsParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@ -21,16 +21,16 @@ export async function getSessionMetrics(
async function relationalQuery(
websiteId: string,
type: string,
parameters: SessionMetricsParameters,
filters: QueryFilters,
limit: number | string = 500,
offset: number | string = 0,
) {
const { type, limit = 500, offset = 0 } = parameters;
const column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = prisma;
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters(
const { filterQuery, joinSessionQuery, queryParams } = parseFilters(
{
...filters,
websiteId,
eventType: EVENT_TYPE.pageView,
},
{
@ -57,21 +57,21 @@ async function relationalQuery(
limit ${limit}
offset ${offset}
`,
queryParams,
{ ...queryParams, ...parameters },
);
}
async function clickhouseQuery(
websiteId: string,
type: string,
parameters: SessionMetricsParameters,
filters: QueryFilters,
limit: number | string = 500,
offset: number | string = 0,
): Promise<{ x: string; y: number }[]> {
const { type, limit = 500, offset = 0 } = parameters;
const column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({
const { filterQuery, queryParams } = parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.pageView,
});
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) {
const { timezone = 'utc', unit = 'day' } = filters;
const { getDateSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({
const { filterQuery, joinSessionQuery, queryParams } = parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.pageView,
});
@ -43,8 +44,9 @@ async function clickhouseQuery(
): Promise<{ x: string; y: number }[]> {
const { timezone = 'utc', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateSQL } = clickhouse;
const { filterQuery, queryParams } = await parseFilters({
const { filterQuery, queryParams } = parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.pageView,
});

View file

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

View file

@ -12,7 +12,14 @@ export async function getWebsiteSessions(...args: [websiteId: string, filters: Q
async function relationalQuery(websiteId: string, filters: QueryFilters) {
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(
`
@ -38,6 +45,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
where website_event.website_id = {{websiteId::uuid}}
${dateQuery}
${filterQuery}
${searchQuery}
group by session.session_id,
session.website_id,
website_event.hostname,
@ -58,7 +66,13 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
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(
`
@ -83,6 +97,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
where website_id = {websiteId:UUID}
${dateQuery}
${filterQuery}
${searchQuery}
group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city
order by lastAt desc
`,

View file

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