This commit is contained in:
Carl-Gerhard Lindesvärd
2025-11-24 15:50:28 +01:00
parent 548747d826
commit 1fa61b1ae9
20 changed files with 321 additions and 295 deletions

View File

@@ -1,21 +1,15 @@
import { slug } from '@openpanel/common';
import { alphabetIds } from '@openpanel/constants';
import type {
IChartBreakdown,
IChartEvent,
IChartEventItem,
} from '@openpanel/validation';
import type { IChartEventItem } from '@openpanel/validation';
import { getSettingsForProject } from '../services/organization.service';
import type { ConcreteSeries, Plan } from './types';
import type { NormalizedInput } from './normalize';
import type { ConcreteSeries, Plan } from './types';
/**
* Create an execution plan from normalized input
* This sets up ConcreteSeries placeholders - actual breakdown expansion happens during fetch
*/
export async function plan(
normalized: NormalizedInput,
): Promise<Plan> {
export async function plan(normalized: NormalizedInput): Promise<Plan> {
const { timezone } = await getSettingsForProject(normalized.projectId);
const concreteSeries: ConcreteSeries[] = [];
@@ -24,7 +18,7 @@ export async function plan(
normalized.series.forEach((definition, index) => {
if (definition.type === 'event') {
const event = definition as IChartEventItem & { type: 'event' };
// For events, create a placeholder
// If breakdowns exist, fetch will return multiple series (one per breakdown value)
// If no breakdowns, fetch will return one series
@@ -54,6 +48,3 @@ export async function plan(
timezone,
};
}
export type NormalizedInput = Awaited<ReturnType<typeof import('./normalize').normalize>>;

View File

@@ -7,6 +7,7 @@ import {
getEventFiltersWhereClause,
getSelectPropertyKey,
} from './chart.service';
import { onlyReportEvents } from './reports.service';
export class ConversionService {
constructor(private client: typeof ch) {}
@@ -18,7 +19,6 @@ export class ConversionService {
funnelGroup,
funnelWindow = 24,
series,
events, // Backward compatibility - use series if available
breakdowns = [],
interval,
timezone,
@@ -31,12 +31,9 @@ export class ConversionService {
);
const breakdownGroupBy = breakdowns.map((b, index) => `b_${index}`);
// Use series if available, otherwise fall back to events (backward compat)
const eventSeries = (series ?? events ?? []).filter(
(item): item is IChartEvent => item.type === 'event',
) as IChartEvent[];
const events = onlyReportEvents(series);
if (eventSeries.length !== 2) {
if (events.length !== 2) {
throw new Error('events must be an array of two events');
}
@@ -44,8 +41,8 @@ export class ConversionService {
throw new Error('startDate and endDate are required');
}
const eventA = eventSeries[0]!;
const eventB = eventSeries[1]!;
const eventA = events[0]!;
const eventB = events[1]!;
const whereA = Object.values(
getEventFiltersWhereClause(eventA.filters),
).join(' AND ');

View File

@@ -14,6 +14,7 @@ import {
getEventFiltersWhereClause,
getSelectPropertyKey,
} from './chart.service';
import { onlyReportEvents } from './reports.service';
export class FunnelService {
constructor(private client: typeof ch) {}
@@ -179,7 +180,6 @@ export class FunnelService {
startDate,
endDate,
series,
events, // Backward compatibility - use series if available
funnelWindow = 24,
funnelGroup,
breakdowns = [],
@@ -189,12 +189,7 @@ export class FunnelService {
throw new Error('startDate and endDate are required');
}
// Use series if available, otherwise fall back to events (backward compat)
const rawSeries = (series ?? events ?? []) as IChartEventItem[];
const eventSeries = rawSeries.filter(
(item): item is IChartEventItem & { type: 'event' } =>
item.type === 'event',
) as IChartEvent[];
const eventSeries = onlyReportEvents(series);
if (eventSeries.length === 0) {
throw new Error('events are required');

View File

@@ -5,21 +5,25 @@ import {
} from '@openpanel/constants';
import type {
IChartBreakdown,
IChartEvent,
IChartEventFilter,
IChartEventItem,
IChartFormula,
IChartLineType,
IChartProps,
IChartRange,
ICriteria,
} from '@openpanel/validation';
import { db } from '../prisma-client';
import type { Report as DbReport, ReportLayout } from '../prisma-client';
import { db } from '../prisma-client';
export type IServiceReport = Awaited<ReturnType<typeof getReportById>>;
export const onlyReportEvents = (
series: NonNullable<IServiceReport>['series'],
) => {
return series.filter((item) => item.type === 'event');
};
export function transformFilter(
filter: Partial<IChartEventFilter>,
index: number,
@@ -34,72 +38,39 @@ export function transformFilter(
}
export function transformReportEventItem(
item: Partial<IChartEventItem> | Partial<IChartEvent>,
item: IChartEventItem,
index: number,
): IChartEventItem {
// If item already has type field, it's the new format
if (item && typeof item === 'object' && 'type' in item) {
if (item.type === 'formula') {
// Transform formula
const formula = item as Partial<IChartFormula>;
return {
type: 'formula',
id: formula.id ?? alphabetIds[index]!,
formula: formula.formula || '',
displayName: formula.displayName,
};
}
// Transform event with type field
const event = item as Partial<IChartEvent>;
if (item.type === 'formula') {
// Transform formula
return {
type: 'event',
segment: event.segment ?? 'event',
filters: (event.filters ?? []).map(transformFilter),
id: event.id ?? alphabetIds[index]!,
name: event.name || 'unknown_event',
displayName: event.displayName,
property: event.property,
type: 'formula',
id: item.id ?? alphabetIds[index]!,
formula: item.formula || '',
displayName: item.displayName,
};
}
// Old format without type field - assume it's an event
const event = item as Partial<IChartEvent>;
// Transform event with type field
return {
type: 'event',
segment: event.segment ?? 'event',
filters: (event.filters ?? []).map(transformFilter),
id: event.id ?? alphabetIds[index]!,
name: event.name || 'unknown_event',
displayName: event.displayName,
property: event.property,
segment: item.segment ?? 'event',
filters: (item.filters ?? []).map(transformFilter),
id: item.id ?? alphabetIds[index]!,
name: item.name || 'unknown_event',
displayName: item.displayName,
property: item.property,
};
}
// Keep the old function for backward compatibility, but it now uses the new transformer
export function transformReportEvent(
event: Partial<IChartEvent>,
index: number,
): IChartEvent {
const transformed = transformReportEventItem(event, index);
if (transformed.type === 'event') {
return transformed;
}
// This shouldn't happen for old code, but handle it gracefully
throw new Error('transformReportEvent called on a formula');
}
export function transformReport(
report: DbReport & { layout?: ReportLayout | null },
): IChartProps & { id: string; layout?: ReportLayout | null } {
// Events can be either old format (IChartEvent[]) or new format (IChartEventItem[])
const eventsData = report.events as unknown as Array<
Partial<IChartEventItem> | Partial<IChartEvent>
>;
return {
id: report.id,
projectId: report.projectId,
series: eventsData.map(transformReportEventItem),
series:
(report.events as IChartEventItem[]).map(transformReportEventItem) ?? [],
breakdowns: report.breakdowns as IChartBreakdown[],
chartType: report.chartType,
lineType: (report.lineType as IChartLineType) ?? lineTypes.monotone,