well deserved clean up (#1)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user