Merge branch 'dev' into feat-funnel-per-step-filters
This commit is contained in:
@@ -1,63 +1,152 @@
|
||||
import type { EventData } from '@/generated/prisma/client';
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { DEFAULT_PAGE_SIZE } from '@/lib/constants';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
|
||||
const FUNCTION_NAME = 'getEventData';
|
||||
|
||||
export async function getEventData(
|
||||
...args: [websiteId: string, eventId: string]
|
||||
): Promise<EventData[]> {
|
||||
export async function getEventData(...args: [websiteId: string, filters: QueryFilters]) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(websiteId: string, eventId: string) {
|
||||
const { rawQuery } = prisma;
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { page = 1, pageSize } = filters;
|
||||
const size = +pageSize || DEFAULT_PAGE_SIZE;
|
||||
const offset = +size * (+page - 1);
|
||||
|
||||
return rawQuery(
|
||||
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
// Selects distinct event IDs matching all filters — reused for count and paged data
|
||||
const eventQuery = `
|
||||
select website_event.event_id
|
||||
from website_event
|
||||
join event_data on event_data.website_event_id = website_event.event_id
|
||||
and event_data.website_id = {{websiteId::uuid}}
|
||||
and event_data.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 website_event.event_id
|
||||
`;
|
||||
|
||||
const count = await rawQuery(
|
||||
`select count(*) as num from (${eventQuery}) t`,
|
||||
queryParams,
|
||||
).then((res: any) => res[0].num);
|
||||
|
||||
const data = await 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"
|
||||
with paged_events as (
|
||||
${eventQuery}
|
||||
order by max(website_event.created_at) desc
|
||||
limit ${size} offset ${offset}
|
||||
)
|
||||
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}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
join paged_events on paged_events.event_id = event_data.website_event_id
|
||||
where event_data.website_id = {{websiteId::uuid}}
|
||||
and event_data.website_event_id = {{eventId::uuid}}
|
||||
and event_data.created_at between {{startDate}} and {{endDate}}
|
||||
order by event_data.created_at desc
|
||||
`,
|
||||
{ websiteId, eventId },
|
||||
queryParams,
|
||||
FUNCTION_NAME,
|
||||
);
|
||||
|
||||
return { data, count, page: +page, pageSize: size };
|
||||
}
|
||||
|
||||
async function clickhouseQuery(websiteId: string, eventId: string): Promise<EventData[]> {
|
||||
const { rawQuery } = clickhouse;
|
||||
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { page = 1, pageSize } = filters;
|
||||
const size = +pageSize || DEFAULT_PAGE_SIZE;
|
||||
const offset = +size * (+page - 1);
|
||||
|
||||
return rawQuery(
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
|
||||
|
||||
// Selects distinct event IDs matching all filters — reused for count and paged data
|
||||
const eventQuery = `
|
||||
select event_data.event_id
|
||||
from event_data
|
||||
any left join (
|
||||
select event_id, session_id, website_id, event_name, created_at
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2
|
||||
) website_event
|
||||
on website_event.event_id = event_data.event_id
|
||||
and website_event.session_id = event_data.session_id
|
||||
and website_event.website_id = event_data.website_id
|
||||
${cohortQuery}
|
||||
where event_data.website_id = {websiteId:UUID}
|
||||
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
group by event_data.event_id
|
||||
`;
|
||||
|
||||
const count = await rawQuery(
|
||||
`select count(*) as num from (${eventQuery}) t`,
|
||||
queryParams,
|
||||
).then((res: any) => res[0].num);
|
||||
|
||||
const data = await 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}
|
||||
with paged_events as (
|
||||
${eventQuery}
|
||||
order by max(event_data.created_at) desc
|
||||
limit ${size} offset ${offset}
|
||||
)
|
||||
select
|
||||
event_data.website_id as websiteId,
|
||||
event_data.event_id as eventId,
|
||||
website_event.event_name as eventName,
|
||||
data_key as dataKey,
|
||||
string_value as stringValue,
|
||||
number_value as numberValue,
|
||||
date_value as dateValue,
|
||||
data_type as dataType,
|
||||
event_data.created_at as createdAt
|
||||
from event_data
|
||||
any left join (
|
||||
select event_id, session_id, website_id, event_name
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2
|
||||
) website_event
|
||||
on website_event.event_id = event_data.event_id
|
||||
and website_event.session_id = event_data.session_id
|
||||
and website_event.website_id = event_data.website_id
|
||||
inner join paged_events on paged_events.event_id = event_data.event_id
|
||||
where event_data.website_id = {websiteId:UUID}
|
||||
and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
order by event_data.created_at desc
|
||||
`,
|
||||
{ websiteId, eventId },
|
||||
queryParams,
|
||||
FUNCTION_NAME,
|
||||
);
|
||||
}
|
||||
|
||||
return { data, count, page: +page, pageSize: size };
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { EventData } from '@/generated/prisma/client';
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
const FUNCTION_NAME = 'getEventDataById';
|
||||
|
||||
export async function getEventDataById(
|
||||
...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,
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export async function getEventDataEvents(
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { event } = filters;
|
||||
const { queryParams } = parseFilters({
|
||||
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
@@ -42,9 +42,11 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
from event_data
|
||||
inner join website_event
|
||||
on website_event.event_id = event_data.website_event_id
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where event_data.website_id = {{websiteId::uuid}}
|
||||
and event_data.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_name = {{event}}
|
||||
${filterQuery}
|
||||
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
|
||||
`,
|
||||
@@ -63,8 +65,11 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
from event_data
|
||||
inner join website_event
|
||||
on website_event.event_id = event_data.website_event_id
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where event_data.website_id = {{websiteId::uuid}}
|
||||
and event_data.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
limit 500
|
||||
`,
|
||||
queryParams,
|
||||
@@ -93,15 +98,18 @@ async function clickhouseQuery(
|
||||
string_value as propertyValue,
|
||||
count(*) as total
|
||||
from event_data
|
||||
join website_event
|
||||
any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2) website_event
|
||||
on website_event.event_id = event_data.event_id
|
||||
and website_event.session_id = event_data.session_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
|
||||
@@ -120,11 +128,15 @@ async function clickhouseQuery(
|
||||
data_type as dataType,
|
||||
count(*) as total
|
||||
from event_data
|
||||
join website_event
|
||||
any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2) website_event
|
||||
on website_event.event_id = event_data.event_id
|
||||
and website_event.session_id = event_data.session_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}
|
||||
|
||||
@@ -65,11 +65,15 @@ async function clickhouseQuery(
|
||||
string_value) as "value",
|
||||
count(*) as "total"
|
||||
from event_data
|
||||
join website_event
|
||||
any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2) website_event
|
||||
on website_event.event_id = event_data.event_id
|
||||
and website_event.session_id = event_data.session_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}
|
||||
|
||||
@@ -69,11 +69,15 @@ async function clickhouseQuery(
|
||||
data_key as propertyName,
|
||||
count(*) as total
|
||||
from event_data
|
||||
join website_event
|
||||
any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2) website_event
|
||||
on website_event.event_id = event_data.event_id
|
||||
and website_event.session_id = event_data.session_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}
|
||||
|
||||
@@ -72,11 +72,15 @@ async function clickhouseQuery(
|
||||
data_key,
|
||||
count(*) as "total"
|
||||
from event_data
|
||||
join website_event
|
||||
any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2) website_event
|
||||
on website_event.event_id = event_data.event_id
|
||||
and website_event.session_id = event_data.session_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}
|
||||
|
||||
@@ -72,16 +72,19 @@ async function clickhouseQuery(
|
||||
string_value) as "value",
|
||||
count(*) as "total"
|
||||
from event_data
|
||||
join website_event
|
||||
any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2) website_event
|
||||
on website_event.event_id = event_data.event_id
|
||||
and website_event.session_id = event_data.session_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.data_key = {propertyName:String}
|
||||
and event_data.event_name = {event:String}
|
||||
${filterQuery}
|
||||
group by value
|
||||
order by 2 desc
|
||||
|
||||
@@ -58,7 +58,7 @@ async function relationalQuery(
|
||||
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
|
||||
from (
|
||||
select
|
||||
${column} name,
|
||||
${column} as "name",
|
||||
website_event.session_id,
|
||||
website_event.visit_id,
|
||||
count(*) as "c",
|
||||
@@ -72,6 +72,7 @@ async function relationalQuery(
|
||||
${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}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { EVENT_TYPE } from '@/lib/constants';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
|
||||
const FUNCTION_NAME = 'getEventStats';
|
||||
|
||||
export interface EventStatsParameters {
|
||||
limit?: number | string;
|
||||
}
|
||||
|
||||
interface WebsiteEventMetric {
|
||||
x: string;
|
||||
t: string;
|
||||
@@ -13,7 +16,7 @@ interface WebsiteEventMetric {
|
||||
}
|
||||
|
||||
export async function getEventStats(
|
||||
...args: [websiteId: string, filters: QueryFilters]
|
||||
...args: [websiteId: string, parameters: EventStatsParameters, filters: QueryFilters]
|
||||
): Promise<WebsiteEventMetric[]> {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
@@ -21,15 +24,32 @@ export async function getEventStats(
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
parameters: EventStatsParameters,
|
||||
filters: QueryFilters,
|
||||
) {
|
||||
const { limit } = parameters;
|
||||
const { timezone = 'utc', unit = 'day' } = filters;
|
||||
const { rawQuery, getDateSQL, parseFilters } = prisma;
|
||||
const { filterQuery, cohortQuery, joinSessionQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
eventType: EVENT_TYPE.customEvent,
|
||||
});
|
||||
|
||||
const limitQuery = limit
|
||||
? `and event_name in (
|
||||
select event_name
|
||||
from website_event
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and event_type = 2
|
||||
group by event_name
|
||||
order by count(*) desc
|
||||
limit ${limit}
|
||||
)`
|
||||
: '';
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select
|
||||
@@ -41,7 +61,9 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type = 2
|
||||
${filterQuery}
|
||||
${limitQuery}
|
||||
group by 1, 2
|
||||
order by 2
|
||||
`,
|
||||
@@ -52,16 +74,30 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
parameters: EventStatsParameters,
|
||||
filters: QueryFilters,
|
||||
): Promise<{ x: string; t: string; y: number }[]> {
|
||||
const { limit } = parameters;
|
||||
const { timezone = 'UTC', unit = 'day' } = filters;
|
||||
const { rawQuery, getDateSQL, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
eventType: EVENT_TYPE.customEvent,
|
||||
});
|
||||
|
||||
const limitQuery = limit
|
||||
? `and event_name in (
|
||||
select event_name
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2
|
||||
group by event_name
|
||||
order by count(*) desc
|
||||
limit ${limit}
|
||||
)`
|
||||
: '';
|
||||
|
||||
let sql = '';
|
||||
|
||||
if (filterQuery || cohortQuery) {
|
||||
@@ -74,7 +110,9 @@ async function clickhouseQuery(
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2
|
||||
${filterQuery}
|
||||
${limitQuery}
|
||||
group by x, t
|
||||
order by t
|
||||
`;
|
||||
@@ -90,7 +128,8 @@ async function clickhouseQuery(
|
||||
from website_event_stats_hourly 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
|
||||
${limitQuery}
|
||||
) as g
|
||||
group by x, t
|
||||
order by t
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
|
||||
const FUNCTION_NAME = 'getWebsiteEventStats';
|
||||
|
||||
export interface WebsiteEventStatsData {
|
||||
events: number;
|
||||
visitors: number;
|
||||
visits: number;
|
||||
uniqueEvents: number;
|
||||
}
|
||||
|
||||
export async function getWebsiteEventStats(
|
||||
...args: [websiteId: string, filters: QueryFilters]
|
||||
): Promise<WebsiteEventStatsData[]> {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
filters: QueryFilters,
|
||||
): Promise<WebsiteEventStatsData[]> {
|
||||
const { parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select
|
||||
cast(coalesce(sum(t.c), 0) as bigint) as "events",
|
||||
count(distinct t.session_id) as "visitors",
|
||||
count(distinct t.visit_id) as "visits",
|
||||
count(distinct t.event_name) as "uniqueEvents"
|
||||
from (
|
||||
select
|
||||
website_event.session_id,
|
||||
website_event.visit_id,
|
||||
website_event.event_name,
|
||||
count(*) as "c"
|
||||
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 1, 2, 3
|
||||
) as t
|
||||
`,
|
||||
queryParams,
|
||||
FUNCTION_NAME,
|
||||
).then(result => result?.[0]);
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
filters: QueryFilters,
|
||||
): Promise<WebsiteEventStatsData[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select
|
||||
sum(t.c) as "events",
|
||||
uniq(t.session_id) as "visitors",
|
||||
uniq(t.visit_id) as "visits",
|
||||
count(distinct t.event_name) as "uniqueEvents"
|
||||
from (
|
||||
select
|
||||
session_id,
|
||||
visit_id,
|
||||
event_name,
|
||||
count(*) c
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2
|
||||
${filterQuery}
|
||||
group by session_id, visit_id, event_name
|
||||
) as t;
|
||||
`,
|
||||
queryParams,
|
||||
FUNCTION_NAME,
|
||||
).then(result => result?.[0]);
|
||||
}
|
||||
@@ -53,6 +53,13 @@ export interface SaveEventArgs {
|
||||
ttclid?: string;
|
||||
lifatid?: string;
|
||||
twclid?: string;
|
||||
|
||||
// Performance
|
||||
lcp?: number;
|
||||
inp?: number;
|
||||
cls?: number;
|
||||
fcp?: number;
|
||||
ttfb?: number;
|
||||
}
|
||||
|
||||
export async function saveEvent(args: SaveEventArgs) {
|
||||
@@ -89,6 +96,11 @@ async function relationalQuery({
|
||||
ttclid,
|
||||
lifatid,
|
||||
twclid,
|
||||
lcp,
|
||||
inp,
|
||||
cls,
|
||||
fcp,
|
||||
ttfb,
|
||||
}: SaveEventArgs) {
|
||||
const websiteEventId = uuid();
|
||||
|
||||
@@ -119,6 +131,11 @@ async function relationalQuery({
|
||||
eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
|
||||
tag,
|
||||
hostname,
|
||||
lcp,
|
||||
inp,
|
||||
cls,
|
||||
fcp,
|
||||
ttfb,
|
||||
createdAt,
|
||||
},
|
||||
});
|
||||
@@ -186,6 +203,11 @@ async function clickhouseQuery({
|
||||
ttclid,
|
||||
lifatid,
|
||||
twclid,
|
||||
lcp,
|
||||
inp,
|
||||
cls,
|
||||
fcp,
|
||||
ttfb,
|
||||
}: SaveEventArgs) {
|
||||
const { insert, getUTCString } = clickhouse;
|
||||
const { sendMessage } = kafka;
|
||||
@@ -221,12 +243,17 @@ async function clickhouseQuery({
|
||||
tag: tag,
|
||||
distinct_id: distinctId,
|
||||
created_at: getUTCString(createdAt),
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
hostname,
|
||||
browser: browser,
|
||||
os: os,
|
||||
device: device,
|
||||
screen: screen,
|
||||
language: language,
|
||||
hostname: hostname,
|
||||
lcp: lcp,
|
||||
inp: inp,
|
||||
cls: cls,
|
||||
fcp: fcp,
|
||||
ttfb: ttfb,
|
||||
};
|
||||
|
||||
if (kafka.enabled) {
|
||||
|
||||
@@ -41,10 +41,11 @@ async function relationalQuery(
|
||||
filters: QueryFilters,
|
||||
): Promise<ChannelExpandedMetricsData[]> {
|
||||
const { rawQuery, parseFilters, getTimestampDiffSQL } = prisma;
|
||||
const { queryParams, filterQuery, joinSessionQuery, cohortQuery, dateQuery } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
const { queryParams, filterQuery, joinSessionQuery, cohortQuery, excludeBounceQuery, dateQuery } =
|
||||
parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
@@ -59,14 +60,16 @@ async function relationalQuery(
|
||||
website_event.utm_source,
|
||||
website_event.session_id,
|
||||
website_event.visit_id,
|
||||
website_event.hostname,
|
||||
count(*) c,
|
||||
min(website_event.created_at) min_time,
|
||||
max(website_event.created_at) max_time
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
group by prefix,
|
||||
@@ -75,7 +78,8 @@ async function relationalQuery(
|
||||
website_event.utm_medium,
|
||||
website_event.utm_source,
|
||||
website_event.session_id,
|
||||
website_event.visit_id),
|
||||
website_event.visit_id,
|
||||
website_event.hostname),
|
||||
|
||||
channels as (
|
||||
select case
|
||||
@@ -89,7 +93,8 @@ async function relationalQuery(
|
||||
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,
|
||||
when referrer_domain != regexp_replace(hostname, '^www.', '') and referrer_domain != '' then 'referral'
|
||||
else '' end as "name",
|
||||
session_id,
|
||||
visit_id,
|
||||
c,
|
||||
@@ -119,7 +124,7 @@ async function clickhouseQuery(
|
||||
filters: QueryFilters,
|
||||
): Promise<ChannelExpandedMetricsData[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { queryParams, filterQuery, cohortQuery } = parseFilters({
|
||||
const { queryParams, filterQuery, cohortQuery, excludeBounceQuery } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
@@ -134,31 +139,33 @@ async function clickhouseQuery(
|
||||
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
|
||||
select
|
||||
case when multiSearchAny(lower(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(
|
||||
when multiSearchAny(lower(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(
|
||||
when multiSearchAny(lower(utm_medium), ['referral', 'app','link']) != 0 then 'referral'
|
||||
when position(lower(utm_medium), 'affiliate') > 0 then 'affiliate'
|
||||
when position(lower(utm_medium), 'sms') > 0 or position(lower(utm_source), 'sms') > 0 then 'sms'
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
SEARCH_DOMAINS,
|
||||
)}]) != 0 or position(utm_medium, 'organic') > 0 then concat(prefix, 'Search')
|
||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
||||
)}]) != 0 or position(lower(utm_medium), 'organic') > 0 then concat(prefix, 'Search')
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
SOCIAL_DOMAINS,
|
||||
)}]) != 0 then concat(prefix, 'Social')
|
||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
EMAIL_DOMAINS,
|
||||
)}]) != 0 or position(utm_medium, 'mail') > 0 then 'email'
|
||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
||||
)}]) != 0 or position(lower(utm_medium), 'mail') > 0 then 'email'
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
SHOPPING_DOMAINS,
|
||||
)}]) != 0 or position(utm_medium, 'shop') > 0 then concat(prefix, 'Shopping')
|
||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
||||
)}]) != 0 or position(lower(utm_medium), 'shop') > 0 then concat(prefix, 'Shopping')
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
VIDEO_DOMAINS,
|
||||
)}]) != 0 or position(utm_medium, 'video') > 0 then concat(prefix, 'Video')
|
||||
else '' end AS name,
|
||||
)}]) != 0 or position(lower(utm_medium), 'video') > 0 then concat(prefix, 'Video')
|
||||
when referrer_domain != hostname and referrer_domain != '' then 'referral'
|
||||
else '' end AS "name",
|
||||
session_id,
|
||||
visit_id,
|
||||
count(*) c,
|
||||
@@ -166,9 +173,10 @@ async function clickhouseQuery(
|
||||
max(created_at) max_time
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
and name != ''
|
||||
${filterQuery}
|
||||
group by prefix, name, session_id, visit_id
|
||||
|
||||
@@ -22,10 +22,11 @@ export async function getChannelMetrics(...args: [websiteId: string, filters?: Q
|
||||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { queryParams, filterQuery, joinSessionQuery, cohortQuery, dateQuery } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
const { queryParams, filterQuery, joinSessionQuery, cohortQuery, excludeBounceQuery, dateQuery } =
|
||||
parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
@@ -38,12 +39,14 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
website_event.url_query,
|
||||
website_event.utm_medium,
|
||||
website_event.utm_source,
|
||||
website_event.session_id
|
||||
website_event.session_id,
|
||||
website_event.hostname
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${dateQuery}
|
||||
${filterQuery}),
|
||||
|
||||
@@ -59,6 +62,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
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')
|
||||
when referrer_domain != regexp_replace(hostname, '^www.', '') and referrer_domain != '' then 'referral'
|
||||
else '' end AS x,
|
||||
count(distinct session_id) y
|
||||
from prefix
|
||||
@@ -81,43 +85,46 @@ async function clickhouseQuery(
|
||||
filters: QueryFilters,
|
||||
): Promise<{ x: string; y: number }[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { queryParams, filterQuery, cohortQuery, dateQuery } = parseFilters({
|
||||
const { queryParams, filterQuery, cohortQuery, excludeBounceQuery, dateQuery } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
const sql = `
|
||||
WITH channels as (
|
||||
select case when multiSearchAny(utm_medium, ['cp', 'ppc', 'retargeting', 'paid']) != 0 then 'paid' else 'organic' end prefix,
|
||||
case
|
||||
select
|
||||
case when multiSearchAny(lower(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(
|
||||
when multiSearchAny(lower(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(
|
||||
when multiSearchAny(lower(utm_medium), ['referral', 'app','link']) != 0 then 'referral'
|
||||
when position(lower(utm_medium), 'affiliate') > 0 then 'affiliate'
|
||||
when position(lower(utm_medium), 'sms') > 0 or position(lower(utm_source), 'sms') > 0 then 'sms'
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
SEARCH_DOMAINS,
|
||||
)}]) != 0 or position(utm_medium, 'organic') > 0 then concat(prefix, 'Search')
|
||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
||||
)}]) != 0 or position(lower(utm_medium), 'organic') > 0 then concat(prefix, 'Search')
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
SOCIAL_DOMAINS,
|
||||
)}]) != 0 then concat(prefix, 'Social')
|
||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
EMAIL_DOMAINS,
|
||||
)}]) != 0 or position(utm_medium, 'mail') > 0 then 'email'
|
||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
||||
)}]) != 0 or position(lower(utm_medium), 'mail') > 0 then 'email'
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
SHOPPING_DOMAINS,
|
||||
)}]) != 0 or position(utm_medium, 'shop') > 0 then concat(prefix, 'Shopping')
|
||||
when multiSearchAny(referrer_domain, [${toClickHouseStringArray(
|
||||
)}]) != 0 or position(lower(utm_medium), 'shop') > 0 then concat(prefix, 'Shopping')
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
VIDEO_DOMAINS,
|
||||
)}]) != 0 or position(utm_medium, 'video') > 0 then concat(prefix, 'Video')
|
||||
else '' end AS x,
|
||||
)}]) != 0 or position(lower(utm_medium), 'video') > 0 then concat(prefix, 'Video')
|
||||
when referrer_domain != hostname and referrer_domain != '' then 'referral'
|
||||
else '' end AS x,
|
||||
count(distinct session_id) y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
group by 1, 2
|
||||
|
||||
@@ -30,7 +30,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
session.device,
|
||||
session.country,
|
||||
website_event.url_path as "urlPath",
|
||||
website_event.referrer_domain as "referrerDomain"
|
||||
website_event.referrer_domain as "referrerDomain",
|
||||
website_event.hostname
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
inner join session
|
||||
@@ -65,7 +66,8 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promis
|
||||
device,
|
||||
country,
|
||||
url_path as urlPath,
|
||||
referrer_domain as referrerDomain
|
||||
referrer_domain as referrerDomain,
|
||||
hostname
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
|
||||
@@ -23,7 +23,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query
|
||||
let excludeDomain = '';
|
||||
|
||||
if (column === 'referrer_domain') {
|
||||
excludeDomain = `and website_event.referrer_domain != website_event.hostname
|
||||
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
||||
and website_event.referrer_domain != ''`;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,14 @@ async function relationalQuery(
|
||||
filters: QueryFilters,
|
||||
): Promise<WebsiteStatsData[]> {
|
||||
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, excludeBounceQuery, queryParams } =
|
||||
parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
const { excludeBounce } = filters;
|
||||
const bounceQuery = excludeBounce ? '0' : 'coalesce(sum(case when t.c = 1 then 1 else 0 end), 0)';
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
@@ -39,7 +43,7 @@ async function relationalQuery(
|
||||
cast(coalesce(sum(t.c), 0) as bigint) as "pageviews",
|
||||
count(distinct t.session_id) as "visitors",
|
||||
count(distinct t.visit_id) as "visits",
|
||||
coalesce(sum(case when t.c = 1 then 1 else 0 end), 0) as "bounces",
|
||||
${bounceQuery} as "bounces",
|
||||
cast(coalesce(sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}), 0) as bigint) as "totaltime"
|
||||
from (
|
||||
select
|
||||
@@ -50,12 +54,14 @@ async function relationalQuery(
|
||||
max(website_event.created_at) as "max_time"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by 1, 2
|
||||
|
||||
) as t
|
||||
`,
|
||||
queryParams,
|
||||
@@ -68,12 +74,14 @@ async function clickhouseQuery(
|
||||
filters: QueryFilters,
|
||||
): Promise<WebsiteStatsData[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
const { filterQuery, cohortQuery, excludeBounceQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
let sql = '';
|
||||
const { excludeBounce } = filters;
|
||||
const bounceQuery = excludeBounce ? '0' : 'sumIf(1, t.c = 1)';
|
||||
|
||||
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) {
|
||||
sql = `
|
||||
@@ -81,7 +89,7 @@ async function clickhouseQuery(
|
||||
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",
|
||||
${bounceQuery} as "bounces",
|
||||
sum(max_time-min_time) as "totaltime"
|
||||
from (
|
||||
select
|
||||
@@ -92,9 +100,10 @@ async function clickhouseQuery(
|
||||
max(created_at) max_time
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by session_id, visit_id
|
||||
) as t;
|
||||
@@ -105,7 +114,7 @@ async function clickhouseQuery(
|
||||
sum(t.c) as "pageviews",
|
||||
uniq(session_id) as "visitors",
|
||||
uniq(visit_id) as "visits",
|
||||
sumIf(1, t.c = 1) as "bounces",
|
||||
${bounceQuery} as "bounces",
|
||||
sum(max_time-min_time) as "totaltime"
|
||||
from (select
|
||||
session_id,
|
||||
@@ -115,9 +124,10 @@ async function clickhouseQuery(
|
||||
max(max_time) max_time
|
||||
from website_event_stats_hourly "website_event"
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by session_id, visit_id
|
||||
) as t;
|
||||
|
||||
@@ -14,12 +14,13 @@ export async function getWeeklyTraffic(...args: [websiteId: string, filters: Que
|
||||
}
|
||||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const timezone = 'utc';
|
||||
const { timezone = 'utc' } = filters;
|
||||
const { rawQuery, getDateWeeklySQL, parseFilters } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, excludeBounceQuery, queryParams } =
|
||||
parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
@@ -28,12 +29,14 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
count(distinct website_event.session_id) as value
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by time
|
||||
order by 2
|
||||
order by 1
|
||||
`,
|
||||
queryParams,
|
||||
FUNCTION_NAME,
|
||||
@@ -43,7 +46,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { timezone = 'utc' } = filters;
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = await parseFilters({ ...filters, websiteId });
|
||||
const { filterQuery, cohortQuery, excludeBounceQuery, queryParams } = await parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
let sql = '';
|
||||
|
||||
@@ -54,8 +60,10 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
count(distinct session_id) as value
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by time
|
||||
order by time
|
||||
@@ -67,8 +75,10 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
count(distinct session_id) as value
|
||||
from website_event_stats_hourly website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by time
|
||||
order by time
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './events/getEventDataById';
|
||||
export * from './events/getEventDataEvents';
|
||||
export * from './events/getEventDataFields';
|
||||
export * from './events/getEventDataProperties';
|
||||
@@ -22,6 +23,10 @@ export * from './getWeeklyTraffic';
|
||||
export * from './pageviews/getPageviewExpandedMetrics';
|
||||
export * from './pageviews/getPageviewMetrics';
|
||||
export * from './pageviews/getPageviewStats';
|
||||
export * from './performance/getPerformanceStats';
|
||||
export * from './replays/getReplayChunks';
|
||||
export * from './replays/getSessionReplays';
|
||||
export * from './replays/saveRecording';
|
||||
export * from './reports/getBreakdown';
|
||||
export * from './reports/getFunnel';
|
||||
export * from './reports/getJourney';
|
||||
|
||||
@@ -38,19 +38,20 @@ async function relationalQuery(
|
||||
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) },
|
||||
);
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, excludeBounceQuery, 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
|
||||
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
||||
and website_event.referrer_domain != ''`;
|
||||
if (type === 'domain') {
|
||||
column = toPostgresGroupedReferrer(GROUPED_DOMAINS);
|
||||
@@ -67,7 +68,7 @@ async function relationalQuery(
|
||||
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
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
group by visit_id
|
||||
) x
|
||||
on x.visit_id = website_event.visit_id
|
||||
@@ -86,7 +87,7 @@ async function relationalQuery(
|
||||
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
|
||||
from (
|
||||
select
|
||||
${column} as name,
|
||||
${column} as "name",
|
||||
website_event.session_id,
|
||||
website_event.visit_id,
|
||||
count(*) as "c",
|
||||
@@ -94,11 +95,12 @@ async function relationalQuery(
|
||||
max(website_event.created_at) as "max_time"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
${entryExitQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${excludeDomain}
|
||||
${filterQuery}
|
||||
group by ${column}, website_event.session_id, website_event.visit_id
|
||||
@@ -122,7 +124,7 @@ async function clickhouseQuery(
|
||||
const { type, limit = 500, offset = 0 } = parameters;
|
||||
let column = FILTER_COLUMNS[type] || type;
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
const { filterQuery, cohortQuery, excludeBounceQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
@@ -147,7 +149,7 @@ async function clickhouseQuery(
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
group by visit_id) x
|
||||
ON x.visit_id = website_event.visit_id`;
|
||||
}
|
||||
@@ -171,10 +173,11 @@ async function clickhouseQuery(
|
||||
max(created_at) max_time
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${entryExitQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
and name != ''
|
||||
${excludeDomain}
|
||||
${filterQuery}
|
||||
|
||||
@@ -34,19 +34,20 @@ async function relationalQuery(
|
||||
const { type, limit = 500, offset = 0 } = parameters;
|
||||
let column = FILTER_COLUMNS[type] || type;
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters(
|
||||
{
|
||||
...filters,
|
||||
websiteId,
|
||||
},
|
||||
{ joinSession: SESSION_COLUMNS.includes(type) },
|
||||
);
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, excludeBounceQuery, 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
|
||||
excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '')
|
||||
and website_event.referrer_domain != ''`;
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ async function relationalQuery(
|
||||
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
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
order by visit_id, created_at ${order}
|
||||
) x
|
||||
on x.visit_id = website_event.visit_id
|
||||
@@ -75,11 +76,12 @@ async function relationalQuery(
|
||||
count(distinct website_event.session_id) as y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
${entryExitQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${excludeDomain}
|
||||
${filterQuery}
|
||||
group by 1
|
||||
@@ -100,7 +102,7 @@ async function clickhouseQuery(
|
||||
const { type, limit = 500, offset = 0 } = parameters;
|
||||
let column = FILTER_COLUMNS[type] || type;
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
const { filterQuery, cohortQuery, excludeBounceQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
@@ -125,7 +127,7 @@ async function clickhouseQuery(
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
group by visit_id) x
|
||||
ON x.visit_id = website_event.visit_id`;
|
||||
}
|
||||
@@ -135,10 +137,11 @@ async function clickhouseQuery(
|
||||
uniq(website_event.session_id) as y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${entryExitQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${excludeDomain}
|
||||
${filterQuery}
|
||||
group by x
|
||||
@@ -174,9 +177,10 @@ async function clickhouseQuery(
|
||||
${columnQuery} as t
|
||||
from website_event_stats_hourly as website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${excludeDomain}
|
||||
${filterQuery}
|
||||
${groupByQuery}) as g
|
||||
|
||||
@@ -16,10 +16,11 @@ 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, joinSessionQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
const { filterQuery, cohortQuery, excludeBounceQuery, joinSessionQuery, queryParams } =
|
||||
parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
@@ -28,10 +29,11 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
count(*) y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by 1
|
||||
order by 1
|
||||
@@ -47,7 +49,7 @@ async function clickhouseQuery(
|
||||
): Promise<{ x: string; y: number }[]> {
|
||||
const { timezone = 'UTC', unit = 'day' } = filters;
|
||||
const { parseFilters, rawQuery, getDateSQL } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
const { filterQuery, cohortQuery, excludeBounceQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
@@ -65,9 +67,10 @@ async function clickhouseQuery(
|
||||
count(*) as y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by t
|
||||
) as g
|
||||
@@ -84,9 +87,10 @@ async function clickhouseQuery(
|
||||
sum(views) as y
|
||||
from website_event_stats_hourly as website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by t
|
||||
) as g
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
|
||||
export interface PerformanceStatsResult {
|
||||
lcp: number;
|
||||
inp: number;
|
||||
cls: number;
|
||||
fcp: number;
|
||||
ttfb: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export async function getPerformanceStats(...args: [websiteId: string, filters: QueryFilters]) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
filters: QueryFilters,
|
||||
): Promise<PerformanceStatsResult> {
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
const result = await rawQuery(
|
||||
`
|
||||
select
|
||||
percentile_cont(0.75) within group (order by lcp) as lcp,
|
||||
percentile_cont(0.75) within group (order by inp) as inp,
|
||||
percentile_cont(0.75) within group (order by cls) as cls,
|
||||
percentile_cont(0.75) within group (order by fcp) as fcp,
|
||||
percentile_cont(0.75) within group (order by ttfb) as ttfb,
|
||||
count(*) as count
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
return result?.[0] || { lcp: 0, inp: 0, cls: 0, fcp: 0, ttfb: 0, count: 0 };
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
filters: QueryFilters,
|
||||
): Promise<PerformanceStatsResult> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
|
||||
|
||||
const result = await rawQuery<PerformanceStatsResult>(
|
||||
`
|
||||
select
|
||||
quantile(0.75)(lcp) as lcp,
|
||||
quantile(0.75)(inp) as inp,
|
||||
quantile(0.75)(cls) as cls,
|
||||
quantile(0.75)(fcp) as fcp,
|
||||
quantile(0.75)(ttfb) as ttfb,
|
||||
count() as count
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_event.website_id = {websiteId:UUID}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
return result?.[0] || { lcp: 0, inp: 0, cls: 0, fcp: 0, ttfb: 0, count: 0 };
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { gunzipSync } from 'node:zlib';
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
const FUNCTION_NAME = 'getReplayChunks';
|
||||
|
||||
export interface ReplayChunk {
|
||||
sessionId: string;
|
||||
visitId: string;
|
||||
events: any[];
|
||||
chunkIndex: number;
|
||||
eventCount: number;
|
||||
startedAt: Date;
|
||||
endedAt: Date;
|
||||
}
|
||||
|
||||
export async function getReplayChunks(websiteId: string, visitId: string): Promise<ReplayChunk[]> {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(websiteId, visitId),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(websiteId, visitId),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(websiteId: string, visitId: string): Promise<ReplayChunk[]> {
|
||||
const { rawQuery } = prisma;
|
||||
|
||||
const chunks: {
|
||||
sessionId: string;
|
||||
visitId: string;
|
||||
events: Buffer;
|
||||
chunkIndex: number;
|
||||
eventCount: number;
|
||||
startedAt: Date;
|
||||
endedAt: Date;
|
||||
}[] = await rawQuery(
|
||||
`
|
||||
select
|
||||
session_id as "sessionId",
|
||||
visit_id as "visitId",
|
||||
events,
|
||||
chunk_index as "chunkIndex",
|
||||
event_count as "eventCount",
|
||||
started_at as "startedAt",
|
||||
ended_at as "endedAt"
|
||||
from session_replay
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and visit_id = {{visitId::uuid}}
|
||||
order by chunk_index asc
|
||||
`,
|
||||
{ websiteId, visitId },
|
||||
FUNCTION_NAME,
|
||||
);
|
||||
|
||||
return chunks.map(chunk => ({
|
||||
...chunk,
|
||||
events: JSON.parse(gunzipSync(Buffer.from(chunk.events)).toString('utf-8')),
|
||||
}));
|
||||
}
|
||||
|
||||
async function clickhouseQuery(websiteId: string, visitId: string): Promise<ReplayChunk[]> {
|
||||
const { rawQuery } = clickhouse;
|
||||
|
||||
const results = await rawQuery<
|
||||
{
|
||||
sessionId: string;
|
||||
visitId: string;
|
||||
events: string;
|
||||
chunk_index: number;
|
||||
event_count: number;
|
||||
started_at: string;
|
||||
ended_at: string;
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
select
|
||||
session_id as sessionId,
|
||||
visit_id as visitId,
|
||||
events,
|
||||
chunk_index,
|
||||
event_count,
|
||||
started_at,
|
||||
ended_at
|
||||
from session_replay
|
||||
where website_id = {websiteId:UUID}
|
||||
and visit_id = {visitId:UUID}
|
||||
order by chunk_index asc
|
||||
`,
|
||||
{ websiteId, visitId },
|
||||
FUNCTION_NAME,
|
||||
);
|
||||
|
||||
return results.map(row => ({
|
||||
sessionId: row.sessionId,
|
||||
visitId: row.visitId,
|
||||
events: JSON.parse(row.events),
|
||||
chunkIndex: row.chunk_index,
|
||||
eventCount: row.event_count,
|
||||
startedAt: new Date(row.started_at),
|
||||
endedAt: new Date(row.ended_at),
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
|
||||
const FUNCTION_NAME = 'getSessionReplays';
|
||||
|
||||
export function getSessionReplays(
|
||||
...args: [websiteId: string, filters: QueryFilters, sessionId?: string]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters, sessionId?: string) {
|
||||
const { pagedRawQuery, parseFilters } = prisma;
|
||||
const { search } = filters;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
search: search ? `%${search}%` : undefined,
|
||||
});
|
||||
|
||||
const joinQuery =
|
||||
filterQuery || cohortQuery
|
||||
? `join (select distinct website_id, session_id, visit_id
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}) website_event
|
||||
on website_event.website_id = sr.website_id
|
||||
and website_event.session_id = sr.session_id
|
||||
and website_event.visit_id = sr.visit_id`
|
||||
: '';
|
||||
|
||||
const sessionFilter = sessionId ? 'and sr.session_id = {{sessionId::uuid}}' : '';
|
||||
|
||||
const searchQuery = search
|
||||
? `and (session.distinct_id ilike {{search}}
|
||||
or session.city ilike {{search}}
|
||||
or session.browser ilike {{search}}
|
||||
or session.os ilike {{search}}
|
||||
or session.device ilike {{search}})`
|
||||
: '';
|
||||
|
||||
return pagedRawQuery(
|
||||
`
|
||||
select
|
||||
sr.visit_id as "id",
|
||||
sr.session_id as "sessionId",
|
||||
sr.website_id as "websiteId",
|
||||
session.browser,
|
||||
session.os,
|
||||
session.device,
|
||||
session.country,
|
||||
session.city,
|
||||
sum(sr.event_count) as "eventCount",
|
||||
count(sr.replay_id) as "chunkCount",
|
||||
min(sr.started_at) as "startedAt",
|
||||
max(sr.ended_at) as "endedAt",
|
||||
sum(extract(epoch from sr.ended_at - sr.started_at) * 1000)::bigint as "duration",
|
||||
max(sr.created_at) as "createdAt"
|
||||
from session_replay sr
|
||||
join session on session.session_id = sr.session_id
|
||||
and session.website_id = sr.website_id
|
||||
${joinQuery}
|
||||
where sr.website_id = {{websiteId::uuid}}
|
||||
and sr.created_at between {{startDate}} and {{endDate}}
|
||||
${sessionFilter}
|
||||
${searchQuery}
|
||||
group by sr.visit_id,
|
||||
sr.session_id,
|
||||
sr.website_id,
|
||||
session.browser,
|
||||
session.os,
|
||||
session.device,
|
||||
session.country,
|
||||
session.city
|
||||
order by max(sr.created_at) desc
|
||||
`,
|
||||
{ ...queryParams, sessionId },
|
||||
filters,
|
||||
FUNCTION_NAME,
|
||||
);
|
||||
}
|
||||
|
||||
async function clickhouseQuery(websiteId: string, filters: QueryFilters, sessionId?: string) {
|
||||
const { pagedRawQuery, parseFilters } = clickhouse;
|
||||
const { search } = filters;
|
||||
const { queryParams, cohortQuery, filterQuery } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
const sessionFilter = sessionId ? 'and session_replay.session_id = {sessionId:UUID}' : '';
|
||||
|
||||
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))`
|
||||
: '';
|
||||
|
||||
return pagedRawQuery(
|
||||
`
|
||||
select
|
||||
session_replay.visit_id as id,
|
||||
session_replay.session_id as sessionId,
|
||||
session_replay.website_id as websiteId,
|
||||
website_event.browser,
|
||||
website_event.os,
|
||||
website_event.device,
|
||||
website_event.country,
|
||||
website_event.city,
|
||||
sum(session_replay.event_count) as eventCount,
|
||||
count(session_replay.replay_id) as chunkCount,
|
||||
min(session_replay.started_at) as startedAt,
|
||||
max(session_replay.ended_at) as endedAt,
|
||||
toInt64(sum(dateDiff('millisecond', session_replay.started_at, session_replay.ended_at))) as duration,
|
||||
max(session_replay.created_at) as createdAt
|
||||
from session_replay
|
||||
join (
|
||||
select distinct website_id, session_id, visit_id, browser, os, device, country, city
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
${searchQuery}
|
||||
) website_event
|
||||
on website_event.session_id = session_replay.session_id
|
||||
and website_event.website_id = session_replay.website_id
|
||||
and website_event.visit_id = session_replay.visit_id
|
||||
where session_replay.website_id = {websiteId:UUID}
|
||||
and session_replay.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${sessionFilter}
|
||||
group by session_replay.visit_id, session_replay.session_id, session_replay.website_id, website_event.browser, website_event.os, website_event.device, website_event.country, website_event.city
|
||||
order by max(created_at) desc
|
||||
`,
|
||||
{ ...queryParams, sessionId },
|
||||
filters,
|
||||
FUNCTION_NAME,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { gzipSync } from 'node:zlib';
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import kafka from '@/lib/kafka';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export interface SaveRecordingArgs {
|
||||
websiteId: string;
|
||||
sessionId: string;
|
||||
visitId: string;
|
||||
chunkIndex: number;
|
||||
events: any[];
|
||||
eventCount: number;
|
||||
startedAt: Date;
|
||||
endedAt: Date;
|
||||
}
|
||||
|
||||
export async function saveRecording(args: SaveRecordingArgs) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery({
|
||||
websiteId,
|
||||
sessionId,
|
||||
visitId,
|
||||
chunkIndex,
|
||||
events,
|
||||
eventCount,
|
||||
startedAt,
|
||||
endedAt,
|
||||
}: SaveRecordingArgs) {
|
||||
const compressed = gzipSync(Buffer.from(JSON.stringify(events), 'utf-8'));
|
||||
|
||||
return prisma.client.sessionReplay.create({
|
||||
data: {
|
||||
id: uuid(),
|
||||
websiteId,
|
||||
sessionId,
|
||||
visitId,
|
||||
chunkIndex,
|
||||
events: compressed as any,
|
||||
eventCount,
|
||||
startedAt,
|
||||
endedAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function clickhouseQuery({
|
||||
websiteId,
|
||||
sessionId,
|
||||
visitId,
|
||||
chunkIndex,
|
||||
events,
|
||||
eventCount,
|
||||
startedAt,
|
||||
endedAt,
|
||||
}: SaveRecordingArgs) {
|
||||
const { insert, getUTCString } = clickhouse;
|
||||
const { sendMessage } = kafka;
|
||||
|
||||
const message = {
|
||||
replay_id: uuid(),
|
||||
website_id: websiteId,
|
||||
session_id: sessionId,
|
||||
visit_id: visitId,
|
||||
chunk_index: chunkIndex,
|
||||
events: JSON.stringify(events),
|
||||
event_count: eventCount,
|
||||
started_at: getUTCString(startedAt),
|
||||
ended_at: getUTCString(endedAt),
|
||||
};
|
||||
|
||||
if (kafka.enabled) {
|
||||
return sendMessage('session_replay', message);
|
||||
}
|
||||
|
||||
return insert('session_replay', [message]);
|
||||
}
|
||||
@@ -10,7 +10,6 @@ export interface AttributionParameters {
|
||||
model: string;
|
||||
type: string;
|
||||
step: string;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
export interface AttributionResult {
|
||||
@@ -38,7 +37,7 @@ async function relationalQuery(
|
||||
parameters: AttributionParameters,
|
||||
filters: QueryFilters,
|
||||
): Promise<AttributionResult> {
|
||||
const { model, type, currency } = parameters;
|
||||
const { model, type } = parameters;
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const eventType = type === 'path' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
|
||||
const column = type === 'path' ? 'url_path' : 'event_name';
|
||||
@@ -51,17 +50,16 @@ async function relationalQuery(
|
||||
|
||||
function getUTMQuery(utmColumn: string) {
|
||||
return `
|
||||
select
|
||||
coalesce(we.${utmColumn}, '') name,
|
||||
${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} value
|
||||
select
|
||||
coalesce(we.${utmColumn}, '') as "name",
|
||||
count(distinct we.session_id) as "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}} and {{endDate}}
|
||||
${currency ? '' : `and we.${utmColumn} != ''`}
|
||||
and we.${utmColumn} != ''
|
||||
group by 1
|
||||
order by 2 desc
|
||||
limit 20`;
|
||||
@@ -80,30 +78,9 @@ async function relationalQuery(
|
||||
${filterQuery}
|
||||
group by 1),`;
|
||||
|
||||
const revenueEventQuery = `WITH events AS (
|
||||
select
|
||||
revenue.session_id,
|
||||
max(revenue.created_at) max_dt,
|
||||
sum(revenue.revenue) value
|
||||
from revenue
|
||||
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 === 'first-click'
|
||||
? `\n
|
||||
? `\n
|
||||
model AS (select e.session_id,
|
||||
min(we.created_at) created_at
|
||||
from events e
|
||||
@@ -112,39 +89,34 @@ async function relationalQuery(
|
||||
where we.website_id = {{websiteId::uuid}}
|
||||
and we.created_at between {{startDate}} and {{endDate}}
|
||||
group by e.session_id)`
|
||||
: `\n
|
||||
: `\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}} and {{endDate}}
|
||||
and we.created_at between {{startDate}} and {{endDate}}
|
||||
and we.created_at < e.max_dt
|
||||
group by e.session_id)`;
|
||||
}
|
||||
|
||||
const referrerRes = await rawQuery(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
select coalesce(we.referrer_domain, '') name,
|
||||
${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} value
|
||||
select coalesce(we.referrer_domain, '') as "name",
|
||||
count(distinct 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
|
||||
join session s
|
||||
on s.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}} and {{endDate}}
|
||||
${
|
||||
currency
|
||||
? ''
|
||||
: `and we.referrer_domain != hostname
|
||||
and we.referrer_domain != ''`
|
||||
}
|
||||
and we.referrer_domain != regexp_replace(we.hostname, '^www.', '')
|
||||
and we.referrer_domain != ''
|
||||
group by 1
|
||||
order by 2 desc
|
||||
limit 20
|
||||
@@ -154,40 +126,39 @@ async function relationalQuery(
|
||||
|
||||
const paidAdsres = await rawQuery(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)},
|
||||
|
||||
results AS (
|
||||
select case
|
||||
when coalesce(gclid, '') != '' then 'Google Ads'
|
||||
when coalesce(fbclid, '') != '' then 'Facebook / Meta'
|
||||
when coalesce(msclkid, '') != '' then 'Microsoft Ads'
|
||||
when coalesce(ttclid, '') != '' then 'TikTok Ads'
|
||||
when coalesce(li_fat_id, '') != '' then 'LinkedIn Ads'
|
||||
when coalesce(gclid, '') != '' then 'Google Ads'
|
||||
when coalesce(fbclid, '') != '' then 'Facebook / Meta'
|
||||
when coalesce(msclkid, '') != '' then 'Microsoft Ads'
|
||||
when coalesce(ttclid, '') != '' then 'TikTok Ads'
|
||||
when coalesce(li_fat_id, '') != '' then 'LinkedIn Ads'
|
||||
when coalesce(twclid, '') != '' then 'Twitter Ads (X)'
|
||||
else ''
|
||||
end name,
|
||||
${currency ? 'sum(e.value)' : 'count(distinct we.session_id)'} value
|
||||
end as "name",
|
||||
count(distinct we.session_id) as "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}} and {{endDate}}
|
||||
group by 1
|
||||
order by 2 desc
|
||||
limit 20)
|
||||
SELECT *
|
||||
SELECT *
|
||||
FROM results
|
||||
${currency ? '' : `WHERE name != ''`}
|
||||
WHERE name != ''
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
const sourceRes = await rawQuery(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
${getUTMQuery('utm_source')}
|
||||
`,
|
||||
@@ -196,7 +167,7 @@ async function relationalQuery(
|
||||
|
||||
const mediumRes = await rawQuery(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
${getUTMQuery('utm_medium')}
|
||||
`,
|
||||
@@ -205,7 +176,7 @@ async function relationalQuery(
|
||||
|
||||
const campaignRes = await rawQuery(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
${getUTMQuery('utm_campaign')}
|
||||
`,
|
||||
@@ -214,7 +185,7 @@ async function relationalQuery(
|
||||
|
||||
const contentRes = await rawQuery(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
${getUTMQuery('utm_content')}
|
||||
`,
|
||||
@@ -223,7 +194,7 @@ async function relationalQuery(
|
||||
|
||||
const termRes = await rawQuery(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
${getUTMQuery('utm_term')}
|
||||
`,
|
||||
@@ -232,7 +203,7 @@ async function relationalQuery(
|
||||
|
||||
const totalRes = await rawQuery(
|
||||
`
|
||||
select
|
||||
select
|
||||
count(*) as "pageviews",
|
||||
count(distinct website_event.session_id) as "visitors",
|
||||
count(distinct website_event.visit_id) as "visits"
|
||||
@@ -264,7 +235,7 @@ async function clickhouseQuery(
|
||||
parameters: AttributionParameters,
|
||||
filters: QueryFilters,
|
||||
): Promise<AttributionResult> {
|
||||
const { model, type, currency } = parameters;
|
||||
const { model, type } = parameters;
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const eventType = type === 'path' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
|
||||
const column = type === 'path' ? 'url_path' : 'event_name';
|
||||
@@ -277,17 +248,19 @@ async function clickhouseQuery(
|
||||
|
||||
function getUTMQuery(utmColumn: string) {
|
||||
return `
|
||||
select
|
||||
select
|
||||
we.${utmColumn} name,
|
||||
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
|
||||
uniqExact(we.session_id) value
|
||||
from model m
|
||||
join website_event we
|
||||
join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
) 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} != ''`}
|
||||
where we.${utmColumn} != ''
|
||||
group by 1
|
||||
order by 2 desc
|
||||
limit 20
|
||||
@@ -296,14 +269,17 @@ async function clickhouseQuery(
|
||||
|
||||
function getModelQuery(model: string) {
|
||||
if (model === 'first-click') {
|
||||
return `
|
||||
return `
|
||||
model AS (select e.session_id,
|
||||
min(we.created_at) created_at
|
||||
from events e
|
||||
join website_event we
|
||||
join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
) 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)
|
||||
`;
|
||||
}
|
||||
@@ -312,11 +288,14 @@ async function clickhouseQuery(
|
||||
model AS (select e.session_id,
|
||||
max(we.created_at) created_at
|
||||
from events e
|
||||
join website_event we
|
||||
join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
) 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
|
||||
where we.created_at < e.max_dt
|
||||
group by e.session_id)
|
||||
`;
|
||||
}
|
||||
@@ -333,26 +312,6 @@ async function clickhouseQuery(
|
||||
${filterQuery}
|
||||
group by 1),`;
|
||||
|
||||
const revenueEventQuery = `WITH events AS (
|
||||
select
|
||||
website_revenue.session_id,
|
||||
max(website_revenue.created_at) max_dt,
|
||||
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.${column} = {step:String}
|
||||
and website_revenue.currency = {currency:String}
|
||||
${filterQuery}
|
||||
group by 1),`;
|
||||
|
||||
const referrerRes = await rawQuery<
|
||||
{
|
||||
name: string;
|
||||
@@ -360,23 +319,21 @@ async function clickhouseQuery(
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
select we.referrer_domain name,
|
||||
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
|
||||
uniqExact(we.session_id) value
|
||||
from model m
|
||||
join website_event we
|
||||
join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
) 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.referrer_domain != hostname
|
||||
and we.referrer_domain != ''`
|
||||
}
|
||||
where we.referrer_domain != hostname
|
||||
and we.referrer_domain != ''
|
||||
group by 1
|
||||
order by 2 desc
|
||||
limit 20
|
||||
@@ -391,23 +348,25 @@ async function clickhouseQuery(
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
select multiIf(gclid != '', 'Google Ads',
|
||||
fbclid != '', 'Facebook / Meta',
|
||||
msclkid != '', 'Microsoft Ads',
|
||||
ttclid != '', 'TikTok Ads',
|
||||
li_fat_id != '', 'LinkedIn Ads',
|
||||
select multiIf(gclid != '', 'Google Ads',
|
||||
fbclid != '', 'Facebook / Meta',
|
||||
msclkid != '', 'Microsoft Ads',
|
||||
ttclid != '', 'TikTok Ads',
|
||||
li_fat_id != '', 'LinkedIn Ads',
|
||||
twclid != '', 'Twitter Ads (X)','') name,
|
||||
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
|
||||
uniqExact(we.session_id) value
|
||||
from model m
|
||||
join website_event we
|
||||
join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
) 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 name != ''`}
|
||||
where name != ''
|
||||
group by 1
|
||||
order by 2 desc
|
||||
limit 20
|
||||
@@ -422,7 +381,7 @@ async function clickhouseQuery(
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
${getUTMQuery('utm_source')}
|
||||
`,
|
||||
@@ -436,7 +395,7 @@ async function clickhouseQuery(
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
${getUTMQuery('utm_medium')}
|
||||
`,
|
||||
@@ -450,7 +409,7 @@ async function clickhouseQuery(
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
${getUTMQuery('utm_campaign')}
|
||||
`,
|
||||
@@ -464,7 +423,7 @@ async function clickhouseQuery(
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
${getUTMQuery('utm_content')}
|
||||
`,
|
||||
@@ -478,7 +437,7 @@ async function clickhouseQuery(
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
${currency ? revenueEventQuery : eventQuery}
|
||||
${eventQuery}
|
||||
${getModelQuery(model)}
|
||||
${getUTMQuery('utm_term')}
|
||||
`,
|
||||
|
||||
@@ -71,7 +71,7 @@ async function relationalQuery(
|
||||
website_event.session_id, website_event.visit_id
|
||||
) as t
|
||||
group by ${parseFieldsByName(fields)}
|
||||
order by 1 desc, 2 desc
|
||||
order by 2 desc, 1 desc
|
||||
limit 500
|
||||
`,
|
||||
queryParams,
|
||||
@@ -119,7 +119,7 @@ async function clickhouseQuery(
|
||||
session_id, visit_id
|
||||
) as t
|
||||
group by ${parseFieldsByName(fields)}
|
||||
order by 1 desc, 2 desc
|
||||
order by 2 desc, 1 desc
|
||||
limit 500
|
||||
`,
|
||||
queryParams,
|
||||
@@ -131,5 +131,5 @@ function parseFields(fields: string[]) {
|
||||
}
|
||||
|
||||
function parseFieldsByName(fields: string[]) {
|
||||
return `${fields.map(name => name).join(',')}`;
|
||||
return `${fields.map(name => `"${name}"`).join(',')}`;
|
||||
}
|
||||
@@ -9,8 +9,6 @@ export interface GoalParameters {
|
||||
endDate: Date;
|
||||
type: string;
|
||||
value: string;
|
||||
operator?: string;
|
||||
property?: string;
|
||||
}
|
||||
|
||||
export async function getGoal(
|
||||
@@ -31,15 +29,29 @@ async function relationalQuery(
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const eventType = type === 'path' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
|
||||
const column = type === 'path' ? 'url_path' : 'event_name';
|
||||
|
||||
let operator = '=';
|
||||
let paramValue = value;
|
||||
if (value.startsWith('*') || value.endsWith('*')) {
|
||||
operator = 'like';
|
||||
paramValue = value.replace(/^\*|\*$/g, '%');
|
||||
}
|
||||
|
||||
const { filterQuery, dateQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
value,
|
||||
value: paramValue,
|
||||
startDate,
|
||||
endDate,
|
||||
eventType,
|
||||
});
|
||||
|
||||
const excludeEventTypeFilterQuery = filterQuery
|
||||
.split('\n')
|
||||
.filter(filter => !filter.includes('event_type'))
|
||||
.join('\n')
|
||||
.trim();
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select count(distinct website_event.session_id) as num,
|
||||
@@ -50,13 +62,13 @@ async function relationalQuery(
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
${excludeEventTypeFilterQuery}
|
||||
) as total
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and ${column} = {{value}}
|
||||
and ${column} ${operator} {{value}}
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
`,
|
||||
@@ -73,15 +85,29 @@ async function clickhouseQuery(
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const eventType = type === 'path' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
|
||||
const column = type === 'path' ? 'url_path' : 'event_name';
|
||||
|
||||
let operator = '=';
|
||||
let paramValue = value;
|
||||
if (value.startsWith('*') || value.endsWith('*')) {
|
||||
operator = 'like';
|
||||
paramValue = value.replace(/^\*|\*$/g, '%');
|
||||
}
|
||||
|
||||
const { filterQuery, dateQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
value,
|
||||
value: paramValue,
|
||||
startDate,
|
||||
endDate,
|
||||
eventType,
|
||||
});
|
||||
|
||||
const excludeEventTypeFilterQuery = filterQuery
|
||||
.split('\n')
|
||||
.filter(filter => !filter.includes('event_type'))
|
||||
.join('\n')
|
||||
.trim();
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select count(distinct session_id) as num,
|
||||
@@ -91,12 +117,12 @@ async function clickhouseQuery(
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
${excludeEventTypeFilterQuery}
|
||||
) as total
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and ${column} = {value:String}
|
||||
and ${column} ${operator} {value:String}
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
`,
|
||||
|
||||
@@ -60,7 +60,7 @@ async function relationalQuery(
|
||||
endStepQuery: string;
|
||||
params: Record<string, string>;
|
||||
} {
|
||||
const params = {};
|
||||
const params: { startStep?: string; endStep?: string } = {};
|
||||
let sequenceQuery = '';
|
||||
let startStepQuery = '';
|
||||
let endStepQuery = '';
|
||||
@@ -119,7 +119,7 @@ async function relationalQuery(
|
||||
select distinct
|
||||
website_event.visit_id,
|
||||
website_event.referrer_path,
|
||||
coalesce(nullIf(website_event.event_name, ''), website_event.url_path) event,
|
||||
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
|
||||
${cohortQuery}
|
||||
@@ -172,7 +172,7 @@ async function clickhouseQuery(
|
||||
endStepQuery: string;
|
||||
params: Record<string, string>;
|
||||
} {
|
||||
const params = {};
|
||||
const params: { startStep?: string; endStep?: string } = {};
|
||||
let sequenceQuery = '';
|
||||
let startStepQuery = '';
|
||||
let endStepQuery = '';
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
|
||||
export interface PerformanceParameters {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
unit: string;
|
||||
timezone: string;
|
||||
metric: string;
|
||||
}
|
||||
|
||||
export interface PerformanceResult {
|
||||
chart: { t: string; p50: number; p75: number; p95: number }[];
|
||||
summary: {
|
||||
lcp: { p50: number; p75: number; p95: number };
|
||||
inp: { p50: number; p75: number; p95: number };
|
||||
cls: { p50: number; p75: number; p95: number };
|
||||
fcp: { p50: number; p75: number; p95: number };
|
||||
ttfb: { p50: number; p75: number; p95: number };
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPerformance(
|
||||
...args: [websiteId: string, parameters: PerformanceParameters, filters: QueryFilters]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
parameters: PerformanceParameters,
|
||||
filters: QueryFilters,
|
||||
): Promise<PerformanceResult> {
|
||||
const { startDate, endDate, unit = 'day', timezone = 'utc', metric = 'lcp' } = parameters;
|
||||
const { getDateSQL, rawQuery, parseFilters } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
const chart = await rawQuery(
|
||||
`
|
||||
select
|
||||
${getDateSQL('created_at', unit, timezone)} t,
|
||||
percentile_cont(0.5) within group (order by ${metric}) as p50,
|
||||
percentile_cont(0.75) within group (order by ${metric}) as p75,
|
||||
percentile_cont(0.95) within group (order by ${metric}) as p95
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
group by t
|
||||
order by t
|
||||
`,
|
||||
{ ...queryParams, startDate, endDate },
|
||||
);
|
||||
|
||||
const summaryResult = await rawQuery(
|
||||
`
|
||||
select
|
||||
percentile_cont(0.5) within group (order by lcp) as lcp_p50,
|
||||
percentile_cont(0.75) within group (order by lcp) as lcp_p75,
|
||||
percentile_cont(0.95) within group (order by lcp) as lcp_p95,
|
||||
percentile_cont(0.5) within group (order by inp) as inp_p50,
|
||||
percentile_cont(0.75) within group (order by inp) as inp_p75,
|
||||
percentile_cont(0.95) within group (order by inp) as inp_p95,
|
||||
percentile_cont(0.5) within group (order by cls) as cls_p50,
|
||||
percentile_cont(0.75) within group (order by cls) as cls_p75,
|
||||
percentile_cont(0.95) within group (order by cls) as cls_p95,
|
||||
percentile_cont(0.5) within group (order by fcp) as fcp_p50,
|
||||
percentile_cont(0.75) within group (order by fcp) as fcp_p75,
|
||||
percentile_cont(0.95) within group (order by fcp) as fcp_p95,
|
||||
percentile_cont(0.5) within group (order by ttfb) as ttfb_p50,
|
||||
percentile_cont(0.75) within group (order by ttfb) as ttfb_p75,
|
||||
percentile_cont(0.95) within group (order by ttfb) as ttfb_p95,
|
||||
count(*) as count
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
`,
|
||||
{ ...queryParams, startDate, endDate },
|
||||
).then(result => result?.[0]);
|
||||
|
||||
const summary = {
|
||||
lcp: {
|
||||
p50: Number(summaryResult?.lcp_p50 || 0),
|
||||
p75: Number(summaryResult?.lcp_p75 || 0),
|
||||
p95: Number(summaryResult?.lcp_p95 || 0),
|
||||
},
|
||||
inp: {
|
||||
p50: Number(summaryResult?.inp_p50 || 0),
|
||||
p75: Number(summaryResult?.inp_p75 || 0),
|
||||
p95: Number(summaryResult?.inp_p95 || 0),
|
||||
},
|
||||
cls: {
|
||||
p50: Number(summaryResult?.cls_p50 || 0),
|
||||
p75: Number(summaryResult?.cls_p75 || 0),
|
||||
p95: Number(summaryResult?.cls_p95 || 0),
|
||||
},
|
||||
fcp: {
|
||||
p50: Number(summaryResult?.fcp_p50 || 0),
|
||||
p75: Number(summaryResult?.fcp_p75 || 0),
|
||||
p95: Number(summaryResult?.fcp_p95 || 0),
|
||||
},
|
||||
ttfb: {
|
||||
p50: Number(summaryResult?.ttfb_p50 || 0),
|
||||
p75: Number(summaryResult?.ttfb_p75 || 0),
|
||||
p95: Number(summaryResult?.ttfb_p95 || 0),
|
||||
},
|
||||
count: Number(summaryResult?.count || 0),
|
||||
};
|
||||
|
||||
return { chart, summary };
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
parameters: PerformanceParameters,
|
||||
filters: QueryFilters,
|
||||
): Promise<PerformanceResult> {
|
||||
const { startDate, endDate, unit = 'day', timezone = 'utc', metric = 'lcp' } = parameters;
|
||||
const { getDateSQL, rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
|
||||
|
||||
const chart = await rawQuery<{ t: string; p50: number; p75: number; p95: number }[]>(
|
||||
`
|
||||
select
|
||||
${getDateSQL('created_at', unit, timezone)} t,
|
||||
quantile(0.5)(${metric}) as p50,
|
||||
quantile(0.75)(${metric}) as p75,
|
||||
quantile(0.95)(${metric}) as p95
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_event.website_id = {websiteId:UUID}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
group by t
|
||||
order by t
|
||||
`,
|
||||
{ ...queryParams, startDate, endDate },
|
||||
);
|
||||
|
||||
const summaryResult = await rawQuery<any>(
|
||||
`
|
||||
select
|
||||
quantile(0.5)(lcp) as lcp_p50,
|
||||
quantile(0.75)(lcp) as lcp_p75,
|
||||
quantile(0.95)(lcp) as lcp_p95,
|
||||
quantile(0.5)(inp) as inp_p50,
|
||||
quantile(0.75)(inp) as inp_p75,
|
||||
quantile(0.95)(inp) as inp_p95,
|
||||
quantile(0.5)(cls) as cls_p50,
|
||||
quantile(0.75)(cls) as cls_p75,
|
||||
quantile(0.95)(cls) as cls_p95,
|
||||
quantile(0.5)(fcp) as fcp_p50,
|
||||
quantile(0.75)(fcp) as fcp_p75,
|
||||
quantile(0.95)(fcp) as fcp_p95,
|
||||
quantile(0.5)(ttfb) as ttfb_p50,
|
||||
quantile(0.75)(ttfb) as ttfb_p75,
|
||||
quantile(0.95)(ttfb) as ttfb_p95,
|
||||
count() as count
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_event.website_id = {websiteId:UUID}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
`,
|
||||
{ ...queryParams, startDate, endDate },
|
||||
).then(result => result?.[0]);
|
||||
|
||||
const summary = {
|
||||
lcp: {
|
||||
p50: Number(summaryResult?.lcp_p50 || 0),
|
||||
p75: Number(summaryResult?.lcp_p75 || 0),
|
||||
p95: Number(summaryResult?.lcp_p95 || 0),
|
||||
},
|
||||
inp: {
|
||||
p50: Number(summaryResult?.inp_p50 || 0),
|
||||
p75: Number(summaryResult?.inp_p75 || 0),
|
||||
p95: Number(summaryResult?.inp_p95 || 0),
|
||||
},
|
||||
cls: {
|
||||
p50: Number(summaryResult?.cls_p50 || 0),
|
||||
p75: Number(summaryResult?.cls_p75 || 0),
|
||||
p95: Number(summaryResult?.cls_p95 || 0),
|
||||
},
|
||||
fcp: {
|
||||
p50: Number(summaryResult?.fcp_p50 || 0),
|
||||
p75: Number(summaryResult?.fcp_p75 || 0),
|
||||
p95: Number(summaryResult?.fcp_p95 || 0),
|
||||
},
|
||||
ttfb: {
|
||||
p50: Number(summaryResult?.ttfb_p50 || 0),
|
||||
p75: Number(summaryResult?.ttfb_p75 || 0),
|
||||
p95: Number(summaryResult?.ttfb_p95 || 0),
|
||||
},
|
||||
count: Number(summaryResult?.count || 0),
|
||||
};
|
||||
|
||||
return { chart, summary };
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { SESSION_COLUMNS } from '@/lib/constants';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
import type { PerformanceParameters } from './getPerformance';
|
||||
|
||||
export interface PerformanceMetricsData {
|
||||
name: string;
|
||||
p50: number;
|
||||
p75: number;
|
||||
p95: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export async function getPerformanceMetrics(
|
||||
...args: [
|
||||
websiteId: string,
|
||||
parameters: PerformanceParameters,
|
||||
filters: QueryFilters,
|
||||
column: string,
|
||||
limit?: number,
|
||||
]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
parameters: PerformanceParameters,
|
||||
filters: QueryFilters,
|
||||
column: string,
|
||||
limit?: number,
|
||||
): Promise<PerformanceMetricsData[]> {
|
||||
const { startDate, endDate, metric = 'lcp' } = parameters;
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters(
|
||||
{ ...filters, websiteId },
|
||||
{ joinSession: SESSION_COLUMNS.includes(column) },
|
||||
);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select
|
||||
${column} as "name",
|
||||
percentile_cont(0.5) within group (order by ${metric}) as p50,
|
||||
percentile_cont(0.75) within group (order by ${metric}) as p75,
|
||||
percentile_cont(0.95) within group (order by ${metric}) as p95,
|
||||
count(*) as count
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${filterQuery}
|
||||
group by ${column}
|
||||
order by p75 desc
|
||||
${limit ? `limit ${limit}` : ''}
|
||||
`,
|
||||
{ ...queryParams, startDate, endDate },
|
||||
);
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
parameters: PerformanceParameters,
|
||||
filters: QueryFilters,
|
||||
column: string,
|
||||
limit?: number,
|
||||
): Promise<PerformanceMetricsData[]> {
|
||||
const { startDate, endDate, metric = 'lcp' } = parameters;
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
|
||||
|
||||
return rawQuery<PerformanceMetricsData[]>(
|
||||
`
|
||||
select
|
||||
${column} as "name",
|
||||
quantile(0.5)(${metric}) as p50,
|
||||
quantile(0.75)(${metric}) as p75,
|
||||
quantile(0.95)(${metric}) as p95,
|
||||
count() as count
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_event.website_id = {websiteId:UUID}
|
||||
and website_event.event_type = 5
|
||||
and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
${filterQuery}
|
||||
group by ${column}
|
||||
order by p75 desc
|
||||
${limit ? `limit ${limit}` : ''}
|
||||
`,
|
||||
{ ...queryParams, startDate, endDate },
|
||||
);
|
||||
}
|
||||
@@ -132,7 +132,7 @@ async function clickhouseQuery(
|
||||
),
|
||||
user_activities AS (
|
||||
select distinct
|
||||
website_event.session_id,
|
||||
website_event.session_id as session_id,
|
||||
toInt32((${getDateSQL('created_at', unit, timezone)} - cohort_items.cohort_date) / 86400) as day_number
|
||||
from website_event
|
||||
join cohort_items
|
||||
|
||||
@@ -9,12 +9,7 @@ export interface RevenuParameters {
|
||||
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 };
|
||||
compare?: string;
|
||||
}
|
||||
|
||||
export async function getRevenue(
|
||||
@@ -30,7 +25,7 @@ async function relationalQuery(
|
||||
websiteId: string,
|
||||
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({
|
||||
@@ -41,28 +36,32 @@ async function relationalQuery(
|
||||
currency,
|
||||
});
|
||||
|
||||
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 joinQuery =
|
||||
filterQuery || cohortQuery
|
||||
? `join (select *
|
||||
from website_event
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and event_type = 2) 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`
|
||||
: '';
|
||||
|
||||
const chart = await rawQuery(
|
||||
`
|
||||
select
|
||||
revenue.event_name x,
|
||||
${getDateSQL('revenue.created_at', unit, timezone)} t,
|
||||
sum(revenue.revenue) y
|
||||
sum(revenue.revenue) y,
|
||||
count(revenue.event_id) count
|
||||
from revenue
|
||||
${joinQuery}
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where revenue.website_id = {{websiteId::uuid}}
|
||||
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||
and revenue.currency = upper({{currency}})
|
||||
and upper(revenue.currency) = {{currency}}
|
||||
${filterQuery}
|
||||
group by x, t
|
||||
order by t
|
||||
@@ -70,54 +69,14 @@ async function relationalQuery(
|
||||
queryParams,
|
||||
);
|
||||
|
||||
const country = await rawQuery(
|
||||
`
|
||||
select
|
||||
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 = upper({{currency}})
|
||||
${filterQuery}
|
||||
group by session.country
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
const total = await rawQuery(
|
||||
`
|
||||
select
|
||||
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 = upper({{currency}})
|
||||
${filterQuery}
|
||||
`,
|
||||
queryParams,
|
||||
).then(result => result?.[0]);
|
||||
|
||||
total.average = total.count > 0 ? Number(total.sum) / Number(total.count) : 0;
|
||||
|
||||
return { chart, country, total };
|
||||
return { chart };
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
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({
|
||||
@@ -129,32 +88,30 @@ async function clickhouseQuery(
|
||||
});
|
||||
|
||||
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}`
|
||||
? `any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2) 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`
|
||||
: '';
|
||||
|
||||
const chart = await rawQuery<
|
||||
{
|
||||
x: string;
|
||||
t: string;
|
||||
y: number;
|
||||
}[]
|
||||
>(
|
||||
const chart = await rawQuery<{ x: string; t: string; y: number; count: number }[]>(
|
||||
`
|
||||
select
|
||||
website_revenue.event_name x,
|
||||
${getDateSQL('website_revenue.created_at', unit, timezone)} t,
|
||||
sum(website_revenue.revenue) y
|
||||
sum(website_revenue.revenue) y,
|
||||
count(website_revenue.event_id) count
|
||||
from website_revenue
|
||||
${joinQuery}
|
||||
${cohortQuery}
|
||||
where website_revenue.website_id = {websiteId:UUID}
|
||||
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and website_revenue.currency = upper({currency:String})
|
||||
and upper(website_revenue.currency) = {currency:String}
|
||||
${filterQuery}
|
||||
group by x, t
|
||||
order by t
|
||||
@@ -162,56 +119,5 @@ async function clickhouseQuery(
|
||||
queryParams,
|
||||
);
|
||||
|
||||
const country = await rawQuery<
|
||||
{
|
||||
name: string;
|
||||
value: number;
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
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 = upper({currency:String})
|
||||
${filterQuery}
|
||||
group by website_event.country
|
||||
order by value desc
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
const total = await rawQuery<{
|
||||
sum: number;
|
||||
count: number;
|
||||
unique_count: number;
|
||||
}>(
|
||||
`
|
||||
select
|
||||
sum(website_revenue.revenue) as sum,
|
||||
uniqExact(website_revenue.event_id) as count,
|
||||
uniqExact(website_revenue.session_id) as unique_count
|
||||
from website_revenue
|
||||
${joinQuery}
|
||||
${cohortQuery}
|
||||
where website_revenue.website_id = {websiteId:UUID}
|
||||
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and website_revenue.currency = upper({currency:String})
|
||||
${filterQuery}
|
||||
`,
|
||||
queryParams,
|
||||
).then(result => result?.[0]);
|
||||
|
||||
total.average = total.count > 0 ? total.sum / total.count : 0;
|
||||
|
||||
return { chart, country, total };
|
||||
return { chart };
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
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 type { QueryFilters } from '@/lib/types';
|
||||
import type { RevenuParameters } from './getRevenue';
|
||||
|
||||
export interface RevenueMetricsResult {
|
||||
country: { name: string; value: number }[];
|
||||
region: { name: string; value: number; country: string }[];
|
||||
referrer: { name: string; value: number }[];
|
||||
channel: { name: string; value: number }[];
|
||||
}
|
||||
|
||||
export async function getRevenueMetrics(
|
||||
...args: [websiteId: string, parameters: RevenuParameters, filters: QueryFilters]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
parameters: RevenuParameters,
|
||||
filters: QueryFilters,
|
||||
): Promise<RevenueMetricsResult> {
|
||||
const { startDate, endDate, currency } = parameters;
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { queryParams, filterQuery, cohortQuery, joinSessionQuery } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
currency,
|
||||
});
|
||||
|
||||
const joinQuery =
|
||||
filterQuery || cohortQuery
|
||||
? `join (select *
|
||||
from website_event
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and event_type = 2) 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`
|
||||
: '';
|
||||
|
||||
const country = await rawQuery(
|
||||
`
|
||||
select
|
||||
session.country as "name",
|
||||
sum(revenue) as "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 upper(revenue.currency) = {{currency}}
|
||||
${filterQuery}
|
||||
group by session.country
|
||||
order by value desc
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
const region = await rawQuery(
|
||||
`
|
||||
select
|
||||
session.country,
|
||||
session.region as "name",
|
||||
sum(revenue.revenue) as "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 upper(revenue.currency) = {{currency}}
|
||||
${filterQuery}
|
||||
group by session.country, session.region
|
||||
order by value desc
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
const referrer = await rawQuery(
|
||||
`
|
||||
WITH events AS (
|
||||
select
|
||||
revenue.website_id,
|
||||
revenue.session_id,
|
||||
sum(revenue.revenue) as "value"
|
||||
from revenue
|
||||
${joinQuery}
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where revenue.website_id = {{websiteId::uuid}}
|
||||
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||
and upper(revenue.currency) = {{currency}}
|
||||
${filterQuery}
|
||||
group by revenue.website_id, revenue.session_id),
|
||||
|
||||
revenue_data AS (
|
||||
select
|
||||
e.website_id,
|
||||
e.session_id,
|
||||
e.value,
|
||||
we.min_date as created_at
|
||||
from events e
|
||||
join (
|
||||
select session_id, min(created_at) as min_date
|
||||
from website_event
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
group by session_id
|
||||
) we on we.session_id = e.session_id)
|
||||
|
||||
select
|
||||
we.referrer_domain as "name",
|
||||
sum(revenue_data.value) as "value"
|
||||
from revenue_data
|
||||
join (
|
||||
select website_id, session_id, referrer_domain, created_at
|
||||
from website_event
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}) we
|
||||
on we.website_id = revenue_data.website_id
|
||||
and we.session_id = revenue_data.session_id
|
||||
and we.created_at = revenue_data.created_at
|
||||
group by we.referrer_domain
|
||||
order by value desc
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
const channel = await rawQuery(
|
||||
`
|
||||
WITH events AS (
|
||||
select
|
||||
revenue.website_id,
|
||||
revenue.session_id,
|
||||
sum(revenue.revenue) as "value"
|
||||
from revenue
|
||||
${joinQuery}
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where revenue.website_id = {{websiteId::uuid}}
|
||||
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||
and upper(revenue.currency) = {{currency}}
|
||||
${filterQuery}
|
||||
group by revenue.website_id, revenue.session_id),
|
||||
|
||||
revenue_data AS (
|
||||
select
|
||||
e.website_id,
|
||||
e.session_id,
|
||||
e.value,
|
||||
we.min_date as created_at
|
||||
from events e
|
||||
join (
|
||||
select session_id, min(created_at) as min_date
|
||||
from website_event
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
group by session_id
|
||||
) we on we.session_id = e.session_id),
|
||||
|
||||
revenue_prefix AS (
|
||||
select
|
||||
case when we.utm_medium ilike '%cp%' OR
|
||||
we.utm_medium ilike '%ppc%' OR
|
||||
we.utm_medium ilike '%retargeting%' OR
|
||||
we.utm_medium ilike '%paid%' then 'paid' else 'organic' end AS prefix,
|
||||
we.referrer_domain,
|
||||
we.url_query,
|
||||
we.utm_medium,
|
||||
we.utm_source,
|
||||
we.hostname,
|
||||
r.value
|
||||
from revenue_data r
|
||||
join (
|
||||
select website_id, session_id, referrer_domain, url_query, utm_medium, utm_source, hostname, created_at
|
||||
from website_event
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}) we
|
||||
on we.website_id = r.website_id
|
||||
and we.session_id = r.session_id
|
||||
and we.created_at = r.created_at),
|
||||
|
||||
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')
|
||||
when referrer_domain != regexp_replace(hostname, '^www.', '') and referrer_domain != '' then 'referral'
|
||||
else 'Unknown' end AS "name",
|
||||
value
|
||||
from revenue_prefix)
|
||||
|
||||
select name, sum(value) as value
|
||||
from channels
|
||||
group by name
|
||||
order by value desc
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
return { country, region, referrer, channel };
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
parameters: RevenuParameters,
|
||||
filters: QueryFilters,
|
||||
): Promise<RevenueMetricsResult> {
|
||||
const { startDate, endDate, currency } = parameters;
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
currency,
|
||||
});
|
||||
|
||||
const joinQuery = filterQuery
|
||||
? `any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2) 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`
|
||||
: '';
|
||||
|
||||
const country = await rawQuery<{ name: string; value: number }[]>(
|
||||
`
|
||||
select
|
||||
website_event.country as "name",
|
||||
sum(website_revenue.revenue) as "value"
|
||||
from website_revenue
|
||||
any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2) 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
|
||||
${cohortQuery}
|
||||
where website_revenue.website_id = {websiteId:UUID}
|
||||
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and upper(website_revenue.currency) = {currency:String}
|
||||
${filterQuery}
|
||||
group by website_event.country
|
||||
order by value desc
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
const region = await rawQuery<{ name: string; value: number; country: string }[]>(
|
||||
`
|
||||
select
|
||||
website_event.country,
|
||||
website_event.region as "name",
|
||||
sum(website_revenue.revenue) as "value"
|
||||
from website_revenue
|
||||
any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2) 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
|
||||
${cohortQuery}
|
||||
where website_revenue.website_id = {websiteId:UUID}
|
||||
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and upper(website_revenue.currency) = {currency:String}
|
||||
${filterQuery}
|
||||
group by 1,2
|
||||
order by value desc
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
const referrer = await rawQuery<{ name: string; value: number }[]>(
|
||||
`
|
||||
WITH events AS (
|
||||
select distinct
|
||||
website_id,
|
||||
session_id,
|
||||
sum(revenue) as "value"
|
||||
from website_revenue
|
||||
${joinQuery}
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and upper(currency) = {currency:String}
|
||||
${filterQuery}
|
||||
group by 1,2),
|
||||
|
||||
revenue AS (
|
||||
select
|
||||
e.website_id,
|
||||
e.session_id,
|
||||
e.value,
|
||||
we.min_date as created_at
|
||||
from events e
|
||||
join (select session_id, min(created_at) min_date
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
group by 1
|
||||
) we
|
||||
on we.session_id = e.session_id)
|
||||
|
||||
select
|
||||
website_event.referrer_domain as "name",
|
||||
sum(revenue.value) as "value"
|
||||
from revenue
|
||||
any left join (
|
||||
select website_id, session_id, referrer_domain, created_at
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}) website_event
|
||||
on website_event.website_id = revenue.website_id
|
||||
and website_event.session_id = revenue.session_id
|
||||
and website_event.created_at = revenue.created_at
|
||||
group by 1
|
||||
order by value desc
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
const channel = await rawQuery<{ name: string; value: number }[]>(
|
||||
`
|
||||
WITH events AS (
|
||||
select distinct
|
||||
website_id,
|
||||
session_id,
|
||||
sum(revenue) as "value"
|
||||
from website_revenue
|
||||
${joinQuery}
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and upper(currency) = {currency:String}
|
||||
${filterQuery}
|
||||
group by 1,2),
|
||||
|
||||
revenue AS (
|
||||
select
|
||||
e.website_id,
|
||||
e.session_id,
|
||||
e.value,
|
||||
we.min_date as created_at
|
||||
from events e
|
||||
join (select session_id, min(created_at) min_date
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
group by 1
|
||||
) we
|
||||
on we.session_id = e.session_id),
|
||||
|
||||
channels AS (
|
||||
select
|
||||
case when multiSearchAny(lower(utm_medium), ['cp', 'ppc', 'retargeting', 'paid']) != 0 then 'paid' else 'organic' end prefix,
|
||||
case
|
||||
when referrer_domain = '' and url_query = '' then 'direct'
|
||||
when multiSearchAny(lower(url_query), [${toClickHouseStringArray(
|
||||
PAID_AD_PARAMS,
|
||||
)}]) != 0 then 'paidAds'
|
||||
when multiSearchAny(lower(utm_medium), ['referral', 'app','link']) != 0 then 'referral'
|
||||
when position(lower(utm_medium), 'affiliate') > 0 then 'affiliate'
|
||||
when position(lower(utm_medium), 'sms') > 0 or position(lower(utm_source), 'sms') > 0 then 'sms'
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
SEARCH_DOMAINS,
|
||||
)}]) != 0 or position(lower(utm_medium), 'organic') > 0 then concat(prefix, 'Search')
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
SOCIAL_DOMAINS,
|
||||
)}]) != 0 then concat(prefix, 'Social')
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
EMAIL_DOMAINS,
|
||||
)}]) != 0 or position(lower(utm_medium), 'mail') > 0 then 'email'
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
SHOPPING_DOMAINS,
|
||||
)}]) != 0 or position(lower(utm_medium), 'shop') > 0 then concat(prefix, 'Shopping')
|
||||
when multiSearchAny(lower(referrer_domain), [${toClickHouseStringArray(
|
||||
VIDEO_DOMAINS,
|
||||
)}]) != 0 or position(lower(utm_medium), 'video') > 0 then concat(prefix, 'Video')
|
||||
when referrer_domain != hostname and referrer_domain != '' then 'referral'
|
||||
else 'Unknown' end AS "name",
|
||||
sum(revenue.value) as "value"
|
||||
from revenue
|
||||
any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}) website_event
|
||||
on website_event.website_id = revenue.website_id
|
||||
and website_event.session_id = revenue.session_id
|
||||
and website_event.created_at = revenue.created_at
|
||||
group by 1, 2)
|
||||
|
||||
select name, sum(value) value
|
||||
from channels
|
||||
group by 1
|
||||
order by value desc;
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
return { country, region, referrer, channel };
|
||||
}
|
||||
|
||||
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 ');
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
|
||||
const FUNCTION_NAME = 'getRevenueSessions';
|
||||
|
||||
export async function getRevenueSessions(
|
||||
...args: [websiteId: string, currency: string, filters: QueryFilters]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(websiteId: string, currency: string, filters: QueryFilters) {
|
||||
const { pagedRawQuery, parseFilters } = prisma;
|
||||
const { search } = filters;
|
||||
const { filterQuery, dateQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
currency,
|
||||
search: search ? `%${search}%` : undefined,
|
||||
});
|
||||
|
||||
const searchQuery = search
|
||||
? `and (session.browser ilike {{search}}
|
||||
or session.os ilike {{search}}
|
||||
or session.device ilike {{search}}
|
||||
or session.city ilike {{search}})`
|
||||
: '';
|
||||
|
||||
return pagedRawQuery(
|
||||
`
|
||||
select
|
||||
session.session_id as "id",
|
||||
session.website_id as "websiteId",
|
||||
website_event.hostname,
|
||||
session.browser,
|
||||
session.os,
|
||||
session.device,
|
||||
session.screen,
|
||||
session.language,
|
||||
session.country,
|
||||
session.region,
|
||||
session.city,
|
||||
min(website_event.created_at) as "firstAt",
|
||||
max(website_event.created_at) as "lastAt",
|
||||
count(distinct website_event.visit_id) as "visits",
|
||||
sum(case when website_event.event_type = 1 then 1 else 0 end) as "views",
|
||||
sum(case when website_event.event_type = 2 then 1 else 0 end) as "events",
|
||||
max(website_event.created_at) as "createdAt"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
join session
|
||||
on session.session_id = website_event.session_id
|
||||
and session.website_id = website_event.website_id
|
||||
join (
|
||||
select distinct session_id
|
||||
from revenue
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||
and upper(currency) = {{currency}}
|
||||
) rev on rev.session_id = website_event.session_id
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
${searchQuery}
|
||||
group by
|
||||
session.session_id,
|
||||
session.website_id,
|
||||
website_event.hostname,
|
||||
session.browser,
|
||||
session.os,
|
||||
session.device,
|
||||
session.screen,
|
||||
session.language,
|
||||
session.country,
|
||||
session.region,
|
||||
session.city
|
||||
order by max(website_event.created_at) desc
|
||||
`,
|
||||
queryParams,
|
||||
filters,
|
||||
FUNCTION_NAME,
|
||||
);
|
||||
}
|
||||
|
||||
async function clickhouseQuery(websiteId: string, currency: string, filters: QueryFilters) {
|
||||
const { pagedRawQuery, parseFilters, getDateStringSQL } = clickhouse;
|
||||
const { search } = filters;
|
||||
const { filterQuery, dateQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
currency,
|
||||
});
|
||||
|
||||
const searchQuery = search
|
||||
? `and ((positionCaseInsensitive(browser, {search:String}) > 0)
|
||||
or (positionCaseInsensitive(city, {search:String}) > 0)
|
||||
or (positionCaseInsensitive(os, {search:String}) > 0)
|
||||
or (positionCaseInsensitive(device, {search:String}) > 0))`
|
||||
: '';
|
||||
|
||||
return pagedRawQuery(
|
||||
`
|
||||
select
|
||||
session_id as id,
|
||||
website_id as websiteId,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
${getDateStringSQL('min(created_at)')} as firstAt,
|
||||
${getDateStringSQL('max(created_at)')} as lastAt,
|
||||
uniq(visit_id) as visits,
|
||||
sumIf(1, event_type = 1) as views,
|
||||
sumIf(1, event_type = 2) as events,
|
||||
max(created_at) as createdAt
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
${searchQuery}
|
||||
and session_id in (
|
||||
select distinct session_id
|
||||
from website_revenue
|
||||
where website_id = {websiteId:UUID}
|
||||
${dateQuery}
|
||||
and upper(currency) = {currency:String}
|
||||
)
|
||||
group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city
|
||||
order by max(created_at) desc
|
||||
`,
|
||||
queryParams,
|
||||
filters,
|
||||
FUNCTION_NAME,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
import type { RevenuParameters } from './getRevenue';
|
||||
|
||||
export interface RevenueStatsResult {
|
||||
sum: number;
|
||||
count: number;
|
||||
average: number;
|
||||
unique_count: number;
|
||||
arpu: number;
|
||||
}
|
||||
|
||||
export async function getRevenueStats(
|
||||
...args: [websiteId: string, parameters: RevenuParameters, filters: QueryFilters]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
parameters: RevenuParameters,
|
||||
filters: QueryFilters,
|
||||
): Promise<RevenueStatsResult> {
|
||||
const { startDate, endDate, currency } = parameters;
|
||||
const { rawQuery, parseFilters } = prisma;
|
||||
const { queryParams, filterQuery, cohortQuery, joinSessionQuery } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
currency,
|
||||
});
|
||||
|
||||
const joinQuery =
|
||||
filterQuery || cohortQuery
|
||||
? `join (select *
|
||||
from website_event
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and event_type = 2) 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`
|
||||
: '';
|
||||
|
||||
const total = await rawQuery(
|
||||
`
|
||||
select
|
||||
sum(revenue.revenue) as sum,
|
||||
count(distinct revenue.event_id) as count,
|
||||
count(distinct revenue.session_id) as unique_count,
|
||||
(select count(distinct session_id)
|
||||
from website_event
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}) as total_sessions
|
||||
from revenue
|
||||
${joinQuery}
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where revenue.website_id = {{websiteId::uuid}}
|
||||
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||
and upper(revenue.currency) = {{currency}}
|
||||
${filterQuery}
|
||||
`,
|
||||
queryParams,
|
||||
).then(result => result?.[0]);
|
||||
|
||||
total.average = total.count > 0 ? Number(total.sum) / Number(total.count) : 0;
|
||||
total.arpu = total.total_sessions > 0 ? Number(total.sum) / Number(total.total_sessions) : 0;
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
parameters: RevenuParameters,
|
||||
filters: QueryFilters,
|
||||
): Promise<RevenueStatsResult> {
|
||||
const { startDate, endDate, currency } = parameters;
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
currency,
|
||||
});
|
||||
|
||||
const joinQuery = filterQuery
|
||||
? `any left join (
|
||||
select *
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = 2) 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`
|
||||
: '';
|
||||
|
||||
const total = await rawQuery<{
|
||||
sum: number;
|
||||
count: number;
|
||||
unique_count: number;
|
||||
total_sessions: number;
|
||||
}>(
|
||||
`
|
||||
select
|
||||
sum(website_revenue.revenue) as sum,
|
||||
uniqExact(website_revenue.event_id) as count,
|
||||
uniqExact(website_revenue.session_id) as unique_count,
|
||||
(select uniqExact(session_id)
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}) as total_sessions
|
||||
from website_revenue
|
||||
${joinQuery}
|
||||
${cohortQuery}
|
||||
where website_revenue.website_id = {websiteId:UUID}
|
||||
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and upper(website_revenue.currency) = {currency:String}
|
||||
${filterQuery}
|
||||
`,
|
||||
queryParams,
|
||||
).then(result => result?.[0]);
|
||||
|
||||
total.average = total.count > 0 ? total.sum / total.count : 0;
|
||||
total.arpu = total.total_sessions > 0 ? total.sum / total.total_sessions : 0;
|
||||
|
||||
return total;
|
||||
}
|
||||
@@ -47,6 +47,7 @@ async function relationalQuery(
|
||||
${filterQuery}
|
||||
group by 1
|
||||
order by 2 desc
|
||||
limit 50
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
@@ -78,6 +79,7 @@ async function clickhouseQuery(
|
||||
${filterQuery}
|
||||
group by 1
|
||||
order by 2 desc
|
||||
limit 50
|
||||
`,
|
||||
queryParams,
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ async function relationalQuery(websiteId: string, sessionId: string, filters: Qu
|
||||
event_type as "eventType",
|
||||
event_name as "eventName",
|
||||
visit_id as "visitId",
|
||||
hostname,
|
||||
event_id IN (select website_event_id
|
||||
from event_data
|
||||
where website_id = {{websiteId::uuid}}
|
||||
@@ -60,6 +61,7 @@ async function clickhouseQuery(websiteId: string, sessionId: string, filters: Qu
|
||||
event_type as eventType,
|
||||
event_name as eventName,
|
||||
visit_id as visitId,
|
||||
hostname,
|
||||
event_id IN (select event_id
|
||||
from event_data
|
||||
where website_id = {websiteId:UUID}
|
||||
|
||||
@@ -38,15 +38,16 @@ async function relationalQuery(
|
||||
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 { filterQuery, joinSessionQuery, cohortQuery, excludeBounceQuery, queryParams } =
|
||||
parseFilters(
|
||||
{
|
||||
...filters,
|
||||
websiteId,
|
||||
},
|
||||
{
|
||||
joinSession: SESSION_COLUMNS.includes(type),
|
||||
},
|
||||
);
|
||||
const includeCountry = column === 'city' || column === 'region';
|
||||
|
||||
if (type === 'language') {
|
||||
@@ -65,7 +66,7 @@ async function relationalQuery(
|
||||
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
|
||||
from (
|
||||
select
|
||||
${column} name,
|
||||
${column} as "name",
|
||||
${includeCountry ? 'country,' : ''}
|
||||
website_event.session_id,
|
||||
website_event.visit_id,
|
||||
@@ -74,14 +75,16 @@ async function relationalQuery(
|
||||
max(website_event.created_at) as "max_time"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by name, website_event.session_id, website_event.visit_id
|
||||
${includeCountry ? ', country' : ''}
|
||||
) as t
|
||||
where name != ''
|
||||
group by name
|
||||
${includeCountry ? ', country' : ''}
|
||||
order by visitors desc, visits desc
|
||||
@@ -101,7 +104,7 @@ async function clickhouseQuery(
|
||||
const { type, limit = 500, offset = 0 } = parameters;
|
||||
let column = FILTER_COLUMNS[type] || type;
|
||||
const { parseFilters, rawQuery } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
const { filterQuery, cohortQuery, excludeBounceQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
@@ -132,9 +135,10 @@ async function clickhouseQuery(
|
||||
max(created_at) max_time
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
and name != ''
|
||||
${filterQuery}
|
||||
group by name, session_id, visit_id
|
||||
|
||||
@@ -29,15 +29,16 @@ async function relationalQuery(
|
||||
const { type, limit = 500, offset = 0 } = parameters;
|
||||
let column = FILTER_COLUMNS[type] || type;
|
||||
const { parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters(
|
||||
{
|
||||
...filters,
|
||||
websiteId,
|
||||
},
|
||||
{
|
||||
joinSession: SESSION_COLUMNS.includes(type),
|
||||
},
|
||||
);
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, excludeBounceQuery, queryParams } =
|
||||
parseFilters(
|
||||
{
|
||||
...filters,
|
||||
websiteId,
|
||||
},
|
||||
{
|
||||
joinSession: SESSION_COLUMNS.includes(type),
|
||||
},
|
||||
);
|
||||
const includeCountry = column === 'city' || column === 'region';
|
||||
|
||||
if (type === 'language') {
|
||||
@@ -52,10 +53,11 @@ async function relationalQuery(
|
||||
${includeCountry ? ', country' : ''}
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by 1
|
||||
${includeCountry ? ', 3' : ''}
|
||||
@@ -76,7 +78,7 @@ async function clickhouseQuery(
|
||||
const { type, limit = 500, offset = 0 } = parameters;
|
||||
let column = FILTER_COLUMNS[type] || type;
|
||||
const { parseFilters, rawQuery } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
const { filterQuery, cohortQuery, excludeBounceQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
@@ -96,9 +98,10 @@ async function clickhouseQuery(
|
||||
${includeCountry ? ', country' : ''}
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by x
|
||||
${includeCountry ? ', country' : ''}
|
||||
@@ -114,9 +117,10 @@ async function clickhouseQuery(
|
||||
${includeCountry ? ', country' : ''}
|
||||
from website_event_stats_hourly as website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by x
|
||||
${includeCountry ? ', country' : ''}
|
||||
|
||||
@@ -16,10 +16,11 @@ export async function getSessionStats(...args: [websiteId: string, filters: Quer
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { timezone = 'utc', unit = 'day' } = filters;
|
||||
const { getDateSQL, parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
const { filterQuery, joinSessionQuery, cohortQuery, excludeBounceQuery, queryParams } =
|
||||
parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
@@ -28,10 +29,11 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
count(distinct website_event.session_id) y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
and website_event.event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by 1
|
||||
order by 1
|
||||
@@ -47,7 +49,7 @@ async function clickhouseQuery(
|
||||
): Promise<{ x: string; y: number }[]> {
|
||||
const { timezone = 'UTC', unit = 'day' } = filters;
|
||||
const { parseFilters, rawQuery, getDateSQL } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
const { filterQuery, cohortQuery, excludeBounceQuery, queryParams } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
});
|
||||
@@ -65,9 +67,10 @@ async function clickhouseQuery(
|
||||
count(distinct session_id) as y
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by t
|
||||
) as g
|
||||
@@ -84,9 +87,10 @@ async function clickhouseQuery(
|
||||
uniq(session_id) as y
|
||||
from website_event_stats_hourly as website_event
|
||||
${cohortQuery}
|
||||
${excludeBounceQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
and event_type NOT IN (2, 5)
|
||||
${filterQuery}
|
||||
group by t
|
||||
) as g
|
||||
|
||||
@@ -70,7 +70,7 @@ async function clickhouseQuery(
|
||||
uniq(session_id) as "visitors",
|
||||
uniq(visit_id) as "visits",
|
||||
uniq(country) as "countries",
|
||||
sum(length(event_name)) as "events"
|
||||
sumIf(1, event_type = 2) as "events"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
|
||||
@@ -48,6 +48,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
max(website_event.created_at) as "lastAt",
|
||||
count(distinct website_event.visit_id) as "visits",
|
||||
sum(case when website_event.event_type = 1 then 1 else 0 end) as "views",
|
||||
sum(case when website_event.event_type = 2 then 1 else 0 end) as "events",
|
||||
max(website_event.created_at) as "createdAt"
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
@@ -112,6 +113,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
${getDateStringSQL('max(created_at)')} as lastAt,
|
||||
uniq(visit_id) as visits,
|
||||
sumIf(1, event_type = 1) as views,
|
||||
sumIf(1, event_type = 2) as events,
|
||||
lastAt as createdAt
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
@@ -140,6 +142,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
${getDateStringSQL('max(max_time)')} as lastAt,
|
||||
uniq(visit_id) as visits,
|
||||
sumIf(views, event_type = 1) as views,
|
||||
sum(length(event_name)) as events,
|
||||
lastAt as createdAt
|
||||
from website_event_stats_hourly as website_event
|
||||
${cohortQuery}
|
||||
|
||||
@@ -46,31 +46,23 @@ export async function relationalQuery({
|
||||
createdAt,
|
||||
}));
|
||||
|
||||
const existing = await client.sessionData.findMany({
|
||||
where: {
|
||||
sessionId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
sessionId: true,
|
||||
dataKey: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const data of flattenedData) {
|
||||
const { sessionId, dataKey, ...props } = data;
|
||||
const record = existing.find(e => e.sessionId === sessionId && e.dataKey === dataKey);
|
||||
|
||||
if (record) {
|
||||
await client.sessionData.update({
|
||||
where: {
|
||||
id: record.id,
|
||||
},
|
||||
data: {
|
||||
...props,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Try to update existing record using compound where clause
|
||||
// This is safer than using id from a previous query due to race conditions
|
||||
const updateResult = await client.sessionData.updateMany({
|
||||
where: {
|
||||
sessionId,
|
||||
dataKey,
|
||||
},
|
||||
data: {
|
||||
...props,
|
||||
},
|
||||
});
|
||||
|
||||
// If no record was updated, create a new one
|
||||
if (updateResult.count === 0) {
|
||||
await client.sessionData.create({
|
||||
data,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user