Merge branch 'dev' into patch-1

This commit is contained in:
Mike Cao 2025-11-14 11:44:20 -08:00 committed by GitHub
commit b1dc690e2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1140 changed files with 52229 additions and 29220 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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