support event filters for funnels
This commit is contained in:
@@ -7,7 +7,12 @@ 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, repeat, reverse, sort, uniq } from 'ramda';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { chQuery, createSqlBuilder, formatClickhouseDate } from '@mixan/db';
|
import {
|
||||||
|
chQuery,
|
||||||
|
createSqlBuilder,
|
||||||
|
formatClickhouseDate,
|
||||||
|
getEventFiltersWhereClause,
|
||||||
|
} from '@mixan/db';
|
||||||
import { zChartInput } from '@mixan/validation';
|
import { zChartInput } from '@mixan/validation';
|
||||||
import type { IChartEvent, IChartInput } from '@mixan/validation';
|
import type { IChartEvent, IChartInput } from '@mixan/validation';
|
||||||
|
|
||||||
@@ -20,27 +25,29 @@ import {
|
|||||||
|
|
||||||
async function getFunnelData({ projectId, ...payload }: IChartInput) {
|
async function getFunnelData({ projectId, ...payload }: IChartInput) {
|
||||||
const { startDate, endDate } = getChartStartEndDate(payload);
|
const { startDate, endDate } = getChartStartEndDate(payload);
|
||||||
|
|
||||||
if (payload.events.length === 0) {
|
if (payload.events.length === 0) {
|
||||||
return {
|
return {
|
||||||
totalSessions: 0,
|
totalSessions: 0,
|
||||||
steps: [],
|
steps: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const sql = `SELECT
|
|
||||||
level,
|
const funnels = payload.events.map((event) => {
|
||||||
count() AS count
|
const { sb, getWhere } = createSqlBuilder();
|
||||||
FROM
|
sb.where = getEventFiltersWhereClause(event.filters);
|
||||||
(
|
sb.where.name = `name = '${event.name}'`;
|
||||||
SELECT
|
return getWhere().replace('WHERE ', '');
|
||||||
session_id,
|
});
|
||||||
windowFunnel(6048000000000000,'strict_increase')(toUnixTimestamp(created_at), ${payload.events.map((event) => `name = '${event.name}'`).join(', ')}) AS level
|
|
||||||
FROM events
|
const innerSql = `SELECT
|
||||||
WHERE (project_id = '${projectId}' AND created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')
|
session_id,
|
||||||
GROUP BY session_id
|
windowFunnel(6048000000000000,'strict_increase')(toUnixTimestamp(created_at), ${funnels.join(', ')}) AS level
|
||||||
)
|
FROM events
|
||||||
GROUP BY level
|
WHERE (project_id = '${projectId}' AND created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}')
|
||||||
ORDER BY level DESC;
|
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([
|
const [funnelRes, sessionRes] = await Promise.all([
|
||||||
chQuery<{ level: number; count: number }>(sql),
|
chQuery<{ level: number; count: number }>(sql),
|
||||||
@@ -98,10 +105,6 @@ async function getFunnelData({ projectId, ...payload }: IChartInput) {
|
|||||||
before: prev.count,
|
before: prev.count,
|
||||||
current: item.count,
|
current: item.count,
|
||||||
dropoff: {
|
dropoff: {
|
||||||
bajs: {
|
|
||||||
prev,
|
|
||||||
item,
|
|
||||||
},
|
|
||||||
count: prev.count - item.count,
|
count: prev.count - item.count,
|
||||||
percent: 100 - (item.count / prev.count) * 100,
|
percent: 100 - (item.count / prev.count) * 100,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,14 +21,13 @@ export function getChartSql({
|
|||||||
const { sb, join, getWhere, getFrom, getSelect, getOrderBy, getGroupBy } =
|
const { sb, join, getWhere, getFrom, getSelect, getOrderBy, getGroupBy } =
|
||||||
createSqlBuilder();
|
createSqlBuilder();
|
||||||
|
|
||||||
|
sb.where = getEventFiltersWhereClause(event.filters);
|
||||||
sb.where.projectId = `project_id = '${projectId}'`;
|
sb.where.projectId = `project_id = '${projectId}'`;
|
||||||
if (event.name !== '*') {
|
if (event.name !== '*') {
|
||||||
sb.select.label = `'${event.name}' as label`;
|
sb.select.label = `'${event.name}' as label`;
|
||||||
sb.where.eventName = `name = '${event.name}'`;
|
sb.where.eventName = `name = '${event.name}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEventFiltersWhereClause(sb, event.filters);
|
|
||||||
|
|
||||||
sb.select.count = `count(*) as count`;
|
sb.select.count = `count(*) as count`;
|
||||||
switch (interval) {
|
switch (interval) {
|
||||||
case 'minute': {
|
case 'minute': {
|
||||||
@@ -109,10 +108,8 @@ export function getChartSql({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEventFiltersWhereClause(
|
export function getEventFiltersWhereClause(filters: IChartEventFilter[]) {
|
||||||
sb: SqlBuilderObject,
|
const where: Record<string, string> = {};
|
||||||
filters: IChartEventFilter[]
|
|
||||||
) {
|
|
||||||
filters.forEach((filter, index) => {
|
filters.forEach((filter, index) => {
|
||||||
const id = `f${index}`;
|
const id = `f${index}`;
|
||||||
const { name, value, operator } = filter;
|
const { name, value, operator } = filter;
|
||||||
@@ -126,25 +123,25 @@ export function getEventFiltersWhereClause(
|
|||||||
|
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case 'is': {
|
case 'is': {
|
||||||
sb.where[id] = `arrayExists(x -> ${value
|
where[id] = `arrayExists(x -> ${value
|
||||||
.map((val) => `x = '${String(val).trim()}'`)
|
.map((val) => `x = '${String(val).trim()}'`)
|
||||||
.join(' OR ')}, ${whereFrom})`;
|
.join(' OR ')}, ${whereFrom})`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'isNot': {
|
case 'isNot': {
|
||||||
sb.where[id] = `arrayExists(x -> ${value
|
where[id] = `arrayExists(x -> ${value
|
||||||
.map((val) => `x != '${String(val).trim()}'`)
|
.map((val) => `x != '${String(val).trim()}'`)
|
||||||
.join(' OR ')}, ${whereFrom})`;
|
.join(' OR ')}, ${whereFrom})`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'contains': {
|
case 'contains': {
|
||||||
sb.where[id] = `arrayExists(x -> ${value
|
where[id] = `arrayExists(x -> ${value
|
||||||
.map((val) => `x LIKE '%${String(val).trim()}%'`)
|
.map((val) => `x LIKE '%${String(val).trim()}%'`)
|
||||||
.join(' OR ')}, ${whereFrom})`;
|
.join(' OR ')}, ${whereFrom})`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'doesNotContain': {
|
case 'doesNotContain': {
|
||||||
sb.where[id] = `arrayExists(x -> ${value
|
where[id] = `arrayExists(x -> ${value
|
||||||
.map((val) => `x NOT LIKE '%${String(val).trim()}%'`)
|
.map((val) => `x NOT LIKE '%${String(val).trim()}%'`)
|
||||||
.join(' OR ')}, ${whereFrom})`;
|
.join(' OR ')}, ${whereFrom})`;
|
||||||
break;
|
break;
|
||||||
@@ -153,25 +150,25 @@ export function getEventFiltersWhereClause(
|
|||||||
} else {
|
} else {
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case 'is': {
|
case 'is': {
|
||||||
sb.where[id] = `${name} IN (${value
|
where[id] = `${name} IN (${value
|
||||||
.map((val) => `'${String(val).trim()}'`)
|
.map((val) => `'${String(val).trim()}'`)
|
||||||
.join(', ')})`;
|
.join(', ')})`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'isNot': {
|
case 'isNot': {
|
||||||
sb.where[id] = `${name} NOT IN (${value
|
where[id] = `${name} NOT IN (${value
|
||||||
.map((val) => `'${String(val).trim()}'`)
|
.map((val) => `'${String(val).trim()}'`)
|
||||||
.join(', ')})`;
|
.join(', ')})`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'contains': {
|
case 'contains': {
|
||||||
sb.where[id] = value
|
where[id] = value
|
||||||
.map((val) => `${name} LIKE '%${String(val).trim()}%'`)
|
.map((val) => `${name} LIKE '%${String(val).trim()}%'`)
|
||||||
.join(' OR ');
|
.join(' OR ');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'doesNotContain': {
|
case 'doesNotContain': {
|
||||||
sb.where[id] = value
|
where[id] = value
|
||||||
.map((val) => `${name} NOT LIKE '%${String(val).trim()}%'`)
|
.map((val) => `${name} NOT LIKE '%${String(val).trim()}%'`)
|
||||||
.join(' OR ');
|
.join(' OR ');
|
||||||
break;
|
break;
|
||||||
@@ -180,5 +177,5 @@ export function getEventFiltersWhereClause(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return sb;
|
return where;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,7 +275,10 @@ export async function getEventList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filters) {
|
if (filters) {
|
||||||
getEventFiltersWhereClause(sb, filters);
|
sb.where = {
|
||||||
|
...sb.where,
|
||||||
|
...getEventFiltersWhereClause(filters),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (cursor) {
|
// if (cursor) {
|
||||||
@@ -307,7 +310,10 @@ export async function getEventsCount({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filters) {
|
if (filters) {
|
||||||
getEventFiltersWhereClause(sb, filters);
|
sb.where = {
|
||||||
|
...sb.where,
|
||||||
|
...getEventFiltersWhereClause(filters),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await chQuery<{ count: number }>(
|
const res = await chQuery<{ count: number }>(
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ export async function getProfileList({
|
|||||||
const { sb, getSql } = createSqlBuilder();
|
const { sb, getSql } = createSqlBuilder();
|
||||||
sb.from = getProfileInnerSelect(projectId);
|
sb.from = getProfileInnerSelect(projectId);
|
||||||
if (filters) {
|
if (filters) {
|
||||||
getEventFiltersWhereClause(sb, filters);
|
sb.where = {
|
||||||
|
...sb.where,
|
||||||
|
...getEventFiltersWhereClause(filters),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
sb.limit = take;
|
sb.limit = take;
|
||||||
sb.offset = (cursor ?? 0) * take;
|
sb.offset = (cursor ?? 0) * take;
|
||||||
@@ -95,7 +98,10 @@ export async function getProfileListCount({
|
|||||||
sb.select.count = 'count(id) as count';
|
sb.select.count = 'count(id) as count';
|
||||||
sb.from = getProfileInnerSelect(projectId);
|
sb.from = getProfileInnerSelect(projectId);
|
||||||
if (filters) {
|
if (filters) {
|
||||||
getEventFiltersWhereClause(sb, filters);
|
sb.where = {
|
||||||
|
...sb.where,
|
||||||
|
...getEventFiltersWhereClause(filters),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const [data] = await chQuery<{ count: number }>(getSql());
|
const [data] = await chQuery<{ count: number }>(getSql());
|
||||||
return data?.count ?? 0;
|
return data?.count ?? 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user