diff --git a/apps/web/src/server/api/routers/chart.ts b/apps/web/src/server/api/routers/chart.ts index c286e6d1..d0d23fa8 100644 --- a/apps/web/src/server/api/routers/chart.ts +++ b/apps/web/src/server/api/routers/chart.ts @@ -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 { z } from 'zod'; -import { chQuery, createSqlBuilder, formatClickhouseDate } from '@mixan/db'; +import { + chQuery, + createSqlBuilder, + formatClickhouseDate, + getEventFiltersWhereClause, +} from '@mixan/db'; import { zChartInput } from '@mixan/validation'; import type { IChartEvent, IChartInput } from '@mixan/validation'; @@ -20,27 +25,29 @@ import { async function getFunnelData({ projectId, ...payload }: IChartInput) { const { startDate, endDate } = getChartStartEndDate(payload); + if (payload.events.length === 0) { return { totalSessions: 0, steps: [], }; } - const sql = `SELECT - level, - count() AS count - FROM - ( - SELECT - session_id, - windowFunnel(6048000000000000,'strict_increase')(toUnixTimestamp(created_at), ${payload.events.map((event) => `name = '${event.name}'`).join(', ')}) AS level - FROM events - WHERE (project_id = '${projectId}' AND created_at >= '${formatClickhouseDate(startDate)}') AND (created_at <= '${formatClickhouseDate(endDate)}') - GROUP BY session_id - ) - GROUP BY level - ORDER BY level DESC; - `; + + 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), @@ -98,10 +105,6 @@ async function getFunnelData({ projectId, ...payload }: IChartInput) { before: prev.count, current: item.count, dropoff: { - bajs: { - prev, - item, - }, count: prev.count - item.count, percent: 100 - (item.count / prev.count) * 100, }, diff --git a/packages/db/src/services/chart.service.ts b/packages/db/src/services/chart.service.ts index 56290e44..b4009f64 100644 --- a/packages/db/src/services/chart.service.ts +++ b/packages/db/src/services/chart.service.ts @@ -21,14 +21,13 @@ export function getChartSql({ const { sb, join, getWhere, getFrom, getSelect, getOrderBy, getGroupBy } = createSqlBuilder(); + sb.where = getEventFiltersWhereClause(event.filters); sb.where.projectId = `project_id = '${projectId}'`; if (event.name !== '*') { sb.select.label = `'${event.name}' as label`; sb.where.eventName = `name = '${event.name}'`; } - getEventFiltersWhereClause(sb, event.filters); - sb.select.count = `count(*) as count`; switch (interval) { case 'minute': { @@ -109,10 +108,8 @@ export function getChartSql({ ); } -export function getEventFiltersWhereClause( - sb: SqlBuilderObject, - filters: IChartEventFilter[] -) { +export function getEventFiltersWhereClause(filters: IChartEventFilter[]) { + const where: Record = {}; filters.forEach((filter, index) => { const id = `f${index}`; const { name, value, operator } = filter; @@ -126,25 +123,25 @@ export function getEventFiltersWhereClause( switch (operator) { case 'is': { - sb.where[id] = `arrayExists(x -> ${value + where[id] = `arrayExists(x -> ${value .map((val) => `x = '${String(val).trim()}'`) .join(' OR ')}, ${whereFrom})`; break; } case 'isNot': { - sb.where[id] = `arrayExists(x -> ${value + where[id] = `arrayExists(x -> ${value .map((val) => `x != '${String(val).trim()}'`) .join(' OR ')}, ${whereFrom})`; break; } case 'contains': { - sb.where[id] = `arrayExists(x -> ${value + where[id] = `arrayExists(x -> ${value .map((val) => `x LIKE '%${String(val).trim()}%'`) .join(' OR ')}, ${whereFrom})`; break; } case 'doesNotContain': { - sb.where[id] = `arrayExists(x -> ${value + where[id] = `arrayExists(x -> ${value .map((val) => `x NOT LIKE '%${String(val).trim()}%'`) .join(' OR ')}, ${whereFrom})`; break; @@ -153,25 +150,25 @@ export function getEventFiltersWhereClause( } else { switch (operator) { case 'is': { - sb.where[id] = `${name} IN (${value + where[id] = `${name} IN (${value .map((val) => `'${String(val).trim()}'`) .join(', ')})`; break; } case 'isNot': { - sb.where[id] = `${name} NOT IN (${value + where[id] = `${name} NOT IN (${value .map((val) => `'${String(val).trim()}'`) .join(', ')})`; break; } case 'contains': { - sb.where[id] = value + where[id] = value .map((val) => `${name} LIKE '%${String(val).trim()}%'`) .join(' OR '); break; } case 'doesNotContain': { - sb.where[id] = value + where[id] = value .map((val) => `${name} NOT LIKE '%${String(val).trim()}%'`) .join(' OR '); break; @@ -180,5 +177,5 @@ export function getEventFiltersWhereClause( } }); - return sb; + return where; } diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts index 999309da..912c7e35 100644 --- a/packages/db/src/services/event.service.ts +++ b/packages/db/src/services/event.service.ts @@ -275,7 +275,10 @@ export async function getEventList({ } if (filters) { - getEventFiltersWhereClause(sb, filters); + sb.where = { + ...sb.where, + ...getEventFiltersWhereClause(filters), + }; } // if (cursor) { @@ -307,7 +310,10 @@ export async function getEventsCount({ } if (filters) { - getEventFiltersWhereClause(sb, filters); + sb.where = { + ...sb.where, + ...getEventFiltersWhereClause(filters), + }; } const res = await chQuery<{ count: number }>( diff --git a/packages/db/src/services/profile.service.ts b/packages/db/src/services/profile.service.ts index 4f680c9e..3633b177 100644 --- a/packages/db/src/services/profile.service.ts +++ b/packages/db/src/services/profile.service.ts @@ -78,7 +78,10 @@ export async function getProfileList({ const { sb, getSql } = createSqlBuilder(); sb.from = getProfileInnerSelect(projectId); if (filters) { - getEventFiltersWhereClause(sb, filters); + sb.where = { + ...sb.where, + ...getEventFiltersWhereClause(filters), + }; } sb.limit = take; sb.offset = (cursor ?? 0) * take; @@ -95,7 +98,10 @@ export async function getProfileListCount({ sb.select.count = 'count(id) as count'; sb.from = getProfileInnerSelect(projectId); if (filters) { - getEventFiltersWhereClause(sb, filters); + sb.where = { + ...sb.where, + ...getEventFiltersWhereClause(filters), + }; } const [data] = await chQuery<{ count: number }>(getSql()); return data?.count ?? 0;