update funnels for CH. Add wildcards and event steps

This commit is contained in:
Francis Cao 2024-04-02 00:19:15 -07:00
parent 6767f95c0d
commit 05079f3810
3 changed files with 80 additions and 50 deletions

View File

@ -40,7 +40,7 @@ export function FunnelParameters() {
const handleRemoveUrl = (url: string) => { const handleRemoveUrl = (url: string) => {
const urls = [...parameters.urls]; const urls = [...parameters.urls];
updateReport({ parameters: { urls: urls.filter(n => n.url !== url) } }); updateReport({ parameters: { urls: urls.filter(n => n !== url) } });
}; };
const AddUrlButton = () => { const AddUrlButton = () => {

View File

@ -8,7 +8,7 @@ import * as yup from 'yup';
export interface FunnelRequestBody { export interface FunnelRequestBody {
websiteId: string; websiteId: string;
urls: string[]; steps: { type: string; value: string }[];
window: number; window: number;
dateRange: { dateRange: {
startDate: string; startDate: string;
@ -17,7 +17,7 @@ export interface FunnelRequestBody {
} }
export interface FunnelResponse { export interface FunnelResponse {
urls: string[]; steps: { type: string; value: string }[];
window: number; window: number;
startAt: number; startAt: number;
endAt: number; endAt: number;
@ -26,7 +26,15 @@ export interface FunnelResponse {
const schema = { const schema = {
POST: yup.object().shape({ POST: yup.object().shape({
websiteId: yup.string().uuid().required(), websiteId: yup.string().uuid().required(),
urls: yup.array().min(2).of(yup.string()).required(), steps: yup
.array()
.of(
yup.object().shape({
type: yup.string().required(),
value: yup.string().required(),
}),
)
.min(2),
window: yup.number().positive().required(), window: yup.number().positive().required(),
dateRange: yup dateRange: yup
.object() .object()
@ -49,7 +57,7 @@ export default async (
if (req.method === 'POST') { if (req.method === 'POST') {
const { const {
websiteId, websiteId,
urls, steps,
window, window,
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
} = req.body; } = req.body;
@ -61,7 +69,7 @@ export default async (
const data = await getFunnel(websiteId, { const data = await getFunnel(websiteId, {
startDate: new Date(startDate), startDate: new Date(startDate),
endDate: new Date(endDate), endDate: new Date(endDate),
urls, steps,
windowMinutes: +window, windowMinutes: +window,
}); });

View File

@ -2,8 +2,9 @@ import clickhouse from 'lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
const formatResults = (urls: string[]) => (results: unknown) => { const formatResults = (steps: { type: string; value: string }[]) => (results: unknown) => {
return urls.map((url: string, i: number) => { return steps.map((steps: { type: string; value: string }, i: number) => {
const url = steps.value;
const visitors = Number(results[i]?.count) || 0; const visitors = Number(results[i]?.count) || 0;
const previous = Number(results[i - 1]?.count) || 0; const previous = Number(results[i - 1]?.count) || 0;
const dropped = previous > 0 ? previous - visitors : 0; const dropped = previous > 0 ? previous - visitors : 0;
@ -28,7 +29,7 @@ export async function getFunnel(
windowMinutes: number; windowMinutes: number;
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
urls: string[]; steps: { type: string; value: string }[];
}, },
] ]
) { ) {
@ -44,7 +45,7 @@ async function relationalQuery(
windowMinutes: number; windowMinutes: number;
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
urls: string[]; steps: { type: string; value: string }[];
}, },
): Promise< ): Promise<
{ {
@ -53,23 +54,37 @@ async function relationalQuery(
dropoff: number; dropoff: number;
}[] }[]
> { > {
const { windowMinutes, startDate, endDate, urls } = criteria; const { windowMinutes, startDate, endDate, steps } = criteria;
const { rawQuery, getAddIntervalQuery } = prisma; const { rawQuery, getAddIntervalQuery } = prisma;
const { levelQuery, sumQuery } = getFunnelQuery(urls, windowMinutes); const { levelOneQuery, levelQuery, sumQuery, params } = getFunnelQuery(steps, windowMinutes);
function getFunnelQuery( function getFunnelQuery(
urls: string[], steps: { type: string; value: string }[],
windowMinutes: number, windowMinutes: number,
): { ): {
levelOneQuery: string;
levelQuery: string; levelQuery: string;
sumQuery: string; sumQuery: string;
params: string[];
} { } {
return urls.reduce( return steps.reduce(
(pv, cv, i) => { (pv, cv, i) => {
const levelNumber = i + 1; const levelNumber = i + 1;
const startSum = i > 0 ? 'union ' : ''; const startSum = i > 0 ? 'union ' : '';
const operator = cv.type === 'url' && cv.value.endsWith('*') ? 'like' : '=';
const column = cv.type === 'url' ? 'url_path' : 'event_name';
const paramValue = cv.value.endsWith('*') ? cv.value.replace('*', '%') : cv.value;
if (levelNumber >= 2) { if (levelNumber === 1) {
pv.levelOneQuery = `
WITH level1 AS (
select distinct session_id, created_at
from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and ${column} ${operator} {param${i}:String}
)`;
} else {
pv.levelQuery += ` pv.levelQuery += `
, level${levelNumber} AS ( , level${levelNumber} AS (
select distinct we.session_id, we.created_at select distinct we.session_id, we.created_at
@ -81,32 +96,28 @@ async function relationalQuery(
`l.created_at `, `l.created_at `,
`${windowMinutes} minute`, `${windowMinutes} minute`,
)} )}
and we.referrer_path = {{${i - 1}}} and we.${column} ${operator} {param${i}:String}
and we.url_path = {{${i}}}
and we.created_at <= {{endDate}} and we.created_at <= {{endDate}}
)`; )`;
} }
pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`; pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`;
pv.params.push(paramValue);
return pv; return pv;
}, },
{ {
levelOneQuery: '',
levelQuery: '', levelQuery: '',
sumQuery: '', sumQuery: '',
params: [],
}, },
); );
} }
return rawQuery( return rawQuery(
` `
WITH level1 AS ( ${levelOneQuery}
select distinct session_id, created_at
from website_event
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
and url_path = {{0}}
)
${levelQuery} ${levelQuery}
${sumQuery} ${sumQuery}
ORDER BY level; ORDER BY level;
@ -115,9 +126,9 @@ async function relationalQuery(
websiteId, websiteId,
startDate, startDate,
endDate, endDate,
...urls, ...params,
}, },
).then(formatResults(urls)); ).then(formatResults(steps));
} }
async function clickhouseQuery( async function clickhouseQuery(
@ -126,7 +137,7 @@ async function clickhouseQuery(
windowMinutes: number; windowMinutes: number;
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
urls: string[]; steps: { type: string; value: string }[];
}, },
): Promise< ): Promise<
{ {
@ -135,52 +146,67 @@ async function clickhouseQuery(
dropoff: number; dropoff: number;
}[] }[]
> { > {
const { windowMinutes, startDate, endDate, urls } = criteria; const { windowMinutes, startDate, endDate, steps } = criteria;
const { rawQuery } = clickhouse; const { rawQuery } = clickhouse;
const { levelQuery, sumQuery, urlFilterQuery, urlParams } = getFunnelQuery(urls, windowMinutes); const { levelOneQuery, levelQuery, sumQuery, stepFilterQuery, params } = getFunnelQuery(
steps,
windowMinutes,
);
function getFunnelQuery( function getFunnelQuery(
urls: string[], steps: { type: string; value: string }[],
windowMinutes: number, windowMinutes: number,
): { ): {
levelOneQuery: string;
levelQuery: string; levelQuery: string;
sumQuery: string; sumQuery: string;
urlFilterQuery: string; stepFilterQuery: string;
urlParams: { [key: string]: string }; params: { [key: string]: string };
} { } {
return urls.reduce( return steps.reduce(
(pv, cv, i) => { (pv, cv, i) => {
const levelNumber = i + 1; const levelNumber = i + 1;
const startSum = i > 0 ? 'union all ' : ''; const startSum = i > 0 ? 'union all ' : '';
const startFilter = i > 0 ? ', ' : ''; const startFilter = i > 0 ? 'or' : '';
const operator = cv.type === 'url' && cv.value.endsWith('*') ? 'like' : '=';
const column = cv.type === 'url' ? 'url_path' : 'event_name';
const paramValue = cv.value.endsWith('*') ? cv.value.replace('*', '%') : cv.value;
if (levelNumber >= 2) { if (levelNumber === 1) {
pv.levelOneQuery = `\n
level1 AS (
select *
from level0
where ${column} ${operator} {param${i}:String}
)`;
} else {
pv.levelQuery += `\n pv.levelQuery += `\n
, level${levelNumber} AS ( , level${levelNumber} AS (
select distinct y.session_id as session_id, select distinct y.session_id as session_id,
y.url_path as url_path, y.url_path as url_path,
y.referrer_path as referrer_path, y.referrer_path as referrer_path,
y.event_name,
y.created_at as created_at y.created_at as created_at
from level${i} x from level${i} x
join level0 y join level0 y
on x.session_id = y.session_id 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 ${windowMinutes} minute
and y.referrer_path = {url${i - 1}:String} and y.${column} ${operator} {param${i}:String}
and y.url_path = {url${i}:String}
)`; )`;
} }
pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`; pv.sumQuery += `\n${startSum}select ${levelNumber} as level, count(distinct(session_id)) as count from level${levelNumber}`;
pv.urlFilterQuery += `${startFilter}{url${i}:String} `; pv.stepFilterQuery += `${startFilter} ${column} ${operator} {param${i}:String} `;
pv.urlParams[`url${i}`] = cv; pv.params[`param${i}`] = paramValue;
return pv; return pv;
}, },
{ {
levelOneQuery: '',
levelQuery: '', levelQuery: '',
sumQuery: '', sumQuery: '',
urlFilterQuery: '', stepFilterQuery: '',
urlParams: {}, params: {},
}, },
); );
} }
@ -188,17 +214,13 @@ async function clickhouseQuery(
return rawQuery( return rawQuery(
` `
WITH level0 AS ( WITH level0 AS (
select distinct session_id, url_path, referrer_path, created_at select distinct session_id, url_path, referrer_path, event_name, created_at
from umami.website_event from umami.website_event
where url_path in (${urlFilterQuery}) where (${stepFilterQuery})
and website_id = {websiteId:UUID} and website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64} and created_at between {startDate:DateTime64} and {endDate:DateTime64}
), ),
level1 AS ( ${levelOneQuery}
select *
from level0
where url_path = {url0:String}
)
${levelQuery} ${levelQuery}
select * select *
from ( from (
@ -209,7 +231,7 @@ async function clickhouseQuery(
websiteId, websiteId,
startDate, startDate,
endDate, endDate,
...urlParams, ...params,
}, },
).then(formatResults(urls)); ).then(formatResults(steps));
} }