Refactor filter handling for queries.

This commit is contained in:
Mike Cao 2025-07-02 01:44:12 -07:00
parent 5b300f1ff5
commit ee6c68d27c
107 changed files with 731 additions and 835 deletions

View file

@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
db-type: [postgresql, mysql]
db-type: [postgresql]
steps:
- uses: actions/checkout@v3
@ -55,4 +55,4 @@ jobs:
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
password: ${{ secrets.DOCKER_PASSWORD }}

View file

@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
db-type: [postgresql, mysql]
db-type: [postgresql]
steps:
- uses: actions/checkout@v3
@ -47,4 +47,4 @@ jobs:
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
password: ${{ secrets.DOCKER_PASSWORD }}

View file

@ -2,6 +2,7 @@
import { Button, Grid, Column, Heading } from '@umami/react-zen';
import Link from 'next/link';
import Script from 'next/script';
import { Panel } from '@/components/common/Panel';
import { PageBody } from '@/components/common/PageBody';
import { EventsChart } from '@/components/metrics/EventsChart';
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
@ -115,87 +116,91 @@ export function TestConsolePage({ websiteId }: { websiteId: string }) {
src={`${process.env.basePath || ''}/script.js`}
data-cache="true"
/>
<Grid columns="1fr 1fr 1fr" gap>
<Column gap>
<Heading>Page links</Heading>
<div>
<Link href={`/console/${websiteId}?page=1`}>page one</Link>
</div>
<div>
<Link href={`/console/${websiteId}?page=2 `}>page two</Link>
</div>
<div>
<a href="https://www.google.com" data-umami-event="external-link-direct">
external link (direct)
</a>
</div>
<div>
<a
href="https://www.google.com"
data-umami-event="external-link-tab"
target="_blank"
rel="noreferrer"
>
external link (tab)
</a>
</div>
</Column>
<Column gap>
<Heading>Click events</Heading>
<Button id="send-event-button" data-umami-event="button-click" variant="primary">
Send event
</Button>
<Button
id="send-event-data-button"
data-umami-event="button-click"
data-umami-event-name="bob"
data-umami-event-id="123"
variant="primary"
>
Send event with data
</Button>
<Button
id="generate-revenue-button"
data-umami-event="checkout-cart"
data-umami-event-revenue={(Math.random() * 10000).toFixed(2).toString()}
data-umami-event-currency="USD"
variant="primary"
>
Generate revenue data
</Button>
<Button
id="button-with-div-button"
data-umami-event="button-click"
data-umami-event-name={'bob'}
data-umami-event-id="123"
variant="primary"
>
<div>Button with div</div>
</Button>
<div data-umami-event="div-click">DIV with attribute</div>
<div data-umami-event="div-click-one">
<div data-umami-event="div-click-two">
<div data-umami-event="div-click-three">Nested DIV</div>
<Panel>
<Grid columns="1fr 1fr 1fr" gap>
<Column gap>
<Heading>Page links</Heading>
<div>
<Link href={`/console/${websiteId}?page=1`}>page one</Link>
</div>
</div>
</Column>
<Column gap>
<Heading>Javascript events</Heading>
<Button id="manual-button" variant="primary" onClick={handleRunScript}>
Run script
</Button>
<Button id="manual-button" variant="primary" onClick={handleRunIdentify}>
Run identify
</Button>
<Button id="manual-button" variant="primary" onClick={handleRunRevenue}>
Revenue script
</Button>
</Column>
</Grid>
<div>
<Link href={`/console/${websiteId}?page=2 `}>page two</Link>
</div>
<div>
<a href="https://www.google.com" data-umami-event="external-link-direct">
external link (direct)
</a>
</div>
<div>
<a
href="https://www.google.com"
data-umami-event="external-link-tab"
target="_blank"
rel="noreferrer"
>
external link (tab)
</a>
</div>
</Column>
<Column gap>
<Heading>Click events</Heading>
<Button id="send-event-button" data-umami-event="button-click" variant="primary">
Send event
</Button>
<Button
id="send-event-data-button"
data-umami-event="button-click"
data-umami-event-name="bob"
data-umami-event-id="123"
variant="primary"
>
Send event with data
</Button>
<Button
id="generate-revenue-button"
data-umami-event="checkout-cart"
data-umami-event-revenue={(Math.random() * 10000).toFixed(2).toString()}
data-umami-event-currency="USD"
variant="primary"
>
Generate revenue data
</Button>
<Button
id="button-with-div-button"
data-umami-event="button-click"
data-umami-event-name={'bob'}
data-umami-event-id="123"
variant="primary"
>
<div>Button with div</div>
</Button>
<div data-umami-event="div-click">DIV with attribute</div>
<div data-umami-event="div-click-one">
<div data-umami-event="div-click-two">
<div data-umami-event="div-click-three">Nested DIV</div>
</div>
</div>
</Column>
<Column gap>
<Heading>Javascript events</Heading>
<Button id="manual-button" variant="primary" onClick={handleRunScript}>
Run script
</Button>
<Button id="manual-button" variant="primary" onClick={handleRunIdentify}>
Run identify
</Button>
<Button id="manual-button" variant="primary" onClick={handleRunRevenue}>
Revenue script
</Button>
</Column>
</Grid>
</Panel>
<Heading>Pageviews</Heading>
<WebsiteChart websiteId={websiteId} />
<Heading>Events</Heading>
<EventsChart websiteId={websiteId} />
<Panel>
<EventsChart websiteId={websiteId} />
</Panel>
</Column>
</PageBody>
);

View file

@ -24,7 +24,7 @@ export function PasswordEditForm({ onSave, onClose }) {
});
};
const samePassword = (value: string, values: { [key: string]: any }) => {
const samePassword = (value: string, values: Record<string, any>) => {
if (value !== values.newPassword) {
return formatMessage(messages.noMatchPassword);
}

View file

@ -1,9 +1,9 @@
import { WebsitesTable } from '@/app/(main)/settings/websites/WebsitesTable';
import { DataGrid } from '@/components/common/DataGrid';
import { useWebsites } from '@/components/hooks';
import { useWebsitesQuery } from '@/components/hooks';
export function UserWebsites({ userId }) {
const queryResult = useWebsites({ userId });
const queryResult = useWebsitesQuery({ userId });
return (
<DataGrid queryResult={queryResult}>

View file

@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import { WebsitesTable } from '@/app/(main)/settings/websites/WebsitesTable';
import { DataGrid } from '@/components/common/DataGrid';
import { useWebsites } from '@/components/hooks';
import { useWebsitesQuery } from '@/components/hooks';
export function WebsitesDataTable({
teamId,
@ -16,10 +16,10 @@ export function WebsitesDataTable({
showActions?: boolean;
children?: ReactNode;
}) {
const queryResult = useWebsites({ teamId });
const queryResult = useWebsitesQuery({ teamId });
return (
<DataGrid queryResult={queryResult} renderEmpty={() => children}>
<DataGrid queryResult={queryResult} renderEmpty={() => children} allowSearch allowPaging>
{({ data }) => (
<WebsitesTable
teamId={teamId}

View file

@ -31,7 +31,7 @@ export function WebsitesTable({
return (
<DataTable data={data}>
<DataColumn id="name" label={formatMessage(labels.name)}>
{(row: any) => <Link href={renderUrl(`/websites/${row.id}`)}>{row.name}</Link>}
{(row: any) => <Link href={renderUrl(`/websites/${row.id}`, false)}>{row.name}</Link>}
</DataColumn>
<DataColumn id="domain" label={formatMessage(labels.domain)} />
{showActions && (

View file

@ -3,7 +3,6 @@ import { LoadingPanel } from '@/components/common/LoadingPanel';
import { PageviewsChart } from '@/components/metrics/PageviewsChart';
import { useWebsitePageviewsQuery } from '@/components/hooks/queries/useWebsitePageviewsQuery';
import { useDateRange } from '@/components/hooks';
import { Panel } from '@/components/common/Panel';
export function WebsiteChart({
websiteId,
@ -48,16 +47,14 @@ export function WebsiteChart({
}, [data, startDate, endDate, unit]);
return (
<Panel height="520px">
<LoadingPanel data={data} isFetching={isFetching} isLoading={isLoading} error={error}>
<PageviewsChart
key={value}
data={chartData}
minDate={startDate}
maxDate={endDate}
unit={unit}
/>
</LoadingPanel>
</Panel>
<LoadingPanel data={data} isFetching={isFetching} isLoading={isLoading} error={error}>
<PageviewsChart
key={value}
data={chartData}
minDate={startDate}
maxDate={endDate}
unit={unit}
/>
</LoadingPanel>
);
}

View file

@ -1,6 +1,7 @@
'use client';
import { Column } from '@umami/react-zen';
import { useNavigation } from '@/components/hooks';
import { Panel } from '@/components/common/Panel';
import { WebsiteChart } from './WebsiteChart';
import { WebsiteExpandedView } from './WebsiteExpandedView';
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
@ -16,8 +17,10 @@ export function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
return (
<Column gap>
<WebsiteControls websiteId={websiteId} allowCompare={true} />
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} />
<WebsiteChart websiteId={websiteId} compareMode={compare} />
<WebsiteMetricsBar websiteId={websiteId} showChange={true} />
<Panel>
<WebsiteChart websiteId={websiteId} compareMode={compare} />
</Panel>
{!view && !compare && <WebsiteTableView websiteId={websiteId} />}
{view && !compare && <WebsiteExpandedView websiteId={websiteId} />}
{compare && <WebsiteCompareTables websiteId={websiteId} />}

View file

@ -11,13 +11,12 @@ import {
import { Fragment } from 'react';
import { More, Share, Edit } from '@/components/icons';
import { useMessages, useNavigation } from '@/components/hooks';
import { InputItem } from '@/lib/types';
export function WebsiteMenu({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
const { router, updateParams, renderUrl } = useNavigation();
const menuItems: InputItem[] = [
const menuItems = [
{ id: 'share', label: formatMessage(labels.share), icon: <Share /> },
{ id: 'edit', label: formatMessage(labels.edit), icon: <Edit />, seperator: true },
];

View file

@ -3,26 +3,18 @@ import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar';
import { formatShortTime, formatLongNumber } from '@/lib/format';
import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery';
import { useWebsites } from '@/store/websites';
import { LoadingPanel } from '@/components/common/LoadingPanel';
export function WebsiteMetricsBar({
websiteId,
showChange = false,
compareMode = false,
}: {
websiteId: string;
showChange?: boolean;
compareMode?: boolean;
showFilter?: boolean;
}) {
const { dateRange } = useDateRange(websiteId);
const { formatMessage, labels } = useMessages();
const dateCompare = useWebsites(state => state[websiteId]?.dateCompare);
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(
websiteId,
compareMode && dateCompare,
);
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
const isAllTime = dateRange.value === 'all';
const { pageviews, visitors, visits, bounces, totaltime, previous } = data || {};
@ -87,8 +79,7 @@ export function WebsiteMetricsBar({
change={change}
formatValue={formatValue}
reverseColors={reverseColors}
showChange={!isAllTime && (compareMode || showChange)}
showPrevious={!isAllTime && compareMode}
showChange={!isAllTime}
/>
);
})}

View file

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

View file

@ -73,7 +73,7 @@ export function UTM({ websiteId, startDate, endDate }: UTMProps) {
);
}
function toArray(data: { [key: string]: number } = {}) {
function toArray(data: Record<string, number> = {}) {
return Object.keys(data)
.map(key => {
return { name: key, value: data[key] };

View file

@ -14,7 +14,7 @@ export function SessionsDataTable({
const queryResult = useWebsiteSessionsQuery(websiteId);
return (
<DataGrid queryResult={queryResult} allowSearch={true} renderEmpty={() => children}>
<DataGrid queryResult={queryResult} renderEmpty={() => children} allowPaging>
{({ data }) => <SessionsTable data={data} showDomain={!websiteId} />}
</DataGrid>
);

View file

@ -1,12 +1,12 @@
import { ReactNode } from 'react';
import { Icon, TextField, Column, Row, Label, Text } from '@umami/react-zen';
import { useFormat, useLocale, useMessages, useRegionNames, useTimezone } from '@/components/hooks';
import { useFormat, useLocale, useMessages, useRegionNames } from '@/components/hooks';
import { TypeIcon } from '@/components/common/TypeIcon';
import { Location, KeyRound, Calendar } from '@/components/icons';
import { DateDistance } from '@/components/common/DateDistance';
export function SessionInfo({ data }) {
const { locale } = useLocale();
const { formatTimezoneDate } = useTimezone();
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();
const { getRegionName } = useRegionNames(locale);
@ -22,11 +22,11 @@ export function SessionInfo({ data }) {
</Info>
<Info label={formatMessage(labels.lastSeen)} icon={<Calendar />}>
{formatTimezoneDate(data?.lastAt, 'PPPPpp')}
<DateDistance date={new Date(data.lastAt)} />
</Info>
<Info label={formatMessage(labels.firstSeen)} icon={<Calendar />}>
{formatTimezoneDate(data?.firstAt, 'PPPPpp')}
<DateDistance date={new Date(data.firstAt)} />
</Info>
<Info

View file

@ -3,7 +3,7 @@ import { getRealtimeData } from '@/queries';
import { canViewWebsite } from '@/lib/auth';
import { startOfMinute, subMinutes } from 'date-fns';
import { REALTIME_RANGE } from '@/lib/constants';
import { parseRequest } from '@/lib/request';
import { parseRequest, getQueryFilters } from '@/lib/request';
export async function GET(
request: Request,
@ -16,15 +16,19 @@ export async function GET(
}
const { websiteId } = await params;
const { timezone } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
const filters = await getQueryFilters({
...query,
websiteId,
startAt: subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(),
endAt: Date.now(),
});
const data = await getRealtimeData(websiteId, { startDate, timezone });
const data = await getRealtimeData(websiteId, 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 } from '@/lib/request';
import { getGoal } from '@/queries/sql/reports/getGoal';
import { reportResultSchema } from '@/lib/schema';
@ -15,21 +15,22 @@ export async function POST(request: Request) {
websiteId,
dateRange: { startDate, endDate },
parameters: { type, value, property, operator },
...filters
} = body;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const filters = await getQueryFilters(body.filters);
const data = await getGoal(websiteId, {
...filters,
startDate: new Date(startDate),
endDate: new Date(endDate),
type,
value,
property,
operator,
filters,
});
return json(data);

View file

@ -1,7 +1,7 @@
import { z } from 'zod';
import { unauthorized, json } from '@/lib/response';
import { canViewTeam } from '@/lib/auth';
import { parseRequest } from '@/lib/request';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { pagingParams } from '@/lib/schema';
import { getTeamWebsites } from '@/queries';
@ -20,7 +20,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
return unauthorized();
}
const websites = await getTeamWebsites(teamId, query);
const filters = await getQueryFilters(query);
const websites = await getTeamWebsites(teamId, filters);
return json(websites);
}

View file

@ -3,7 +3,7 @@ import { json, unauthorized } from '@/lib/response';
import { getAllUserWebsitesIncludingTeamOwner } from '@/queries/prisma/website';
import { getEventUsage } from '@/queries/sql/events/getEventUsage';
import { getEventDataUsage } from '@/queries/sql/events/getEventDataUsage';
import { parseRequest, getRequestDateRange } from '@/lib/request';
import { parseRequest, getQueryFilters } from '@/lib/request';
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const schema = z.object({
@ -22,14 +22,14 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
}
const { userId } = await params;
const { startDate, endDate } = await getRequestDateRange(query);
const filters = await getQueryFilters(query);
const websites = await getAllUserWebsitesIncludingTeamOwner(userId);
const websiteIds = websites.map(a => a.id);
const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate);
const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate);
const websiteEventUsage = await getEventUsage(websiteIds, filters);
const eventDataUsage = await getEventDataUsage(websiteIds, filters);
const websiteUsage = websites.map(a => ({
websiteId: a.id,

View file

@ -2,7 +2,7 @@ import { z } from 'zod';
import { unauthorized, json } from '@/lib/response';
import { getUserWebsites } from '@/queries/prisma/website';
import { pagingParams } from '@/lib/schema';
import { parseRequest } from '@/lib/request';
import { getQueryFilters, parseRequest } from '@/lib/request';
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const schema = z.object({
@ -21,7 +21,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
return unauthorized();
}
const websites = await getUserWebsites(userId, query);
const filters = await getQueryFilters(query);
const websites = await getUserWebsites(userId, filters);
return json(websites);
}

View file

@ -1,5 +1,5 @@
import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { getEventDataEvents } from '@/queries/sql/events/getEventDataEvents';
@ -20,16 +20,16 @@ export async function GET(
}
const { websiteId } = await params;
const { event } = query;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const { event } = query;
const filters = await getQueryFilters(query);
const data = await getEventDataEvents(websiteId, {
startDate,
endDate,
...filters,
event,
});

View file

@ -1,5 +1,5 @@
import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { getEventDataFields } from '@/queries';
@ -20,16 +20,14 @@ export async function GET(
}
const { websiteId } = await params;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getEventDataFields(websiteId, {
startDate,
endDate,
});
const filters = await getQueryFilters(query);
const data = await getEventDataFields(websiteId, filters);
return json(data);
}

View file

@ -1,5 +1,5 @@
import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { getEventDataProperties } from '@/queries';
@ -21,14 +21,15 @@ export async function GET(
}
const { websiteId } = await params;
const { propertyName } = query;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getEventDataProperties(websiteId, { startDate, endDate, propertyName });
const { propertyName } = query;
const filters = await getQueryFilters(query);
const data = await getEventDataProperties(websiteId, { ...filters, propertyName });
return json(data);
}

View file

@ -1,5 +1,5 @@
import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { getEventDataStats } from '@/queries';
@ -21,13 +21,14 @@ export async function GET(
}
const { websiteId } = await params;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getEventDataStats(websiteId, { startDate, endDate });
const filters = await getQueryFilters(query);
const data = await getEventDataStats(websiteId, filters);
return json(data);
}

View file

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

View file

@ -1,5 +1,5 @@
import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { pagingParams } from '@/lib/schema';
@ -22,13 +22,14 @@ export async function GET(
}
const { websiteId } = await params;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getWebsiteEvents(websiteId, { ...query, startDate, endDate }, query);
const filters = await getQueryFilters(query);
const data = await getWebsiteEvents(websiteId, filters);
return json(data);
}

View file

@ -1,5 +1,5 @@
import { z } from 'zod';
import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request';
import { parseRequest, getQueryFilters } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { filterParams, timezoneParam, unitParam } from '@/lib/schema';
@ -24,20 +24,12 @@ export async function GET(
}
const { websiteId } = await params;
const { timezone } = query;
const { startDate, endDate, unit } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const filters = {
...getRequestFilters(query),
startDate,
endDate,
timezone,
unit,
};
const filters = await getQueryFilters({ ...query, websiteId });
const data = await getEventMetrics(websiteId, filters);

View file

@ -13,7 +13,7 @@ import {
VIDEO_DOMAINS,
PAID_AD_PARAMS,
} from '@/lib/constants';
import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request';
import { parseRequest, getQueryFilters } from '@/lib/request';
import { json, unauthorized, badRequest } from '@/lib/response';
import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries';
import { filterParams } from '@/lib/schema';
@ -45,13 +45,8 @@ export async function GET(
return unauthorized();
}
const { startDate, endDate } = await getRequestDateRange(query);
const column = FILTER_COLUMNS[type] || type;
const filters = {
...getRequestFilters(query),
startDate,
endDate,
};
const filters = await getQueryFilters({ ...query, websiteId });
if (search) {
filters[type] = {

View file

@ -1,7 +1,7 @@
import { z } from 'zod';
import { canViewWebsite } from '@/lib/auth';
import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request';
import { unitParam, timezoneParam, filterParams } from '@/lib/schema';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { dateRangeParams, filterParams } from '@/lib/schema';
import { getCompareDate } from '@/lib/date';
import { unauthorized, json } from '@/lib/response';
import { getPageviewStats, getSessionStats } from '@/queries';
@ -11,11 +11,7 @@ export async function GET(
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
unit: unitParam,
timezone: timezoneParam,
compare: z.string().optional(),
...dateRangeParams,
...filterParams,
});
@ -26,32 +22,23 @@ export async function GET(
}
const { websiteId } = await params;
const { timezone, compare } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const { startDate, endDate, unit } = await getRequestDateRange(query);
const filters = {
...getRequestFilters(query),
startDate,
endDate,
timezone,
unit,
};
const filters = await getQueryFilters({ ...query, websiteId });
const [pageviews, sessions] = await Promise.all([
getPageviewStats(websiteId, filters),
getSessionStats(websiteId, filters),
]);
if (compare) {
if (filters.compare) {
const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
compare,
startDate,
endDate,
filters.compare,
filters.startDate,
filters.endDate,
);
const [comparePageviews, compareSessions] = await Promise.all([
@ -70,8 +57,8 @@ export async function GET(
return json({
pageviews,
sessions,
startDate,
endDate,
startDate: filters.startDate,
endDate: filters.endDate,
compare: {
pageviews: comparePageviews,
sessions: compareSessions,

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { z } from 'zod';
import { parseRequest, getRequestDateRange } from '@/lib/request';
import { parseRequest, getQueryFilters } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { getSessionActivity } from '@/queries';
@ -20,13 +20,14 @@ export async function GET(
}
const { websiteId, sessionId } = await params;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getSessionActivity(websiteId, sessionId, startDate, endDate);
const filters = await getQueryFilters(query);
const data = await getSessionActivity(websiteId, sessionId, filters);
return json(data);
}

View file

@ -1,8 +1,8 @@
import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { pagingParams } from '@/lib/schema';
import { dateRangeParams, filterParams, pagingParams } from '@/lib/schema';
import { getWebsiteSessions } 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,
});
@ -21,14 +21,15 @@ export async function GET(
return error();
}
const { startDate, endDate } = await getRequestDateRange(query);
const { websiteId } = await params;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getWebsiteSessions(websiteId, { startDate, endDate }, query);
const filters = await getQueryFilters({ ...query, websiteId });
const data = await getWebsiteSessions(websiteId, filters);
return json(data);
}

View file

@ -1,5 +1,5 @@
import { z } from 'zod';
import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request';
import { parseRequest, getQueryFilters } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { filterParams } from '@/lib/schema';
@ -27,15 +27,9 @@ export async function GET(
return unauthorized();
}
const { startDate, endDate } = await getRequestDateRange(query);
const filters = await getQueryFilters(query);
const filters = getRequestFilters(query);
const metrics = await getWebsiteSessionStats(websiteId, {
...filters,
startDate,
endDate,
});
const metrics = await getWebsiteSessionStats(websiteId, filters);
const data = Object.keys(metrics[0]).reduce((obj, key) => {
obj[key] = {

View file

@ -1,5 +1,5 @@
import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { pagingParams, timezoneParam } from '@/lib/schema';
@ -23,14 +23,14 @@ export async function GET(
}
const { websiteId } = await params;
const { timezone } = query;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate, timezone });
const filters = await getQueryFilters(query);
const data = await getWebsiteSessionsWeekly(websiteId, filters);
return json(data);
}

View file

@ -1,8 +1,7 @@
import { z } from 'zod';
import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request';
import { parseRequest, getQueryFilters } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { getCompareDate } from '@/lib/date';
import { filterParams } from '@/lib/schema';
import { getWebsiteStats } from '@/queries';
@ -24,32 +23,20 @@ export async function GET(
}
const { websiteId } = await params;
const { compare } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const { startDate, endDate } = await getRequestDateRange(query);
const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
compare,
startDate,
endDate,
);
const filters = await getQueryFilters({ ...query, websiteId });
const filters = getRequestFilters(query);
const metrics = await getWebsiteStats(websiteId, {
...filters,
startDate,
endDate,
});
const data = await getWebsiteStats(websiteId, filters);
const previous = await getWebsiteStats(websiteId, {
...filters,
startDate: compareStartDate,
endDate: compareEndDate,
startDate: filters.compareStartDate,
endDate: filters.compareEndDate,
});
return json({ ...metrics, previous });
return json({ ...data, previous });
}

View file

@ -2,7 +2,7 @@ import { z } from 'zod';
import { canViewWebsite } from '@/lib/auth';
import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { getValues } from '@/queries';
import { parseRequest, getRequestDateRange } from '@/lib/request';
import { parseRequest, getQueryFilters } from '@/lib/request';
import { badRequest, json, unauthorized } from '@/lib/response';
export async function GET(
@ -23,7 +23,7 @@ export async function GET(
}
const { websiteId } = await params;
const { type, search } = query;
const { type } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
@ -33,9 +33,9 @@ export async function GET(
return badRequest('Invalid type.');
}
const { startDate, endDate } = await getRequestDateRange(query);
const filters = await getQueryFilters(query);
const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search);
const values = await getValues(websiteId, FILTER_COLUMNS[type], { ...filters });
return json(values.filter(n => n).sort());
}

View file

@ -1,14 +1,13 @@
import { ReactNode } from 'react';
import { ReactNode, useState } from 'react';
import { SearchField, Row, Column } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks';
import { Pager } from '@/components/common/Pager';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { PagedQueryResult } from '@/lib/types';
const DEFAULT_SEARCH_DELAY = 600;
export interface DataTableProps {
queryResult: PagedQueryResult<any>;
queryResult: any;
searchDelay?: number;
allowSearch?: boolean;
allowPaging?: boolean;
@ -20,31 +19,30 @@ export interface DataTableProps {
export function DataGrid({
queryResult,
searchDelay = 600,
allowSearch = true,
allowPaging = true,
allowSearch,
allowPaging,
autoFocus,
children,
}: DataTableProps) {
const { formatMessage, labels } = useMessages();
const { result, params, setParams, query } = queryResult || {};
const { error, isLoading, isFetching } = query || {};
const { page, pageSize, count, data } = result || {};
const { search } = params || {};
const hasData = Boolean(!isLoading && data?.length);
const { data, error, isLoading, isFetching, setParams } = queryResult || {};
const { router, updateParams } = useNavigation();
const [search, setSearch] = useState('');
const handleSearch = (search: string) => {
setParams({ ...params, search });
setSearch(search);
setParams(params => ({ ...params, search }));
router.push(updateParams({ search }));
};
const handlePageChange = (page: number) => {
setParams({ ...params, page });
setParams(params => ({ ...params, page }));
router.push(updateParams({ page }));
};
return (
<Column gap="4" minHeight="300px">
{allowSearch && (hasData || search) && (
{allowSearch && (data || search) && (
<Row width="280px" alignItems="center">
<SearchField
value={search}
@ -57,11 +55,16 @@ export function DataGrid({
)}
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}>
<Column>
{hasData ? (typeof children === 'function' ? children(result) : children) : null}
{data ? (typeof children === 'function' ? children(data) : children) : null}
</Column>
{allowPaging && hasData && (
{allowPaging && data && (
<Row marginTop="6">
<Pager page={page} pageSize={pageSize} count={count} onPageChange={handlePageChange} />
<Pager
page={data.page}
pageSize={data.pageSize}
count={data.count}
onPageChange={handlePageChange}
/>
</Row>
)}
</LoadingPanel>

View file

@ -19,7 +19,7 @@ export function FilterEditForm({ websiteId, data = [], onChange, onClose }: Filt
dateRange: { startDate, endDate },
} = useDateRange(websiteId);
const updateFilter = (name: string, props: { [key: string]: any }) => {
const updateFilter = (name: string, props: Record<string, any>) => {
setFilters(filters =>
filters.map(filter => (filter.name === name ? { ...filter, ...props } : filter)),
);

View file

@ -24,7 +24,7 @@ export * from './queries/useTeamMembersQuery';
export * from './queries/useUserQuery';
export * from './queries/useUsersQuery';
export * from './queries/useWebsiteQuery';
export * from './queries/useWebsites';
export * from './queries/useWebsitesQuery';
export * from './queries/useWebsiteEventsQuery';
export * from './queries/useWebsiteEventsSeriesQuery';
export * from './queries/useWebsiteMetricsQuery';

View file

@ -1,6 +1,6 @@
import { useApi, useModified } from '@/components/hooks';
export function useDeleteQuery(path: string, params?: { [key: string]: any }) {
export function useDeleteQuery(path: string, params?: Record<string, any>) {
const { del, useMutation } = useApi();
const { mutate, isPending, error } = useMutation({
mutationFn: () => del(path, params),

View file

@ -1,8 +1,27 @@
import { useTimezone } from '@/components/hooks/useTimezone';
import { REALTIME_INTERVAL } from '@/lib/constants';
import { RealtimeData } from '@/lib/types';
import { useApi } from '../useApi';
export interface RealtimeData {
countries: Record<string, number>;
events: any[];
pageviews: any[];
referrers: Record<string, number>;
timestamp: number;
series: {
views: any[];
visitors: any[];
};
totals: {
views: number;
visitors: number;
events: number;
countries: number;
};
urls: Record<string, number>;
visitors: any[];
}
export function useRealtimeQuery(websiteId: string) {
const { get, useQuery } = useApi();
const { timezone } = useTimezone();

View file

@ -4,7 +4,7 @@ import { ReactQueryOptions } from '@/lib/types';
export function useResultQuery<T = any>(
type: string,
params?: { [key: string]: any },
params?: Record<string, any>,
options?: ReactQueryOptions<T>,
) {
const { websiteId } = params;
@ -21,7 +21,7 @@ export function useResultQuery<T = any>(
...params,
},
],
queryFn: () => post(`/reports/${type}`, { type, ...filters, ...params }),
queryFn: () => post(`/reports/${type}`, { type, filters, ...params }),
enabled: !!type,
...options,
});

View file

@ -1,6 +1,6 @@
import { useApi } from '../useApi';
export function useUserQuery(userId: string, options?: { [key: string]: any }) {
export function useUserQuery(userId: string, options?: Record<string, any>) {
const { get, useQuery } = useApi();
return useQuery({
queryKey: ['users', userId],

View file

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

View file

@ -15,7 +15,7 @@ export function useWebsiteMetricsQuery(
options?: ReactQueryOptions<WebsiteMetricsData>,
) {
const { get, useQuery } = useApi();
const filterParams = useFilterParams(websiteId);
const queryParams = useFilterParams(websiteId);
const searchParams = useSearchParams();
return useQuery<WebsiteMetricsData>({
@ -23,13 +23,13 @@ export function useWebsiteMetricsQuery(
'websites:metrics',
{
websiteId,
...filterParams,
...queryParams,
...params,
},
],
queryFn: async () =>
get(`/websites/${websiteId}/metrics`, {
...filterParams,
...queryParams,
[searchParams.get('view')]: undefined,
...params,
}),

View file

@ -12,11 +12,11 @@ export function useWebsitePageviewsQuery(
options?: ReactQueryOptions<WebsitePageviewsData>,
) {
const { get, useQuery } = useApi();
const filterParams = useFilterParams(websiteId);
const queryParams = useFilterParams(websiteId);
return useQuery<WebsitePageviewsData>({
queryKey: ['websites:pageviews', { websiteId, compare, ...filterParams }],
queryFn: () => get(`/websites/${websiteId}/pageviews`, { compare, ...filterParams }),
queryKey: ['websites:pageviews', { websiteId, compare, ...queryParams }],
queryFn: () => get(`/websites/${websiteId}/pageviews`, { compare, ...queryParams }),
enabled: !!websiteId,
...options,
});

View file

@ -1,6 +1,6 @@
import { useApi } from '../useApi';
export function useWebsiteQuery(websiteId: string, options?: { [key: string]: any }) {
export function useWebsiteQuery(websiteId: string, options?: Record<string, any>) {
const { get, useQuery } = useApi();
return useQuery({

View file

@ -1,10 +1,7 @@
import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams';
export function useWebsiteSessionStatsQuery(
websiteId: string,
options?: { [key: string]: string },
) {
export function useWebsiteSessionStatsQuery(websiteId: string, options?: Record<string, string>) {
const { get, useQuery } = useApi();
const params = useFilterParams(websiteId);

View file

@ -5,7 +5,7 @@ import { useFilterParams } from '@/components/hooks/useFilterParams';
export function useWebsiteSessionsQuery(
websiteId: string,
params?: { [key: string]: string | number },
params?: Record<string, string | number>,
) {
const { get } = useApi();
const { modified } = useModified(`sessions`);

View file

@ -4,7 +4,7 @@ import { useFilterParams } from '@/components/hooks/useFilterParams';
export function useWebsiteSessionsWeeklyQuery(
websiteId: string,
params?: { [key: string]: string | number },
params?: Record<string, string | number>,
) {
const { get, useQuery } = useApi();
const { modified } = useModified(`sessions`);

View file

@ -19,15 +19,14 @@ export interface WebsiteStatsData {
export function useWebsiteStatsQuery(
websiteId: string,
compare?: string,
options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>,
) {
const { get, useQuery } = useApi();
const params = useFilterParams(websiteId);
const filterParams = useFilterParams(websiteId);
return useQuery<WebsiteStatsData>({
queryKey: ['websites:stats', { websiteId, ...params, compare }],
queryFn: () => get(`/websites/${websiteId}/stats`, { ...params, compare }),
queryKey: ['websites:stats', { websiteId, ...filterParams }],
queryFn: () => get(`/websites/${websiteId}/stats`, { ...filterParams }),
enabled: !!websiteId,
...options,
});

View file

@ -2,10 +2,12 @@ import { useApi } from '../useApi';
import { usePagedQuery } from '../usePagedQuery';
import { useLoginQuery } from './useLoginQuery';
import { useModified } from '../useModified';
import { ReactQueryOptions } from '@/lib/types';
export function useWebsites(
export function useWebsitesQuery(
{ userId, teamId }: { userId?: string; teamId?: string },
params?: { [key: string]: string | number },
params?: Record<string, any>,
options?: ReactQueryOptions,
) {
const { get } = useApi();
const { user } = useLoginQuery();
@ -13,10 +15,12 @@ export function useWebsites(
return usePagedQuery({
queryKey: ['websites', { userId, teamId, modified, ...params }],
queryFn: () => {
queryFn: pageParams => {
return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId || user.id}/websites`, {
...params,
...pageParams,
});
},
...options,
});
}

View file

@ -40,7 +40,7 @@ export function useFormat() {
return languageNames[value?.split('-')[0]] || value;
};
const formatValue = (value: string, type: string, data?: { [key: string]: any }): string => {
const formatValue = (value: string, type: string, data?: Record<string, any>): string => {
switch (type) {
case 'os':
return formatOS(value);

View file

@ -15,7 +15,7 @@ export function useMessages(): any {
id: string;
defaultMessage: string;
},
values?: { [key: string]: string },
values?: Record<string, string>,
opts?: any,
) => {
return descriptor ? intl.formatMessage(descriptor, values, opts) : null;

View file

@ -9,11 +9,11 @@ export function useNavigation() {
const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || [];
const query = Object.fromEntries(searchParams);
const updateParams = (params?: { [key: string]: string | number }) => {
const updateParams = (params?: Record<string, string | number>) => {
return !params ? pathname : buildUrl(pathname, { ...query, ...params });
};
const renderUrl = (path: string, params?: { [key: string]: string | number } | false) => {
const renderUrl = (path: string, params?: Record<string, string | number> | false) => {
return buildUrl(
teamId ? `/teams/${teamId}${path}` : path,
params === false ? {} : { ...query, ...params },

View file

@ -1,30 +1,27 @@
import { UseQueryOptions } from '@tanstack/react-query';
import { useState } from 'react';
import { PageResult, PageParams, PagedQueryResult } from '@/lib/types';
import { useApi } from './useApi';
import { useNavigation } from './useNavigation';
export function usePagedQuery<T = any>({
export function usePagedQuery({
queryKey,
queryFn,
...options
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): PagedQueryResult<T> {
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }) {
const { query: queryParams } = useNavigation();
const [params, setParams] = useState<PageParams>({
search: '',
page: queryParams?.page || '1',
const [params, setParams] = useState({
search: queryParams?.search ?? '',
page: queryParams?.page ?? '1',
});
const { useQuery } = useApi();
const { data, ...query } = useQuery({
queryKey: [{ ...queryKey, ...params }],
queryFn: () => queryFn(params as any),
...options,
});
return {
result: data as PageResult<T>,
query,
...useQuery({
queryKey: [{ ...queryKey, ...params }],
queryFn: () => queryFn(params),
...options,
}),
params,
setParams,
};

View file

@ -1,7 +1,6 @@
import { useState } from 'react';
import { Select, ListItem } from '@umami/react-zen';
import { useWebsites, useMessages } from '@/components/hooks';
import type { SelectProps } from '@umami/react-zen/Select';
import { Select, SelectProps, ListItem } from '@umami/react-zen';
import { useWebsitesQuery, useMessages } from '@/components/hooks';
export function WebsiteSelect({
websiteId,
@ -12,14 +11,14 @@ export function WebsiteSelect({
}: {
websiteId?: string;
teamId?: string;
variant?: 'primary' | 'secondary' | 'outline' | 'quiet' | 'danger' | 'zero';
variant?: 'primary' | 'outline' | 'quiet' | 'danger' | 'zero';
onSelect?: (key: any) => void;
} & SelectProps) {
const { formatMessage, labels } = useMessages();
const [search, setSearch] = useState('');
const [selectedId, setSelectedId] = useState(websiteId);
const queryResult = useWebsites({ teamId }, { search, pageSize: 5 });
const { data, isLoading } = useWebsitesQuery({ teamId }, { search, pageSize: 5 });
const handleSelect = (value: any) => {
setSelectedId(value);
@ -30,15 +29,17 @@ export function WebsiteSelect({
setSearch(value);
};
const items = queryResult?.result?.data as any[];
if (!data) {
return null;
}
return (
<Select
{...props}
items={items}
items={data?.['data'] || []}
value={selectedId}
placeholder={formatMessage(labels.selectWebsite)}
isLoading={queryResult.query.isLoading}
isLoading={isLoading}
buttonProps={{ variant }}
allowSearch={true}
searchValue={search}

View file

@ -13,7 +13,6 @@ export interface MetricCardProps {
formatValue?: (n: any) => string;
showLabel?: boolean;
showChange?: boolean;
showPrevious?: boolean;
}
export const MetricCard = ({
@ -24,13 +23,11 @@ export const MetricCard = ({
formatValue = formatNumber,
showLabel = true,
showChange = false,
showPrevious = false,
}: MetricCardProps) => {
const diff = value - change;
const pct = ((value - diff) / diff) * 100;
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } });
const prevProps = useSpring({ x: Number(diff) || 0, from: { x: 0 } });
return (
<Column
@ -54,9 +51,6 @@ export const MetricCard = ({
<AnimatedDiv>{changeProps?.x?.to(x => `${Math.abs(~~x)}%`)}</AnimatedDiv>
</ChangeLabel>
)}
{showPrevious && (
<AnimatedDiv title={diff.toString()}>{prevProps?.x?.to(x => formatValue(x))}</AnimatedDiv>
)}
</Column>
);
};

View file

@ -18,7 +18,7 @@ export interface MetricsTableProps extends ListTableProps {
allowSearch?: boolean;
searchFormattedValues?: boolean;
showMore?: boolean;
params?: { [key: string]: any };
params?: Record<string, any>;
onDataLoad?: (data: any) => any;
className?: string;
children?: ReactNode;

View file

@ -2,11 +2,9 @@ import { ClickHouseClient, createClient } from '@clickhouse/client';
import { formatInTimeZone } from 'date-fns-tz';
import debug from 'debug';
import { CLICKHOUSE } from '@/lib/db';
import { getWebsite } from '@/queries';
import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants';
import { maxDate } from './date';
import { filtersToArray } from './params';
import { PageParams, QueryFilters, QueryOptions } from './types';
import { QueryFilters, QueryOptions } from './types';
export const CLICKHOUSE_DATE_FORMATS = {
utc: '%Y-%m-%dT%H:%i:%SZ',
@ -89,7 +87,7 @@ function mapFilter(column: string, operator: string, name: string, type: string
}
}
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) {
function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}) {
const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => {
if (column) {
arr.push(`and ${mapFilter(column, operator, name)}`);
@ -105,7 +103,7 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {})
return query.join('\n');
}
function getDateQuery(filters: QueryFilters = {}) {
function getDateQuery(filters: Record<string, any>) {
const { startDate, endDate, timezone } = filters;
if (startDate) {
@ -125,36 +123,33 @@ function getDateQuery(filters: QueryFilters = {}) {
return '';
}
function getFilterParams(filters: QueryFilters = {}) {
return filtersToArray(filters).reduce((obj, { name, value }) => {
if (name && value !== undefined) {
obj[name] = value;
}
return obj;
}, {});
}
async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
const website = await getWebsite(websiteId);
function getQueryParams(filters: Record<string, any>) {
return {
filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters),
filterParams: {
...getFilterParams(filters),
websiteId,
startDate: maxDate(filters.startDate, new Date(website?.resetAt)),
},
...filters,
...filtersToArray(filters).reduce((obj, { name, value }) => {
if (name && value !== undefined) {
obj[name] = value;
}
return obj;
}, {}),
};
}
async function pagedQuery(
async function parseFilters(filters: Record<string, any>, options?: QueryOptions) {
return {
filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters),
queryParams: getQueryParams(filters),
};
}
async function pagedRawQuery(
query: string,
queryParams: { [key: string]: any },
pageParams: PageParams = {},
queryParams: Record<string, any>,
filters: QueryFilters,
) {
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams;
const { page = 1, pageSize, orderBy, sortDescending = false } = filters;
const size = +pageSize || DEFAULT_PAGE_SIZE;
const offset = +size * (+page - 1);
const direction = sortDescending ? 'desc' : 'asc';
@ -236,7 +231,7 @@ export default {
getFilterQuery,
getUTCString,
parseFilters,
pagedQuery,
pagedRawQuery,
findUnique,
findFirst,
rawQuery,

View file

@ -19,7 +19,7 @@ export const DEFAULT_ANIMATION_DURATION = 300;
export const DEFAULT_DATE_RANGE_VALUE = '24hour';
export const DEFAULT_WEBSITE_LIMIT = 10;
export const DEFAULT_RESET_DATE = '2000-01-01';
export const DEFAULT_PAGE_SIZE = 10;
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_DATE_COMPARE = 'prev';
export const REALTIME_RANGE = 30;

View file

@ -2,7 +2,7 @@ import { DATA_TYPE, DATETIME_REGEX } from './constants';
import { DynamicDataType } from './types';
export function flattenJSON(
eventData: { [key: string]: any },
eventData: Record<string, any>,
keyValues: { key: string; value: any; dataType: DynamicDataType }[] = [],
parentKey = '',
): { key: string; value: any; dataType: DynamicDataType }[] {

View file

@ -292,9 +292,13 @@ export function getCompareDate(compare: string, startDate: Date, endDate: Date)
return { startDate: subYears(startDate, 1), endDate: subYears(endDate, 1) };
}
const diff = differenceInMinutes(endDate, startDate);
if (compare === 'prev') {
const diff = differenceInMinutes(endDate, startDate);
return { startDate: subMinutes(startDate, diff), endDate: subMinutes(endDate, diff) };
return { startDate: subMinutes(startDate, diff), endDate: subMinutes(endDate, diff) };
}
return {};
}
export function getDayOfWeekAsDate(dayOfWeek: number) {

View file

@ -64,7 +64,7 @@ async function getProducer(): Promise<Producer> {
async function sendMessage(
topic: string,
message: { [key: string]: string | number } | { [key: string]: string | number }[],
message: Record<string, string | number> | Record<string, string | number>[],
): Promise<RecordMetadata[]> {
try {
await connect();

View file

@ -18,7 +18,7 @@ export function isSearchOperator(operator: any) {
return [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator);
}
export function filtersToArray(filters: QueryFilters = {}, options: QueryOptions = {}) {
export function filtersToArray(filters: QueryFilters, options: QueryOptions = {}) {
return Object.keys(filters).reduce((arr, key) => {
const filter = filters[key];

View file

@ -2,12 +2,9 @@ import debug from 'debug';
import { PrismaClient } from '@/generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { readReplicas } from '@prisma/extension-read-replicas';
import { formatInTimeZone } from 'date-fns-tz';
import { MYSQL, POSTGRESQL, getDatabaseType } from '@/lib/db';
import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants';
import { fetchWebsite } from './load';
import { maxDate } from './date';
import { QueryFilters, QueryOptions, PageParams } from './types';
import { QueryOptions, QueryFilters } from './types';
import { filtersToArray } from './params';
const log = debug('umami:prisma');
@ -22,14 +19,6 @@ const PRISMA_LOG_OPTIONS = {
],
};
const MYSQL_DATE_FORMATS = {
minute: '%Y-%m-%dT%H:%i:00',
hour: '%Y-%m-%d %H:00:00',
day: '%Y-%m-%d 00:00:00',
month: '%Y-%m-01 00:00:00',
year: '%Y-01-01 00:00:00',
};
const POSTGRESQL_DATE_FORMATS = {
minute: 'YYYY-MM-DD HH24:MI:00',
hour: 'YYYY-MM-DD HH24:00:00',
@ -75,59 +64,23 @@ function getCastColumnQuery(field: string, type: string): string {
}
function getDateSQL(field: string, unit: string, timezone?: string): string {
const db = getDatabaseType();
if (db === POSTGRESQL) {
if (timezone) {
return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
}
return `to_char(date_trunc('${unit}', ${field}), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
if (timezone) {
return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
}
if (db === MYSQL) {
if (timezone) {
const tz = formatInTimeZone(new Date(), timezone, 'xxx');
return `date_format(convert_tz(${field},'+00:00','${tz}'), '${MYSQL_DATE_FORMATS[unit]}')`;
}
return `date_format(${field}, '${MYSQL_DATE_FORMATS[unit]}')`;
}
return `to_char(date_trunc('${unit}', ${field}), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
}
function getDateWeeklySQL(field: string, timezone?: string) {
const db = getDatabaseType();
if (db === POSTGRESQL) {
return `concat(extract(dow from (${field} at time zone '${timezone}')), ':', to_char((${field} at time zone '${timezone}'), 'HH24'))`;
}
if (db === MYSQL) {
const tz = formatInTimeZone(new Date(), timezone, 'xxx');
return `date_format(convert_tz(${field},'+00:00','${tz}'), '%w:%H')`;
}
return `concat(extract(dow from (${field} at time zone '${timezone}')), ':', to_char((${field} at time zone '${timezone}'), 'HH24'))`;
}
export function getTimestampSQL(field: string) {
const db = getDatabaseType();
if (db === POSTGRESQL) {
return `floor(extract(epoch from ${field}))`;
}
if (db === MYSQL) {
return `UNIX_TIMESTAMP(${field})`;
}
return `floor(extract(epoch from ${field}))`;
}
function getTimestampDiffSQL(field1: string, field2: string): string {
const db = getDatabaseType();
if (db === POSTGRESQL) {
return `floor(extract(epoch from (${field2} - ${field1})))`;
}
if (db === MYSQL) {
return `timestampdiff(second, ${field1}, ${field2})`;
}
return `floor(extract(epoch from (${field2} - ${field1})))`;
}
function getSearchSQL(column: string, param: string = 'search'): string {
@ -156,7 +109,7 @@ function mapFilter(column: string, operator: string, name: string, type: string
}
}
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): string {
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)}`);
@ -174,7 +127,7 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}):
return query.join('\n');
}
function getDateQuery(filters: QueryFilters = {}) {
function getDateQuery(filters: Record<string, any>) {
const { startDate, endDate } = filters;
if (startDate) {
@ -188,38 +141,32 @@ function getDateQuery(filters: QueryFilters = {}) {
return '';
}
function getFilterParams(filters: QueryFilters = {}) {
return filtersToArray(filters).reduce((obj, { name, operator, value }) => {
obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator)
? `%${value}%`
: value;
function getQueryParams(filters: Record<string, any>) {
return {
...filters,
...filtersToArray(filters).reduce((obj, { name, operator, value }) => {
obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator)
? `%${value}%`
: value;
return obj;
}, {});
return obj;
}, {}),
};
}
async function parseFilters(
websiteId: string,
filters: QueryFilters = {},
options: QueryOptions = {},
) {
const website = await fetchWebsite(websiteId);
async function parseFilters(filters: Record<string, any>, options?: QueryOptions) {
const joinSession = Object.keys(filters).find(key =>
['referrer', ...SESSION_COLUMNS].includes(key),
);
return {
joinSession:
joinSessionQuery:
options?.joinSession || joinSession
? `inner join session on website_event.session_id = session.session_id`
: '',
filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters),
filterParams: {
...getFilterParams(filters),
websiteId,
startDate: maxDate(filters.startDate, website?.resetAt),
},
filterQuery: getFilterQuery(filters, options),
queryParams: getQueryParams(filters),
};
}
@ -251,8 +198,8 @@ async function rawQuery(sql: string, data: object): Promise<any> {
: client.$queryRawUnsafe(query, ...params);
}
async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams) {
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams || {};
async function pagedQuery<T>(model: string, criteria: T, filters?: QueryFilters) {
const { page = 1, pageSize, orderBy, sortDescending = false } = filters || {};
const size = +pageSize || DEFAULT_PAGE_SIZE;
const data = await client[model].findMany({
@ -276,10 +223,10 @@ async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams)
async function pagedRawQuery(
query: string,
queryParams: { [key: string]: any },
pageParams: PageParams = {},
filters: QueryFilters,
queryParams: Record<string, any>,
) {
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams;
const { page = 1, pageSize, orderBy, sortDescending = false } = filters;
const size = +pageSize || DEFAULT_PAGE_SIZE;
const offset = +size * (+page - 1);
const direction = sortDescending ? 'desc' : 'asc';
@ -310,11 +257,11 @@ function getQueryMode(): { mode?: 'default' | 'insensitive' } {
return {};
}
function getSearchParameters(query: string, filters: { [key: string]: any }[]) {
function getSearchParameters(query: string, filters: Record<string, any>[]) {
if (!query) return;
const mode = getQueryMode();
const parseFilter = (filter: { [key: string]: any }) => {
const parseFilter = (filter: Record<string, any>) => {
const [[key, value]] = Object.entries(filter);
return {

View file

@ -1,8 +1,10 @@
import { z } from 'zod/v4';
import { FILTER_COLUMNS } from '@/lib/constants';
import { FILTER_COLUMNS, DEFAULT_PAGE_SIZE } from '@/lib/constants';
import { badRequest, unauthorized } from '@/lib/response';
import { getAllowedUnits, getCompareDate, getMinimumUnit } from '@/lib/date';
import { getAllowedUnits, getCompareDate, getMinimumUnit, maxDate } from '@/lib/date';
import { checkAuth } from '@/lib/auth';
import { fetchWebsite } from '@/lib/load';
import { QueryFilters } from '@/lib/types';
export async function parseRequest(
request: Request,
@ -47,8 +49,8 @@ export async function getJsonBody(request: Request) {
}
}
export async function getRequestDateRange(query: Record<string, string>) {
const { startAt, endAt, unit, compare } = query;
export function getRequestDateRange(query: Record<string, string>) {
const { startAt, endAt, unit, compare, timezone } = query;
const startDate = new Date(+startAt);
const endDate = new Date(+endAt);
@ -62,8 +64,10 @@ export async function getRequestDateRange(query: Record<string, string>) {
return {
startDate,
endDate,
compare,
compareStartDate,
compareEndDate,
timezone,
unit: getAllowedUnits(startDate, endDate).includes(unit)
? unit
: getMinimumUnit(startDate, endDate),
@ -81,3 +85,30 @@ export function getRequestFilters(query: Record<string, any>) {
return obj;
}, {});
}
export async function getQueryFilters(params: Record<string, any>): Promise<QueryFilters> {
const dateRange = getRequestDateRange(params);
const filters = getRequestFilters(params);
const data = {
...dateRange,
...filters,
page: params?.page,
pageSize: params?.page ? params?.pageSize || DEFAULT_PAGE_SIZE : undefined,
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

@ -2,6 +2,22 @@ import { z } from 'zod';
import { isValidTimezone } from '@/lib/date';
import { UNIT_TYPES } from './constants';
export const timezoneParam = z.string().refine(value => isValidTimezone(value), {
message: 'Invalid timezone',
});
export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), {
message: 'Invalid unit',
});
export const dateRangeParams = {
startAt: z.coerce.number(),
endAt: z.coerce.number(),
timezone: timezoneParam.optional(),
unit: unitParam.optional(),
compare: z.string().optional(),
};
export const filterParams = {
path: z.string().optional(),
referrer: z.string().optional(),
@ -22,17 +38,13 @@ export const filterParams = {
export const pagingParams = {
page: z.coerce.number().int().positive().optional(),
pageSize: z.coerce.number().int().positive().optional(),
orderBy: z.string().optional(),
search: z.string().optional(),
};
export const timezoneParam = z.string().refine(value => isValidTimezone(value), {
message: 'Invalid timezone',
});
export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), {
message: 'Invalid unit',
});
export const sortingParams = {
orderBy: z.string().optional(),
sortDescending: z.string().optional(),
};
export const userRoleParam = z.enum(['admin', 'user', 'view-only']);
@ -86,11 +98,11 @@ export const reportParms = {
dateRange: z.object({
startDate: z.coerce.date(),
endDate: z.coerce.date(),
num: z.coerce.number().optional(),
offset: z.coerce.number().optional(),
timezone: timezoneParam.optional(),
unit: unitParam.optional(),
value: z.string().optional(),
compare: z.string().optional(),
compareStartDate: z.coerce.date().optional(),
compareEndDate: z.coerce.date().optional(),
}),
};
@ -191,7 +203,7 @@ export const reportSchema = z.intersection(reportBaseSchema, reportTypeSchema);
export const reportResultSchema = z.intersection(
z.object({
...reportParms,
...filterParams,
filters: z.object({ ...filterParams }),
}),
reportTypeSchema,
);

View file

@ -1,45 +1,16 @@
import { Dispatch, SetStateAction } from 'react';
import { UseQueryOptions } from '@tanstack/react-query';
import { DATA_TYPE, PERMISSIONS, ROLES } from './constants';
import { TIME_UNIT } from './date';
export type ObjectValues<T> = T[keyof T];
export type ReactQueryOptions<T> = Omit<UseQueryOptions<T, Error, T>, 'queryKey' | 'queryFn'>;
export type ReactQueryOptions<T = any> = Omit<UseQueryOptions<T, Error, T>, 'queryKey' | 'queryFn'>;
export type TimeUnit = ObjectValues<typeof TIME_UNIT>;
export type Permission = ObjectValues<typeof PERMISSIONS>;
export type Role = ObjectValues<typeof ROLES>;
export type DynamicDataType = ObjectValues<typeof DATA_TYPE>;
export interface PageParams {
search?: string;
page?: string | number;
pageSize?: string;
orderBy?: string;
sortDescending?: boolean;
}
export interface PageResult<T> {
data: T;
count: number;
page: number;
pageSize: number;
orderBy?: string;
sortDescending?: boolean;
}
export interface PagedQueryResult<T = any> {
result: PageResult<T>;
query: any;
params: PageParams;
setParams: Dispatch<SetStateAction<T | PageParams>>;
}
export interface DynamicData {
[key: string]: number | string | number[] | string[];
}
export interface Auth {
user?: {
id: string;
@ -54,20 +25,35 @@ export interface Auth {
}
export interface DateRange {
value: string;
startDate: Date;
endDate: Date;
value?: string;
unit?: TimeUnit;
num?: number;
offset?: number;
}
export interface DynamicData {
[key: string]: number | string | number[] | string[];
}
export interface QueryOptions {
joinSession?: boolean;
columns?: Record<string, string>;
limit?: number;
}
export interface QueryFilters {
websiteId?: string;
// Date range
startDate?: Date;
endDate?: Date;
timezone?: string;
compareStartDate?: Date;
compareEndDate?: Date;
compare?: string;
unit?: string;
eventType?: number;
timezone?: string;
// Filters
path?: string;
referrer?: string;
title?: string;
@ -83,54 +69,29 @@ export interface QueryFilters {
event?: string;
search?: string;
tag?: string;
eventType?: number;
// Paging
page?: number;
pageSize?: number;
// Sorting
orderBy?: string;
sortDescending?: boolean;
}
export interface QueryOptions {
joinSession?: boolean;
columns?: { [key: string]: string };
limit?: number;
export interface PageParams {
page: number;
pageSize: number;
orderBy?: string;
sortDescending?: boolean;
search?: string;
}
export interface RealtimeData {
countries: { [key: string]: number };
events: any[];
pageviews: any[];
referrers: { [key: string]: number };
timestamp: number;
series: {
views: any[];
visitors: any[];
};
totals: {
views: number;
visitors: number;
events: number;
countries: number;
};
urls: { [key: string]: number };
visitors: any[];
}
export interface SessionData {
id: string;
websiteId: string;
visitId: string;
hostname: string;
browser: string;
os: string;
device: string;
screen: string;
language: string;
country: string;
region: string;
city: string;
ip?: string;
userAgent?: string;
}
export interface InputItem {
id: string;
label: string;
icon: any;
seperator?: boolean;
export interface PageResult<T> {
data: T;
count: number;
page: number;
pageSize: number;
orderBy?: string;
sortDescending?: boolean;
search?: string;
}

View file

@ -1,6 +1,6 @@
import { Prisma, Report } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
import { PageResult, PageParams } from '@/lib/types';
import { PageResult, QueryFilters } from '@/lib/types';
import ReportFindManyArgs = Prisma.ReportFindManyArgs;
async function findReport(criteria: Prisma.ReportFindUniqueArgs): Promise<Report> {
@ -17,9 +17,9 @@ export async function getReport(reportId: string): Promise<Report> {
export async function getReports(
criteria: ReportFindManyArgs,
pageParams: PageParams = {},
filters: QueryFilters = {},
): Promise<PageResult<Report[]>> {
const { search } = pageParams;
const { search } = filters;
const where: Prisma.ReportWhereInput = {
...criteria.where,
@ -45,12 +45,12 @@ export async function getReports(
]),
};
return prisma.pagedQuery('report', { ...criteria, where }, pageParams);
return prisma.pagedQuery('report', { ...criteria, where }, filters);
}
export async function getUserReports(
userId: string,
filters?: PageParams,
filters?: QueryFilters,
): Promise<PageResult<Report[]>> {
return getReports(
{
@ -72,7 +72,7 @@ export async function getUserReports(
export async function getWebsiteReports(
websiteId: string,
filters: PageParams = {},
filters: QueryFilters = {},
): Promise<PageResult<Report[]>> {
return getReports(
{

View file

@ -2,7 +2,7 @@ import { Prisma, Team } from '@/generated/prisma/client';
import { ROLES } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
import prisma from '@/lib/prisma';
import { PageResult, PageParams } from '@/lib/types';
import { PageResult, QueryFilters } from '@/lib/types';
import TeamFindManyArgs = Prisma.TeamFindManyArgs;
export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise<Team> {
@ -22,7 +22,7 @@ export async function getTeam(teamId: string, options: { includeMembers?: boolea
export async function getTeams(
criteria: TeamFindManyArgs,
filters: PageParams = {},
filters: QueryFilters,
): Promise<PageResult<Team[]>> {
const { getSearchParameters } = prisma;
const { search } = filters;
@ -42,7 +42,7 @@ export async function getTeams(
);
}
export async function getUserTeams(userId: string, filters: PageParams = {}) {
export async function getUserTeams(userId: string, filters: QueryFilters) {
return getTeams(
{
where: {

View file

@ -1,7 +1,7 @@
import { Prisma, TeamUser } from '@/generated/prisma/client';
import { uuid } from '@/lib/crypto';
import prisma from '@/lib/prisma';
import { PageResult, PageParams } from '@/lib/types';
import { PageResult, QueryFilters } from '@/lib/types';
import TeamUserFindManyArgs = Prisma.TeamUserFindManyArgs;
export async function findTeamUser(criteria: Prisma.TeamUserFindUniqueArgs): Promise<TeamUser> {
@ -19,7 +19,7 @@ export async function getTeamUser(teamId: string, userId: string): Promise<TeamU
export async function getTeamUsers(
criteria: TeamUserFindManyArgs,
filters?: PageParams,
filters?: QueryFilters,
): Promise<PageResult<TeamUser[]>> {
const { search } = filters;

View file

@ -1,7 +1,7 @@
import { Prisma, User } from '@/generated/prisma/client';
import { ROLES } from '@/lib/constants';
import prisma from '@/lib/prisma';
import { PageResult, Role, PageParams } from '@/lib/types';
import { PageResult, Role, QueryFilters } from '@/lib/types';
import { getRandomChars } from '@/lib/crypto';
import UserFindManyArgs = Prisma.UserFindManyArgs;
@ -49,9 +49,9 @@ export async function getUserByUsername(username: string, options: GetUserOption
export async function getUsers(
criteria: UserFindManyArgs,
pageParams?: PageParams,
filters: QueryFilters = {},
): Promise<PageResult<User[]>> {
const { search } = pageParams;
const { search } = filters;
const where: Prisma.UserWhereInput = {
...criteria.where,
@ -68,7 +68,7 @@ export async function getUsers(
{
orderBy: 'createdAt',
sortDescending: true,
...pageParams,
...filters,
},
);
}

View file

@ -1,7 +1,7 @@
import { Prisma, Website } from '@/generated/prisma/client';
import redis from '@/lib/redis';
import prisma from '@/lib/prisma';
import { PageResult, PageParams } from '@/lib/types';
import { PageResult, QueryFilters } from '@/lib/types';
import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs;
import { ROLES } from '@/lib/constants';
@ -28,9 +28,9 @@ export async function getSharedWebsite(shareId: string) {
export async function getWebsites(
criteria: WebsiteFindManyArgs,
pageParams: PageParams,
filters: QueryFilters,
): Promise<PageResult<Website[]>> {
const { search } = pageParams;
const { search } = filters;
const where: Prisma.WebsiteWhereInput = {
...criteria.where,
@ -43,7 +43,7 @@ export async function getWebsites(
deletedAt: null,
};
return prisma.pagedQuery('website', { ...criteria, where }, pageParams);
return prisma.pagedQuery('website', { ...criteria, where }, filters);
}
export async function getAllWebsites(userId: string) {
@ -90,7 +90,7 @@ export async function getAllUserWebsitesIncludingTeamOwner(userId: string) {
export async function getUserWebsites(
userId: string,
filters?: PageParams,
filters?: QueryFilters,
): Promise<PageResult<Website[]>> {
return getWebsites(
{
@ -115,7 +115,7 @@ export async function getUserWebsites(
export async function getTeamWebsites(
teamId: string,
filters?: PageParams,
filters?: QueryFilters,
): Promise<PageResult<Website[]>> {
return getWebsites(
{

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 { filterParams } = await parseFilters(websiteId, filters);
const { queryParams } = await parseFilters(filters);
if (event) {
return rawQuery(
@ -43,7 +43,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
group by website_event.event_name, event_data.data_key, event_data.data_type, event_data.string_value
order by 1 asc, 2 asc, 3 asc, 5 desc
`,
filterParams,
queryParams,
);
}
@ -63,7 +63,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
order by 1 asc, 2 asc
limit 500
`,
filterParams,
queryParams,
);
}
@ -73,7 +73,7 @@ async function clickhouseQuery(
): Promise<{ eventName: string; propertyName: string; dataType: number; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { event } = filters;
const { filterParams } = await parseFilters(websiteId, filters);
const { queryParams } = await parseFilters(filters);
if (event) {
return rawQuery(
@ -92,7 +92,7 @@ async function clickhouseQuery(
order by 1 asc, 2 asc, 3 asc, 5 desc
limit 500
`,
filterParams,
queryParams,
);
}
@ -110,6 +110,6 @@ async function clickhouseQuery(
order by 1 asc, 2 asc
limit 500
`,
filterParams,
queryParams,
);
}

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, filterParams } = await parseFilters(websiteId, filters);
const { filterQuery, queryParams } = await parseFilters(filters);
return rawQuery(
`
@ -34,7 +34,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
order by 2 desc
limit 100
`,
filterParams,
queryParams,
);
}
@ -43,7 +43,7 @@ async function clickhouseQuery(
filters: QueryFilters,
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, filters);
const { filterQuery, queryParams } = await parseFilters(filters);
return rawQuery(
`
@ -62,6 +62,6 @@ async function clickhouseQuery(
order by 2 desc
limit 100
`,
filterParams,
queryParams,
);
}

View file

@ -1,11 +1,11 @@
import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import { QueryFilters, WebsiteEventData } from '@/lib/types';
import { QueryFilters } from '@/lib/types';
export async function getEventDataProperties(
...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }]
): Promise<WebsiteEventData[]> {
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
@ -17,7 +17,7 @@ async function relationalQuery(
filters: QueryFilters & { propertyName?: string },
) {
const { rawQuery, parseFilters } = prisma;
const { filterQuery, filterParams } = await parseFilters(websiteId, filters, {
const { filterQuery, queryParams } = await parseFilters(filters, {
columns: { propertyName: 'data_key' },
});
@ -36,7 +36,7 @@ async function relationalQuery(
order by 3 desc
limit 500
`,
filterParams,
queryParams,
);
}
@ -45,7 +45,7 @@ async function clickhouseQuery(
filters: QueryFilters & { propertyName?: string },
): Promise<{ eventName: string; propertyName: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, filters, {
const { filterQuery, queryParams } = await parseFilters(filters, {
columns: { propertyName: 'data_key' },
});
@ -63,6 +63,6 @@ async function clickhouseQuery(
order by 1, 3 desc
limit 500
`,
filterParams,
queryParams,
);
}

View file

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

View file

@ -1,7 +1,8 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from '@/lib/db';
import { QueryFilters } from '@/lib/types';
export function getEventDataUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) {
export function getEventDataUsage(...args: [websiteIds: string[], filters: QueryFilters]) {
return runQuery({
[PRISMA]: notImplemented,
[CLICKHOUSE]: () => clickhouseQuery(...args),
@ -10,10 +11,10 @@ export function getEventDataUsage(...args: [websiteIds: string[], startDate: Dat
function clickhouseQuery(
websiteIds: string[],
startDate: Date,
endDate: Date,
filters: QueryFilters,
): Promise<{ websiteId: string; count: number }[]> {
const { rawQuery } = clickhouse;
const { startDate, endDate } = filters;
return rawQuery(
`

View file

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

View file

@ -22,7 +22,7 @@ export async function getEventMetrics(
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = prisma;
const { filterQuery, joinSession, filterParams } = await parseFilters(websiteId, {
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({
...filters,
eventType: EVENT_TYPE.customEvent,
});
@ -34,7 +34,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
${getDateSQL('website_event.created_at', unit, timezone)} t,
count(*) y
from website_event
${joinSession}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
@ -42,7 +42,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
group by 1, 2
order by 2
`,
filterParams,
queryParams,
);
}
@ -52,7 +52,7 @@ async function clickhouseQuery(
): Promise<{ x: string; t: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, {
const { filterQuery, queryParams } = await parseFilters({
...filters,
eventType: EVENT_TYPE.customEvent,
});
@ -92,5 +92,5 @@ async function clickhouseQuery(
`;
}
return rawQuery(sql, filterParams);
return rawQuery(sql, queryParams);
}

View file

@ -1,7 +1,8 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from '@/lib/db';
import { QueryFilters } from '@/lib/types';
export function getEventUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) {
export function getEventUsage(...args: [websiteIds: string[], filters: QueryFilters]) {
return runQuery({
[PRISMA]: notImplemented,
[CLICKHOUSE]: () => clickhouseQuery(...args),
@ -10,10 +11,10 @@ export function getEventUsage(...args: [websiteIds: string[], startDate: Date, e
function clickhouseQuery(
websiteIds: string[],
startDate: Date,
endDate: Date,
filters: QueryFilters,
): Promise<{ websiteId: string; count: number }[]> {
const { rawQuery } = clickhouse;
const { startDate, endDate } = filters;
return rawQuery(
`

View file

@ -1,26 +1,27 @@
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 { PageParams, QueryFilters } from '@/lib/types';
import { QueryFilters } from '@/lib/types';
export function getWebsiteEvents(
...args: [websiteId: string, filters: QueryFilters, pageParams?: PageParams]
) {
export function getWebsiteEvents(...args: [websiteId: string, filters: QueryFilters]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters } = prisma;
const { search } = pageParams;
const { filterQuery, filterParams } = await parseFilters(websiteId, {
const { search } = filters;
const { filterQuery, queryParams } = await parseFilters({
...filters,
websiteId,
});
const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like';
const searchQuery = filters.search
? `and ((event_name ilike {{search}} and event_type = 2)
or (url_path ilike {{search}} and event_type = 1))`
: '';
return pagedRawQuery(
`
@ -41,25 +42,27 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
${filterQuery}
${
search
? `and ((event_name ${like} {{search}} and event_type = 2)
or (url_path ${like} {{search}} and event_type = 1))`
: ''
}
${searchQuery}
order by created_at desc
`,
{ ...filterParams, search: `%${search}%` },
pageParams,
{ ...queryParams, search: `%${search}%` },
filters,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { pagedQuery, parseFilters } = clickhouse;
const { filterParams, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
const { search } = pageParams;
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters } = clickhouse;
const { queryParams, dateQuery, filterQuery } = await parseFilters({
...filters,
websiteId,
});
return pagedQuery(
const searchQuery = filters.search
? `and ((positionCaseInsensitive(event_name, {search:String}) > 0 and event_type = 2)
or (positionCaseInsensitive(url_path, {search:String}) > 0 and event_type = 1))`
: '';
return pagedRawQuery(
`
select
event_id as id,
@ -78,15 +81,10 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
where website_id = {websiteId:UUID}
${dateQuery}
${filterQuery}
${
search
? `and ((positionCaseInsensitive(event_name, {search:String}) > 0 and event_type = 2)
or (positionCaseInsensitive(url_path, {search:String}) > 0 and event_type = 1))`
: ''
}
${searchQuery}
order by created_at desc
`,
{ ...filterParams, search },
pageParams,
queryParams,
filters,
);
}

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 { filterParams, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
const { queryParams, filterQuery, dateQuery } = await parseFilters(filters);
return rawQuery(
`
@ -27,7 +27,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
group by 1, 2
order by visitors desc
`,
filterParams,
queryParams,
);
}
@ -36,7 +36,7 @@ async function clickhouseQuery(
filters: QueryFilters,
): Promise<{ x: string; y: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterParams, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
const { queryParams, filterQuery, dateQuery } = await parseFilters(filters);
const sql = `
select
@ -51,5 +51,5 @@ async function clickhouseQuery(
order by visitors desc
`;
return rawQuery(sql, filterParams);
return rawQuery(sql, queryParams);
}

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 { filterParams, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
const { queryParams, filterQuery, dateQuery } = await parseFilters(filters);
return rawQuery(
`
@ -35,13 +35,13 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
order by website_event.created_at desc
limit 100
`,
filterParams,
queryParams,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promise<{ x: number }> {
const { rawQuery, parseFilters } = clickhouse;
const { filterParams, filterQuery, dateQuery } = await parseFilters(websiteId, filters);
const { queryParams, filterQuery, dateQuery } = await parseFilters(filters);
return rawQuery(
`
@ -62,6 +62,6 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promis
order by createdAt desc
limit 100
`,
{ ...filters, ...filterParams },
{ ...filters, ...queryParams },
);
}

View file

@ -1,4 +1,5 @@
import { getPageviewStats, getRealtimeActivity, getSessionStats } from '@/queries';
import { QueryFilters } from '@/lib/types';
function increment(data: object, key: string) {
if (key) {
@ -10,12 +11,7 @@ function increment(data: object, key: string) {
}
}
export async function getRealtimeData(
websiteId: string,
criteria: { startDate: Date; timezone: string },
) {
const { startDate, timezone } = criteria;
const filters = { startDate, endDate: new Date(), unit: 'minute', timezone };
export async function getRealtimeData(websiteId: string, filters: QueryFilters) {
const [activity, pageviews, sessions] = await Promise.all([
getRealtimeActivity(websiteId, filters),
getPageviewStats(websiteId, filters),

View file

@ -1,9 +1,10 @@
import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db';
import { QueryFilters } from '@/lib/types';
export async function getValues(
...args: [websiteId: string, column: string, startDate: Date, endDate: Date, search: string]
...args: [websiteId: string, column: string, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@ -11,15 +12,11 @@ export async function getValues(
});
}
async function relationalQuery(
websiteId: string,
column: string,
startDate: Date,
endDate: Date,
search: string,
) {
async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) {
const { rawQuery, getSearchSQL } = prisma;
const params = {};
const { startDate, endDate, search } = filters;
let searchQuery = '';
if (search) {
@ -63,15 +60,11 @@ async function relationalQuery(
);
}
async function clickhouseQuery(
websiteId: string,
column: string,
startDate: Date,
endDate: Date,
search: string,
) {
async function clickhouseQuery(websiteId: string, column: string, filters: QueryFilters) {
const { rawQuery, getSearchSQL } = clickhouse;
const params = {};
const { startDate, endDate, search } = filters;
let searchQuery = '';
if (search) {

View file

@ -12,8 +12,9 @@ export async function getWebsiteDateRange(...args: [websiteId: string]) {
async function relationalQuery(websiteId: string) {
const { rawQuery, parseFilters } = prisma;
const { filterParams } = await parseFilters(websiteId, {
const { queryParams } = await parseFilters({
startDate: new Date(DEFAULT_RESET_DATE),
websiteId,
});
const result = await rawQuery(
@ -25,7 +26,7 @@ async function relationalQuery(websiteId: string) {
where website_id = {{websiteId::uuid}}
and created_at >= {{startDate}}
`,
filterParams,
queryParams,
);
return result[0] ?? null;
@ -33,8 +34,9 @@ async function relationalQuery(websiteId: string) {
async function clickhouseQuery(websiteId: string) {
const { rawQuery, parseFilters } = clickhouse;
const { filterParams } = await parseFilters(websiteId, {
const { queryParams } = await parseFilters({
startDate: new Date(DEFAULT_RESET_DATE),
websiteId,
});
const result = await rawQuery(
@ -46,7 +48,7 @@ async function clickhouseQuery(websiteId: string) {
where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime64}
`,
filterParams,
queryParams,
);
return result[0] ?? null;

View file

@ -5,11 +5,17 @@ import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
import { EVENT_COLUMNS } from '@/lib/constants';
export interface WebsiteStatsData {
pageviews: number;
visitors: number;
visits: number;
bounces: number;
totaltime: number;
}
export async function getWebsiteStats(
...args: [websiteId: string, filters: QueryFilters]
): Promise<
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> {
): Promise<WebsiteStatsData[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
@ -19,12 +25,11 @@ export async function getWebsiteStats(
async function relationalQuery(
websiteId: string,
filters: QueryFilters,
): Promise<
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> {
): Promise<WebsiteStatsData[]> {
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, filterParams } = await parseFilters(websiteId, {
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.pageView,
});
@ -44,7 +49,7 @@ async function relationalQuery(
min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time"
from website_event
${joinSession}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
@ -52,19 +57,18 @@ async function relationalQuery(
group by 1, 2
) as t
`,
filterParams,
queryParams,
);
}
async function clickhouseQuery(
websiteId: string,
filters: QueryFilters,
): Promise<
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> {
): Promise<WebsiteStatsData[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, {
const { filterQuery, queryParams } = await parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.pageView,
});
@ -117,5 +121,5 @@ async function clickhouseQuery(
`;
}
return rawQuery(sql, filterParams).then(result => result?.[0]);
return rawQuery(sql, queryParams).then(result => result?.[0]);
}

View file

@ -28,8 +28,7 @@ async function relationalQuery(
) {
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = prisma;
const { filterQuery, joinSession, filterParams } = await parseFilters(
websiteId,
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters(
{
...filters,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
@ -70,7 +69,7 @@ async function relationalQuery(
select ${column} x,
${column === 'referrer_domain' ? 'count(distinct website_event.session_id)' : 'count(*)'} as y
from website_event
${joinSession}
${joinSessionQuery}
${entryExitQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
@ -82,7 +81,7 @@ async function relationalQuery(
limit ${limit}
offset ${offset}
`,
filterParams,
queryParams,
);
}
@ -95,7 +94,7 @@ async function clickhouseQuery(
): Promise<{ x: string; y: number }[]> {
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, {
const { filterQuery, queryParams } = await parseFilters({
...filters,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
});
@ -180,5 +179,5 @@ async function clickhouseQuery(
`;
}
return rawQuery(sql, filterParams);
return rawQuery(sql, queryParams);
}

View file

@ -14,8 +14,9 @@ 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, joinSession, filterParams } = await parseFilters(websiteId, {
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.pageView,
});
@ -25,7 +26,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
${getDateSQL('website_event.created_at', unit, timezone)} x,
count(*) y
from website_event
${joinSession}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
@ -33,7 +34,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
group by 1
order by 1
`,
filterParams,
queryParams,
);
}
@ -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, filterParams } = await parseFilters(websiteId, {
const { filterQuery, queryParams } = await parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.pageView,
});
@ -88,5 +90,5 @@ async function clickhouseQuery(
`;
}
return rawQuery(sql, filterParams);
return rawQuery(sql, queryParams);
}

View file

@ -244,7 +244,7 @@ async function clickhouseQuery(
const { rawQuery, parseFilters } = clickhouse;
const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = type === 'page' ? 'url_path' : 'event_name';
const { filterQuery, filterParams } = await parseFilters(websiteId, criteria);
const { filterQuery, queryParams } = await parseFilters(criteria);
function getUTMQuery(utmColumn: string) {
return `
@ -345,7 +345,7 @@ async function clickhouseQuery(
order by 2 desc
limit 20
`,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
);
const paidAdsres = await rawQuery<
@ -376,7 +376,7 @@ async function clickhouseQuery(
order by 2 desc
limit 20
`,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
);
const sourceRes = await rawQuery<
@ -390,7 +390,7 @@ async function clickhouseQuery(
${getModelQuery(model)}
${getUTMQuery('utm_source')}
`,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
);
const mediumRes = await rawQuery<
@ -404,7 +404,7 @@ async function clickhouseQuery(
${getModelQuery(model)}
${getUTMQuery('utm_medium')}
`,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
);
const campaignRes = await rawQuery<
@ -418,7 +418,7 @@ async function clickhouseQuery(
${getModelQuery(model)}
${getUTMQuery('utm_campaign')}
`,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
);
const contentRes = await rawQuery<
@ -432,7 +432,7 @@ async function clickhouseQuery(
${getModelQuery(model)}
${getUTMQuery('utm_content')}
`,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
);
const termRes = await rawQuery<
@ -446,7 +446,7 @@ async function clickhouseQuery(
${getModelQuery(model)}
${getUTMQuery('utm_term')}
`,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
);
const totalRes = await rawQuery<{ pageviews: number; visitors: number; visits: number }>(
@ -462,7 +462,7 @@ async function clickhouseQuery(
and event_type = {eventType:UInt32}
${filterQuery}
`,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
{ ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
).then(result => result?.[0]);
return {

View file

@ -24,8 +24,7 @@ async function relationalQuery(
}[]
> {
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, filterParams } = await parseFilters(
websiteId,
const { filterQuery, joinSessionQuery, queryParams } = await parseFilters(
{
...filters,
eventType: EVENT_TYPE.pageView,
@ -53,7 +52,7 @@ async function relationalQuery(
min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time"
from website_event
${joinSession}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
@ -65,7 +64,7 @@ async function relationalQuery(
order by 1 desc, 2 desc
limit 500
`,
filterParams,
queryParams,
);
}
@ -80,7 +79,7 @@ async function clickhouseQuery(
}[]
> {
const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, {
const { filterQuery, queryParams } = await parseFilters({
...filters,
eventType: EVENT_TYPE.pageView,
});
@ -114,7 +113,7 @@ async function clickhouseQuery(
order by 1 desc, 2 desc
limit 500
`,
filterParams,
queryParams,
);
}

View file

@ -126,7 +126,7 @@ async function clickhouseQuery(
steps,
windowMinutes,
);
const { filterQuery, filterParams: filterParams } = await parseFilters(websiteId, criteria);
const { filterQuery, queryParams } = await parseFilters(criteria);
function getFunnelQuery(
steps: { type: string; value: string }[],
@ -136,7 +136,7 @@ async function clickhouseQuery(
levelQuery: string;
sumQuery: string;
stepFilterQuery: string;
params: { [key: string]: string };
params: Record<string, string>;
} {
return steps.reduce(
(pv, cv, i) => {
@ -215,7 +215,7 @@ async function clickhouseQuery(
startDate,
endDate,
...params,
...filterParams,
...queryParams,
},
).then(formatResults(steps));
}

View file

@ -1,6 +1,7 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface GoalCriteria {
startDate: Date;
@ -9,6 +10,7 @@ export interface GoalCriteria {
value: string;
operator?: string;
property?: string;
filters: QueryFilters;
}
export async function getGoal(...args: [websiteId: string, criteria: GoalCriteria]) {
@ -19,9 +21,13 @@ export async function getGoal(...args: [websiteId: string, criteria: GoalCriteri
}
async function relationalQuery(websiteId: string, criteria: GoalCriteria) {
const { type, value } = criteria;
const { type, value, filters } = criteria;
const { rawQuery, parseFilters } = prisma;
const { filterQuery, dateQuery, filterParams } = await parseFilters(websiteId, criteria);
const { filterQuery, dateQuery, queryParams } = await parseFilters({
...filters,
websiteId,
value,
});
const isPage = type === 'page';
const column = isPage ? 'url_path' : 'event_name';
const eventType = isPage ? 1 : 2;
@ -43,14 +49,18 @@ async function relationalQuery(websiteId: string, criteria: GoalCriteria) {
${dateQuery}
${filterQuery}
`,
{ ...filterParams, value },
queryParams,
);
}
async function clickhouseQuery(websiteId: string, criteria: GoalCriteria) {
const { type, value } = criteria;
const { type, value, filters } = criteria;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, dateQuery, filterParams } = await parseFilters(websiteId, criteria);
const { filterQuery, dateQuery, queryParams } = await parseFilters({
...filters,
websiteId,
value,
});
const isPage = type === 'page';
const column = isPage ? 'url_path' : 'event_name';
const eventType = isPage ? 1 : 2;
@ -71,6 +81,6 @@ async function clickhouseQuery(websiteId: string, criteria: GoalCriteria) {
${dateQuery}
${filterQuery}
`,
{ ...filterParams, value },
queryParams,
).then(results => results?.[0]);
}

View file

@ -57,7 +57,7 @@ async function relationalQuery(
sequenceQuery: string;
startStepQuery: string;
endStepQuery: string;
params: { [key: string]: string };
params: Record<string, string>;
} {
const params = {};
let sequenceQuery = '';
@ -108,7 +108,7 @@ async function relationalQuery(
sequenceQuery,
startStepQuery,
endStepQuery,
filterParams: params,
params,
};
}
@ -167,7 +167,7 @@ async function clickhouseQuery(
sequenceQuery: string;
startStepQuery: string;
endStepQuery: string;
params: { [key: string]: string };
params: Record<string, string>;
} {
const params = {};
let sequenceQuery = '';
@ -218,11 +218,11 @@ async function clickhouseQuery(
sequenceQuery,
startStepQuery,
endStepQuery,
filterParams: params,
params,
};
}
const { filterQuery, filterParams: filterParams } = await parseFilters(websiteId, filters);
const { filterQuery, queryParams } = await parseFilters(filters);
return rawQuery(
`
@ -249,7 +249,7 @@ async function clickhouseQuery(
startDate,
endDate,
...params,
...filterParams,
...queryParams,
},
).then(parseResult);
}

View file

@ -31,7 +31,7 @@ async function relationalQuery(
const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery, parseFilters } = prisma;
const unit = 'day';
const { filterQuery, filterParams } = await parseFilters(websiteId, criteria);
const { filterQuery, queryParams } = await parseFilters(criteria);
return rawQuery(
`
@ -85,7 +85,7 @@ async function relationalQuery(
websiteId,
startDate,
endDate,
...filterParams,
...queryParams,
},
);
}
@ -98,7 +98,7 @@ async function clickhouseQuery(
const { getDateSQL, rawQuery, parseFilters } = clickhouse;
const unit = 'day';
const { filterQuery, filterParams } = await parseFilters(websiteId, criteria);
const { filterQuery, queryParams } = await parseFilters(criteria);
return rawQuery(
`
@ -154,7 +154,7 @@ async function clickhouseQuery(
websiteId,
startDate,
endDate,
...filterParams,
...queryParams,
},
);
}

View file

@ -17,7 +17,7 @@ export async function getUTM(...args: [websiteId: string, criteria: UTMCriteria]
async function relationalQuery(websiteId: string, criteria: UTMCriteria) {
const { startDate, endDate } = criteria;
const { rawQuery, parseFilters } = prisma;
const { filterQuery, filterParams } = await parseFilters(websiteId, criteria);
const { filterQuery, queryParams } = await parseFilters(criteria);
return rawQuery(
`
@ -31,7 +31,7 @@ async function relationalQuery(websiteId: string, criteria: UTMCriteria) {
group by 1
`,
{
...filterParams,
...queryParams,
websiteId,
startDate,
endDate,
@ -42,7 +42,7 @@ async function relationalQuery(websiteId: string, criteria: UTMCriteria) {
async function clickhouseQuery(websiteId: string, criteria: UTMCriteria) {
const { startDate, endDate } = criteria;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, criteria);
const { filterQuery, queryParams } = await parseFilters(criteria);
return rawQuery(
`
@ -56,7 +56,7 @@ async function clickhouseQuery(websiteId: string, criteria: UTMCriteria) {
group by 1
`,
{
...filterParams,
...queryParams,
websiteId,
startDate,
endDate,

View file

@ -1,9 +1,10 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export async function getSessionActivity(
...args: [websiteId: string, sessionId: string, startDate: Date, endDate: Date]
...args: [websiteId: string, sessionId: string, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@ -11,12 +12,9 @@ export async function getSessionActivity(
});
}
async function relationalQuery(
websiteId: string,
sessionId: string,
startDate: Date,
endDate: Date,
) {
async function relationalQuery(websiteId: string, sessionId: string, filters: QueryFilters) {
const { startDate, endDate } = filters;
return prisma.client.websiteEvent.findMany({
where: {
sessionId,
@ -28,13 +26,9 @@ async function relationalQuery(
});
}
async function clickhouseQuery(
websiteId: string,
sessionId: string,
startDate: Date,
endDate: Date,
) {
async function clickhouseQuery(websiteId: string, sessionId: string, filters: QueryFilters) {
const { rawQuery } = clickhouse;
const { startDate, endDate } = filters;
return rawQuery(
`

Some files were not shown because too many files have changed in this diff Show more