chore:little fixes and formating and linting and patches
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import { ifNaN } from '@openpanel/common';
|
||||
import type { IChartEvent, IReportInput } from '@openpanel/validation';
|
||||
import { last, reverse, uniq } from 'ramda';
|
||||
import { last, reverse } from 'ramda';
|
||||
import sqlstring from 'sqlstring';
|
||||
import { ch } from '../clickhouse/client';
|
||||
import { TABLE_NAMES } from '../clickhouse/client';
|
||||
import { ch, TABLE_NAMES } from '../clickhouse/client';
|
||||
import { clix } from '../clickhouse/query-builder';
|
||||
import { createSqlBuilder } from '../sql-builder';
|
||||
import {
|
||||
@@ -34,7 +33,10 @@ export class FunnelService {
|
||||
return group === 'profile_id' ? 'profile_id' : 'session_id';
|
||||
}
|
||||
|
||||
getFunnelConditions(events: IChartEvent[] = [], projectId?: string): string[] {
|
||||
getFunnelConditions(
|
||||
events: IChartEvent[] = [],
|
||||
projectId?: string
|
||||
): string[] {
|
||||
return events.map((event) => {
|
||||
const { sb, getWhere } = createSqlBuilder();
|
||||
sb.where = getEventFiltersWhereClause(event.filters, projectId);
|
||||
@@ -92,7 +94,7 @@ export class FunnelService {
|
||||
.where(
|
||||
'events.name',
|
||||
'IN',
|
||||
eventSeries.map((e) => e.name),
|
||||
eventSeries.map((e) => e.name)
|
||||
)
|
||||
.groupBy([primaryKey, ...additionalGroupBy]);
|
||||
}
|
||||
@@ -120,7 +122,7 @@ export class FunnelService {
|
||||
|
||||
private fillFunnel(
|
||||
funnel: { level: number; count: number }[],
|
||||
steps: number,
|
||||
steps: number
|
||||
) {
|
||||
const filled = Array.from({ length: steps }, (_, index) => {
|
||||
const level = index + 1;
|
||||
@@ -146,7 +148,7 @@ export class FunnelService {
|
||||
toSeries(
|
||||
funnel: { level: number; count: number; [key: string]: any }[],
|
||||
breakdowns: { name: string }[] = [],
|
||||
limit: number | undefined = undefined,
|
||||
limit: number | undefined = undefined
|
||||
) {
|
||||
if (!breakdowns.length) {
|
||||
return [
|
||||
@@ -175,7 +177,7 @@ export class FunnelService {
|
||||
acc[key]!.push({
|
||||
id: key,
|
||||
breakdowns: breakdowns.map((b, index) =>
|
||||
normalizeBreakdownValue(f[`b_${index}`]),
|
||||
normalizeBreakdownValue(f[`b_${index}`])
|
||||
),
|
||||
level: f.level,
|
||||
count: f.count,
|
||||
@@ -190,7 +192,7 @@ export class FunnelService {
|
||||
level: number;
|
||||
count: number;
|
||||
}[]
|
||||
>,
|
||||
>
|
||||
);
|
||||
|
||||
return Object.values(series);
|
||||
@@ -200,7 +202,7 @@ export class FunnelService {
|
||||
return events.flatMap((e) =>
|
||||
e.filters
|
||||
?.filter((f) => f.name.startsWith('profile.'))
|
||||
.map((f) => f.name.replace('profile.', '')),
|
||||
.map((f) => f.name.replace('profile.', ''))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -214,7 +216,7 @@ export class FunnelService {
|
||||
limit,
|
||||
timezone = 'UTC',
|
||||
}: IReportInput & { timezone: string; events?: IChartEvent[] }) {
|
||||
if (!startDate || !endDate) {
|
||||
if (!(startDate && endDate)) {
|
||||
throw new Error('startDate and endDate are required');
|
||||
}
|
||||
|
||||
@@ -234,20 +236,20 @@ export class FunnelService {
|
||||
const profileFilters = this.getProfileFilters(eventSeries);
|
||||
const anyFilterOnProfile = profileFilters.length > 0;
|
||||
const anyBreakdownOnProfile = breakdowns.some((b) =>
|
||||
b.name.startsWith('profile.'),
|
||||
b.name.startsWith('profile.')
|
||||
);
|
||||
const anyFilterOnGroup = eventSeries.some((e) =>
|
||||
e.filters?.some((f) => f.name.startsWith('group.')),
|
||||
e.filters?.some((f) => f.name.startsWith('group.'))
|
||||
);
|
||||
const anyBreakdownOnGroup = breakdowns.some((b) =>
|
||||
b.name.startsWith('group.'),
|
||||
b.name.startsWith('group.')
|
||||
);
|
||||
const needsGroupArrayJoin =
|
||||
anyFilterOnGroup || anyBreakdownOnGroup || funnelGroup === 'group';
|
||||
|
||||
// Create the funnel CTE (session-level)
|
||||
const breakdownSelects = breakdowns.map(
|
||||
(b, index) => `${getSelectPropertyKey(b.name, projectId)} as b_${index}`,
|
||||
(b, index) => `${getSelectPropertyKey(b.name, projectId)} as b_${index}`
|
||||
);
|
||||
const breakdownGroupBy = breakdowns.map((b, index) => `b_${index}`);
|
||||
|
||||
@@ -281,7 +283,7 @@ export class FunnelService {
|
||||
funnelCte.leftJoin(
|
||||
`(SELECT ${profileSelectColumns} FROM ${TABLE_NAMES.profiles} FINAL
|
||||
WHERE project_id = ${sqlstring.escape(projectId)}) as profile`,
|
||||
'profile.id = events.profile_id',
|
||||
'profile.id = events.profile_id'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -296,7 +298,7 @@ export class FunnelService {
|
||||
if (needsGroupArrayJoin) {
|
||||
funnelQuery.with(
|
||||
'_g',
|
||||
`SELECT id, name, type, properties FROM ${TABLE_NAMES.groups} FINAL WHERE project_id = ${sqlstring.escape(projectId)}`,
|
||||
`SELECT id, name, type, properties FROM ${TABLE_NAMES.groups} FINAL WHERE project_id = ${sqlstring.escape(projectId)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -304,10 +306,7 @@ export class FunnelService {
|
||||
|
||||
// windowFunnel is computed per the primary key (profile_id or session_id),
|
||||
// so we just filter out level=0 rows — no re-aggregation needed.
|
||||
funnelQuery.with(
|
||||
'funnel',
|
||||
'SELECT * FROM session_funnel WHERE level != 0',
|
||||
);
|
||||
funnelQuery.with('funnel', 'SELECT * FROM session_funnel WHERE level != 0');
|
||||
|
||||
funnelQuery
|
||||
.select<{
|
||||
@@ -331,7 +330,7 @@ export class FunnelService {
|
||||
const maxLevel = eventSeries.length;
|
||||
const filledFunnelRes = this.fillFunnel(
|
||||
data.map((d) => ({ level: d.level, count: d.count })),
|
||||
maxLevel,
|
||||
maxLevel
|
||||
);
|
||||
|
||||
const totalSessions = last(filledFunnelRes)?.count ?? 0;
|
||||
@@ -367,7 +366,7 @@ export class FunnelService {
|
||||
dropoffPercent: number | null;
|
||||
previousCount: number;
|
||||
nextCount: number | null;
|
||||
}[],
|
||||
}[]
|
||||
)
|
||||
.map((step, index, list) => {
|
||||
return {
|
||||
@@ -376,13 +375,15 @@ export class FunnelService {
|
||||
dropoffPercent: ifNaN(step.dropoffPercent, 0),
|
||||
isHighestDropoff: (() => {
|
||||
// Skip if current step has no dropoff
|
||||
if (!step?.dropoffCount) return false;
|
||||
if (!step?.dropoffCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get maximum dropoff count, excluding 0s
|
||||
const maxDropoff = Math.max(
|
||||
...list
|
||||
.map((s) => s.dropoffCount || 0)
|
||||
.filter((count) => count > 0),
|
||||
.filter((count) => count > 0)
|
||||
);
|
||||
|
||||
// Check if this is the first step with the highest dropoff
|
||||
|
||||
Reference in New Issue
Block a user