Merge branch 'dev' into patch-1

This commit is contained in:
Mike Cao
2025-11-14 11:44:20 -08:00
committed by GitHub
1140 changed files with 52229 additions and 29220 deletions

View File

@@ -0,0 +1,63 @@
import { EventData } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
const FUNCTION_NAME = 'getEventData';
export async function getEventData(
...args: [websiteId: string, eventId: string]
): Promise<EventData[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, eventId: string) {
const { rawQuery } = prisma;
return rawQuery(
`
select event_data.website_id as "websiteId",
event_data.website_event_id as "eventId",
website_event.event_name as "eventName",
event_data.data_key as "dataKey",
event_data.string_value as "stringValue",
event_data.number_value as "numberValue",
event_data.date_value as "dateValue",
event_data.data_type as "dataType",
event_data.created_at as "createdAt"
from event_data
join website_event on website_event.event_id = event_data.website_event_id
and website_event.website_id = {{websiteId::uuid}}
where event_data.website_id = {{websiteId::uuid}}
and event_data.website_event_id = {{eventId::uuid}}
`,
{ websiteId, eventId },
FUNCTION_NAME,
);
}
async function clickhouseQuery(websiteId: string, eventId: string): Promise<EventData[]> {
const { rawQuery } = clickhouse;
return rawQuery(
`
select website_id as websiteId,
event_id as eventId,
event_name as eventName,
data_key as dataKey,
string_value as stringValue,
number_value as numberValue,
date_value as dateValue,
data_type as dataType,
created_at as createdAt
from event_data
where website_id = {websiteId:UUID}
and event_id = {eventId:UUID}
`,
{ websiteId, eventId },
FUNCTION_NAME,
);
}

View File

@@ -1,7 +1,17 @@
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';
const FUNCTION_NAME = 'getEventDataEvents';
export interface WebsiteEventData {
eventName?: string;
propertyName: string;
dataType: number;
propertyValue?: string;
total: number;
}
export async function getEventDataEvents(
...args: [websiteId: string, filters: QueryFilters]
@@ -15,7 +25,10 @@ export async function getEventDataEvents(
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma;
const { event } = filters;
const { params } = await parseFilters(websiteId, filters);
const { queryParams } = parseFilters({
...filters,
websiteId,
});
if (event) {
return rawQuery(
@@ -35,7 +48,8 @@ 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
`,
params,
queryParams,
FUNCTION_NAME,
);
}
@@ -51,11 +65,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
on website_event.event_id = event_data.website_event_id
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
group by website_event.event_name, event_data.data_key, event_data.data_type
order by 1 asc, 2 asc
limit 500
`,
params,
queryParams,
FUNCTION_NAME,
);
}
@@ -65,7 +78,10 @@ async function clickhouseQuery(
): Promise<{ eventName: string; propertyName: string; dataType: number; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { event } = filters;
const { params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
if (event) {
return rawQuery(
@@ -77,14 +93,22 @@ async function clickhouseQuery(
string_value as propertyValue,
count(*) as total
from event_data
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_name = {event:String}
join website_event
on website_event.event_id = event_data.event_id
and website_event.website_id = event_data.website_id
and website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_data.event_name = {event:String}
${filterQuery}
group by data_key, data_type, string_value, event_name
order by 1 asc, 2 asc, 3 asc, 5 desc
limit 500
`,
params,
queryParams,
FUNCTION_NAME,
);
}
@@ -96,12 +120,20 @@ async function clickhouseQuery(
data_type as dataType,
count(*) as total
from event_data
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
join website_event
on website_event.event_id = event_data.event_id
and website_event.website_id = event_data.website_id
and website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
group by data_key, data_type, event_name
order by 1 asc, 2 asc
limit 500
`,
params,
queryParams,
FUNCTION_NAME,
);
}

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 prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export async function getEventDataFields(
...args: [websiteId: string, filters: QueryFilters]
): Promise<WebsiteEventData[]> {
const FUNCTION_NAME = 'getEventDataFields';
export async function getEventDataFields(...args: [websiteId: string, filters: QueryFilters]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
@@ -14,7 +14,10 @@ export async function getEventDataFields(
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters, getDateSQL } = prisma;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
`
@@ -32,6 +35,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
${joinSessionQuery}
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
${filterQuery}
@@ -39,7 +43,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
order by 2 desc
limit 100
`,
params,
queryParams,
FUNCTION_NAME,
);
}
@@ -48,7 +53,7 @@ async function clickhouseQuery(
filters: QueryFilters,
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
return rawQuery(
`
@@ -59,15 +64,21 @@ async function clickhouseQuery(
data_type = 4, toString(date_trunc('hour', date_value)),
string_value) as "value",
count(*) as "total"
from event_data website_event
from event_data
join website_event
on website_event.event_id = event_data.event_id
and website_event.website_id = event_data.website_id
and website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
group by data_key, data_type, value
order by 2 desc
limit 100
`,
params,
queryParams,
FUNCTION_NAME,
);
}

View File

@@ -1,11 +1,13 @@
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';
const FUNCTION_NAME = 'getEventDataProperties';
export async function getEventDataProperties(
...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }]
): Promise<WebsiteEventData[]> {
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
@@ -17,9 +19,12 @@ async function relationalQuery(
filters: QueryFilters & { propertyName?: string },
) {
const { rawQuery, parseFilters } = prisma;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
columns: { propertyName: 'data_key' },
});
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters(
{ ...filters, websiteId },
{
columns: { propertyName: 'data_key' },
},
);
return rawQuery(
`
@@ -32,6 +37,7 @@ async function relationalQuery(
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
${joinSessionQuery}
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
${filterQuery}
@@ -39,7 +45,8 @@ async function relationalQuery(
order by 3 desc
limit 500
`,
params,
queryParams,
FUNCTION_NAME,
);
}
@@ -48,9 +55,12 @@ async function clickhouseQuery(
filters: QueryFilters & { propertyName?: string },
): Promise<{ eventName: string; propertyName: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
columns: { propertyName: 'data_key' },
});
const { filterQuery, cohortQuery, queryParams } = parseFilters(
{ ...filters, websiteId },
{
columns: { propertyName: 'data_key' },
},
);
return rawQuery(
`
@@ -58,15 +68,21 @@ async function clickhouseQuery(
event_name as eventName,
data_key as propertyName,
count(*) as total
from event_data website_event
from event_data
join website_event
on website_event.event_id = event_data.event_id
and website_event.website_id = event_data.website_id
and website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
group by event_name, data_key
order by 1, 3 desc
limit 500
`,
params,
queryParams,
FUNCTION_NAME,
);
}

View File

@@ -3,6 +3,8 @@ import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getEventDataStats';
export async function getEventDataStats(
...args: [websiteId: string, filters: QueryFilters]
): Promise<{
@@ -18,7 +20,10 @@ export async function getEventDataStats(
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
`
@@ -33,16 +38,18 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
count(*) as "total"
from event_data
join website_event on website_event.event_id = event_data.website_event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
${joinSessionQuery}
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
${filterQuery}
group by website_event_id, data_key
) as t
`,
params,
queryParams,
FUNCTION_NAME,
);
}
@@ -51,7 +58,7 @@ async function clickhouseQuery(
filters: QueryFilters,
): Promise<{ events: number; properties: number; records: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
return rawQuery(
`
@@ -64,14 +71,20 @@ async function clickhouseQuery(
event_id,
data_key,
count(*) as "total"
from event_data website_event
from event_data
join website_event
on website_event.event_id = event_data.event_id
and website_event.website_id = event_data.website_id
and website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
group by event_id, data_key
) as t
`,
params,
queryParams,
FUNCTION_NAME,
);
}

View File

@@ -1,7 +1,10 @@
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]) {
const FUNCTION_NAME = 'getEventDataUsage';
export function getEventDataUsage(...args: [websiteIds: string[], filters: QueryFilters]) {
return runQuery({
[PRISMA]: notImplemented,
[CLICKHOUSE]: () => clickhouseQuery(...args),
@@ -10,10 +13,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(
`
@@ -30,5 +33,6 @@ function clickhouseQuery(
startDate,
endDate,
},
FUNCTION_NAME,
);
}

View File

@@ -1,13 +1,17 @@
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';
const FUNCTION_NAME = 'getEventDataValues';
interface WebsiteEventData {
value: string;
total: number;
}
export async function getEventDataValues(
...args: [
websiteId: string,
filters: QueryFilters & { eventName?: string; propertyName?: string },
]
...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }]
): Promise<WebsiteEventData[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -17,10 +21,13 @@ export async function getEventDataValues(
async function relationalQuery(
websiteId: string,
filters: QueryFilters & { eventName?: string; propertyName?: string },
filters: QueryFilters & { propertyName?: string },
) {
const { rawQuery, parseFilters, getDateSQL } = prisma;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
`
@@ -36,25 +43,26 @@ async function relationalQuery(
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
${joinSessionQuery}
where event_data.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
and event_data.data_key = {{propertyName}}
and website_event.event_name = {{eventName}}
${filterQuery}
group by value
order by 2 desc
limit 100
`,
params,
queryParams,
FUNCTION_NAME,
);
}
async function clickhouseQuery(
websiteId: string,
filters: QueryFilters & { eventName?: string; propertyName?: string },
filters: QueryFilters & { propertyName?: string },
): Promise<{ value: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
return rawQuery(
`
@@ -63,17 +71,23 @@ async function clickhouseQuery(
data_type = 4, toString(date_trunc('hour', date_value)),
string_value) as "value",
count(*) as "total"
from event_data website_event
from event_data
join website_event
on website_event.event_id = event_data.event_id
and website_event.website_id = event_data.website_id
and website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and data_key = {propertyName:String}
and event_name = {eventName:String}
where event_data.website_id = {websiteId:UUID}
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_data.data_key = {propertyName:String}
and event_data.event_name = {event:String}
${filterQuery}
group by value
order by 2 desc
limit 100
`,
params,
queryParams,
FUNCTION_NAME,
);
}

View File

@@ -0,0 +1,132 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getEventExpandedMetrics';
export interface EventExpandedMetricParameters {
type: string;
limit?: string;
offset?: string;
}
export interface EventExpandedMetricData {
name: string;
pageviews: number;
visitors: number;
visits: number;
bounces: number;
totaltime: number;
}
export async function getEventExpandedMetrics(
...args: [websiteId: string, parameters: EventExpandedMetricParameters, filters: QueryFilters]
): Promise<EventExpandedMetricData[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
parameters: EventExpandedMetricParameters,
filters: QueryFilters,
) {
const { type, limit = 500, offset = 0 } = parameters;
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters, getTimestampDiffSQL } = prisma;
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters(
{
...filters,
websiteId,
eventType: EVENT_TYPE.customEvent,
},
{ joinSession: SESSION_COLUMNS.includes(type) },
);
return rawQuery(
`
select
name,
sum(t.c) as "pageviews",
count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits",
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from (
select
${column} name,
website_event.session_id,
website_event.visit_id,
count(*) as "c",
min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time"
from website_event
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${filterQuery}
group by name, website_event.session_id, website_event.visit_id
) as t
group by name
order by visitors desc, visits desc
limit ${limit}
offset ${offset}
`,
queryParams,
FUNCTION_NAME,
);
}
async function clickhouseQuery(
websiteId: string,
parameters: EventExpandedMetricParameters,
filters: QueryFilters,
): Promise<EventExpandedMetricData[]> {
const { type, limit = 500, offset = 0 } = parameters;
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.customEvent,
});
return rawQuery(
`
select
name,
sum(t.c) as "pageviews",
uniq(t.session_id) as "visitors",
uniq(t.visit_id) as "visits",
sum(if(t.c = 1, 1, 0)) as "bounces",
sum(max_time-min_time) as "totaltime"
from (
select
${column} name,
session_id,
visit_id,
count(*) c,
min(created_at) min_time,
max(created_at) max_time
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and name != ''
${filterQuery}
group by name, session_id, visit_id
) as t
group by name
order by visitors desc, visits desc
limit ${limit}
offset ${offset}
`,
{ ...queryParams, ...parameters },
FUNCTION_NAME,
);
}

View File

@@ -4,15 +4,23 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getEventMetrics';
export interface EventMetricParameters {
type: string;
limit?: string;
offset?: string;
}
export interface EventMetricData {
x: string;
t: string;
y: number;
}
export async function getEventMetrics(
...args: [
websiteId: string,
type: string,
filters: QueryFilters,
limit?: number | string,
offset?: number | string,
]
) {
...args: [websiteId: string, parameters: EventMetricParameters, filters: QueryFilters]
): Promise<EventMetricData[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
@@ -21,17 +29,16 @@ export async function getEventMetrics(
async function relationalQuery(
websiteId: string,
type: string,
parameters: EventMetricParameters,
filters: QueryFilters,
limit: number | string = 500,
offset: number | string = 0,
) {
const { type, limit = 500, offset = 0 } = parameters;
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = prisma;
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(
websiteId,
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters(
{
...filters,
websiteId,
eventType: EVENT_TYPE.customEvent,
},
{ joinSession: SESSION_COLUMNS.includes(type) },
@@ -43,48 +50,48 @@ async function relationalQuery(
count(*) as y
from website_event
${cohortQuery}
${joinSession}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
${filterQuery}
group by 1
order by 2 desc
limit ${limit}
offset ${offset}
`,
params,
{ ...queryParams, ...parameters },
FUNCTION_NAME,
);
}
async function clickhouseQuery(
websiteId: string,
type: string,
parameters: EventMetricParameters,
filters: QueryFilters,
limit: number | string = 500,
offset: number | string = 0,
): Promise<{ x: string; y: number }[]> {
): Promise<EventMetricData[]> {
const { type, limit = 500, offset = 0 } = parameters;
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.customEvent,
});
return rawQuery(
`select ${column} x,
count(*) as y
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
${filterQuery}
group by x
order by y desc
limit ${limit}
offset ${offset}
`select ${column} x,
count(*) as y
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
group by x
order by y desc
limit ${limit}
offset ${offset}
`,
params,
{ ...queryParams, ...parameters },
FUNCTION_NAME,
);
}

View File

@@ -2,7 +2,15 @@ import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters, WebsiteEventMetric } from '@/lib/types';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getEventStats';
interface WebsiteEventMetric {
x: string;
t: string;
y: number;
}
export async function getEventStats(
...args: [websiteId: string, filters: QueryFilters]
@@ -16,8 +24,9 @@ export async function getEventStats(
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = prisma;
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.customEvent,
});
@@ -29,15 +38,15 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
count(*) y
from website_event
${cohortQuery}
${joinSession}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
${filterQuery}
group by 1, 2
order by 2
`,
params,
queryParams,
FUNCTION_NAME,
);
}
@@ -47,8 +56,9 @@ async function clickhouseQuery(
): Promise<{ x: string; t: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
eventType: EVENT_TYPE.customEvent,
});
@@ -64,7 +74,6 @@ async function clickhouseQuery(
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
${filterQuery}
group by x, t
order by t
@@ -88,5 +97,5 @@ async function clickhouseQuery(
`;
}
return rawQuery(sql, params);
return rawQuery(sql, queryParams, FUNCTION_NAME);
}

View File

@@ -1,7 +1,10 @@
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]) {
const FUNCTION_NAME = 'getEventUsage';
export function getEventUsage(...args: [websiteIds: string[], filters: QueryFilters]) {
return runQuery({
[PRISMA]: notImplemented,
[CLICKHOUSE]: () => clickhouseQuery(...args),
@@ -10,10 +13,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(
`
@@ -30,5 +33,6 @@ function clickhouseQuery(
startDate,
endDate,
},
FUNCTION_NAME,
);
}

View File

@@ -1,94 +1,119 @@
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]
) {
const FUNCTION_NAME = 'getWebsiteEvents';
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, cohortQuery, params } = await parseFilters(websiteId, {
const { search } = filters;
const { filterQuery, dateQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like';
const searchQuery = search
? `and ((event_name ilike {{search}} and event_type = 2)
or (url_path ilike {{search}} and event_type = 1))`
: '';
return pagedRawQuery(
`
select
event_id as "id",
website_id as "websiteId",
session_id as "sessionId",
created_at as "createdAt",
url_path as "urlPath",
url_query as "urlQuery",
referrer_path as "referrerPath",
referrer_query as "referrerQuery",
referrer_domain as "referrerDomain",
website_event.event_id as "id",
website_event.website_id as "websiteId",
website_event.session_id as "sessionId",
website_event.created_at as "createdAt",
website_event.hostname,
website_event.url_path as "urlPath",
website_event.url_query as "urlQuery",
website_event.referrer_path as "referrerPath",
website_event.referrer_query as "referrerQuery",
website_event.referrer_domain as "referrerDomain",
session.country as country,
city as city,
device as device,
os as os,
browser as browser,
page_title as "pageTitle",
event_type as "eventType",
event_name as "eventName"
website_event.event_type as "eventType",
website_event.event_name as "eventName",
event_id IN (select website_event_id
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}) AS "hasData"
from website_event
${cohortQuery}
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
join session on session.session_id = website_event.session_id
and session.website_id = website_event.website_id
where website_event.website_id = {{websiteId::uuid}}
${dateQuery}
${filterQuery}
${
search
? `and ((event_name ${like} {{search}} and event_type = 2)
or (url_path ${like} {{search}} and event_type = 1))`
: ''
}
order by created_at desc
${searchQuery}
order by website_event.created_at desc
`,
{ ...params, search: `%${search}%` },
pageParams,
queryParams,
filters,
FUNCTION_NAME,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { pagedQuery, parseFilters } = clickhouse;
const { params, dateQuery, filterQuery, cohortQuery } = await parseFilters(websiteId, filters);
const { search } = pageParams;
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters } = clickhouse;
const { search } = filters;
const { queryParams, dateQuery, cohortQuery, filterQuery } = parseFilters({
...filters,
websiteId,
});
return pagedQuery(
const searchQuery = 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,
website_id as websiteId,
session_id as sessionId,
created_at as createdAt,
hostname,
url_path as urlPath,
url_query as urlQuery,
referrer_path as referrerPath,
referrer_query as referrerQuery,
referrer_domain as referrerDomain,
country as country,
city as city,
device as device,
os as os,
browser as browser,
page_title as pageTitle,
event_type as eventType,
event_name as eventName
event_name as eventName,
event_id IN (select event_id
from event_data
where website_id = {websiteId:UUID}
${dateQuery}) as hasData
from website_event
${cohortQuery}
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
`,
{ ...params, search },
pageParams,
queryParams,
filters,
FUNCTION_NAME,
);
}

View File

@@ -1,9 +1,9 @@
import { EVENT_NAME_LENGTH, URL_LENGTH, EVENT_TYPE, PAGE_TITLE_LENGTH } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
import { EVENT_NAME_LENGTH, URL_LENGTH, PAGE_TITLE_LENGTH } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import clickhouse from '@/lib/clickhouse';
import kafka from '@/lib/kafka';
import prisma from '@/lib/prisma';
import { uuid } from '@/lib/crypto';
import { saveEventData } from './saveEventData';
import { saveRevenue } from './saveRevenue';
@@ -11,6 +11,7 @@ export interface SaveEventArgs {
websiteId: string;
sessionId: string;
visitId: string;
eventType: number;
createdAt?: Date;
// Page
@@ -65,9 +66,9 @@ async function relationalQuery({
websiteId,
sessionId,
visitId,
eventType,
createdAt,
pageTitle,
tag,
hostname,
urlPath,
urlQuery,
@@ -76,6 +77,7 @@ async function relationalQuery({
referrerDomain,
eventName,
eventData,
tag,
utmSource,
utmMedium,
utmCampaign,
@@ -113,7 +115,7 @@ async function relationalQuery({
ttclid,
lifatid,
twclid,
eventType: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
eventType,
eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
tag,
hostname,
@@ -152,9 +154,16 @@ async function clickhouseQuery({
websiteId,
sessionId,
visitId,
distinctId,
eventType,
createdAt,
pageTitle,
hostname,
urlPath,
urlQuery,
referrerPath,
referrerQuery,
referrerDomain,
distinctId,
browser,
os,
device,
@@ -163,15 +172,9 @@ async function clickhouseQuery({
country,
region,
city,
tag,
hostname,
urlPath,
urlQuery,
referrerPath,
referrerQuery,
referrerDomain,
eventName,
eventData,
tag,
utmSource,
utmMedium,
utmCampaign,
@@ -213,7 +216,7 @@ async function clickhouseQuery({
ttclid: ttclid,
li_fat_id: lifatid,
twclid: twclid,
event_type: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
event_type: eventType,
event_name: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
tag: tag,
distinct_id: distinctId,

View File

@@ -3,6 +3,8 @@ import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db';
const FUNCTION_NAME = 'getActiveVisitors';
export async function getActiveVisitors(...args: [websiteId: string]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -12,22 +14,25 @@ export async function getActiveVisitors(...args: [websiteId: string]) {
async function relationalQuery(websiteId: string) {
const { rawQuery } = prisma;
const startDate = subMinutes(new Date(), 5);
const result = await rawQuery(
`
select count(distinct session_id) as visitors
select count(distinct session_id) as "visitors"
from website_event
where website_id = {{websiteId::uuid}}
and created_at >= {{startDate}}
`,
{ websiteId, startDate: subMinutes(new Date(), 5) },
{ websiteId, startDate },
FUNCTION_NAME,
);
return result[0] ?? null;
return result?.[0] ?? null;
}
async function clickhouseQuery(websiteId: string): Promise<{ x: number }> {
const { rawQuery } = clickhouse;
const startDate = subMinutes(new Date(), 5);
const result = await rawQuery(
`
@@ -37,7 +42,8 @@ async function clickhouseQuery(websiteId: string): Promise<{ x: number }> {
where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime64}
`,
{ websiteId, startDate: subMinutes(new Date(), 5) },
{ websiteId, startDate },
FUNCTION_NAME,
);
return result[0] ?? null;

View File

@@ -0,0 +1,190 @@
import clickhouse from '@/lib/clickhouse';
import {
EMAIL_DOMAINS,
PAID_AD_PARAMS,
SEARCH_DOMAINS,
SHOPPING_DOMAINS,
SOCIAL_DOMAINS,
VIDEO_DOMAINS,
} from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getChannelExpandedMetrics';
export interface ChannelExpandedMetricsParameters {
limit?: number | string;
offset?: number | string;
}
export interface ChannelExpandedMetricsData {
name: string;
pageviews: number;
visitors: number;
visits: number;
bounces: number;
totaltime: number;
}
export async function getChannelExpandedMetrics(
...args: [websiteId: string, filters?: QueryFilters]
): Promise<ChannelExpandedMetricsData[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
filters: QueryFilters,
): Promise<ChannelExpandedMetricsData[]> {
const { rawQuery, parseFilters, getTimestampDiffSQL } = prisma;
const { queryParams, filterQuery, joinSessionQuery, cohortQuery, dateQuery } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
`
WITH prefix AS (
select case when website_event.utm_medium LIKE 'p%' OR
website_event.utm_medium LIKE '%ppc%' OR
website_event.utm_medium LIKE '%retargeting%' OR
website_event.utm_medium LIKE '%paid%' then 'paid' else 'organic' end prefix,
website_event.referrer_domain,
website_event.url_query,
website_event.utm_medium,
website_event.utm_source,
website_event.session_id,
website_event.visit_id,
count(*) c,
min(website_event.created_at) min_time,
max(website_event.created_at) max_time
from website_event
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.event_type != 2
${dateQuery}
${filterQuery}
group by prefix,
website_event.referrer_domain,
website_event.url_query,
website_event.utm_medium,
website_event.utm_source,
website_event.session_id,
website_event.visit_id),
channels as (
select case
when referrer_domain = '' and url_query = '' then 'direct'
when ${toPostgresPositionClause('url_query', PAID_AD_PARAMS)} then 'paidAds'
when ${toPostgresPositionClause('utm_medium', ['referral', 'app', 'link'])} then 'referral'
when utm_medium ilike '%affiliate%' then 'affiliate'
when utm_medium ilike '%sms%' or utm_source ilike '%sms%' then 'sms'
when ${toPostgresPositionClause('referrer_domain', SEARCH_DOMAINS)} or utm_medium ilike '%organic%' then concat(prefix, 'Search')
when ${toPostgresPositionClause('referrer_domain', SOCIAL_DOMAINS)} then concat(prefix, 'Social')
when ${toPostgresPositionClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
when ${toPostgresPositionClause('referrer_domain', SHOPPING_DOMAINS)} or utm_medium ilike '%shop%' then concat(prefix, 'Shopping')
when ${toPostgresPositionClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video')
else '' end AS name,
session_id,
visit_id,
c,
min_time,
max_time
from prefix)
select
name,
sum(c) as "pageviews",
count(distinct session_id) as "visitors",
count(distinct visit_id) as "visits",
sum(case when c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffSQL('min_time', 'max_time')}) as "totaltime"
from channels
where name != ''
group by name
order by visitors desc, visits desc
`,
queryParams,
FUNCTION_NAME,
).then(results => results.map(item => ({ ...item, y: Number(item.y) })));
}
async function clickhouseQuery(
websiteId: string,
filters: QueryFilters,
): Promise<ChannelExpandedMetricsData[]> {
const { rawQuery, parseFilters } = clickhouse;
const { queryParams, filterQuery, cohortQuery } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
`
select
name,
sum(t.c) as "pageviews",
uniq(t.session_id) as "visitors",
uniq(t.visit_id) as "visits",
sum(if(t.c = 1, 1, 0)) as "bounces",
sum(max_time-min_time) as "totaltime"
from (
select case when multiSearchAny(utm_medium, ['cp', 'ppc', 'retargeting', 'paid']) != 0 then 'paid' else 'organic' end prefix,
case
when referrer_domain = '' and url_query = '' then 'direct'
when multiSearchAny(url_query, [${toClickHouseStringArray(
PAID_AD_PARAMS,
)}]) != 0 then 'paidAds'
when multiSearchAny(utm_medium, ['referral', 'app','link']) != 0 then 'referral'
when position(utm_medium, 'affiliate') > 0 then 'affiliate'
when position(utm_medium, 'sms') > 0 or position(utm_source, 'sms') > 0 then 'sms'
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
SEARCH_DOMAINS,
)}]) != 0 or position(utm_medium, 'organic') > 0 then concat(prefix, 'Search')
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
SOCIAL_DOMAINS,
)}]) != 0 then concat(prefix, 'Social')
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
EMAIL_DOMAINS,
)}]) != 0 or position(utm_medium, 'mail') > 0 then 'email'
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
SHOPPING_DOMAINS,
)}]) != 0 or position(utm_medium, 'shop') > 0 then concat(prefix, 'Shopping')
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
VIDEO_DOMAINS,
)}]) != 0 or position(utm_medium, 'video') > 0 then concat(prefix, 'Video')
else '' end AS name,
session_id,
visit_id,
count(*) c,
min(created_at) min_time,
max(created_at) max_time
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type != 2
and name != ''
${filterQuery}
group by prefix, name, session_id, visit_id
) as t
group by name
order by visitors desc, visits desc;
`,
queryParams,
FUNCTION_NAME,
);
}
function toClickHouseStringArray(arr: string[]): string {
return arr.map(p => `'${p.replace(/'/g, "\\'")}'`).join(', ');
}
function toPostgresPositionClause(column: string, arr: string[]) {
return arr.map(val => `${column} ilike '%${val.replace(/'/g, "''")}%'`).join(' OR\n ');
}

View File

@@ -1,8 +1,18 @@
import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db';
import {
EMAIL_DOMAINS,
PAID_AD_PARAMS,
SEARCH_DOMAINS,
SHOPPING_DOMAINS,
SOCIAL_DOMAINS,
VIDEO_DOMAINS,
} from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getChannelMetrics';
export async function getChannelMetrics(...args: [websiteId: string, filters?: QueryFilters]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -12,24 +22,58 @@ export async function getChannelMetrics(...args: [websiteId: string, filters?: Q
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma;
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
const { queryParams, filterQuery, joinSessionQuery, cohortQuery, dateQuery } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
`
select
referrer_domain as domain,
url_query as query,
count(distinct session_id) as visitors
from website_event
${cohortQuery}
where website_id = {{websiteId::uuid}}
${filterQuery}
WITH prefix AS (
select case when website_event.utm_medium LIKE 'p%' OR
website_event.utm_medium LIKE '%ppc%' OR
website_event.utm_medium LIKE '%retargeting%' OR
website_event.utm_medium LIKE '%paid%' then 'paid' else 'organic' end prefix,
website_event.referrer_domain,
website_event.url_query,
website_event.utm_medium,
website_event.utm_source,
website_event.session_id
from website_event
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.event_type != 2
${dateQuery}
group by 1, 2
order by visitors desc
${filterQuery}),
channels as (
select case
when referrer_domain = '' and url_query = '' then 'direct'
when ${toPostgresLikeClause('url_query', PAID_AD_PARAMS)} then 'paidAds'
when ${toPostgresLikeClause('utm_medium', ['referral', 'app', 'link'])} then 'referral'
when utm_medium ilike '%affiliate%' then 'affiliate'
when utm_medium ilike '%sms%' or utm_source ilike '%sms%' then 'sms'
when ${toPostgresLikeClause('referrer_domain', SEARCH_DOMAINS)} or utm_medium ilike '%organic%' then concat(prefix, 'Search')
when ${toPostgresLikeClause('referrer_domain', SOCIAL_DOMAINS)} then concat(prefix, 'Social')
when ${toPostgresLikeClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
when ${toPostgresLikeClause('referrer_domain', SHOPPING_DOMAINS)} or utm_medium ilike '%shop%' then concat(prefix, 'Shopping')
when ${toPostgresLikeClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video')
else '' end AS x,
count(distinct session_id) y
from prefix
group by 1
order by y desc)
select x, sum(y) y
from channels
where x != ''
group by x
order by y desc;
`,
params,
);
queryParams,
FUNCTION_NAME,
).then(results => results.map(item => ({ ...item, y: Number(item.y) })));
}
async function clickhouseQuery(
@@ -37,21 +81,62 @@ async function clickhouseQuery(
filters: QueryFilters,
): Promise<{ x: string; y: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
const { queryParams, filterQuery, cohortQuery, dateQuery } = parseFilters({
...filters,
websiteId,
});
const sql = `
select
referrer_domain as domain,
url_query as query,
uniq(session_id) as visitors
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
${filterQuery}
${dateQuery}
group by 1, 2
order by visitors desc
WITH channels as (
select case when multiSearchAny(utm_medium, ['cp', 'ppc', 'retargeting', 'paid']) != 0 then 'paid' else 'organic' end prefix,
case
when referrer_domain = '' and url_query = '' then 'direct'
when multiSearchAny(url_query, [${toClickHouseStringArray(
PAID_AD_PARAMS,
)}]) != 0 then 'paidAds'
when multiSearchAny(utm_medium, ['referral', 'app','link']) != 0 then 'referral'
when position(utm_medium, 'affiliate') > 0 then 'affiliate'
when position(utm_medium, 'sms') > 0 or position(utm_source, 'sms') > 0 then 'sms'
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
SEARCH_DOMAINS,
)}]) != 0 or position(utm_medium, 'organic') > 0 then concat(prefix, 'Search')
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
SOCIAL_DOMAINS,
)}]) != 0 then concat(prefix, 'Social')
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
EMAIL_DOMAINS,
)}]) != 0 or position(utm_medium, 'mail') > 0 then 'email'
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
SHOPPING_DOMAINS,
)}]) != 0 or position(utm_medium, 'shop') > 0 then concat(prefix, 'Shopping')
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
VIDEO_DOMAINS,
)}]) != 0 or position(utm_medium, 'video') > 0 then concat(prefix, 'Video')
else '' end AS x,
count(distinct session_id) y
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and event_type != 2
${dateQuery}
${filterQuery}
group by 1, 2
order by y desc)
select x, sum(y) y
from channels
where x != ''
group by x
order by y desc;
`;
return rawQuery(sql, params);
return rawQuery(sql, queryParams, FUNCTION_NAME);
}
function toClickHouseStringArray(arr: string[]): string {
return arr.map(p => `'${p.replace(/'/g, "\\'")}'`).join(', ');
}
function toPostgresLikeClause(column: string, arr: string[]) {
return arr.map(val => `${column} ilike '%${val.replace(/'/g, "''")}%'`).join(' OR\n ');
}

View File

@@ -3,6 +3,8 @@ import clickhouse from '@/lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getRealtimeActivity';
export async function getRealtimeActivity(...args: [websiteId: string, filters: QueryFilters]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -12,7 +14,10 @@ export async function getRealtimeActivity(...args: [websiteId: string, filters:
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma;
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
const { queryParams, filterQuery, cohortQuery, dateQuery } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
`
@@ -30,19 +35,24 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
${cohortQuery}
inner join session
on session.session_id = website_event.session_id
and session.website_id = website_event.website_id
where website_event.website_id = {{websiteId::uuid}}
${filterQuery}
${dateQuery}
order by website_event.created_at desc
limit 100
`,
params,
queryParams,
FUNCTION_NAME,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promise<{ x: number }> {
const { rawQuery, parseFilters } = clickhouse;
const { params, filterQuery, cohortQuery, dateQuery } = await parseFilters(websiteId, filters);
const { queryParams, filterQuery, cohortQuery, dateQuery } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
`
@@ -64,6 +74,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promis
order by createdAt desc
limit 100
`,
params,
queryParams,
FUNCTION_NAME,
);
}

View File

@@ -1,4 +1,7 @@
import { getPageviewStats, getRealtimeActivity, getSessionStats } from '@/queries';
import { QueryFilters } from '@/lib/types';
import { getRealtimeActivity } from '@/queries/sql/getRealtimeActivity';
import { getPageviewStats } from '@/queries/sql/pageviews/getPageviewStats';
import { getSessionStats } from '@/queries/sql/sessions/getSessionStats';
function increment(data: object, key: string) {
if (key) {
@@ -10,12 +13,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,12 @@
import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getValues';
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 +14,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 = '';
let excludeDomain = '';
@@ -52,6 +51,7 @@ async function relationalQuery(
from website_event
inner join session
on session.session_id = website_event.session_id
and session.website_id = website_event.website_id
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${searchQuery}
@@ -67,18 +67,15 @@ async function relationalQuery(
search: `%${search}%`,
...params,
},
FUNCTION_NAME,
);
}
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 = '';
let excludeDomain = '';
@@ -127,5 +124,6 @@ async function clickhouseQuery(
search,
...params,
},
FUNCTION_NAME,
);
}

View File

@@ -12,18 +12,21 @@ export async function getWebsiteDateRange(...args: [websiteId: string]) {
async function relationalQuery(websiteId: string) {
const { rawQuery, parseFilters } = prisma;
const { params } = await parseFilters(websiteId, { startDate: new Date(DEFAULT_RESET_DATE) });
const { queryParams } = parseFilters({
startDate: new Date(DEFAULT_RESET_DATE),
websiteId,
});
const result = await rawQuery(
`
select
min(created_at) as mindate,
max(created_at) as maxdate
min(created_at) as "startDate",
max(created_at) as "endDate"
from website_event
where website_id = {{websiteId::uuid}}
and created_at >= {{startDate}}
`,
params,
queryParams,
);
return result[0] ?? null;
@@ -31,18 +34,21 @@ async function relationalQuery(websiteId: string) {
async function clickhouseQuery(websiteId: string) {
const { rawQuery, parseFilters } = clickhouse;
const { params } = await parseFilters(websiteId, { startDate: new Date(DEFAULT_RESET_DATE) });
const { queryParams } = parseFilters({
startDate: new Date(DEFAULT_RESET_DATE),
websiteId,
});
const result = await rawQuery(
`
select
min(created_at) as mindate,
max(created_at) as maxdate
min(created_at) as startDate,
max(created_at) as endDate
from website_event_stats_hourly
where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime64}
`,
params,
queryParams,
);
return result[0] ?? null;

View File

@@ -1,15 +1,22 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE } from '@/lib/constants';
import { EVENT_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
import { EVENT_COLUMNS } from '@/lib/constants';
const FUNCTION_NAME = 'getWebsiteStats';
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,23 +26,21 @@ 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, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
eventType: EVENT_TYPE.pageView,
websiteId,
});
return rawQuery(
`
select
sum(t.c) as "pageviews",
cast(coalesce(sum(t.c), 0) as bigint) as "pageviews",
count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits",
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
coalesce(sum(case when t.c = 1 then 1 else 0 end), 0) as "bounces",
cast(coalesce(sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}), 0) as bigint) as "totaltime"
from (
select
website_event.session_id,
@@ -44,29 +49,28 @@ async function relationalQuery(
min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time"
from website_event
${cohortQuery}
${joinSession}
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
and website_event.event_type != 2
${filterQuery}
group by 1, 2
) as t
`,
params,
);
queryParams,
FUNCTION_NAME,
).then(result => result?.[0]);
}
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, cohortQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
eventType: EVENT_TYPE.pageView,
websiteId,
});
let sql = '';
@@ -90,7 +94,7 @@ async function clickhouseQuery(
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
and event_type != 2
${filterQuery}
group by session_id, visit_id
) as t;
@@ -113,12 +117,12 @@ async function clickhouseQuery(
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
and event_type != 2
${filterQuery}
group by session_id, visit_id
) as t;
`;
}
return rawQuery(sql, params);
return rawQuery(sql, queryParams, FUNCTION_NAME).then(result => result?.[0]);
}

View File

@@ -4,9 +4,9 @@ import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db';
import { QueryFilters } from '@/lib/types';
import { EVENT_COLUMNS } from '@/lib/constants';
export async function getWebsiteSessionsWeekly(
...args: [websiteId: string, filters?: QueryFilters]
) {
const FUNCTION_NAME = 'getWeeklyTraffic';
export async function getWeeklyTraffic(...args: [websiteId: string, filters: QueryFilters]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
@@ -14,29 +14,36 @@ export async function getWebsiteSessionsWeekly(
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc' } = filters;
const timezone = 'utc';
const { rawQuery, getDateWeeklySQL, parseFilters } = prisma;
const { params } = await parseFilters(websiteId, filters);
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
`
select
${getDateWeeklySQL('created_at', timezone)} as time,
count(distinct session_id) as value
${getDateWeeklySQL('website_event.created_at', timezone)} as time,
count(distinct website_event.session_id) as value
from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${filterQuery}
group by time
order by 2
`,
params,
queryParams,
FUNCTION_NAME,
).then(formatResults);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc' } = filters;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, queryParams } = await parseFilters({ ...filters, websiteId });
let sql = '';
@@ -68,7 +75,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
`;
}
return rawQuery(sql, params).then(formatResults);
return rawQuery(sql, queryParams, FUNCTION_NAME).then(formatResults);
}
function formatResults(data: any) {

41
src/queries/sql/index.ts Normal file
View File

@@ -0,0 +1,41 @@
export * from './events/getEventDataEvents';
export * from './events/getEventDataFields';
export * from './events/getEventDataProperties';
export * from './events/getEventDataValues';
export * from './events/getEventDataStats';
export * from './events/getEventDataUsage';
export * from './events/getEventMetrics';
export * from './events/getEventExpandedMetrics';
export * from './events/getEventStats';
export * from './events/getWebsiteEvents';
export * from './events/getEventUsage';
export * from './events/saveEvent';
export * from './reports/getFunnel';
export * from './reports/getJourney';
export * from './reports/getRetention';
export * from './reports/getBreakdown';
export * from './reports/getUTM';
export * from './pageviews/getPageviewMetrics';
export * from './pageviews/getPageviewExpandedMetrics';
export * from './pageviews/getPageviewStats';
export * from './sessions/createSession';
export * from './sessions/getWebsiteSession';
export * from './sessions/getSessionData';
export * from './sessions/getSessionDataProperties';
export * from './sessions/getSessionDataValues';
export * from './sessions/getSessionMetrics';
export * from './sessions/getSessionExpandedMetrics';
export * from './sessions/getWebsiteSessions';
export * from './sessions/getWebsiteSessionStats';
export * from './sessions/getSessionActivity';
export * from './sessions/getSessionStats';
export * from './sessions/saveSessionData';
export * from './getActiveVisitors';
export * from './getChannelMetrics';
export * from './getChannelExpandedMetrics';
export * from './getRealtimeActivity';
export * from './getRealtimeData';
export * from './getValues';
export * from './getWebsiteDateRange';
export * from './getWebsiteStats';
export * from './getWeeklyTraffic';

View File

@@ -0,0 +1,227 @@
import clickhouse from '@/lib/clickhouse';
import { FILTER_COLUMNS, GROUPED_DOMAINS, SESSION_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getPageviewExpandedMetrics';
export interface PageviewExpandedMetricsParameters {
type: string;
limit?: number | string;
offset?: number | string;
}
export interface PageviewExpandedMetricsData {
name: string;
pageviews: number;
visitors: number;
visits: number;
bounces: number;
totaltime: number;
}
export async function getPageviewExpandedMetrics(
...args: [websiteId: string, parameters: PageviewExpandedMetricsParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
parameters: PageviewExpandedMetricsParameters,
filters: QueryFilters,
): Promise<PageviewExpandedMetricsData[]> {
const { type, limit = 500, offset = 0 } = parameters;
let column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters, getTimestampDiffSQL } = prisma;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters(
{
...filters,
websiteId,
},
{ joinSession: SESSION_COLUMNS.includes(type) },
);
let entryExitQuery = '';
let excludeDomain = '';
if (column === 'referrer_domain') {
excludeDomain = `and website_event.referrer_domain != website_event.hostname
and website_event.referrer_domain != ''`;
if (type === 'domain') {
column = toPostgresGroupedReferrer(GROUPED_DOMAINS);
}
}
if (type === 'entry' || type === 'exit') {
const aggregrate = type === 'entry' ? 'min' : 'max';
entryExitQuery = `
join (
select visit_id,
${aggregrate}(created_at) target_created_at
from website_event
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and website_event.event_type != 2
group by visit_id
) x
on x.visit_id = website_event.visit_id
and x.target_created_at = website_event.created_at
`;
}
return rawQuery(
`
select
name,
sum(t.c) as "pageviews",
count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits",
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from (
select
${column} name,
website_event.session_id,
website_event.visit_id,
count(*) as "c",
min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time"
from website_event
${cohortQuery}
${joinSessionQuery}
${entryExitQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and website_event.event_type != 2
${excludeDomain}
${filterQuery}
group by name, website_event.session_id, website_event.visit_id
) as t
where name != ''
group by name
order by visitors desc, visits desc
limit ${limit}
offset ${offset}
`,
queryParams,
FUNCTION_NAME,
);
}
async function clickhouseQuery(
websiteId: string,
parameters: PageviewExpandedMetricsParameters,
filters: QueryFilters,
): Promise<{ x: string; y: number }[]> {
const { type, limit = 500, offset = 0 } = parameters;
let column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
let excludeDomain = '';
let entryExitQuery = '';
if (column === 'referrer_domain') {
excludeDomain = `and referrer_domain != hostname and referrer_domain != ''`;
if (type === 'domain') {
column = toClickHouseGroupedReferrer(GROUPED_DOMAINS);
}
}
if (type === 'entry' || type === 'exit') {
const aggregrate = type === 'entry' ? 'argMin' : 'argMax';
column = `x.${column}`;
entryExitQuery = `
JOIN (select visit_id,
${aggregrate}(url_path, created_at) url_path
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type != 2
group by visit_id) x
ON x.visit_id = website_event.visit_id`;
}
return rawQuery(
`
select
name,
sum(t.c) as "pageviews",
uniq(t.session_id) as "visitors",
uniq(t.visit_id) as "visits",
sum(if(t.c = 1, 1, 0)) as "bounces",
sum(max_time-min_time) as "totaltime"
from (
select
${column} name,
session_id,
visit_id,
count(*) c,
min(created_at) min_time,
max(created_at) max_time
from website_event
${cohortQuery}
${entryExitQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type != 2
and name != ''
${excludeDomain}
${filterQuery}
group by name, session_id, visit_id
) as t
group by name
order by visitors desc, visits desc
limit ${limit}
offset ${offset}
`,
{ ...queryParams, ...parameters },
FUNCTION_NAME,
);
}
export function toClickHouseGroupedReferrer(
domains: any[],
column: string = 'referrer_domain',
): string {
return [
'CASE',
...domains.map(group => {
const matches = Array.isArray(group.match) ? group.match : [group.match];
const formattedArray = matches.map(m => `'${m}'`).join(', ');
return ` WHEN multiSearchAny(${column}, [${formattedArray}]) != 0 THEN '${group.domain}'`;
}),
" ELSE 'Other'",
'END',
].join('\n');
}
export function toPostgresGroupedReferrer(
domains: any[],
column: string = 'referrer_domain',
): string {
return [
'CASE',
...domains.map(group => {
const matches = Array.isArray(group.match) ? group.match : [group.match];
return `WHEN ${toPostgresLikeClause(column, matches)} THEN '${group.domain}'`;
}),
" ELSE 'Other'",
'END',
].join('\n');
}
function toPostgresLikeClause(column: string, arr: string[]) {
return arr.map(val => `${column} ilike '%${val.replace(/'/g, "''")}%'`).join(' OR\n ');
}

View File

@@ -1,17 +1,24 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_COLUMNS, EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getPageviewMetrics';
export interface PageviewMetricsParameters {
type: string;
limit?: number | string;
offset?: number | string;
}
export interface PageviewMetricsData {
x: string;
y: number;
}
export async function getPageviewMetrics(
...args: [
websiteId: string,
type: string,
filters: QueryFilters,
limit?: number | string,
offset?: number | string,
]
...args: [websiteId: string, parameters: PageviewMetricsParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -21,18 +28,16 @@ export async function getPageviewMetrics(
async function relationalQuery(
websiteId: string,
type: string,
parameters: PageviewMetricsParameters,
filters: QueryFilters,
limit: number | string = 500,
offset: number | string = 0,
) {
const column = FILTER_COLUMNS[type] || type;
): Promise<PageviewMetricsData[]> {
const { type, limit = 500, offset = 0 } = parameters;
let column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = prisma;
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(
websiteId,
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters(
{
...filters,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
websiteId,
},
{ joinSession: SESSION_COLUMNS.includes(type) },
);
@@ -46,20 +51,21 @@ async function relationalQuery(
}
if (type === 'entry' || type === 'exit') {
const aggregrate = type === 'entry' ? 'min' : 'max';
const order = type === 'entry' ? 'asc' : 'desc';
column = `x.${column}`;
entryExitQuery = `
join (
select visit_id,
${aggregrate}(created_at) target_created_at
select distinct on (visit_id)
visit_id,
url_path
from website_event
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
group by visit_id
and website_event.event_type != 2
order by visit_id, created_at ${order}
) x
on x.visit_id = website_event.visit_id
and x.target_created_at = website_event.created_at
`;
}
@@ -69,11 +75,11 @@ async function relationalQuery(
count(distinct website_event.session_id) as y
from website_event
${cohortQuery}
${joinSession}
${joinSessionQuery}
${entryExitQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
and website_event.event_type != 2
${excludeDomain}
${filterQuery}
group by 1
@@ -81,22 +87,22 @@ async function relationalQuery(
limit ${limit}
offset ${offset}
`,
params,
{ ...queryParams, ...parameters },
FUNCTION_NAME,
);
}
async function clickhouseQuery(
websiteId: string,
type: string,
parameters: PageviewMetricsParameters,
filters: QueryFilters,
limit: number | string = 500,
offset: number | string = 0,
): Promise<{ x: string; y: number }[]> {
const column = FILTER_COLUMNS[type] || type;
const { type, limit = 500, offset = 0 } = parameters;
let column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
websiteId,
});
let sql = '';
@@ -110,18 +116,18 @@ async function clickhouseQuery(
}
if (type === 'entry' || type === 'exit') {
const aggregrate = type === 'entry' ? 'min' : 'max';
const aggregrate = type === 'entry' ? 'argMin' : 'argMax';
column = `x.${column}`;
entryExitQuery = `
JOIN (select visit_id,
${aggregrate}(created_at) target_created_at
${aggregrate}(url_path, created_at) url_path
from website_event
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
and event_type != 2
group by visit_id) x
ON x.visit_id = website_event.visit_id
and x.target_created_at = website_event.created_at`;
ON x.visit_id = website_event.visit_id`;
}
sql = `
@@ -132,7 +138,7 @@ async function clickhouseQuery(
${entryExitQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
and event_type != 2
${excludeDomain}
${filterQuery}
group by x
@@ -166,11 +172,11 @@ async function clickhouseQuery(
from (
select session_id s,
${columnQuery} as t
from website_event_stats_hourly website_event
from website_event_stats_hourly as website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
and event_type != 2
${excludeDomain}
${filterQuery}
${groupByQuery}) as g
@@ -181,5 +187,5 @@ async function clickhouseQuery(
`;
}
return rawQuery(sql, params);
return rawQuery(sql, { ...queryParams, ...parameters }, FUNCTION_NAME);
}

View File

@@ -1,9 +1,11 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { EVENT_COLUMNS, EVENT_TYPE } from '@/lib/constants';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getPageviewStats';
export async function getPageviewStats(...args: [websiteId: string, filters: QueryFilters]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -14,9 +16,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, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
...filters,
eventType: EVENT_TYPE.pageView,
websiteId,
});
return rawQuery(
@@ -25,16 +27,17 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
${getDateSQL('website_event.created_at', unit, timezone)} x,
count(*) y
from website_event
${cohortQuery}
${joinSession}
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
and website_event.event_type != 2
${filterQuery}
group by 1
order by 1
`,
params,
queryParams,
FUNCTION_NAME,
);
}
@@ -42,11 +45,11 @@ async function clickhouseQuery(
websiteId: string,
filters: QueryFilters,
): Promise<{ x: string; y: number }[]> {
const { timezone = 'utc', unit = 'day' } = filters;
const { timezone = 'UTC', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateSQL } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
eventType: EVENT_TYPE.pageView,
websiteId,
});
let sql = '';
@@ -64,7 +67,7 @@ async function clickhouseQuery(
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
and event_type != 2
${filterQuery}
group by t
) as g
@@ -78,12 +81,12 @@ async function clickhouseQuery(
from (
select
${getDateSQL('website_event.created_at', unit, timezone)} as t,
sum(views)as y
from website_event_stats_hourly website_event
sum(views) as y
from website_event_stats_hourly as website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
and event_type != 2
${filterQuery}
group by t
) as g
@@ -91,5 +94,5 @@ async function clickhouseQuery(
`;
}
return rawQuery(sql, params);
return rawQuery(sql, queryParams, FUNCTION_NAME);
}

View File

@@ -1,19 +1,31 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface AttributionParameters {
startDate: Date;
endDate: Date;
model: string;
type: string;
step: string;
currency?: string;
}
export interface AttributionResult {
referrer: { name: string; value: number }[];
paidAds: { name: string; value: number }[];
utm_source: { name: string; value: number }[];
utm_medium: { name: string; value: number }[];
utm_campaign: { name: string; value: number }[];
utm_content: { name: string; value: number }[];
utm_term: { name: string; value: number }[];
total: { pageviews: number; visitors: number; visits: number };
}
export async function getAttribution(
...args: [
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
model: string;
steps: { type: string; value: string }[];
currency: string;
},
]
...args: [websiteId: string, parameters: AttributionParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -23,30 +35,19 @@ export async function getAttribution(
async function relationalQuery(
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
model: string;
steps: { type: string; value: string }[];
currency: string;
},
): Promise<{
referrer: { name: string; value: number }[];
paidAds: { name: string; value: number }[];
utm_source: { name: string; value: number }[];
utm_medium: { name: string; value: number }[];
utm_campaign: { name: string; value: number }[];
utm_content: { name: string; value: number }[];
utm_term: { name: string; value: number }[];
total: { pageviews: number; visitors: number; visits: number };
}> {
const { startDate, endDate, model, steps, currency } = criteria;
const { rawQuery } = prisma;
const conversionStep = steps[0].value;
const eventType = steps[0].type === 'url' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = steps[0].type === 'url' ? 'url_path' : 'event_name';
const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like';
parameters: AttributionParameters,
filters: QueryFilters,
): Promise<AttributionResult> {
const { model, type, currency } = parameters;
const { rawQuery, parseFilters } = prisma;
const eventType = type === 'path' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = type === 'path' ? 'url_path' : 'event_name';
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
...parameters,
websiteId,
eventType,
});
function getUTMQuery(utmColumn: string) {
return `
@@ -68,29 +69,40 @@ async function relationalQuery(
const eventQuery = `WITH events AS (
select distinct
session_id,
max(created_at) max_dt
website_event.session_id,
max(website_event.created_at) max_dt
from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and ${column} = {{conversionStep}}
and event_type = {{eventType}}
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and website_event.${column} = {{step}}
${filterQuery}
group by 1),`;
const revenueEventQuery = `WITH events AS (
select
session_id,
max(created_at) max_dt,
sum(revenue) value
revenue.session_id,
max(revenue.created_at) max_dt,
sum(revenue.revenue) value
from revenue
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and ${column} = {{conversionStep}}
and currency ${like} {{currency}}
join website_event
on website_event.website_id = revenue.website_id
and website_event.session_id = revenue.session_id
and website_event.event_id = revenue.event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${cohortQuery}
${joinSessionQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
and revenue.${column} = {{step}}
and revenue.currency = {{currency}}
${filterQuery}
group by 1),`;
function getModelQuery(model: string) {
return model === 'firstClick'
return model === 'first-click'
? `\n
model AS (select e.session_id,
min(we.created_at) created_at
@@ -137,7 +149,7 @@ async function relationalQuery(
order by 2 desc
limit 20
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const paidAdsres = await rawQuery(
@@ -170,7 +182,7 @@ async function relationalQuery(
FROM results
${currency ? '' : `WHERE name != ''`}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const sourceRes = await rawQuery(
@@ -179,7 +191,7 @@ async function relationalQuery(
${getModelQuery(model)}
${getUTMQuery('utm_source')}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const mediumRes = await rawQuery(
@@ -188,7 +200,7 @@ async function relationalQuery(
${getModelQuery(model)}
${getUTMQuery('utm_medium')}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const campaignRes = await rawQuery(
@@ -197,7 +209,7 @@ async function relationalQuery(
${getModelQuery(model)}
${getUTMQuery('utm_campaign')}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const contentRes = await rawQuery(
@@ -206,7 +218,7 @@ async function relationalQuery(
${getModelQuery(model)}
${getUTMQuery('utm_content')}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const termRes = await rawQuery(
@@ -215,22 +227,24 @@ async function relationalQuery(
${getModelQuery(model)}
${getUTMQuery('utm_term')}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const totalRes = await rawQuery(
`
select
count(*) as "pageviews",
count(distinct session_id) as "visitors",
count(distinct visit_id) as "visits"
count(distinct website_event.session_id) as "visitors",
count(distinct website_event.visit_id) as "visits"
from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and ${column} = {{conversionStep}}
and event_type = {{eventType}}
${joinSessionQuery}
${cohortQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and website_event.${column} = {{step}}
${filterQuery}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
).then(result => result?.[0]);
return {
@@ -247,45 +261,64 @@ async function relationalQuery(
async function clickhouseQuery(
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
model: string;
steps: { type: string; value: string }[];
currency: string;
},
): Promise<{
referrer: { name: string; value: number }[];
paidAds: { name: string; value: number }[];
utm_source: { name: string; value: number }[];
utm_medium: { name: string; value: number }[];
utm_campaign: { name: string; value: number }[];
utm_content: { name: string; value: number }[];
utm_term: { name: string; value: number }[];
total: { pageviews: number; visitors: number; visits: number };
}> {
const { startDate, endDate, model, steps, currency } = criteria;
const { rawQuery } = clickhouse;
const conversionStep = steps[0].value;
const eventType = steps[0].type === 'url' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = steps[0].type === 'url' ? 'url_path' : 'event_name';
parameters: AttributionParameters,
filters: QueryFilters,
): Promise<AttributionResult> {
const { model, type, currency } = parameters;
const { rawQuery, parseFilters } = clickhouse;
const eventType = type === 'path' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = type === 'path' ? 'url_path' : 'event_name';
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
...parameters,
websiteId,
eventType,
});
function getUTMQuery(utmColumn: string) {
return `
select
we.${utmColumn} name,
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
from model m
join website_event we
on we.created_at = m.created_at
and we.session_id = m.session_id
${currency ? 'join events e on e.session_id = m.session_id' : ''}
where we.website_id = {websiteId:UUID}
select
we.${utmColumn} name,
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
from model m
join website_event we
on we.created_at = m.created_at
and we.session_id = m.session_id
${currency ? 'join events e on e.session_id = m.session_id' : ''}
where we.website_id = {websiteId:UUID}
and we.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${currency ? '' : `and we.${utmColumn} != ''`}
group by 1
order by 2 desc
limit 20
`;
}
function getModelQuery(model: string) {
if (model === 'first-click') {
return `
model AS (select e.session_id,
min(we.created_at) created_at
from events e
join website_event we
on we.session_id = e.session_id
where we.website_id = {websiteId:UUID}
and we.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${currency ? '' : `and we.${utmColumn} != ''`}
group by 1
order by 2 desc
limit 20`;
group by e.session_id)
`;
}
return `
model AS (select e.session_id,
max(we.created_at) created_at
from events e
join website_event we
on we.session_id = e.session_id
where we.website_id = {websiteId:UUID}
and we.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and we.created_at < e.max_dt
group by e.session_id)
`;
}
const eventQuery = `WITH events AS (
@@ -293,47 +326,33 @@ async function clickhouseQuery(
session_id,
max(created_at) max_dt
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and ${column} = {conversionStep:String}
and event_type = {eventType:UInt32}
and ${column} = {step:String}
${filterQuery}
group by 1),`;
const revenueEventQuery = `WITH events AS (
select
session_id,
max(created_at) max_dt,
sum(revenue) as value
website_revenue.session_id,
max(website_revenue.created_at) max_dt,
sum(website_revenue.revenue) as value
from website_revenue
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and ${column} = {conversionStep:String}
and currency = {currency:String}
join website_event
on website_event.website_id = website_revenue.website_id
and website_event.session_id = website_revenue.session_id
and website_event.event_id = website_revenue.event_id
and website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where website_revenue.website_id = {websiteId:UUID}
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and website_revenue.${column} = {step:String}
and website_revenue.currency = {currency:String}
${filterQuery}
group by 1),`;
function getModelQuery(model: string) {
return model === 'firstClick'
? `\n
model AS (select e.session_id,
min(we.created_at) created_at
from events e
join website_event we
on we.session_id = e.session_id
where we.website_id = {websiteId:UUID}
and we.created_at between {startDate:DateTime64} and {endDate:DateTime64}
group by e.session_id)`
: `\n
model AS (select e.session_id,
max(we.created_at) created_at
from events e
join website_event we
on we.session_id = e.session_id
where we.website_id = {websiteId:UUID}
and we.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and we.created_at < e.max_dt
group by e.session_id)`;
}
const referrerRes = await rawQuery<
{
name: string;
@@ -362,7 +381,7 @@ async function clickhouseQuery(
order by 2 desc
limit 20
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const paidAdsres = await rawQuery<
@@ -393,7 +412,7 @@ async function clickhouseQuery(
order by 2 desc
limit 20
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const sourceRes = await rawQuery<
@@ -407,7 +426,7 @@ async function clickhouseQuery(
${getModelQuery(model)}
${getUTMQuery('utm_source')}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const mediumRes = await rawQuery<
@@ -421,7 +440,7 @@ async function clickhouseQuery(
${getModelQuery(model)}
${getUTMQuery('utm_medium')}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const campaignRes = await rawQuery<
@@ -435,7 +454,7 @@ async function clickhouseQuery(
${getModelQuery(model)}
${getUTMQuery('utm_campaign')}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const contentRes = await rawQuery<
@@ -449,7 +468,7 @@ async function clickhouseQuery(
${getModelQuery(model)}
${getUTMQuery('utm_content')}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const termRes = await rawQuery<
@@ -463,7 +482,7 @@ async function clickhouseQuery(
${getModelQuery(model)}
${getUTMQuery('utm_term')}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
);
const totalRes = await rawQuery<{ pageviews: number; visitors: number; visits: number }>(
@@ -473,12 +492,13 @@ async function clickhouseQuery(
uniqExact(session_id) as "visitors",
uniqExact(visit_id) as "visits"
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and ${column} = {conversionStep:String}
and event_type = {eventType:UInt32}
and ${column} = {step:String}
${filterQuery}
`,
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
queryParams,
).then(result => result?.[0]);
return {

View File

@@ -4,8 +4,19 @@ import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { QueryFilters } from '@/lib/types';
export async function getInsights(
...args: [websiteId: string, fields: { name: string; type?: string }[], filters: QueryFilters]
export interface BreakdownParameters {
startDate: Date;
endDate: Date;
fields: string[];
}
export interface BreakdownData {
x: string;
y: number;
}
export async function getBreakdown(
...args: [websiteId: string, parameters: BreakdownParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -15,23 +26,21 @@ export async function getInsights(
async function relationalQuery(
websiteId: string,
fields: { name: string; type?: string }[],
parameters: BreakdownParameters,
filters: QueryFilters,
): Promise<
{
x: string;
y: number;
}[]
> {
): Promise<BreakdownData[]> {
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(
websiteId,
const { startDate, endDate, fields } = parameters;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters(
{
...filters,
websiteId,
startDate,
endDate,
eventType: EVENT_TYPE.pageView,
},
{
joinSession: !!fields.find(({ name }) => SESSION_COLUMNS.includes(name)),
joinSession: !!fields.find((name: string) => SESSION_COLUMNS.includes(name)),
},
);
@@ -53,11 +62,10 @@ async function relationalQuery(
min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time"
from website_event
${cohortQuery}
${joinSession}
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
${filterQuery}
group by ${parseFieldsByName(fields)},
website_event.session_id, website_event.visit_id
@@ -66,23 +74,22 @@ async function relationalQuery(
order by 1 desc, 2 desc
limit 500
`,
params,
queryParams,
);
}
async function clickhouseQuery(
websiteId: string,
fields: { name: string; type?: string }[],
parameters: BreakdownParameters,
filters: QueryFilters,
): Promise<
{
x: string;
y: number;
}[]
> {
): Promise<BreakdownData[]> {
const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
const { startDate, endDate, fields } = parameters;
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
eventType: EVENT_TYPE.pageView,
});
@@ -107,7 +114,6 @@ async function clickhouseQuery(
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
${filterQuery}
group by ${parseFieldsByName(fields)},
session_id, visit_id
@@ -116,14 +122,14 @@ async function clickhouseQuery(
order by 1 desc, 2 desc
limit 500
`,
params,
queryParams,
);
}
function parseFields(fields: { name: any }[]) {
return fields.map(({ name }) => `${FILTER_COLUMNS[name]} as "${name}"`).join(',');
function parseFields(fields: string[]) {
return fields.map(name => `${FILTER_COLUMNS[name]} as "${name}"`).join(',');
}
function parseFieldsByName(fields: { name: any }[]) {
return `${fields.map(({ name }) => name).join(',')}`;
function parseFieldsByName(fields: string[]) {
return `${fields.map(name => name).join(',')}`;
}

View File

@@ -1,36 +1,23 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const formatResults = (steps: { type: string; value: string }[]) => (results: unknown) => {
return steps.map((step: { type: string; value: string }, i: number) => {
const visitors = Number(results[i]?.count) || 0;
const previous = Number(results[i - 1]?.count) || 0;
const dropped = previous > 0 ? previous - visitors : 0;
const dropoff = 1 - visitors / previous;
const remaining = visitors / Number(results[0].count);
export interface FunnelParameters {
startDate: Date;
endDate: Date;
window: number;
steps: { type: string; value: string }[];
}
return {
...step,
visitors,
previous,
dropped,
dropoff,
remaining,
};
});
};
export interface FunnelResult {
value: string;
visitors: number;
dropoff: number;
}
export async function getFunnel(
...args: [
websiteId: string,
criteria: {
windowMinutes: number;
startDate: Date;
endDate: Date;
steps: { type: string; value: string }[];
},
]
...args: [websiteId: string, parameters: FunnelParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -40,26 +27,22 @@ export async function getFunnel(
async function relationalQuery(
websiteId: string,
criteria: {
windowMinutes: number;
startDate: Date;
endDate: Date;
steps: { type: string; value: string }[];
},
): Promise<
{
value: string;
visitors: number;
dropoff: number;
}[]
> {
const { windowMinutes, startDate, endDate, steps } = criteria;
const { rawQuery, getAddIntervalQuery } = prisma;
const { levelOneQuery, levelQuery, sumQuery, params } = getFunnelQuery(steps, windowMinutes);
parameters: FunnelParameters,
filters: QueryFilters,
): Promise<FunnelResult[]> {
const { startDate, endDate, window, steps } = parameters;
const { rawQuery, getAddIntervalQuery, parseFilters } = prisma;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
});
const { levelOneQuery, levelQuery, sumQuery, params } = getFunnelQuery(steps, window);
function getFunnelQuery(
steps: { type: string; value: string }[],
windowMinutes: number,
window: number,
): {
levelOneQuery: string;
levelQuery: string;
@@ -70,7 +53,7 @@ async function relationalQuery(
(pv, cv, i) => {
const levelNumber = i + 1;
const startSum = i > 0 ? 'union ' : '';
const isURL = cv.type === 'url';
const isURL = cv.type === 'path';
const column = isURL ? 'url_path' : 'event_name';
let operator = '=';
@@ -84,11 +67,14 @@ async function relationalQuery(
if (levelNumber === 1) {
pv.levelOneQuery = `
WITH level1 AS (
select distinct session_id, created_at
select distinct website_event.session_id, website_event.created_at
from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and ${column} ${operator} {{${i}}}
${filterQuery}
)`;
} else {
pv.levelQuery += `
@@ -100,7 +86,7 @@ async function relationalQuery(
where we.website_id = {{websiteId::uuid}}
and we.created_at between l.created_at and ${getAddIntervalQuery(
`l.created_at `,
`${windowMinutes} minute`,
`${window} minute`,
)}
and we.${column} ${operator} {{${i}}}
and we.created_at <= {{endDate}}
@@ -129,22 +115,16 @@ async function relationalQuery(
ORDER BY level;
`,
{
websiteId,
startDate,
endDate,
...params,
...queryParams,
},
).then(formatResults(steps));
}
async function clickhouseQuery(
websiteId: string,
criteria: {
windowMinutes: number;
startDate: Date;
endDate: Date;
steps: { type: string; value: string }[];
},
parameters: FunnelParameters,
filters: QueryFilters,
): Promise<
{
value: string;
@@ -152,29 +132,35 @@ async function clickhouseQuery(
dropoff: number;
}[]
> {
const { windowMinutes, startDate, endDate, steps } = criteria;
const { rawQuery } = clickhouse;
const { startDate, endDate, window, steps } = parameters;
const { rawQuery, parseFilters } = clickhouse;
const { levelOneQuery, levelQuery, sumQuery, stepFilterQuery, params } = getFunnelQuery(
steps,
windowMinutes,
window,
);
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
});
function getFunnelQuery(
steps: { type: string; value: string }[],
windowMinutes: number,
window: number,
): {
levelOneQuery: string;
levelQuery: string;
sumQuery: string;
stepFilterQuery: string;
params: { [key: string]: string };
params: Record<string, string>;
} {
return steps.reduce(
(pv, cv, i) => {
const levelNumber = i + 1;
const startSum = i > 0 ? 'union all ' : '';
const startFilter = i > 0 ? 'or' : '';
const isURL = cv.type === 'url';
const isURL = cv.type === 'path';
const column = isURL ? 'url_path' : 'event_name';
let operator = '=';
@@ -203,7 +189,7 @@ async function clickhouseQuery(
from level${i} x
join level0 y
on x.session_id = y.session_id
where y.created_at between x.created_at and x.created_at + interval ${windowMinutes} minute
where y.created_at between x.created_at and x.created_at + interval ${window} minute
and y.${column} ${operator} {param${i}:String}
)`;
}
@@ -229,9 +215,11 @@ async function clickhouseQuery(
WITH level0 AS (
select distinct session_id, url_path, referrer_path, event_name, created_at
from website_event
${cohortQuery}
where (${stepFilterQuery})
and website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
),
${levelOneQuery}
${levelQuery}
@@ -241,10 +229,27 @@ async function clickhouseQuery(
) ORDER BY level;
`,
{
websiteId,
startDate,
endDate,
...params,
...queryParams,
},
).then(formatResults(steps));
}
const formatResults = (steps: { type: string; value: string }[]) => (results: unknown) => {
return steps.map((step: { type: string; value: string }, i: number) => {
const visitors = Number(results[i]?.count) || 0;
const previous = Number(results[i - 1]?.count) || 0;
const dropped = previous > 0 ? previous - visitors : 0;
const dropoff = 1 - visitors / previous;
const remaining = visitors / Number(results[0].count);
return {
...step,
visitors,
previous,
dropped,
dropoff,
remaining,
};
});
};

View File

@@ -0,0 +1,105 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface GoalParameters {
startDate: Date;
endDate: Date;
type: string;
value: string;
operator?: string;
property?: string;
}
export async function getGoal(
...args: [websiteId: string, params: GoalParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
parameters: GoalParameters,
filters: QueryFilters,
) {
const { startDate, endDate, type, value } = parameters;
const { rawQuery, parseFilters } = prisma;
const eventType = type === 'path' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = type === 'path' ? 'url_path' : 'event_name';
const { filterQuery, dateQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
value,
startDate,
endDate,
eventType,
});
return rawQuery(
`
select count(distinct website_event.session_id) as num,
(
select count(distinct website_event.session_id)
from website_event
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
${dateQuery}
${filterQuery}
) as total
from website_event
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and ${column} = {{value}}
${dateQuery}
${filterQuery}
`,
queryParams,
).then(results => results?.[0]);
}
async function clickhouseQuery(
websiteId: string,
parameters: GoalParameters,
filters: QueryFilters,
) {
const { startDate, endDate, type, value } = parameters;
const { rawQuery, parseFilters } = clickhouse;
const eventType = type === 'path' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = type === 'path' ? 'url_path' : 'event_name';
const { filterQuery, dateQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
value,
startDate,
endDate,
eventType,
});
return rawQuery(
`
select count(distinct session_id) as num,
(
select count(distinct session_id)
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
${dateQuery}
${filterQuery}
) as total
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and ${column} = {value:String}
${dateQuery}
${filterQuery}
`,
queryParams,
).then(results => results?.[0]);
}

View File

@@ -1,375 +0,0 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
export async function getGoals(
...args: [
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
goals: { type: string; value: string; goal: number; operator?: string }[];
},
]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
goals: { type: string; value: string; goal: number; operator?: string }[];
},
): Promise<any> {
const { startDate, endDate, goals } = criteria;
const { rawQuery } = prisma;
const urls = goals.filter(a => a.type === 'url');
const events = goals.filter(a => a.type === 'event');
const eventData = goals.filter(a => a.type === 'event-data');
const hasUrl = urls.length > 0;
const hasEvent = events.length > 0;
const hasEventData = eventData.length > 0;
function getParameters(
urls: { type: string; value: string; goal: number }[],
events: { type: string; value: string; goal: number }[],
eventData: {
type: string;
value: string;
goal: number;
operator?: string;
property?: string;
}[],
) {
const urlParam = urls.reduce((acc, cv, i) => {
acc[`${cv.type}${i}`] = cv.value;
return acc;
}, {});
const eventParam = events.reduce((acc, cv, i) => {
acc[`${cv.type}${i}`] = cv.value;
return acc;
}, {});
const eventDataParam = eventData.reduce((acc, cv, i) => {
acc[`eventData${i}`] = cv.value;
acc[`property${i}`] = cv.property;
return acc;
}, {});
return {
urls: { ...urlParam, startDate, endDate, websiteId },
events: { ...eventParam, startDate, endDate, websiteId },
eventData: { ...eventDataParam, startDate, endDate, websiteId },
};
}
function getColumns(
urls: { type: string; value: string; goal: number }[],
events: { type: string; value: string; goal: number }[],
eventData: {
type: string;
value: string;
goal: number;
operator?: string;
property?: string;
}[],
) {
const urlColumns = urls
.map((a, i) => `COUNT(CASE WHEN url_path = {{url${i}}} THEN 1 END) AS URL${i},`)
.join('\n')
.slice(0, -1);
const eventColumns = events
.map((a, i) => `COUNT(CASE WHEN event_name = {{event${i}}} THEN 1 END) AS EVENT${i},`)
.join('\n')
.slice(0, -1);
const eventDataColumns = eventData
.map(
(a, i) =>
`${
a.operator === 'average' ? 'avg' : a.operator
}(CASE WHEN event_name = {{eventData${i}}} AND data_key = {{property${i}}} THEN ${
a.operator === 'count' ? '1' : 'number_value'
} END) AS EVENT_DATA${i},`,
)
.join('\n')
.slice(0, -1);
return { urls: urlColumns, events: eventColumns, eventData: eventDataColumns };
}
function getWhere(
urls: { type: string; value: string; goal: number }[],
events: { type: string; value: string; goal: number }[],
eventData: {
type: string;
value: string;
goal: number;
operator?: string;
property?: string;
}[],
) {
const urlWhere = urls.map((a, i) => `{{url${i}}}`).join(',');
const eventWhere = events.map((a, i) => `{{event${i}}}`).join(',');
const eventDataNameWhere = eventData.map((a, i) => `{{eventData${i}}}`).join(',');
const eventDataKeyWhere = eventData.map((a, i) => `{{property${i}}}`).join(',');
return {
urls: `and url_path in (${urlWhere})`,
events: `and event_name in (${eventWhere})`,
eventData: `and event_name in (${eventDataNameWhere}) and data_key in (${eventDataKeyWhere})`,
};
}
const parameters = getParameters(urls, events, eventData);
const columns = getColumns(urls, events, eventData);
const where = getWhere(urls, events, eventData);
const urlResults = hasUrl
? await rawQuery(
`
select
${columns.urls}
from website_event
where website_id = {{websiteId::uuid}}
${where.urls}
and created_at between {{startDate}} and {{endDate}}
`,
parameters.urls,
).then(a => {
const results = a[0];
return Object.keys(results).map((key, i) => ({
...urls[i],
goal: Number(urls[i].goal),
result: Number(results[key]),
}));
})
: [];
const eventResults = hasEvent
? await rawQuery(
`
select
${columns.events}
from website_event
where website_id = {{websiteId::uuid}}
${where.events}
and created_at between {{startDate}} and {{endDate}}
`,
parameters.events,
).then(a => {
const results = a[0];
return Object.keys(results).map((key, i) => {
return { ...events[i], goal: Number(events[i].goal), result: Number(results[key]) };
});
})
: [];
const eventDataResults = hasEventData
? await rawQuery(
`
select
${columns.eventData}
from website_event w
join event_data d
on d.website_event_id = w.event_id
where w.website_id = {{websiteId::uuid}}
${where.eventData}
and w.created_at between {{startDate}} and {{endDate}}
`,
parameters.eventData,
).then(a => {
const results = a[0];
return Object.keys(results).map((key, i) => {
return { ...eventData[i], goal: Number(eventData[i].goal), result: Number(results[key]) };
});
})
: [];
return [...urlResults, ...eventResults, ...eventDataResults];
}
async function clickhouseQuery(
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
goals: { type: string; value: string; goal: number; operator?: string; property?: string }[];
},
): Promise<{ type: string; value: string; goal: number; result: number }[]> {
const { startDate, endDate, goals } = criteria;
const { rawQuery } = clickhouse;
const urls = goals.filter(a => a.type === 'url');
const events = goals.filter(a => a.type === 'event');
const eventData = goals.filter(a => a.type === 'event-data');
const hasUrl = urls.length > 0;
const hasEvent = events.length > 0;
const hasEventData = eventData.length > 0;
function getParameters(
urls: { type: string; value: string; goal: number }[],
events: { type: string; value: string; goal: number }[],
eventData: {
type: string;
value: string;
goal: number;
operator?: string;
property?: string;
}[],
) {
const urlParam = urls.reduce((acc, cv, i) => {
acc[`${cv.type}${i}`] = cv.value;
return acc;
}, {});
const eventParam = events.reduce((acc, cv, i) => {
acc[`${cv.type}${i}`] = cv.value;
return acc;
}, {});
const eventDataParam = eventData.reduce((acc, cv, i) => {
acc[`eventData${i}`] = cv.value;
acc[`property${i}`] = cv.property;
return acc;
}, {});
return {
urls: { ...urlParam, startDate, endDate, websiteId },
events: { ...eventParam, startDate, endDate, websiteId },
eventData: { ...eventDataParam, startDate, endDate, websiteId },
};
}
function getColumns(
urls: { type: string; value: string; goal: number }[],
events: { type: string; value: string; goal: number }[],
eventData: {
type: string;
value: string;
goal: number;
operator?: string;
property?: string;
}[],
) {
const urlColumns = urls
.map((a, i) => `countIf(url_path = {url${i}:String}) AS URL${i},`)
.join('\n')
.slice(0, -1);
const eventColumns = events
.map((a, i) => `countIf(event_name = {event${i}:String}) AS EVENT${i},`)
.join('\n')
.slice(0, -1);
const eventDataColumns = eventData
.map(
(a, i) =>
`${a.operator === 'average' ? 'avg' : a.operator}If(${
a.operator !== 'count' ? 'number_value, ' : ''
}event_name = {eventData${i}:String} AND data_key = {property${i}:String}) AS EVENT_DATA${i},`,
)
.join('\n')
.slice(0, -1);
return { url: urlColumns, events: eventColumns, eventData: eventDataColumns };
}
function getWhere(
urls: { type: string; value: string; goal: number }[],
events: { type: string; value: string; goal: number }[],
eventData: {
type: string;
value: string;
goal: number;
operator?: string;
property?: string;
}[],
) {
const urlWhere = urls.map((a, i) => `{url${i}:String}`).join(',');
const eventWhere = events.map((a, i) => `{event${i}:String}`).join(',');
const eventDataNameWhere = eventData.map((a, i) => `{eventData${i}:String}`).join(',');
const eventDataKeyWhere = eventData.map((a, i) => `{property${i}:String}`).join(',');
return {
urls: `and url_path in (${urlWhere})`,
events: `and event_name in (${eventWhere})`,
eventData: `and event_name in (${eventDataNameWhere}) and data_key in (${eventDataKeyWhere})`,
};
}
const parameters = getParameters(urls, events, eventData);
const columns = getColumns(urls, events, eventData);
const where = getWhere(urls, events, eventData);
const urlResults = hasUrl
? await rawQuery(
`
select
${columns.url}
from website_event
where website_id = {websiteId:UUID}
${where.urls}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
`,
parameters.urls,
).then(a => {
const results = a[0];
return Object.keys(results).map((key, i) => {
return { ...urls[i], goal: Number(urls[i].goal), result: Number(results[key]) };
});
})
: [];
const eventResults = hasEvent
? await rawQuery(
`
select
${columns.events}
from website_event
where website_id = {websiteId:UUID}
${where.events}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
`,
parameters.events,
).then(a => {
const results = a[0];
return Object.keys(results).map((key, i) => {
return { ...events[i], goal: Number(events[i].goal), result: Number(results[key]) };
});
})
: [];
const eventDataResults = hasEventData
? await rawQuery(
`
select
${columns.eventData}
from event_data
where website_id = {websiteId:UUID}
${where.eventData}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
`,
parameters.eventData,
).then(a => {
const results = a[0];
return Object.keys(results).map((key, i) => {
return { ...eventData[i], goal: Number(eventData[i].goal), result: Number(results[key]) };
});
})
: [];
return [...urlResults, ...eventResults, ...eventDataResults];
}

View File

@@ -1,8 +1,17 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
interface JourneyResult {
export interface JourneyParameters {
startDate: Date;
endDate: Date;
steps: number;
startStep?: string;
endStep?: string;
}
export interface JourneyResult {
e1: string;
e2: string;
e3: string;
@@ -14,16 +23,7 @@ interface JourneyResult {
}
export async function getJourney(
...args: [
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
steps: number;
startStep?: string;
endStep?: string;
},
]
...args: [websiteId: string, parameters: JourneyParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -33,21 +33,22 @@ export async function getJourney(
async function relationalQuery(
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
steps: number;
startStep?: string;
endStep?: string;
},
parameters: JourneyParameters,
filters: QueryFilters,
): Promise<JourneyResult[]> {
const { startDate, endDate, steps, startStep, endStep } = filters;
const { rawQuery } = prisma;
const { startDate, endDate, steps, startStep, endStep } = parameters;
const { rawQuery, parseFilters } = prisma;
const { sequenceQuery, startStepQuery, endStepQuery, params } = getJourneyQuery(
steps,
startStep,
endStep,
);
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
});
function getJourneyQuery(
steps: number,
@@ -57,7 +58,7 @@ async function relationalQuery(
sequenceQuery: string;
startStepQuery: string;
endStepQuery: string;
params: { [key: string]: string };
params: Record<string, string>;
} {
const params = {};
let sequenceQuery = '';
@@ -116,13 +117,16 @@ async function relationalQuery(
`
WITH events AS (
select distinct
visit_id,
referrer_path,
coalesce(nullIf(event_name, ''), url_path) "event",
row_number() OVER (PARTITION BY visit_id ORDER BY created_at) AS event_number
website_event.visit_id,
website_event.referrer_path,
coalesce(nullIf(website_event.event_name, ''), website_event.url_path) event,
row_number() OVER (PARTITION BY visit_id ORDER BY website_event.created_at) AS event_number
from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}),
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${filterQuery}),
${sequenceQuery}
select *
from sequences
@@ -133,31 +137,30 @@ async function relationalQuery(
limit 100
`,
{
websiteId,
startDate,
endDate,
...params,
...queryParams,
},
).then(parseResult);
}
async function clickhouseQuery(
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
steps: number;
startStep?: string;
endStep?: string;
},
parameters: JourneyParameters,
filters: QueryFilters,
): Promise<JourneyResult[]> {
const { startDate, endDate, steps, startStep, endStep } = filters;
const { rawQuery } = clickhouse;
const { startDate, endDate, steps, startStep, endStep } = parameters;
const { rawQuery, parseFilters } = clickhouse;
const { sequenceQuery, startStepQuery, endStepQuery, params } = getJourneyQuery(
steps,
startStep,
endStep,
);
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
});
function getJourneyQuery(
steps: number,
@@ -167,7 +170,7 @@ async function clickhouseQuery(
sequenceQuery: string;
startStepQuery: string;
endStepQuery: string;
params: { [key: string]: string };
params: Record<string, string>;
} {
const params = {};
let sequenceQuery = '';
@@ -230,7 +233,9 @@ async function clickhouseQuery(
coalesce(nullIf(event_name, ''), url_path) "event",
row_number() OVER (PARTITION BY visit_id ORDER BY created_at) AS event_number
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
${filterQuery}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}),
${sequenceQuery}
select *
@@ -242,10 +247,8 @@ async function clickhouseQuery(
limit 100
`,
{
websiteId,
startDate,
endDate,
...params,
...queryParams,
},
).then(parseResult);
}

View File

@@ -1,16 +1,24 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface RetentionParameters {
startDate: Date;
endDate: Date;
timezone?: string;
}
export interface RetentionResult {
date: string;
day: number;
visitors: number;
returnVisitors: number;
percentage: number;
}
export async function getRetention(
...args: [
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
timezone?: string;
},
]
...args: [websiteId: string, parameters: RetentionParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -20,42 +28,45 @@ export async function getRetention(
async function relationalQuery(
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
timezone?: string;
},
): Promise<
{
date: string;
day: number;
visitors: number;
returnVisitors: number;
percentage: number;
}[]
> {
const { startDate, endDate, timezone = 'UTC' } = filters;
const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma;
parameters: RetentionParameters,
filters: QueryFilters,
): Promise<RetentionResult[]> {
const { startDate, endDate, timezone } = parameters;
const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery, parseFilters } = prisma;
const unit = 'day';
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
timezone,
});
return rawQuery(
`
WITH cohort_items AS (
select session_id,
${getDateSQL('created_at', unit, timezone)} as cohort_date
from session
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
select
min(${getDateSQL('website_event.created_at', unit, timezone)}) as cohort_date,
website_event.session_id
from website_event
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${filterQuery}
group by website_event.session_id
),
user_activities AS (
select distinct
w.session_id,
${getDayDiffQuery(getDateSQL('created_at', unit, timezone), 'c.cohort_date')} as day_number
from website_event w
join cohort_items c
on w.session_id = c.session_id
website_event.session_id,
${getDayDiffQuery(getDateSQL('created_at', unit, timezone), 'cohort_items.cohort_date')} as day_number
from website_event
join cohort_items
on website_event.session_id = cohort_items.session_id
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
),
cohort_size as (
select cohort_date,
@@ -85,34 +96,27 @@ async function relationalQuery(
on c.cohort_date = s.cohort_date
where c.day_number <= 31
order by 1, 2`,
{
websiteId,
startDate,
endDate,
},
queryParams,
);
}
async function clickhouseQuery(
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
timezone?: string;
},
): Promise<
{
date: string;
day: number;
visitors: number;
returnVisitors: number;
percentage: number;
}[]
> {
const { startDate, endDate, timezone = 'UTC' } = filters;
const { getDateSQL, rawQuery } = clickhouse;
parameters: RetentionParameters,
filters: QueryFilters,
): Promise<RetentionResult[]> {
const { startDate, endDate, timezone } = parameters;
const { getDateSQL, rawQuery, parseFilters } = clickhouse;
const unit = 'day';
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
timezone,
});
return rawQuery(
`
WITH cohort_items AS (
@@ -120,17 +124,19 @@ async function clickhouseQuery(
min(${getDateSQL('created_at', unit, timezone)}) as cohort_date,
session_id
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
group by session_id
),
user_activities AS (
select distinct
w.session_id,
(${getDateSQL('created_at', unit, timezone)} - c.cohort_date) / 86400 as day_number
from website_event w
join cohort_items c
on w.session_id = c.session_id
website_event.session_id,
toInt32((${getDateSQL('created_at', unit, timezone)} - cohort_items.cohort_date) / 86400) as day_number
from website_event
join cohort_items
on website_event.session_id = cohort_items.session_id
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
),
@@ -162,10 +168,6 @@ async function clickhouseQuery(
on c.cohort_date = s.cohort_date
where c.day_number <= 31
order by 1, 2`,
{
websiteId,
startDate,
endDate,
},
queryParams,
);
}

View File

@@ -1,18 +1,24 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface RevenuParameters {
startDate: Date;
endDate: Date;
unit: string;
timezone: string;
currency: string;
}
export interface RevenueResult {
chart: { x: string; t: string; y: number }[];
country: { name: string; value: number }[];
total: { sum: number; count: number; average: number; unique_count: number };
}
export async function getRevenue(
...args: [
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
unit: string;
timezone: string;
currency: string;
},
]
...args: [websiteId: string, parameters: RevenuParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -22,118 +28,116 @@ export async function getRevenue(
async function relationalQuery(
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
unit: string;
timezone: string;
currency: string;
},
): Promise<{
chart: { x: string; t: string; y: number }[];
country: { name: string; value: number }[];
total: { sum: number; count: number; unique_count: number };
table: {
currency: string;
sum: number;
count: number;
unique_count: number;
}[];
}> {
const { startDate, endDate, timezone = 'UTC', unit = 'day', currency } = criteria;
const { getDateSQL, rawQuery } = prisma;
const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like';
parameters: RevenuParameters,
filters: QueryFilters,
): Promise<RevenueResult> {
const { startDate, endDate, unit = 'day', timezone = 'utc', currency } = parameters;
const { getDateSQL, rawQuery, parseFilters } = prisma;
const { queryParams, filterQuery, cohortQuery, joinSessionQuery } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
currency,
});
const chartRes = await rawQuery(
const joinQuery = filterQuery
? `join website_event
on website_event.website_id = revenue.website_id
and website_event.session_id = revenue.session_id
and website_event.event_id = revenue.event_id
and website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}`
: '';
const chart = await rawQuery(
`
select
event_name x,
${getDateSQL('created_at', unit, timezone)} t,
sum(revenue) y
revenue.event_name x,
${getDateSQL('revenue.created_at', unit, timezone)} t,
sum(revenue.revenue) y
from revenue
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and currency ${like} {{currency}}
${joinQuery}
${cohortQuery}
${joinSessionQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
and revenue.currency ilike {{currency}}
${filterQuery}
group by x, t
order by t
`,
{ websiteId, startDate, endDate, unit, timezone, currency },
queryParams,
);
const countryRes = await rawQuery(
const country = await rawQuery(
`
select
s.country as name,
sum(r.revenue) value
from revenue r
join session s
on s.session_id = r.session_id
where r.website_id = {{websiteId::uuid}}
and r.created_at between {{startDate}} and {{endDate}}
and r.currency ${like} {{currency}}
group by s.country
session.country as name,
sum(revenue) value
from revenue
${joinQuery}
join session
on session.website_id = revenue.website_id
and session.session_id = revenue.session_id
${cohortQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
and revenue.currency ilike {{currency}}
${filterQuery}
group by session.country
`,
{ websiteId, startDate, endDate, currency },
queryParams,
);
const totalRes = await rawQuery(
const total = await rawQuery(
`
select
sum(revenue) as sum,
count(distinct event_id) as count,
count(distinct session_id) as unique_count
from revenue r
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and currency ${like} {{currency}}
sum(revenue.revenue) as sum,
count(distinct revenue.event_id) as count,
count(distinct revenue.session_id) as unique_count
from revenue
${joinQuery}
${cohortQuery}
${joinSessionQuery}
where revenue.website_id = {{websiteId::uuid}}
and revenue.created_at between {{startDate}} and {{endDate}}
and revenue.currency ilike {{currency}}
${filterQuery}
`,
{ websiteId, startDate, endDate, currency },
queryParams,
).then(result => result?.[0]);
const tableRes = await rawQuery(
`
select
currency,
sum(revenue) as sum,
count(distinct event_id) as count,
count(distinct session_id) as unique_count
from revenue r
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
group by currency
order by sum desc
`,
{ websiteId, startDate, endDate, unit, timezone, currency },
);
total.average = total.count > 0 ? Number(total.sum) / Number(total.count) : 0;
return { chart: chartRes, country: countryRes, total: totalRes, table: tableRes };
return { chart, country, total };
}
async function clickhouseQuery(
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
unit: string;
timezone: string;
currency: string;
},
): Promise<{
chart: { x: string; t: string; y: number }[];
country: { name: string; value: number }[];
total: { sum: number; count: number; unique_count: number };
table: {
currency: string;
sum: number;
count: number;
unique_count: number;
}[];
}> {
const { startDate, endDate, timezone = 'UTC', unit = 'day', currency } = criteria;
const { getDateSQL, rawQuery } = clickhouse;
parameters: RevenuParameters,
filters: QueryFilters,
): Promise<RevenueResult> {
const { startDate, endDate, unit = 'day', timezone = 'utc', currency } = parameters;
const { getDateSQL, rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
currency,
});
const chartRes = await rawQuery<
const joinQuery = filterQuery
? `join website_event
on website_event.website_id = website_revenue.website_id
and website_event.session_id = website_revenue.session_id
and website_event.event_id = website_revenue.event_id
and website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}`
: '';
const chart = await rawQuery<
{
x: string;
t: string;
@@ -142,86 +146,72 @@ async function clickhouseQuery(
>(
`
select
event_name x,
${getDateSQL('created_at', unit, timezone)} t,
sum(revenue) y
website_revenue.event_name x,
${getDateSQL('website_revenue.created_at', unit, timezone)} t,
sum(website_revenue.revenue) y
from website_revenue
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and currency = {currency:String}
${joinQuery}
${cohortQuery}
where website_revenue.website_id = {websiteId:UUID}
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and website_revenue.currency = {currency:String}
${filterQuery}
group by x, t
order by t
`,
{ websiteId, startDate, endDate, unit, timezone, currency },
queryParams,
);
const countryRes = await rawQuery<
const country = await rawQuery<
{
name: string;
value: number;
}[]
>(
`
select
s.country as name,
sum(w.revenue) as value
from website_revenue w
join (select distinct website_id, session_id, country
from website_event_stats_hourly
where website_id = {websiteId:UUID}) s
on w.website_id = s.website_id
and w.session_id = s.session_id
where w.website_id = {websiteId:UUID}
and w.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and w.currency = {currency:String}
group by s.country
order by value desc
select
website_event.country as name,
sum(website_revenue.revenue) as value
from website_revenue
join website_event
on website_event.website_id = website_revenue.website_id
and website_event.session_id = website_revenue.session_id
and website_event.event_id = website_revenue.event_id
and website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${cohortQuery}
where website_revenue.website_id = {websiteId:UUID}
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and website_revenue.currency = {currency:String}
${filterQuery}
group by website_event.country
order by value desc
`,
{ websiteId, startDate, endDate, currency },
queryParams,
);
const totalRes = await rawQuery<{
const total = await rawQuery<{
sum: number;
avg: number;
count: number;
unique_count: number;
}>(
`
select
sum(revenue) as sum,
uniqExact(event_id) as count,
uniqExact(session_id) as unique_count
sum(website_revenue.revenue) as sum,
uniqExact(website_revenue.event_id) as count,
uniqExact(website_revenue.session_id) as unique_count
from website_revenue
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and currency = {currency:String}
${joinQuery}
${cohortQuery}
where website_revenue.website_id = {websiteId:UUID}
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and website_revenue.currency = {currency:String}
${filterQuery}
`,
{ websiteId, startDate, endDate, currency },
queryParams,
).then(result => result?.[0]);
const tableRes = await rawQuery<
{
currency: string;
sum: number;
avg: number;
count: number;
unique_count: number;
}[]
>(
`
select
currency,
sum(revenue) as sum,
uniqExact(event_id) as count,
uniqExact(session_id) as unique_count
from website_revenue
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
group by currency
order by sum desc
`,
{ websiteId, startDate, endDate, unit, timezone, currency },
);
total.average = total.count > 0 ? total.sum / total.count : 0;
return { chart: chartRes, country: countryRes, total: totalRes, table: tableRes };
return { chart, country, total };
}

View File

@@ -1,75 +0,0 @@
import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA, getDatabaseType, POSTGRESQL } from '@/lib/db';
export async function getRevenueValues(
...args: [
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
},
]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
},
) {
const { rawQuery } = prisma;
const { startDate, endDate } = criteria;
const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like';
return rawQuery(
`
select distinct string_value as currency
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and data_key ${like} '%currency%'
order by currency
`,
{
websiteId,
startDate,
endDate,
},
);
}
async function clickhouseQuery(
websiteId: string,
criteria: {
startDate: Date;
endDate: Date;
},
) {
const { rawQuery } = clickhouse;
const { startDate, endDate } = criteria;
return rawQuery(
`
select distinct string_value as currency
from event_data
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and positionCaseInsensitive(data_key, 'currency') > 0
order by currency
`,
{
websiteId,
startDate,
endDate,
},
);
}

View File

@@ -1,16 +1,17 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface UTMParameters {
column: string;
startDate: Date;
endDate: Date;
}
export async function getUTM(
...args: [
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
timezone?: string;
},
]
...args: [websiteId: string, parameters: UTMParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -20,58 +21,64 @@ export async function getUTM(
async function relationalQuery(
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
timezone?: string;
},
parameters: UTMParameters,
filters: QueryFilters,
) {
const { startDate, endDate } = filters;
const { rawQuery } = prisma;
const { column, startDate, endDate } = parameters;
const { parseFilters, rawQuery } = prisma;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
eventType: EVENT_TYPE.pageView,
});
return rawQuery(
`
select url_query, count(*) as "num"
select website_event.${column} utm, count(*) as views
from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and coalesce(url_query, '') != ''
and event_type = 1
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and coalesce(website_event.${column}, '') != ''
${filterQuery}
group by 1
order by 2 desc
`,
{
websiteId,
startDate,
endDate,
},
queryParams,
);
}
async function clickhouseQuery(
websiteId: string,
filters: {
startDate: Date;
endDate: Date;
timezone?: string;
},
parameters: UTMParameters,
filters: QueryFilters,
) {
const { startDate, endDate } = filters;
const { rawQuery } = clickhouse;
const { column, startDate, endDate } = parameters;
const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
startDate,
endDate,
eventType: EVENT_TYPE.pageView,
});
return rawQuery(
`
select url_query, count(*) as "num"
select ${column} utm, count(*) as views
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and url_query != ''
and event_type = 1
and ${column} != ''
${filterQuery}
group by 1
order by 2 desc
`,
{
websiteId,
startDate,
endDate,
},
queryParams,
);
}

View File

@@ -1,49 +1,44 @@
import { Prisma } from '@prisma/client';
import { Prisma } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
export async function createSession(
data: Prisma.SessionCreateInput,
options = { skipDuplicates: false },
) {
const {
id,
websiteId,
browser,
os,
device,
screen,
language,
country,
region,
city,
distinctId,
} = data;
const FUNCTION_NAME = 'createSession';
try {
return await prisma.client.session.create({
data: {
id,
websiteId,
browser,
os,
device,
screen,
language,
country,
region,
city,
distinctId,
},
});
} catch (e: any) {
// With skipDuplicates flag: ignore unique constraint error and return null
if (
options.skipDuplicates &&
e instanceof Prisma.PrismaClientKnownRequestError &&
e.code === 'P2002'
) {
return null;
}
throw e;
}
export async function createSession(data: Prisma.SessionCreateInput) {
const { rawQuery } = prisma;
await rawQuery(
`
insert into session (
session_id,
website_id,
browser,
os,
device,
screen,
language,
country,
region,
city,
distinct_id,
created_at
)
values (
{{id}},
{{websiteId}},
{{browser}},
{{os}},
{{device}},
{{screen}},
{{language}},
{{country}},
{{region}},
{{city}},
{{distinctId}},
{{createdAt}}
)
on conflict (session_id) do nothing
`,
data,
FUNCTION_NAME,
);
}

View File

@@ -1,9 +1,12 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionActivity';
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,30 +14,40 @@ export async function getSessionActivity(
});
}
async function relationalQuery(
websiteId: string,
sessionId: string,
startDate: Date,
endDate: Date,
) {
return prisma.client.websiteEvent.findMany({
where: {
sessionId,
websiteId,
createdAt: { gte: startDate, lte: endDate },
},
take: 500,
orderBy: { createdAt: 'desc' },
});
async function relationalQuery(websiteId: string, sessionId: string, filters: QueryFilters) {
const { rawQuery } = prisma;
const { startDate, endDate } = filters;
return rawQuery(
`
select
created_at as "createdAt",
url_path as "urlPath",
url_query as "urlQuery",
referrer_domain as "referrerDomain",
event_id as "eventId",
event_type as "eventType",
event_name as "eventName",
visit_id as "visitId",
event_id IN (select website_event_id
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}) AS "hasData"
from website_event
where website_id = {{websiteId::uuid}}
and session_id = {{sessionId::uuid}}
and created_at between {{startDate}} and {{endDate}}
order by created_at desc
limit 500
`,
{ websiteId, sessionId, startDate, endDate },
FUNCTION_NAME,
);
}
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(
`
@@ -46,7 +59,12 @@ async function clickhouseQuery(
event_id as eventId,
event_type as eventType,
event_name as eventName,
visit_id as visitId
visit_id as visitId,
event_id IN (select event_id
from event_data
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}) AS hasData
from website_event
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
@@ -55,5 +73,6 @@ async function clickhouseQuery(
limit 500
`,
{ websiteId, sessionId, startDate, endDate },
FUNCTION_NAME,
);
}

View File

@@ -2,6 +2,8 @@ import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db';
const FUNCTION_NAME = 'getSessionData';
export async function getSessionData(...args: [websiteId: string, sessionId: string]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -29,6 +31,7 @@ async function relationalQuery(websiteId: string, sessionId: string) {
order by data_key asc
`,
{ websiteId, sessionId },
FUNCTION_NAME,
);
}
@@ -52,5 +55,6 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
order by data_key asc
`,
{ websiteId, sessionId },
FUNCTION_NAME,
);
}

View File

@@ -1,24 +1,24 @@
import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import { QueryFilters, WebsiteEventData } from '@/lib/types';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionDataProperties';
export async function getSessionDataProperties(
...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }]
): Promise<WebsiteEventData[]> {
...args: [websiteId: string, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
filters: QueryFilters & { propertyName?: string },
) {
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
columns: { propertyName: 'data_key' },
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
@@ -27,9 +27,11 @@ async function relationalQuery(
data_key as "propertyName",
count(distinct session_data.session_id) as "total"
from website_event
${cohortQuery}
${cohortQuery}
${joinSessionQuery}
join session_data
on session_data.session_id = website_event.session_id
and session_data.website_id = website_event.website_id
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${filterQuery}
@@ -37,18 +39,17 @@ async function relationalQuery(
order by 2 desc
limit 500
`,
params,
queryParams,
FUNCTION_NAME,
);
}
async function clickhouseQuery(
websiteId: string,
filters: QueryFilters & { propertyName?: string },
filters: QueryFilters,
): Promise<{ propertyName: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters, {
columns: { propertyName: 'data_key' },
});
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
return rawQuery(
`
@@ -59,6 +60,7 @@ async function clickhouseQuery(
${cohortQuery}
join session_data final
on session_data.session_id = website_event.session_id
and session_data.website_id = {websiteId:UUID}
where website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and session_data.data_key != ''
@@ -67,6 +69,7 @@ async function clickhouseQuery(
order by 2 desc
limit 500
`,
params,
queryParams,
FUNCTION_NAME,
);
}

View File

@@ -1,11 +1,13 @@
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';
const FUNCTION_NAME = 'getSessionDataValues';
export async function getSessionDataValues(
...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }]
): Promise<WebsiteEventData[]> {
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
@@ -17,7 +19,10 @@ async function relationalQuery(
filters: QueryFilters & { propertyName?: string },
) {
const { rawQuery, parseFilters, getDateSQL } = prisma;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
`
@@ -28,10 +33,12 @@ async function relationalQuery(
else string_value
end as "value",
count(distinct session_data.session_id) as "total"
from website_event e
from website_event
${cohortQuery}
join session_data d
${joinSessionQuery}
join session_data
on session_data.session_id = website_event.session_id
and session_data.website_id = website_event.website_id
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and session_data.data_key = {{propertyName}}
@@ -40,7 +47,8 @@ async function relationalQuery(
order by 2 desc
limit 100
`,
params,
queryParams,
FUNCTION_NAME,
);
}
@@ -49,7 +57,7 @@ async function clickhouseQuery(
filters: QueryFilters & { propertyName?: string },
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, filters);
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
return rawQuery(
`
@@ -58,10 +66,11 @@ async function clickhouseQuery(
data_type = 4, toString(date_trunc('hour', date_value)),
string_value) as "value",
uniq(session_data.session_id) as "total"
from website_event e
from website_event
${cohortQuery}
join session_data d final
join session_data final
on session_data.session_id = website_event.session_id
and session_data.website_id = {websiteId:UUID}
where website_event.website_id = {websiteId:UUID}
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
and session_data.data_key = {propertyName:String}
@@ -70,6 +79,7 @@ async function clickhouseQuery(
order by 2 desc
limit 100
`,
params,
queryParams,
FUNCTION_NAME,
);
}

View File

@@ -0,0 +1,152 @@
import clickhouse from '@/lib/clickhouse';
import { FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionExpandedMetrics';
export interface SessionExpandedMetricsParameters {
type: string;
limit?: number | string;
offset?: number | string;
}
export interface SessionExpandedMetricsData {
name: string;
pageviews: number;
visitors: number;
visits: number;
bounces: number;
totaltime: number;
}
export async function getSessionExpandedMetrics(
...args: [websiteId: string, parameters: SessionExpandedMetricsParameters, filters: QueryFilters]
): Promise<SessionExpandedMetricsData[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
parameters: SessionExpandedMetricsParameters,
filters: QueryFilters,
): Promise<SessionExpandedMetricsData[]> {
const { type, limit = 500, offset = 0 } = parameters;
let column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery, getTimestampDiffSQL } = prisma;
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters(
{
...filters,
websiteId,
},
{
joinSession: SESSION_COLUMNS.includes(type),
},
);
const includeCountry = column === 'city' || column === 'region';
if (type === 'language') {
column = `lower(left(${type}, 2))`;
}
return rawQuery(
`
select
name,
${includeCountry ? 'country,' : ''}
sum(t.c) as "pageviews",
count(distinct t.session_id) as "visitors",
count(distinct t.visit_id) as "visits",
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
from (
select
${column} name,
${includeCountry ? 'country,' : ''}
website_event.session_id,
website_event.visit_id,
count(*) as "c",
min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time"
from website_event
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and website_event.event_type != 2
${filterQuery}
group by name, website_event.session_id, website_event.visit_id
${includeCountry ? ', country' : ''}
) as t
group by name
${includeCountry ? ', country' : ''}
order by visitors desc, visits desc
limit ${limit}
offset ${offset}
`,
{ ...queryParams, ...parameters },
FUNCTION_NAME,
);
}
async function clickhouseQuery(
websiteId: string,
parameters: SessionExpandedMetricsParameters,
filters: QueryFilters,
): Promise<SessionExpandedMetricsData[]> {
const { type, limit = 500, offset = 0 } = parameters;
let column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
const includeCountry = column === 'city' || column === 'region';
if (type === 'language') {
column = `lower(left(${type}, 2))`;
}
return rawQuery(
`
select
name,
${includeCountry ? 'country,' : ''}
sum(t.c) as "pageviews",
uniq(t.session_id) as "visitors",
uniq(t.visit_id) as "visits",
sum(if(t.c = 1, 1, 0)) as "bounces",
sum(max_time-min_time) as "totaltime"
from (
select
${column} name,
${includeCountry ? 'country,' : ''}
session_id,
visit_id,
count(*) c,
min(created_at) min_time,
max(created_at) max_time
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type != 2
and name != ''
${filterQuery}
group by name, session_id, visit_id
${includeCountry ? ', country' : ''}
) as t
group by name
${includeCountry ? ', country' : ''}
order by visitors desc, visits desc
limit ${limit}
offset ${offset}
`,
{ ...queryParams, ...parameters },
FUNCTION_NAME,
);
}

View File

@@ -1,17 +1,19 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_COLUMNS, EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionMetrics';
export interface SessionMetricsParameters {
type: string;
limit?: number | string;
offset?: number | string;
}
export async function getSessionMetrics(
...args: [
websiteId: string,
type: string,
filters: QueryFilters,
limit?: number | string,
offset?: number | string,
]
...args: [websiteId: string, parameters: SessionMetricsParameters, filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -21,18 +23,16 @@ export async function getSessionMetrics(
async function relationalQuery(
websiteId: string,
type: string,
parameters: SessionMetricsParameters,
filters: QueryFilters,
limit: number | string = 500,
offset: number | string = 0,
) {
const column = FILTER_COLUMNS[type] || type;
const { type, limit = 500, offset = 0 } = parameters;
let column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = prisma;
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(
websiteId,
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters(
{
...filters,
eventType: EVENT_TYPE.pageView,
websiteId,
},
{
joinSession: SESSION_COLUMNS.includes(type),
@@ -40,6 +40,10 @@ async function relationalQuery(
);
const includeCountry = column === 'city' || column === 'region';
if (type === 'language') {
column = `lower(left(${type}, 2))`;
}
return rawQuery(
`
select
@@ -48,10 +52,10 @@ async function relationalQuery(
${includeCountry ? ', country' : ''}
from website_event
${cohortQuery}
${joinSession}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and website_event.event_type = {{eventType}}
and website_event.event_type != 2
${filterQuery}
group by 1
${includeCountry ? ', 3' : ''}
@@ -59,25 +63,29 @@ async function relationalQuery(
limit ${limit}
offset ${offset}
`,
params,
{ ...queryParams, ...parameters },
FUNCTION_NAME,
);
}
async function clickhouseQuery(
websiteId: string,
type: string,
parameters: SessionMetricsParameters,
filters: QueryFilters,
limit: number | string = 500,
offset: number | string = 0,
): Promise<{ x: string; y: number }[]> {
const column = FILTER_COLUMNS[type] || type;
const { type, limit = 500, offset = 0 } = parameters;
let column = FILTER_COLUMNS[type] || type;
const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
eventType: EVENT_TYPE.pageView,
websiteId,
});
const includeCountry = column === 'city' || column === 'region';
if (type === 'language') {
column = `lower(left(${type}, 2))`;
}
let sql = '';
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) {
@@ -90,7 +98,7 @@ async function clickhouseQuery(
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
and event_type != 2
${filterQuery}
group by x
${includeCountry ? ', country' : ''}
@@ -104,11 +112,11 @@ async function clickhouseQuery(
${column} x,
uniq(session_id) y
${includeCountry ? ', country' : ''}
from website_event_stats_hourly website_event
from website_event_stats_hourly as website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
and event_type != 2
${filterQuery}
group by x
${includeCountry ? ', country' : ''}
@@ -118,5 +126,5 @@ async function clickhouseQuery(
`;
}
return rawQuery(sql, params);
return rawQuery(sql, { ...queryParams, ...parameters }, FUNCTION_NAME);
}

View File

@@ -1,9 +1,11 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_COLUMNS, EVENT_TYPE } from '@/lib/constants';
import { EVENT_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionStats';
export async function getSessionStats(...args: [websiteId: string, filters: QueryFilters]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -14,9 +16,9 @@ export async function getSessionStats(...args: [websiteId: string, filters: Quer
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters;
const { getDateSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, {
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
...filters,
eventType: EVENT_TYPE.pageView,
websiteId,
});
return rawQuery(
@@ -25,16 +27,17 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
${getDateSQL('website_event.created_at', unit, timezone)} x,
count(distinct website_event.session_id) y
from website_event
${cohortQuery}
${joinSession}
${cohortQuery}
${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
and website_event.event_type != 2
${filterQuery}
group by 1
order by 1
`,
params,
queryParams,
FUNCTION_NAME,
);
}
@@ -42,11 +45,11 @@ async function clickhouseQuery(
websiteId: string,
filters: QueryFilters,
): Promise<{ x: string; y: number }[]> {
const { timezone = 'utc', unit = 'day' } = filters;
const { timezone = 'UTC', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateSQL } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
eventType: EVENT_TYPE.pageView,
websiteId,
});
let sql = '';
@@ -64,7 +67,7 @@ async function clickhouseQuery(
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
and event_type != 2
${filterQuery}
group by t
) as g
@@ -79,11 +82,11 @@ async function clickhouseQuery(
select
${getDateSQL('website_event.created_at', unit, timezone)} as t,
uniq(session_id) as y
from website_event_stats_hourly website_event
from website_event_stats_hourly as website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
and event_type = {eventType:UInt32}
and event_type != 2
${filterQuery}
group by t
) as g
@@ -91,5 +94,5 @@ async function clickhouseQuery(
`;
}
return rawQuery(sql, params);
return rawQuery(sql, queryParams, FUNCTION_NAME);
}

View File

@@ -2,6 +2,8 @@ import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db';
const FUNCTION_NAME = 'getWebsiteSession';
export async function getWebsiteSession(...args: [websiteId: string, sessionId: string]) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -56,6 +58,7 @@ async function relationalQuery(websiteId: string, sessionId: string) {
group by id, distinct_id, website_id, browser, os, device, screen, language, country, region, city;
`,
{ websiteId, sessionId },
FUNCTION_NAME,
).then(result => result?.[0]);
}
@@ -105,5 +108,6 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
group by id, websiteId, distinctId, browser, os, device, screen, language, country, region, city;
`,
{ websiteId, sessionId },
FUNCTION_NAME,
).then(result => result?.[0]);
}

View File

@@ -4,11 +4,19 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getWebsiteSessionStats';
export interface WebsiteSessionStatsData {
pageviews: number;
visitors: number;
visits: number;
countries: number;
events: number;
}
export async function getWebsiteSessionStats(
...args: [websiteId: string, filters: QueryFilters]
): Promise<
{ pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
> {
): Promise<WebsiteSessionStatsData[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
@@ -18,12 +26,11 @@ export async function getWebsiteSessionStats(
async function relationalQuery(
websiteId: string,
filters: QueryFilters,
): Promise<
{ pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
> {
): Promise<WebsiteSessionStatsData[]> {
const { parseFilters, rawQuery } = prisma;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
const { filterQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
return rawQuery(
@@ -37,24 +44,22 @@ async function relationalQuery(
from website_event
${cohortQuery}
join session on website_event.session_id = session.session_id
and website_event.website_id = session.website_id
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${filterQuery}
`,
params,
queryParams,
FUNCTION_NAME,
);
}
async function clickhouseQuery(
websiteId: string,
filters: QueryFilters,
): Promise<
{ pageviews: number; visitors: number; visits: number; countries: number; events: number }[]
> {
): Promise<WebsiteSessionStatsData[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, {
...filters,
});
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
let sql = '';
@@ -88,5 +93,5 @@ async function clickhouseQuery(
`;
}
return rawQuery(sql, params);
return rawQuery(sql, queryParams, FUNCTION_NAME);
}

View File

@@ -1,33 +1,41 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import { EVENT_COLUMNS } from '@/lib/constants';
import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { PageParams, QueryFilters } from '@/lib/types';
import { QueryFilters } from '@/lib/types';
export async function getWebsiteSessions(
...args: [websiteId: string, filters?: QueryFilters, pageParams?: PageParams]
) {
const FUNCTION_NAME = 'getWebsiteSessions';
export async function getWebsiteSessions(...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, cohortQuery, params } = await parseFilters(websiteId, {
const { search } = filters;
const { filterQuery, dateQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
search: search ? `%${search}%` : undefined,
});
const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like';
const searchQuery = search
? `and (distinct_id ilike {{search}}
or city ilike {{search}}
or browser ilike {{search}}
or os ilike {{search}}
or device ilike {{search}})`
: '';
return pagedRawQuery(
`
select
session.session_id as "id",
session.website_id as "websiteId",
website_event.hostname,
session.browser,
session.os,
session.device,
@@ -44,20 +52,14 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
from website_event
${cohortQuery}
join session on session.session_id = website_event.session_id
and session.website_id = website_event.website_id
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${dateQuery}
${filterQuery}
${
search
? `and (distinct_id ${like} {{search}}
or city ${like} {{search}}
or browser ${like} {{search}}
or os ${like} {{search}}
or device ${like} {{search}})`
: ''
}
${searchQuery}
group by session.session_id,
session.website_id,
website_event.hostname,
session.browser,
session.os,
session.device,
@@ -68,15 +70,27 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
session.city
order by max(website_event.created_at) desc
`,
{ ...params, search: `%${search}%` },
pageParams,
queryParams,
filters,
FUNCTION_NAME,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { pagedQuery, parseFilters, getDateStringSQL } = clickhouse;
const { params, dateQuery, filterQuery, cohortQuery } = await parseFilters(websiteId, filters);
const { search } = pageParams;
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters, getDateStringSQL } = clickhouse;
const { search } = filters;
const { filterQuery, dateQuery, cohortQuery, queryParams } = parseFilters({
...filters,
websiteId,
});
const searchQuery = search
? `and ((positionCaseInsensitive(distinct_id, {search:String}) > 0)
or (positionCaseInsensitive(city, {search:String}) > 0)
or (positionCaseInsensitive(browser, {search:String}) > 0)
or (positionCaseInsensitive(os, {search:String}) > 0)
or (positionCaseInsensitive(device, {search:String}) > 0))`
: '';
let sql = '';
@@ -85,6 +99,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
select
session_id as id,
website_id as websiteId,
hostname,
browser,
os,
device,
@@ -96,23 +111,15 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
${getDateStringSQL('min(created_at)')} as firstAt,
${getDateStringSQL('max(created_at)')} as lastAt,
uniq(visit_id) as visits,
sumIf(views, event_type = 1) as views,
sumIf(1, event_type = 1) as views,
lastAt as createdAt
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
${dateQuery}
${filterQuery}
${
search
? `and ((positionCaseInsensitive(distinct_id, {search:String}) > 0)
or (positionCaseInsensitive(city, {search:String}) > 0)
or (positionCaseInsensitive(browser, {search:String}) > 0)
or (positionCaseInsensitive(os, {search:String}) > 0)
or (positionCaseInsensitive(device, {search:String}) > 0))`
: ''
}
group by session_id, website_id, browser, os, device, screen, language, country, region, city
${searchQuery}
group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city
order by lastAt desc
`;
} else {
@@ -120,6 +127,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
select
session_id as id,
website_id as websiteId,
arrayFirst(x -> 1, hostname) hostname,
browser,
os,
device,
@@ -133,24 +141,16 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
uniq(visit_id) as visits,
sumIf(views, event_type = 1) as views,
lastAt as createdAt
from website_event_stats_hourly website_event
from website_event_stats_hourly as website_event
${cohortQuery}
where website_id = {websiteId:UUID}
${dateQuery}
${filterQuery}
${
search
? `and ((positionCaseInsensitive(distinct_id, {search:String}) > 0)
or (positionCaseInsensitive(city, {search:String}) > 0)
or (positionCaseInsensitive(browser, {search:String}) > 0)
or (positionCaseInsensitive(os, {search:String}) > 0)
or (positionCaseInsensitive(device, {search:String}) > 0))`
: ''
}
group by session_id, website_id, browser, os, device, screen, language, country, region, city
${searchQuery}
group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city
order by lastAt desc
`;
}
return pagedQuery(sql, { ...params, search }, pageParams);
return pagedRawQuery(sql, queryParams, filters, FUNCTION_NAME);
}