well deserved clean up (#1)

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-03-18 21:53:07 +01:00
parent 3a8404f704
commit b7513f24d5
106 changed files with 453 additions and 1275 deletions

View File

@@ -4,132 +4,20 @@ import {
publicProcedure,
} from '@/server/api/trpc';
import { average, max, min, round, sum } from '@/utils/math';
import { flatten, map, pipe, prop, repeat, reverse, sort, uniq } from 'ramda';
import { flatten, map, pipe, prop, sort, uniq } from 'ramda';
import { z } from 'zod';
import {
chQuery,
createSqlBuilder,
formatClickhouseDate,
getEventFiltersWhereClause,
} from '@openpanel/db';
import { chQuery, createSqlBuilder } from '@openpanel/db';
import { zChartInput } from '@openpanel/validation';
import type { IChartEvent, IChartInput } from '@openpanel/validation';
import {
getChartData,
getChartPrevStartEndDate,
getChartStartEndDate,
getDatesFromRange,
withFormula,
getFunnelData,
getSeriesFromEvents,
} from './chart.helpers';
async function getFunnelData({ projectId, ...payload }: IChartInput) {
const { startDate, endDate } = getChartStartEndDate(payload);
if (payload.events.length === 0) {
return {
totalSessions: 0,
steps: [],
};
}
const funnels = payload.events.map((event) => {
const { sb, getWhere } = createSqlBuilder();
sb.where = getEventFiltersWhereClause(event.filters);
sb.where.name = `name = '${event.name}'`;
return getWhere().replace('WHERE ', '');
});
const innerSql = `SELECT
session_id,
windowFunnel(6048000000000000,'strict_increase')(toUnixTimestamp(created_at), ${funnels.join(', ')}) AS level
FROM events
WHERE (project_id = '${projectId}' AND created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')
GROUP BY session_id`;
const sql = `SELECT level, count() AS count FROM (${innerSql}) GROUP BY level ORDER BY level DESC`;
const [funnelRes, sessionRes] = await Promise.all([
chQuery<{ level: number; count: number }>(sql),
chQuery<{ count: number }>(
`SELECT count(name) as count FROM events WHERE project_id = '${projectId}' AND name = 'session_start' AND (created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')`
),
]);
if (funnelRes[0]?.level !== payload.events.length) {
funnelRes.unshift({
level: payload.events.length,
count: 0,
});
}
const totalSessions = sessionRes[0]?.count ?? 0;
const filledFunnelRes = funnelRes.reduce(
(acc, item, index) => {
const diff =
index !== 0 ? (acc[acc.length - 1]?.level ?? 0) - item.level : 1;
if (diff > 1) {
acc.push(
...reverse(
repeat({}, diff - 1).map((_, index) => ({
count: acc[acc.length - 1]?.count ?? 0,
level: item.level + index + 1,
}))
)
);
}
return [
...acc,
{
count: item.count + (acc[acc.length - 1]?.count ?? 0),
level: item.level,
},
];
},
[] as typeof funnelRes
);
const steps = reverse(filledFunnelRes)
.filter((item) => item.level !== 0)
.reduce(
(acc, item, index, list) => {
const prev = list[index - 1] ?? { count: totalSessions };
return [
...acc,
{
event: payload.events[item.level - 1]!,
before: prev.count,
current: item.count,
dropoff: {
count: prev.count - item.count,
percent: 100 - (item.count / prev.count) * 100,
},
percent: (item.count / totalSessions) * 100,
prevPercent: (prev.count / totalSessions) * 100,
},
];
},
[] as {
event: IChartEvent;
before: number;
current: number;
dropoff: {
count: number;
percent: number;
};
percent: number;
prevPercent: number;
}[]
);
return {
totalSessions,
steps,
};
}
type PreviousValue = {
value: number;
diff: number | null;
@@ -243,7 +131,7 @@ export const chartRouter = createTRPCRouter({
.replace(/^properties\./, '')
.replace('.*.', '.%.')}')) as values`;
} else {
sb.select.values = `${property} as values`;
sb.select.values = `distinct ${property} as values`;
}
const events = await chQuery<{ values: string[] }>(getSql());
@@ -266,57 +154,19 @@ export const chartRouter = createTRPCRouter({
// TODO: Make this private
chart: publicProcedure.input(zChartInput).query(async ({ input }) => {
const { startDate, endDate } = getChartStartEndDate(input);
let diff = 0;
const currentPeriod = getChartStartEndDate(input);
const previousPeriod = getChartPrevStartEndDate({
range: input.range,
...currentPeriod,
});
switch (input.range) {
case '30min': {
diff = 1000 * 60 * 30;
break;
}
case '1h': {
diff = 1000 * 60 * 60;
break;
}
case '24h':
case 'today': {
diff = 1000 * 60 * 60 * 24;
break;
}
case '7d': {
diff = 1000 * 60 * 60 * 24 * 7;
break;
}
case '14d': {
diff = 1000 * 60 * 60 * 24 * 14;
break;
}
case '1m': {
diff = 1000 * 60 * 60 * 24 * 30;
break;
}
case '3m': {
diff = 1000 * 60 * 60 * 24 * 90;
break;
}
case '6m': {
diff = 1000 * 60 * 60 * 24 * 180;
break;
}
}
const promises = [getSeriesFromEvents(input)];
const promises = [getSeriesFromEvents({ ...input, ...currentPeriod })];
if (input.previous) {
promises.push(
getSeriesFromEvents({
...input,
...{
startDate: new Date(
new Date(startDate).getTime() - diff
).toISOString(),
endDate: new Date(new Date(endDate).getTime() - diff).toISOString(),
},
...previousPeriod,
})
);
}
@@ -407,11 +257,11 @@ export const chartRouter = createTRPCRouter({
final.metrics.max = max(final.series.map((item) => item.metrics.max));
final.metrics.previous = {
sum: getPreviousMetric(
sum(final.series.map((item) => item.metrics.sum)),
final.metrics.sum,
sum(final.series.map((item) => item.metrics.previous.sum?.value ?? 0))
),
average: getPreviousMetric(
round(average(final.series.map((item) => item.metrics.average)), 2),
final.metrics.average,
round(
average(
final.series.map(
@@ -422,15 +272,16 @@ export const chartRouter = createTRPCRouter({
)
),
min: getPreviousMetric(
min(final.series.map((item) => item.metrics.min)),
final.metrics.min,
min(final.series.map((item) => item.metrics.previous.min?.value ?? 0))
),
max: getPreviousMetric(
max(final.series.map((item) => item.metrics.max)),
final.metrics.max,
max(final.series.map((item) => item.metrics.previous.max?.value ?? 0))
),
};
// Sort by sum
final.series = final.series.sort((a, b) => {
if (input.chartType === 'linear') {
const sumA = a.data.reduce((acc, item) => acc + (item.count ?? 0), 0);
@@ -441,16 +292,11 @@ export const chartRouter = createTRPCRouter({
}
});
// await new Promise((res) => {
// setTimeout(() => {
// res();
// }, 100);
// });
return final;
}),
});
function getPreviousMetric(
export function getPreviousMetric(
current: number,
previous: number | null
): PreviousValue {
@@ -483,28 +329,3 @@ function getPreviousMetric(
value: previous,
};
}
async function getSeriesFromEvents(input: IChartInput) {
const { startDate, endDate } =
input.startDate && input.endDate
? {
startDate: input.startDate,
endDate: input.endDate,
}
: getDatesFromRange(input.range);
const series = (
await Promise.all(
input.events.map(async (event) =>
getChartData({
...input,
startDate,
endDate,
event,
})
)
)
).flat();
return withFormula(input, series);
}