improve(funnel): make sure group by profile id works as you would think

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-11-23 21:51:59 +01:00
parent 2f4e3596c1
commit 56edb91dd0
12 changed files with 390 additions and 285 deletions

View File

@@ -29,7 +29,7 @@ interface PreviousDiffIndicatorProps {
children?: React.ReactNode;
inverted?: boolean;
className?: string;
size?: 'sm' | 'lg';
size?: 'sm' | 'lg' | 'md';
}
export function PreviousDiffIndicator({
@@ -80,6 +80,7 @@ export function PreviousDiffIndicator({
'flex size-4 items-center justify-center rounded-full',
variant,
size === 'lg' && 'size-8',
size === 'md' && 'size-6',
)}
>
{renderIcon()}

View File

@@ -39,15 +39,10 @@ export function Chart({
const { isEditMode } = useReportChartContext();
const mostDropoffs = findMostDropoffs(steps);
const lastStep = last(steps)!;
const prevLastStep = last(previous.steps);
const prevLastStep = previous?.steps ? last(previous.steps) : null;
return (
<div
className={cn(
'flex flex-col gap-4 @container',
isEditMode ? 'card' : '-m-4',
)}
>
<div className={cn('col gap-4 @container', isEditMode ? 'card' : '-m-4')}>
<div
className={cn(
'border-b border-border bg-def-100',
@@ -55,66 +50,49 @@ export function Chart({
)}
>
<div className="flex items-center gap-8 p-4 px-8">
<div className="flex flex-1 items-center gap-8">
<div className="flex flex-1 items-center gap-8 min-w-0">
<MetricCardNumber
label="Converted"
value={lastStep.count}
enhancer={
<PreviousDiffIndicator
size="lg"
size="md"
{...getPreviousMetric(lastStep.count, prevLastStep?.count)}
/>
}
/>
<MetricCardNumber
label="Percent"
value={`${totalSessions ? round((lastStep.count / totalSessions) * 100, 1) : 0}%`}
value={`${totalSessions ? round((lastStep.count / totalSessions) * 100, 2) : 0}%`}
enhancer={
<PreviousDiffIndicator
size="lg"
size="md"
{...getPreviousMetric(lastStep.count, prevLastStep?.count)}
/>
}
/>
<MetricCardNumber
className="flex-1"
label="Most dropoffs"
value={mostDropoffs.event.displayName}
enhancer={
<PreviousDiffIndicator
size="lg"
size="md"
{...getPreviousMetric(lastStep.count, prevLastStep?.count)}
/>
}
/>
</div>
<div className="hidden shrink-0 gap-2 @xl:flex">
{steps.map((step) => {
return (
<div
className="flex h-16 w-4 items-end overflow-hidden rounded bg-def-200"
key={step.event.id}
>
<div
className={cn(
'bg-foreground w-full',
step.event.id === mostDropoffs.event.id && 'bg-rose-500',
)}
style={{ height: `${step.percent}%` }}
/>
</div>
);
})}
</div>
</div>
</div>
<div className="flex flex-col divide-y divide-def-200">
<div className="col divide-y divide-def-200">
{steps.map((step, index) => {
const percent = (step.count / totalSessions) * 100;
const isMostDropoffs = mostDropoffs.event.id === step.event.id;
return (
<div
key={step.event.id}
className="flex flex-col gap-4 px-4 py-4 @2xl:flex-row @2xl:px-8"
className="col gap-12 px-4 py-4 @2xl:flex-row @2xl:px-8"
>
<div className="relative flex flex-1 flex-col gap-2 pl-8">
<ColorSquare className="absolute left-0 top-0.5">
@@ -123,27 +101,27 @@ export function Chart({
<div className="font-semibold mt-1">
{step.event.displayName}
</div>
<div className="flex items-center gap-8 text-sm">
<div className="grid grid-cols-4 max-w-lg gap-8 text-sm">
<TooltipComplete
disabled={!previous.steps[index]}
disabled={!previous?.steps?.[index]}
content={
<div className="flex items-center gap-2">
<span>
Last period:{' '}
<span className="font-mono">
{previous.steps[index]?.previousCount}
{previous?.steps?.[index]?.previousCount}
</span>
</span>
<PreviousDiffIndicator
{...getPreviousMetric(
step.previousCount,
previous.steps[index]?.previousCount,
previous?.steps?.[index]?.previousCount,
)}
/>
</div>
}
>
<div className="flex flex-col gap-2">
<div className="col gap-2">
<span className="text-xs text-muted-foreground">
Total:
</span>
@@ -155,26 +133,26 @@ export function Chart({
</div>
</TooltipComplete>
<TooltipComplete
disabled={!previous.steps[index]}
disabled={!previous?.steps?.[index]}
content={
<div className="flex items-center gap-2">
<span>
Last period:{' '}
<span className="font-mono">
{previous.steps[index]?.dropoffCount}
{previous?.steps?.[index]?.dropoffCount}
</span>
</span>
<PreviousDiffIndicator
inverted
{...getPreviousMetric(
step.dropoffCount,
previous.steps[index]?.dropoffCount,
previous?.steps?.[index]?.dropoffCount,
)}
/>
</div>
}
>
<div className="flex flex-col gap-2">
<div className="col gap-2">
<span className="text-xs text-muted-foreground">
Dropoff:
</span>
@@ -192,25 +170,25 @@ export function Chart({
</div>
</TooltipComplete>
<TooltipComplete
disabled={!previous.steps[index]}
disabled={!previous?.steps?.[index]}
content={
<div className="flex items-center gap-2">
<span>
Last period:{' '}
<span className="font-mono">
{previous.steps[index]?.count}
{previous?.steps?.[index]?.count}
</span>
</span>
<PreviousDiffIndicator
{...getPreviousMetric(
step.count,
previous.steps[index]?.count,
previous?.steps?.[index]?.count,
)}
/>
</div>
}
>
<div className="flex flex-col gap-2">
<div className="col gap-2">
<span className="text-xs text-muted-foreground">
Current:
</span>
@@ -219,12 +197,42 @@ export function Chart({
</div>
</div>
</TooltipComplete>
<TooltipComplete
disabled={!previous?.steps?.[index]}
content={
<div className="flex items-center gap-2">
<span>
Last period:{' '}
<span className="font-mono">
{previous?.steps?.[index]?.count}
</span>
</span>
<PreviousDiffIndicator
{...getPreviousMetric(
step.count,
previous?.steps?.[index]?.count,
)}
/>
</div>
}
>
<div className="col gap-2">
<span className="text-xs text-muted-foreground">
Percent:
</span>
<div className="flex items-center gap-4">
<span className="text-lg font-mono">
{Number.isNaN(percent) ? 0 : round(percent, 2)}%
</span>
</div>
</div>
</TooltipComplete>
</div>
</div>
<Progress
size="lg"
className={cn(
'w-full @2xl:w-1/2 text-white bg-def-200 mt-0.5 dark:text-black',
'w-full @2xl:w-1/4 text-white bg-def-200 mt-0.5 dark:text-black',
)}
innerClassName={cn(
'bg-primary',

View File

@@ -13,7 +13,16 @@ import { Chart } from './chart';
export function ReportFunnelChart() {
const {
report: { events, range, projectId, funnelWindow, funnelGroup },
report: {
events,
range,
projectId,
funnelWindow,
funnelGroup,
startDate,
endDate,
previous,
},
isLazyLoading,
} = useReportChartContext();
@@ -26,8 +35,10 @@ export function ReportFunnelChart() {
breakdowns: [],
funnelWindow,
funnelGroup,
previous: false,
previous,
metric: 'sum',
startDate,
endDate,
};
const res = api.chart.funnel.useQuery(input, {
keepPreviousData: true,

View File

@@ -122,19 +122,21 @@ export function MetricCardNumber({
label,
value,
enhancer,
className,
}: {
label: React.ReactNode;
value: React.ReactNode;
enhancer?: React.ReactNode;
className?: string;
}) {
return (
<div className="flex min-w-0 flex-col gap-2">
<div className={cn('flex min-w-0 flex-col gap-2', className)}>
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2 text-left">
<span className="truncate text-muted-foreground">{label}</span>
</div>
</div>
<div className="flex items-end justify-between">
<div className="flex items-end justify-between gap-4">
<div className="truncate font-mono text-3xl font-bold">{value}</div>
{enhancer}
</div>

View File

@@ -1,10 +1,32 @@
import { useDispatch, useSelector } from '@/redux';
import { LineChartIcon } from 'lucide-react';
import {
AreaChartIcon,
ChartBarIcon,
ChartColumnIncreasingIcon,
ConeIcon,
GaugeIcon,
Globe2Icon,
LineChartIcon,
type LucideIcon,
PieChartIcon,
UsersIcon,
} from 'lucide-react';
import { chartTypes } from '@openpanel/constants';
import { objectToZodEnums } from '@openpanel/validation';
import { Combobox } from '../ui/combobox';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/utils/cn';
import { Button } from '../ui/button';
import { changeChartType } from './reportSlice';
interface ReportChartTypeProps {
@@ -13,20 +35,57 @@ interface ReportChartTypeProps {
export function ReportChartType({ className }: ReportChartTypeProps) {
const dispatch = useDispatch();
const type = useSelector((state) => state.report.chartType);
const items = objectToZodEnums(chartTypes).map((key) => ({
label: chartTypes[key],
value: key,
}));
const Icons: Record<keyof typeof chartTypes, LucideIcon> = {
area: AreaChartIcon,
bar: ChartBarIcon,
pie: PieChartIcon,
funnel: ((props) => (
<ConeIcon className={cn('rotate-180', props.className)} />
)) as LucideIcon,
histogram: ChartColumnIncreasingIcon,
linear: LineChartIcon,
metric: GaugeIcon,
retention: UsersIcon,
map: Globe2Icon,
};
return (
<Combobox
icon={LineChartIcon}
className={className}
placeholder="Chart type"
onChange={(value) => {
dispatch(changeChartType(value));
}}
value={type}
items={objectToZodEnums(chartTypes).map((key) => ({
label: chartTypes[key],
value: key,
}))}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
icon={Icons[type]}
className={cn('justify-start', className)}
>
{items.find((item) => item.value === type)?.label}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Available charts</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
{items.map((item) => {
const Icon = Icons[item.value];
return (
<DropdownMenuItem
key={item.value}
onClick={() => dispatch(changeChartType(item.value))}
>
{item.label}
<DropdownMenuShortcut>
<Icon className="size-4" />
</DropdownMenuShortcut>
</DropdownMenuItem>
);
})}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,3 +1,4 @@
import { TooltipPortal } from '@radix-ui/react-tooltip';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
interface TooltipCompleteProps {
@@ -15,12 +16,17 @@ export function TooltipComplete({
}: TooltipCompleteProps) {
return (
<Tooltip>
<TooltipTrigger asChild={typeof children !== 'string'}>
<TooltipTrigger
className="appearance-none"
style={{ textAlign: 'inherit' }}
>
{children}
</TooltipTrigger>
<TooltipContent side={side} disabled={disabled}>
{content}
</TooltipContent>
<TooltipPortal>
<TooltipContent side={side} disabled={disabled}>
{content}
</TooltipContent>
</TooltipPortal>
</Tooltip>
);
}

View File

@@ -30,11 +30,7 @@ const Progress = React.forwardRef<
}}
/>
{value && size !== 'sm' && (
<div
className={
'z-5 absolute bottom-0 top-0 flex items-center px-2 text-sm font-medium font-mono'
}
>
<div className="z-5 absolute bottom-0 top-0 flex items-center px-2 text-sm font-medium font-mono">
<div>{round(value, 2)}%</div>
</div>
)}

View File

@@ -16,5 +16,6 @@ export * from './src/services/reference.service';
export * from './src/services/id.service';
export * from './src/services/retention.service';
export * from './src/services/notification.service';
export * from './src/services/funnel.service';
export * from './src/buffers';
export * from './src/types';

View File

@@ -245,30 +245,30 @@ export async function getEvents(
): Promise<IServiceEvent[]> {
const events = await chQuery<IClickhouseEvent>(sql);
const projectId = events[0]?.project_id;
if (options.profile && projectId) {
const ids = events.map((e) => e.profile_id);
const profiles = await getProfiles(ids, projectId);
const [meta, profiles] = await Promise.all([
options.meta && projectId
? db.eventMeta.findMany({
where: {
name: {
in: uniq(events.map((e) => e.name)),
},
},
})
: null,
options.profile && projectId
? getProfiles(uniq(events.map((e) => e.profile_id)), projectId)
: null,
]);
for (const event of events) {
for (const event of events) {
if (profiles) {
event.profile = profiles.find((p) => p.id === event.profile_id);
}
}
if (options.meta && projectId) {
const names = uniq(events.map((e) => e.name));
const metas = await db.eventMeta.findMany({
where: {
name: {
in: names,
},
projectId,
},
select: options.meta === true ? undefined : options.meta,
});
for (const event of events) {
event.meta = metas.find((m) => m.name === event.name);
if (meta) {
event.meta = meta.find((m) => m.name === event.name);
}
}
return events.map(transformEvent);
}
@@ -477,7 +477,7 @@ export async function getEventList({
}
if (profileId) {
sb.where.deviceId = `(device_id IN (SELECT device_id as did FROM ${TABLE_NAMES.events} WHERE device_id != '' AND profile_id = ${escape(profileId)} group by did) OR profile_id = ${escape(profileId)})`;
sb.where.deviceId = `(device_id IN (SELECT device_id as did FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND device_id != '' AND profile_id = ${escape(profileId)} group by did) OR profile_id = ${escape(profileId)})`;
}
if (startDate && endDate) {

View File

@@ -0,0 +1,200 @@
import type { IChartEvent, IChartInput } from '@openpanel/validation';
import { escape } from 'sqlstring';
import {
TABLE_NAMES,
chQuery,
formatClickhouseDate,
} from '../clickhouse-client';
import { createSqlBuilder } from '../sql-builder';
import { getEventFiltersWhereClause } from './chart.service';
interface FunnelStep {
event: IChartEvent & { displayName: string };
count: number;
percent: number;
dropoffCount: number;
dropoffPercent: number;
previousCount: number;
}
interface FunnelResult {
totalSessions: number;
steps: FunnelStep[];
}
interface RawFunnelData {
level: number;
count: number;
}
interface StepMetrics {
currentStep: number;
currentCount: number;
previousCount: number;
totalUsers: number;
}
// Main function
export async function getFunnelData({
projectId,
startDate,
endDate,
...payload
}: IChartInput): Promise<FunnelResult> {
if (!startDate || !endDate) {
throw new Error('startDate and endDate are required');
}
if (payload.events.length === 0) {
return { totalSessions: 0, steps: [] };
}
const funnelWindow = (payload.funnelWindow || 24) * 3600;
const funnelGroup = payload.funnelGroup || 'session_id';
const sql = buildFunnelQuery(
payload.events,
projectId,
startDate,
endDate,
funnelWindow,
funnelGroup,
);
return await chQuery<RawFunnelData>(sql)
.then((funnel) => fillFunnel(funnel, payload.events.length))
.then((funnel) => ({
totalSessions: funnel[0]?.count ?? 0,
steps: calculateStepMetrics(
funnel,
payload.events,
funnel[0]?.count ?? 0,
),
}));
}
// Helper functions
function buildFunnelQuery(
events: IChartEvent[],
projectId: string,
startDate: string,
endDate: string,
funnelWindow: number,
funnelGroup: string,
): string {
const funnelConditions = events.map((event) => {
const { sb, getWhere } = createSqlBuilder();
sb.where = getEventFiltersWhereClause(event.filters);
sb.where.name = `name = ${escape(event.name)}`;
return getWhere().replace('WHERE ', '');
});
const innerSql = `
SELECT
sp.${funnelGroup},
windowFunnel(${funnelWindow}, 'strict_increase')(
toUnixTimestamp(created_at),
${funnelConditions.join(', ')}
) AS level
FROM ${TABLE_NAMES.events}
LEFT JOIN (
SELECT
session_id,
any(profile_id) AS profile_id
FROM ${TABLE_NAMES.events}
WHERE project_id = ${escape(projectId)}
AND created_at >= '${formatClickhouseDate(startDate)}'
AND created_at <= '${formatClickhouseDate(endDate)}'
GROUP BY session_id
HAVING profile_id IS NOT NULL
) AS sp ON session_id = sp.session_id
WHERE
project_id = ${escape(projectId)} AND
created_at >= '${formatClickhouseDate(startDate)}' AND
created_at <= '${formatClickhouseDate(endDate)}' AND
name IN (${events.map((event) => escape(event.name)).join(', ')})
GROUP BY sp.${funnelGroup}
`;
const sql = `
SELECT
level,
count() AS count
FROM (${innerSql})
WHERE level != 0
GROUP BY level
ORDER BY level DESC`;
return sql;
}
function calculateStepMetrics(
funnelData: RawFunnelData[],
events: IChartEvent[],
totalSessions: number,
): FunnelStep[] {
return funnelData
.sort((a, b) => a.level - b.level) // Ensure steps are in order
.map((data, index, array): FunnelStep => {
const metrics: StepMetrics = {
currentStep: data.level,
currentCount: data.count,
previousCount: index === 0 ? totalSessions : array[index - 1]!.count,
totalUsers: totalSessions,
};
const event = events[data.level - 1]!;
return {
event: {
...event,
displayName: event.displayName ?? event.name,
},
count: metrics.currentCount,
percent: calculatePercent(metrics.currentCount, metrics.totalUsers),
dropoffCount: calculateDropoff(metrics),
dropoffPercent: calculateDropoffPercent(metrics),
previousCount: metrics.previousCount,
};
});
}
function calculatePercent(count: number, total: number): number {
return (count / total) * 100;
}
function calculateDropoff({
currentCount,
previousCount,
}: StepMetrics): number {
return previousCount - currentCount;
}
function calculateDropoffPercent({
currentCount,
previousCount,
}: StepMetrics): number {
return 100 - (currentCount / previousCount) * 100;
}
function fillFunnel(funnel: RawFunnelData[], steps: number): RawFunnelData[] {
const filled = Array.from({ length: steps }, (_, index) => {
const level = index + 1;
const matchingResult = funnel.find((res) => res.level === level);
return {
level,
count: matchingResult ? matchingResult.count : 0,
};
});
// Accumulate counts from top to bottom of the funnel
for (let i = filled.length - 1; i >= 0; i--) {
const step = filled[i]!;
const prevStep = filled[i + 1];
if (prevStep) {
step.count += prevStep.count;
}
}
return filled;
}

View File

@@ -13,9 +13,9 @@ import {
subYears,
} from 'date-fns';
import * as mathjs from 'mathjs';
import { last, pluck, repeat, reverse, uniq } from 'ramda';
import { escape } from 'sqlstring';
import { pluck, uniq } from 'ramda';
import type { ISerieDataItem } from '@openpanel/common';
import {
average,
completeSerie,
@@ -26,17 +26,8 @@ import {
slug,
sum,
} from '@openpanel/common';
import type { ISerieDataItem } from '@openpanel/common';
import { alphabetIds } from '@openpanel/constants';
import {
TABLE_NAMES,
chQuery,
createSqlBuilder,
formatClickhouseDate,
getChartSql,
getEventFiltersWhereClause,
getProfiles,
} from '@openpanel/db';
import { chQuery, getChartSql } from '@openpanel/db';
import type {
FinalChart,
IChartEvent,
@@ -241,28 +232,6 @@ export function getDatesFromRange(range: IChartRange) {
};
}
function fillFunnel(funnel: { level: number; count: number }[], steps: number) {
const filled = Array.from({ length: steps }, (_, index) => {
const level = index + 1;
const matchingResult = funnel.find((res) => res.level === level);
return {
level,
count: matchingResult ? matchingResult.count : 0,
};
});
// Accumulate counts from top to bottom of the funnel
for (let i = filled.length - 1; i >= 0; i--) {
const step = filled[i];
const prevStep = filled[i + 1];
// If there's a previous step, add the count to the current step
if (step && prevStep) {
step.count += prevStep.count;
}
}
return filled.reverse();
}
export function getChartStartEndDate({
startDate,
endDate,
@@ -288,147 +257,6 @@ export function getChartPrevStartEndDate({
};
}
export async function getFunnelData({
projectId,
startDate,
endDate,
...payload
}: IChartInput) {
const funnelWindow = (payload.funnelWindow || 24) * 3600;
const funnelGroup = payload.funnelGroup || 'session_id';
if (!startDate || !endDate) {
throw new Error('startDate and endDate are required');
}
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 = ${escape(event.name)}`;
return getWhere().replace('WHERE ', '');
});
const innerSql = `SELECT
${funnelGroup},
windowFunnel(${funnelWindow}, 'strict_increase')(toUnixTimestamp(created_at), ${funnels.join(', ')}) AS level
FROM ${TABLE_NAMES.events}
WHERE
project_id = ${escape(projectId)} AND
created_at >= '${formatClickhouseDate(startDate)}' AND
created_at <= '${formatClickhouseDate(endDate)}' AND
name IN (${payload.events.map((event) => escape(event.name)).join(', ')})
GROUP BY ${funnelGroup}`;
const sql = `SELECT level, count() AS count FROM (${innerSql}) WHERE level != 0 GROUP BY level ORDER BY level DESC`;
const funnel = await chQuery<{ level: number; count: number }>(sql);
const maxLevel = payload.events.length;
const filledFunnelRes = fillFunnel(funnel, maxLevel);
const totalSessions = last(filledFunnelRes)?.count ?? 0;
const steps = reverse(filledFunnelRes).reduce(
(acc, item, index, list) => {
const prev = list[index - 1] ?? { count: totalSessions };
const event = payload.events[item.level - 1]!;
return [
...acc,
{
event: {
...event,
displayName: event.displayName ?? event.name,
},
count: item.count,
percent: (item.count / totalSessions) * 100,
dropoffCount: prev.count - item.count,
dropoffPercent: 100 - (item.count / prev.count) * 100,
previousCount: prev.count,
},
];
},
[] as {
event: IChartEvent & { displayName: string };
count: number;
percent: number;
dropoffCount: number;
dropoffPercent: number;
previousCount: number;
}[],
);
return {
totalSessions,
steps,
};
}
export async function getFunnelStep({
projectId,
startDate,
endDate,
step,
...payload
}: IChartInput & {
step: number;
}) {
throw new Error('not implemented');
// if (!startDate || !endDate) {
// throw new Error('startDate and endDate are required');
// }
// if (payload.events.length === 0) {
// throw new Error('no events selected');
// }
// const funnels = payload.events.map((event) => {
// const { sb, getWhere } = createSqlBuilder();
// sb.where = getEventFiltersWhereClause(event.filters);
// sb.where.name = `name = ${escape(event.name)}`;
// return getWhere().replace('WHERE ', '');
// });
// const innerSql = `SELECT
// session_id,
// windowFunnel(${ONE_DAY_IN_SECONDS})(toUnixTimestamp(created_at), ${funnels.join(', ')}) AS level
// FROM ${TABLE_NAMES.events}
// WHERE
// project_id = ${escape(projectId)} AND
// created_at >= '${formatClickhouseDate(startDate)}' AND
// created_at <= '${formatClickhouseDate(endDate)}' AND
// name IN (${payload.events.map((event) => escape(event.name)).join(', ')})
// GROUP BY session_id`;
// const profileIdsQuery = `WITH sessions AS (${innerSql})
// SELECT
// DISTINCT e.profile_id as id
// FROM sessions s
// JOIN ${TABLE_NAMES.events} e ON s.session_id = e.session_id
// WHERE
// s.level = ${step} AND
// e.project_id = ${escape(projectId)} AND
// e.created_at >= '${formatClickhouseDate(startDate)}' AND
// e.created_at <= '${formatClickhouseDate(endDate)}' AND
// name IN (${payload.events.map((event) => escape(event.name)).join(', ')})
// ORDER BY e.created_at DESC
// LIMIT 500
// `;
// const res = await chQuery<{
// id: string;
// }>(profileIdsQuery);
// return getProfiles(
// res.map((r) => r.id),
// projectId,
// );
}
export async function getChartSerie(payload: IGetChartDataInput) {
async function getSeries() {
const result = await chQuery<ISerieDataItem>(getChartSql(payload));

View File

@@ -7,6 +7,7 @@ import {
chQuery,
createSqlBuilder,
db,
getFunnelData,
getSelectPropertyKey,
toDate,
} from '@openpanel/db';
@@ -31,8 +32,6 @@ import {
getChart,
getChartPrevStartEndDate,
getChartStartEndDate,
getFunnelData,
getFunnelStep,
} from './chart.helpers';
function utc(date: string | Date) {
@@ -87,9 +86,12 @@ export const chartRouter = createTRPCRouter({
.map((item) => item.replace(/\.([0-9]+)/g, '[*]'))
.map((item) => `properties.${item}`);
if (event === '*') {
properties.push('name');
}
properties.push(
'has_profile',
'name',
'path',
'origin',
'referrer',
@@ -184,7 +186,9 @@ export const chartRouter = createTRPCRouter({
const [current, previous] = await Promise.all([
getFunnelData({ ...input, ...currentPeriod }),
getFunnelData({ ...input, ...previousPeriod }),
input.previous
? getFunnelData({ ...input, ...previousPeriod })
: Promise.resolve(null),
]);
return {
@@ -193,17 +197,6 @@ export const chartRouter = createTRPCRouter({
};
}),
funnelStep: protectedProcedure
.input(
zChartInput.extend({
step: z.number(),
}),
)
.query(async ({ input }) => {
const currentPeriod = getChartStartEndDate(input);
return getFunnelStep({ ...input, ...currentPeriod });
}),
chart: publicProcedure.input(zChartInput).query(async ({ input, ctx }) => {
if (ctx.session.userId) {
const access = await getProjectAccessCached({