Updated session and events queries. Added sessions page.

This commit is contained in:
Mike Cao 2024-07-08 01:45:54 -07:00
parent 082a751ffe
commit db36c37d32
39 changed files with 376 additions and 180 deletions

View File

@ -46,9 +46,14 @@ export function WebsiteHeader({
path: '/reports', path: '/reports',
}, },
{ {
label: formatMessage(labels.eventData), label: formatMessage(labels.sessions),
icon: <Icons.User />,
path: '/sessions',
},
{
label: formatMessage(labels.events),
icon: <Icons.Nodes />, icon: <Icons.Nodes />,
path: '/event-data', path: '/events',
}, },
]; ];

View File

@ -144,7 +144,10 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
const { events, visitors } = data; const { events, visitors } = data;
let logs = [ let logs = [
...events.map(e => ({ __type: e.eventName ? TYPE_EVENT : TYPE_PAGEVIEW, ...e })), ...events.map(e => ({
__type: e.eventName ? TYPE_EVENT : TYPE_PAGEVIEW,
...e,
})),
...visitors.map(v => ({ __type: TYPE_SESSION, ...v })), ...visitors.map(v => ({ __type: TYPE_SESSION, ...v })),
].sort(thenby.firstBy('timestamp', -1)); ].sort(thenby.firstBy('timestamp', -1));

View File

@ -0,0 +1,25 @@
import { useSessions } from 'components/hooks';
import SessionsTable from './SessionsTable';
import DataTable from 'components/common/DataTable';
import { ReactNode } from 'react';
export default function SessionsDataTable({
websiteId,
children,
}: {
websiteId?: string;
teamId?: string;
children?: ReactNode;
}) {
const queryResult = useSessions(websiteId);
if (queryResult?.result?.data?.length === 0) {
return children;
}
return (
<DataTable queryResult={queryResult} allowSearch={false}>
{({ data }) => <SessionsTable data={data} showDomain={!websiteId} />}
</DataTable>
);
}

View File

@ -0,0 +1,14 @@
'use client';
import WebsiteHeader from '../WebsiteHeader';
import SessionsDataTable from './SessionsDataTable';
export function SessionsPage({ websiteId }) {
return (
<>
<WebsiteHeader websiteId={websiteId} />
<SessionsDataTable websiteId={websiteId} />
</>
);
}
export default SessionsPage;

View File

@ -0,0 +1,21 @@
import { GridColumn, GridTable, useBreakpoint } from 'react-basics';
import { useMessages } from 'components/hooks';
export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean }) {
const { formatMessage, labels } = useMessages();
const breakpoint = useBreakpoint();
return (
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
<GridColumn name="id" label="ID" />
<GridColumn name="country" label={formatMessage(labels.country)} />
<GridColumn name="city" label={formatMessage(labels.city)} />
<GridColumn name="browser" label={formatMessage(labels.browser)} />
<GridColumn name="os" label={formatMessage(labels.os)} />
<GridColumn name="device" label={formatMessage(labels.device)} />
<GridColumn name="createdAt" label={formatMessage(labels.created)} />
</GridTable>
);
}
export default SessionsTable;

View File

@ -0,0 +1,10 @@
import SessionsPage from './SessionsPage';
import { Metadata } from 'next';
export default function ({ params: { websiteId } }) {
return <SessionsPage websiteId={websiteId} />;
}
export const metadata: Metadata = {
title: 'Sessions',
};

View File

@ -0,0 +1,28 @@
import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants';
export async function GET() {
if (
process.env.NODE_ENV !== 'production' &&
process.env.DISABLE_TELEMETRY &&
process.env.PRIVATE_MODE
) {
const script = `
(()=>{const i=document.createElement('img');
i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}');
i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;');
document.body.appendChild(i);})();
`;
return new Response(script.replace(/\s\s+/g, ''), {
headers: {
'content-type': 'text/javascript',
},
});
}
return new Response('/* telemetry disabled */', {
headers: {
'content-type': 'text/javascript',
},
});
}

View File

@ -5,6 +5,7 @@ export * from './queries/useLogin';
export * from './queries/useRealtime'; export * from './queries/useRealtime';
export * from './queries/useReport'; export * from './queries/useReport';
export * from './queries/useReports'; export * from './queries/useReports';
export * from './queries/useSessions';
export * from './queries/useShareToken'; export * from './queries/useShareToken';
export * from './queries/useTeam'; export * from './queries/useTeam';
export * from './queries/useTeams'; export * from './queries/useTeams';

View File

@ -0,0 +1,20 @@
import { useApi } from './useApi';
import { useFilterQuery } from './useFilterQuery';
import useModified from '../useModified';
export function useSessions(websiteId: string, params?: { [key: string]: string | number }) {
const { get } = useApi();
const { modified } = useModified(`websites`);
return useFilterQuery({
queryKey: ['sessions', { websiteId, modified, ...params }],
queryFn: (data: any) => {
return get(`/websites/${websiteId}/sessions`, {
...data,
...params,
});
},
});
}
export default useSessions;

View File

@ -2,8 +2,8 @@ import { ClickHouseClient, createClient } from '@clickhouse/client';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import debug from 'debug'; import debug from 'debug';
import { CLICKHOUSE } from 'lib/db'; import { CLICKHOUSE } from 'lib/db';
import { QueryFilters, QueryOptions } from './types'; import { PageParams, QueryFilters, QueryOptions } from './types';
import { OPERATORS } from './constants'; import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants';
import { fetchWebsite } from './load'; import { fetchWebsite } from './load';
import { maxDate } from './date'; import { maxDate } from './date';
import { filtersToArray } from './params'; import { filtersToArray } from './params';
@ -47,11 +47,11 @@ function getClient() {
return client; return client;
} }
function getDateStringQuery(data: any, unit: string | number) { function getDateStringSQL(data: any, unit: string | number) {
return `formatDateTime(${data}, '${CLICKHOUSE_DATE_FORMATS[unit]}')`; return `formatDateTime(${data}, '${CLICKHOUSE_DATE_FORMATS[unit]}')`;
} }
function getDateQuery(field: string, unit: string, timezone?: string) { function getDateSQL(field: string, unit: string, timezone?: string) {
if (timezone) { if (timezone) {
return `date_trunc('${unit}', ${field}, '${timezone}')`; return `date_trunc('${unit}', ${field}, '${timezone}')`;
} }
@ -95,6 +95,20 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {})
return query.join('\n'); return query.join('\n');
} }
function getDateQuery(filters: QueryFilters = {}) {
const { startDate, endDate } = filters;
if (startDate) {
if (endDate) {
return `and created_at between {startDate:DateTime64} and {endDate:DateTime64}`;
} else {
return `and created_at >= {startDate:DateTime64}`;
}
}
return '';
}
function getFilterParams(filters: QueryFilters = {}) { function getFilterParams(filters: QueryFilters = {}) {
return filtersToArray(filters).reduce((obj, { name, value }) => { return filtersToArray(filters).reduce((obj, { name, value }) => {
if (name && value !== undefined) { if (name && value !== undefined) {
@ -110,6 +124,7 @@ async function parseFilters(websiteId: string, filters: QueryFilters = {}, optio
return { return {
filterQuery: getFilterQuery(filters, options), filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters),
params: { params: {
...getFilterParams(filters), ...getFilterParams(filters),
websiteId, websiteId,
@ -119,6 +134,32 @@ async function parseFilters(websiteId: string, filters: QueryFilters = {}, optio
}; };
} }
async function pagedQuery(
query: string,
queryParams: { [key: string]: any },
pageParams: PageParams = {},
) {
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams;
const size = +pageSize || DEFAULT_PAGE_SIZE;
const offset = +size * (page - 1);
const direction = sortDescending ? 'desc' : 'asc';
const statements = [
orderBy && `order by ${orderBy} ${direction}`,
+size > 0 && `limit ${+size} offset ${offset}`,
]
.filter(n => n)
.join('\n');
const count = await rawQuery(`select count(*) as num from (${query}) t`, queryParams).then(
res => res[0].num,
);
const data = await rawQuery(`${query}${statements}`, queryParams);
return { data, count, page: +page, pageSize: size, orderBy };
}
async function rawQuery<T = unknown>( async function rawQuery<T = unknown>(
query: string, query: string,
params: Record<string, unknown> = {}, params: Record<string, unknown> = {},
@ -170,11 +211,12 @@ export default {
client: clickhouse, client: clickhouse,
log, log,
connect, connect,
getDateStringQuery, getDateStringSQL,
getDateQuery, getDateSQL,
getDateFormat, getDateFormat,
getFilterQuery, getFilterQuery,
parseFilters, parseFilters,
pagedQuery,
findUnique, findUnique,
findFirst, findFirst,
rawQuery, rawQuery,

View File

@ -60,7 +60,7 @@ function getCastColumnQuery(field: string, type: string): string {
} }
} }
function getDateQuery(field: string, unit: string, timezone?: string): string { function getDateSQL(field: string, unit: string, timezone?: string): string {
const db = getDatabaseType(); const db = getDatabaseType();
if (db === POSTGRESQL) { if (db === POSTGRESQL) {
@ -81,7 +81,19 @@ function getDateQuery(field: string, unit: string, timezone?: string): string {
} }
} }
function getTimestampDiffQuery(field1: string, field2: string): string { export function getTimestampSQL(field: string) {
const db = getDatabaseType();
if (db === POSTGRESQL) {
return `floor(extract(epoch from ${field}))`;
}
if (db === MYSQL) {
return `UNIX_TIMESTAMP(${field})`;
}
}
function getTimestampDiffSQL(field1: string, field2: string): string {
const db = getDatabaseType(); const db = getDatabaseType();
if (db === POSTGRESQL) { if (db === POSTGRESQL) {
@ -93,7 +105,7 @@ function getTimestampDiffQuery(field1: string, field2: string): string {
} }
} }
function getSearchQuery(column: string): string { function getSearchSQL(column: string): string {
const db = getDatabaseType(); const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like'; const like = db === POSTGRESQL ? 'ilike' : 'like';
@ -137,6 +149,20 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}):
return query.join('\n'); return query.join('\n');
} }
function getDateQuery(filters: QueryFilters = {}) {
const { startDate, endDate } = filters;
if (startDate) {
if (endDate) {
return `and website_event.created_at between {{startDate}} and {{endDate}}`;
} else {
return `and website_event.created_at >= {{startDate}}`;
}
}
return '';
}
function getFilterParams(filters: QueryFilters = {}) { function getFilterParams(filters: QueryFilters = {}) {
return filtersToArray(filters).reduce((obj, { name, operator, value }) => { return filtersToArray(filters).reduce((obj, { name, operator, value }) => {
obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator) obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator)
@ -161,6 +187,7 @@ async function parseFilters(
? `inner join session on website_event.session_id = session.session_id` ? `inner join session on website_event.session_id = session.session_id`
: '', : '',
filterQuery: getFilterQuery(filters, options), filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters),
params: { params: {
...getFilterParams(filters), ...getFilterParams(filters),
websiteId, websiteId,
@ -191,8 +218,8 @@ async function rawQuery(sql: string, data: object): Promise<any> {
return prisma.rawQuery(query, params); return prisma.rawQuery(query, params);
} }
async function pagedQuery<T>(model: string, criteria: T, filters: PageParams) { async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams) {
const { page = 1, pageSize, orderBy, sortDescending = false } = filters || {}; const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams || {};
const size = +pageSize || DEFAULT_PAGE_SIZE; const size = +pageSize || DEFAULT_PAGE_SIZE;
const data = await prisma.client[model].findMany({ const data = await prisma.client[model].findMany({
@ -256,11 +283,11 @@ export default {
getAddIntervalQuery, getAddIntervalQuery,
getCastColumnQuery, getCastColumnQuery,
getDayDiffQuery, getDayDiffQuery,
getDateQuery, getDateSQL,
getFilterQuery, getFilterQuery,
getSearchParameters, getSearchParameters,
getTimestampDiffQuery, getTimestampDiffSQL,
getSearchQuery, getSearchSQL,
getQueryMode, getQueryMode,
pagedQuery, pagedQuery,
parseFilters, parseFilters,

View File

@ -1,24 +0,0 @@
import { ok } from 'next-basics';
import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants';
import { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (process.env.NODE_ENV === 'production') {
res.setHeader('content-type', 'text/javascript');
if (process.env.DISABLE_TELEMETRY || process.env.PRIVATE_MODE) {
return res.send('/* telemetry disabled */');
}
const script = `
(()=>{const i=document.createElement('img');
i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}');
i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;');
document.body.appendChild(i);})();
`;
return res.send(script.replace(/\s\s+/g, ''));
}
return ok(res);
}

View File

@ -0,0 +1,42 @@
import * as yup from 'yup';
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { pageInfo } from 'lib/schema';
import { getSessions } from 'queries';
export interface ReportsRequestQuery extends PageParams {
websiteId: string;
}
const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
...pageInfo,
}),
};
export default async (
req: NextApiRequestQueryBody<ReportsRequestQuery, any>,
res: NextApiResponse,
) => {
await useCors(req, res);
await useAuth(req, res);
await useValidate(schema, req, res);
const { websiteId } = req.query;
if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) {
return unauthorized(res);
}
const data = await getSessions(websiteId, {}, req.query);
return ok(res, data);
}
return methodNotAllowed(res);
};

View File

@ -15,7 +15,7 @@ export async function getEventMetrics(
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { rawQuery, getDateQuery, parseFilters } = prisma; const { rawQuery, getDateSQL, parseFilters } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, { const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.customEvent, eventType: EVENT_TYPE.customEvent,
@ -25,7 +25,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
` `
select select
event_name x, event_name x,
${getDateQuery('website_event.created_at', unit, timezone)} t, ${getDateSQL('website_event.created_at', unit, timezone)} t,
count(*) y count(*) y
from website_event from website_event
${joinSession} ${joinSession}
@ -45,7 +45,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ x: string; t: string; y: number }[]> { ): Promise<{ x: string; t: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters; const { timezone = 'UTC', unit = 'day' } = filters;
const { rawQuery, getDateQuery, parseFilters } = clickhouse; const { rawQuery, getDateSQL, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, { const { filterQuery, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.customEvent, eventType: EVENT_TYPE.customEvent,
@ -55,7 +55,7 @@ async function clickhouseQuery(
` `
select select
event_name x, event_name x,
${getDateQuery('created_at', unit, timezone)} t, ${getDateSQL('created_at', unit, timezone)} t,
count(*) y count(*) y
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}

View File

@ -1,45 +1,33 @@
import clickhouse from 'lib/clickhouse'; import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { QueryFilters } from 'lib/types'; import { PageParams, QueryFilters } from 'lib/types';
export function getEvents(...args: [websiteId: string, filters: QueryFilters]) { export function getEvents(
...args: [websiteId: string, filters: QueryFilters, pageParams?: PageParams]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
}); });
} }
function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { startDate } = filters; const { pagedQuery } = prisma;
return prisma.client.websiteEvent const where = {
.findMany({ ...filters,
where: { id: websiteId,
websiteId,
createdAt: {
gte: startDate,
},
},
orderBy: {
createdAt: 'desc',
},
})
.then(a => {
return Object.values(a).map(a => {
return {
...a,
timestamp: new Date(a.createdAt).getTime() / 1000,
}; };
});
}); return pagedQuery('website_event', { where }, pageParams);
} }
function clickhouseQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { rawQuery } = clickhouse; const { pagedQuery, parseFilters } = clickhouse;
const { startDate } = filters; const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
return rawQuery( return pagedQuery(
` `
select select
event_id as id, event_id as id,
@ -48,16 +36,20 @@ function clickhouseQuery(websiteId: string, filters: QueryFilters) {
created_at as createdAt, created_at as createdAt,
toUnixTimestamp(created_at) as timestamp, toUnixTimestamp(created_at) as timestamp,
url_path as urlPath, url_path as urlPath,
url_query as urlQuery,
referrer_path as referrerPath,
referrer_query as referrerQuery,
referrer_domain as referrerDomain, referrer_domain as referrerDomain,
page_title as pageTitle,
event_type as eventType,
event_name as eventName event_name as eventName
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime64} ${dateQuery}
${filterQuery}
order by created_at desc order by created_at desc
`, `,
{ params,
websiteId, pageParams,
startDate,
},
); );
} }

View File

@ -19,15 +19,15 @@ export async function getRealtimeData(
const { startDate, timezone } = criteria; const { startDate, timezone } = criteria;
const filters = { startDate, endDate: new Date(), unit: 'minute', timezone }; const filters = { startDate, endDate: new Date(), unit: 'minute', timezone };
const [events, sessions, pageviews, sessionviews] = await Promise.all([ const [events, sessions, pageviews, sessionviews] = await Promise.all([
getEvents(websiteId, { startDate }), getEvents(websiteId, { startDate }, { pageSize: 10000 }),
getSessions(websiteId, { startDate }), getSessions(websiteId, { startDate }, { pageSize: 10000 }),
getPageviewStats(websiteId, filters), getPageviewStats(websiteId, filters),
getSessionStats(websiteId, filters), getSessionStats(websiteId, filters),
]); ]);
const uniques = new Set(); const uniques = new Set();
const sessionStats = sessions.reduce( const sessionStats = sessions.data.reduce(
(obj: { visitors: any; countries: any }, session: { id: any; country: any }) => { (obj: { visitors: any; countries: any }, session: { id: any; country: any }) => {
const { countries, visitors } = obj; const { countries, visitors } = obj;
const { id, country } = session; const { id, country } = session;
@ -49,7 +49,7 @@ export async function getRealtimeData(
}, },
); );
const eventStats = events.reduce( const eventStats = events.data.reduce(
( (
obj: { urls: any; referrers: any; events: any }, obj: { urls: any; referrers: any; events: any },
event: { urlPath: any; referrerDomain: any }, event: { urlPath: any; referrerDomain: any },
@ -81,9 +81,9 @@ export async function getRealtimeData(
visitors: sessionviews, visitors: sessionviews,
}, },
totals: { totals: {
views: events.filter(e => !e.eventName).length, views: events.data.filter(e => !e.eventName).length,
visitors: uniques.size, visitors: uniques.size,
events: events.filter(e => e.eventName).length, events: events.data.filter(e => e.eventName).length,
countries: Object.keys(sessionStats.countries).length, countries: Object.keys(sessionStats.countries).length,
}, },
timestamp: Date.now(), timestamp: Date.now(),

View File

@ -18,11 +18,11 @@ async function relationalQuery(
endDate: Date, endDate: Date,
search: string, search: string,
) { ) {
const { rawQuery, getSearchQuery } = prisma; const { rawQuery, getSearchSQL } = prisma;
let searchQuery = ''; let searchQuery = '';
if (search) { if (search) {
searchQuery = getSearchQuery(column); searchQuery = getSearchSQL(column);
} }
return rawQuery( return rawQuery(

View File

@ -21,7 +21,7 @@ async function relationalQuery(
): Promise< ): Promise<
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[] { pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> { > {
const { getTimestampDiffQuery, parseFilters, rawQuery } = prisma; const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, { const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -34,7 +34,7 @@ async function relationalQuery(
count(distinct t.session_id) as "visitors", count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits", count(distinct t.visit_id) as "visits",
sum(case when t.c = 1 then 1 else 0 end) as "bounces", sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffQuery('t.min_time', 't.max_time')}) as "totaltime" sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from ( from (
select select
website_event.session_id, website_event.session_id,

View File

@ -42,15 +42,18 @@ async function relationalQuery(
const aggregrate = type === 'entry' ? 'min' : 'max'; const aggregrate = type === 'entry' ? 'min' : 'max';
entryExitQuery = ` entryExitQuery = `
JOIN (select visit_id, join (
select visit_id,
${aggregrate}(created_at) target_created_at ${aggregrate}(created_at) target_created_at
from website_event from website_event
where website_event.website_id = {{websiteId::uuid}} where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}} and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}} and event_type = {{eventType}}
group by visit_id) x group by visit_id
ON x.visit_id = website_event.visit_id ) x
and x.target_created_at = website_event.created_at`; on x.visit_id = website_event.visit_id
and x.target_created_at = website_event.created_at
`;
} }
return rawQuery( return rawQuery(
@ -97,15 +100,18 @@ async function clickhouseQuery(
const aggregrate = type === 'entry' ? 'min' : 'max'; const aggregrate = type === 'entry' ? 'min' : 'max';
entryExitQuery = ` entryExitQuery = `
JOIN (select visit_id, join (
select visit_id,
${aggregrate}(created_at) target_created_at ${aggregrate}(created_at) target_created_at
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64} and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32} and event_type = {eventType:UInt32}
group by visit_id) x group by visit_id
ON x.visit_id = website_event.visit_id ) x
and x.target_created_at = website_event.created_at`; on x.visit_id = website_event.visit_id
and x.target_created_at = website_event.created_at
`;
} }
return rawQuery( return rawQuery(

View File

@ -13,7 +13,7 @@ export async function getPageviewStats(...args: [websiteId: string, filters: Que
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { getDateQuery, parseFilters, rawQuery } = prisma; const { getDateSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, { const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -22,7 +22,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
return rawQuery( return rawQuery(
` `
select select
${getDateQuery('website_event.created_at', unit, timezone)} x, ${getDateSQL('website_event.created_at', unit, timezone)} x,
count(*) y count(*) y
from website_event from website_event
${joinSession} ${joinSession}
@ -41,7 +41,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters; const { timezone = 'UTC', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse; const { parseFilters, rawQuery, getDateStringSQL, getDateSQL } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, { const { filterQuery, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -50,11 +50,11 @@ async function clickhouseQuery(
return rawQuery( return rawQuery(
` `
select select
${getDateStringQuery('g.t', unit)} as x, ${getDateStringSQL('g.t', unit)} as x,
g.y as y g.y as y
from ( from (
select select
${getDateQuery('created_at', unit, timezone)} as t, ${getDateSQL('created_at', unit, timezone)} as t,
count(*) as y count(*) as y
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}

View File

@ -23,7 +23,7 @@ async function relationalQuery(
y: number; y: number;
}[] }[]
> { > {
const { getTimestampDiffQuery, parseFilters, rawQuery } = prisma; const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters( const { filterQuery, joinSession, params } = await parseFilters(
websiteId, websiteId,
{ {
@ -42,7 +42,7 @@ async function relationalQuery(
count(distinct t.session_id) as "visitors", count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits", count(distinct t.visit_id) as "visits",
sum(case when t.c = 1 then 1 else 0 end) as "bounces", sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffQuery('t.min_time', 't.max_time')}) as "totaltime", sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime",
${parseFieldsByName(fields)} ${parseFieldsByName(fields)}
from ( from (
select select

View File

@ -35,14 +35,14 @@ async function relationalQuery(
}[] }[]
> { > {
const { startDate, endDate, timezone = 'UTC' } = filters; const { startDate, endDate, timezone = 'UTC' } = filters;
const { getDateQuery, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma; const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma;
const unit = 'day'; const unit = 'day';
return rawQuery( return rawQuery(
` `
WITH cohort_items AS ( WITH cohort_items AS (
select session_id, select session_id,
${getDateQuery('created_at', unit, timezone)} as cohort_date ${getDateSQL('created_at', unit, timezone)} as cohort_date
from session from session
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}} and created_at between {{startDate}} and {{endDate}}
@ -50,10 +50,7 @@ async function relationalQuery(
user_activities AS ( user_activities AS (
select distinct select distinct
w.session_id, w.session_id,
${getDayDiffQuery( ${getDayDiffQuery(getDateSQL('created_at', unit, timezone), 'c.cohort_date')} as day_number
getDateQuery('created_at', unit, timezone),
'c.cohort_date',
)} as day_number
from website_event w from website_event w
join cohort_items c join cohort_items c
on w.session_id = c.session_id on w.session_id = c.session_id
@ -115,14 +112,14 @@ async function clickhouseQuery(
}[] }[]
> { > {
const { startDate, endDate, timezone = 'UTC' } = filters; const { startDate, endDate, timezone = 'UTC' } = filters;
const { getDateQuery, getDateStringQuery, rawQuery } = clickhouse; const { getDateSQL, getDateStringSQL, rawQuery } = clickhouse;
const unit = 'day'; const unit = 'day';
return rawQuery( return rawQuery(
` `
WITH cohort_items AS ( WITH cohort_items AS (
select select
min(${getDateQuery('created_at', unit, timezone)}) as cohort_date, min(${getDateSQL('created_at', unit, timezone)}) as cohort_date,
session_id session_id
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
@ -132,7 +129,7 @@ async function clickhouseQuery(
user_activities AS ( user_activities AS (
select distinct select distinct
w.session_id, w.session_id,
(${getDateQuery('created_at', unit, timezone)} - c.cohort_date) / 86400 as day_number (${getDateSQL('created_at', unit, timezone)} - c.cohort_date) / 86400 as day_number
from website_event w from website_event w
join cohort_items c join cohort_items c
on w.session_id = c.session_id on w.session_id = c.session_id
@ -157,7 +154,7 @@ async function clickhouseQuery(
group by 1, 2 group by 1, 2
) )
select select
${getDateStringQuery('c.cohort_date', unit)} as date, ${getDateStringSQL('c.cohort_date', unit)} as date,
c.day_number as day, c.day_number as day,
s.visitors as visitors, s.visitors as visitors,
c.visitors returnVisitors, c.visitors returnVisitors,

View File

@ -46,12 +46,12 @@ async function relationalQuery(
timezone = 'UTC', timezone = 'UTC',
unit = 'day', unit = 'day',
} = criteria; } = criteria;
const { getDateQuery, rawQuery } = prisma; const { getDateSQL, rawQuery } = prisma;
const chartRes = await rawQuery( const chartRes = await rawQuery(
` `
select select
${getDateQuery('website_event.created_at', unit, timezone)} time, ${getDateSQL('website_event.created_at', unit, timezone)} time,
sum(case when data_key = {{revenueProperty}} then number_value else 0 end) sum, sum(case when data_key = {{revenueProperty}} then number_value else 0 end) sum,
avg(case when data_key = {{revenueProperty}} then number_value else 0 end) avg, avg(case when data_key = {{revenueProperty}} then number_value else 0 end) avg,
count(case when data_key = {{revenueProperty}} then 1 else 0 end) count, count(case when data_key = {{revenueProperty}} then 1 else 0 end) count,
@ -110,7 +110,7 @@ async function clickhouseQuery(
timezone = 'UTC', timezone = 'UTC',
unit = 'day', unit = 'day',
} = criteria; } = criteria;
const { getDateStringQuery, getDateQuery, rawQuery } = clickhouse; const { getDateStringSQL, getDateSQL, rawQuery } = clickhouse;
const chartRes = await rawQuery<{ const chartRes = await rawQuery<{
time: string; time: string;
@ -121,14 +121,14 @@ async function clickhouseQuery(
}>( }>(
` `
select select
${getDateStringQuery('g.time', unit)} as time, ${getDateStringSQL('g.time', unit)} as time,
g.sum as sum, g.sum as sum,
g.avg as avg, g.avg as avg,
g.count as count, g.count as count,
g.uniqueCount as uniqueCount g.uniqueCount as uniqueCount
from ( from (
select select
${getDateQuery('created_at', unit, timezone)} as time, ${getDateSQL('created_at', unit, timezone)} as time,
sumIf(number_value, data_key = {revenueProperty:String}) as sum, sumIf(number_value, data_key = {revenueProperty:String}) as sum,
avgIf(number_value, data_key = {revenueProperty:String}) as avg, avgIf(number_value, data_key = {revenueProperty:String}) as avg,
countIf(data_key = {revenueProperty:String}) as count, countIf(data_key = {revenueProperty:String}) as count,

View File

@ -13,7 +13,7 @@ export async function getSessionStats(...args: [websiteId: string, filters: Quer
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { getDateQuery, parseFilters, rawQuery } = prisma; const { getDateSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, params } = await parseFilters(websiteId, { const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -22,7 +22,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
return rawQuery( return rawQuery(
` `
select select
${getDateQuery('website_event.created_at', unit, timezone)} x, ${getDateSQL('website_event.created_at', unit, timezone)} x,
count(distinct website_event.session_id) y count(distinct website_event.session_id) y
from website_event from website_event
${joinSession} ${joinSession}
@ -41,7 +41,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters; const { timezone = 'UTC', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse; const { parseFilters, rawQuery, getDateStringSQL, getDateSQL } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, { const { filterQuery, params } = await parseFilters(websiteId, {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -50,11 +50,11 @@ async function clickhouseQuery(
return rawQuery( return rawQuery(
` `
select select
${getDateStringQuery('g.t', unit)} as x, ${getDateStringSQL('g.t', unit)} as x,
g.y as y g.y as y
from ( from (
select select
${getDateQuery('created_at', unit, timezone)} as t, ${getDateSQL('created_at', unit, timezone)} as t,
count(distinct session_id) as y count(distinct session_id) as y
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}

View File

@ -1,45 +1,33 @@
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import clickhouse from 'lib/clickhouse'; import clickhouse from 'lib/clickhouse';
import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db';
import { QueryFilters } from 'lib/types'; import { PageParams, QueryFilters } from 'lib/types';
export async function getSessions(...args: [websiteId: string, filters: QueryFilters]) { export async function getSessions(
...args: [websiteId: string, filters?: QueryFilters, pageParams?: PageParams]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
}); });
} }
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams: PageParams) {
const { startDate } = filters; const { pagedQuery } = prisma;
return prisma.client.session const where = {
.findMany({ ...filters,
where: { id: websiteId,
websiteId,
createdAt: {
gte: startDate,
},
},
orderBy: {
createdAt: 'desc',
},
})
.then(a => {
return Object.values(a).map(a => {
return {
...a,
timestamp: new Date(a.createdAt).getTime() / 1000,
}; };
});
}); return pagedQuery('session', { where }, pageParams);
} }
async function clickhouseQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { rawQuery } = clickhouse; const { pagedQuery, parseFilters } = clickhouse;
const { startDate } = filters; const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
return rawQuery( return pagedQuery(
` `
select select
session_id as id, session_id as id,
@ -58,12 +46,11 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
city city
from website_event from website_event
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime64} ${dateQuery}
${filterQuery}
order by created_at desc order by created_at desc
`, `,
{ params,
websiteId, pageParams,
startDate,
},
); );
} }

View File

@ -1,8 +1,8 @@
export * from './admin/report'; export * from 'queries/prisma/report';
export * from './admin/team'; export * from 'queries/prisma/team';
export * from './admin/teamUser'; export * from 'queries/prisma/teamUser';
export * from './admin/user'; export * from 'queries/prisma/user';
export * from './admin/website'; export * from 'queries/prisma/website';
export * from './analytics/events/getEventMetrics'; export * from './analytics/events/getEventMetrics';
export * from './analytics/events/getEventUsage'; export * from './analytics/events/getEventUsage';
export * from './analytics/events/getEvents'; export * from './analytics/events/getEvents';

View File

@ -17,9 +17,9 @@ export async function getReport(reportId: string): Promise<Report> {
export async function getReports( export async function getReports(
criteria: ReportFindManyArgs, criteria: ReportFindManyArgs,
filters: PageParams = {}, pageParams: PageParams = {},
): Promise<PageResult<Report[]>> { ): Promise<PageResult<Report[]>> {
const { query } = filters; const { query } = pageParams;
const where: Prisma.ReportWhereInput = { const where: Prisma.ReportWhereInput = {
...criteria.where, ...criteria.where,
@ -45,7 +45,7 @@ export async function getReports(
]), ]),
}; };
return prisma.pagedQuery('report', { ...criteria, where }, filters); return prisma.pagedQuery('report', { ...criteria, where }, pageParams);
} }
export async function getUserReports( export async function getUserReports(

View File

@ -49,9 +49,9 @@ export async function getUserByUsername(username: string, options: GetUserOption
export async function getUsers( export async function getUsers(
criteria: UserFindManyArgs, criteria: UserFindManyArgs,
filters?: PageParams, pageParams?: PageParams,
): Promise<PageResult<User[]>> { ): Promise<PageResult<User[]>> {
const { query } = filters; const { query } = pageParams;
const where: Prisma.UserWhereInput = { const where: Prisma.UserWhereInput = {
...criteria.where, ...criteria.where,
@ -68,7 +68,7 @@ export async function getUsers(
{ {
orderBy: 'createdAt', orderBy: 'createdAt',
sortDescending: true, sortDescending: true,
...filters, ...pageParams,
}, },
); );
} }

View File

@ -27,9 +27,9 @@ export async function getSharedWebsite(shareId: string) {
export async function getWebsites( export async function getWebsites(
criteria: WebsiteFindManyArgs, criteria: WebsiteFindManyArgs,
filters: PageParams, pageParams: PageParams,
): Promise<PageResult<Website[]>> { ): Promise<PageResult<Website[]>> {
const { query } = filters; const { query } = pageParams;
const where: Prisma.WebsiteWhereInput = { const where: Prisma.WebsiteWhereInput = {
...criteria.where, ...criteria.where,
@ -42,7 +42,7 @@ export async function getWebsites(
deletedAt: null, deletedAt: null,
}; };
return prisma.pagedQuery('website', { ...criteria, where }, filters); return prisma.pagedQuery('website', { ...criteria, where }, pageParams);
} }
export async function getAllWebsites(userId: string) { export async function getAllWebsites(userId: string) {