fix: dashboard improvements and query speed improvements

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-01-09 14:42:11 +01:00
parent 4867260ece
commit cabfb1f3f0
49 changed files with 3398 additions and 950 deletions

View File

@@ -1,7 +1,16 @@
import { getPreviousMetric } from '@openpanel/common';
import type { FinalChart, IChartInput } from '@openpanel/validation';
import { getChartPrevStartEndDate } from '../services/chart.service';
import { getPreviousMetric, groupByLabels } from '@openpanel/common';
import type { ISerieDataItem } from '@openpanel/common';
import { alphabetIds } from '@openpanel/constants';
import type {
FinalChart,
IChartEventItem,
IChartInput,
} from '@openpanel/validation';
import { chQuery } from '../clickhouse/client';
import {
getAggregateChartSql,
getChartPrevStartEndDate,
} from '../services/chart.service';
import {
getOrganizationSubscriptionChartEndDate,
getSettingsForProject,
@@ -69,7 +78,280 @@ export async function executeChart(input: IChartInput): Promise<FinalChart> {
return response;
}
/**
* Aggregate Chart Engine - Optimized for bar/pie charts without time series
* Executes a simplified pipeline: normalize -> fetch aggregate -> format
*/
export async function executeAggregateChart(
input: IChartInput,
): Promise<FinalChart> {
// Stage 1: Normalize input
const normalized = await normalize(input);
// Handle subscription end date limit
const endDate = await getOrganizationSubscriptionChartEndDate(
input.projectId,
normalized.endDate,
);
if (endDate) {
normalized.endDate = endDate;
}
const { timezone } = await getSettingsForProject(normalized.projectId);
// Stage 2: Fetch aggregate data for current period (event series only)
const fetchedSeries: ConcreteSeries[] = [];
for (let i = 0; i < normalized.series.length; i++) {
const definition = normalized.series[i]!;
if (definition.type !== 'event') {
// Skip formulas - they'll be computed in the next stage
continue;
}
const event = definition as IChartEventItem & { type: 'event' };
// Build query input
const queryInput = {
event: {
id: event.id,
name: event.name,
segment: event.segment,
filters: event.filters,
displayName: event.displayName,
property: event.property,
},
projectId: normalized.projectId,
startDate: normalized.startDate,
endDate: normalized.endDate,
breakdowns: normalized.breakdowns,
limit: normalized.limit,
timezone,
};
// Execute aggregate query
let queryResult = await chQuery<ISerieDataItem>(
getAggregateChartSql(queryInput),
{
session_timezone: timezone,
},
);
// Fallback: if no results with breakdowns, try without breakdowns
if (queryResult.length === 0 && normalized.breakdowns.length > 0) {
queryResult = await chQuery<ISerieDataItem>(
getAggregateChartSql({
...queryInput,
breakdowns: [],
}),
{
session_timezone: timezone,
},
);
}
// Group by labels (handles breakdown expansion)
const groupedSeries = groupByLabels(queryResult);
// Create concrete series for each grouped result
groupedSeries.forEach((grouped) => {
// Extract breakdown value from name array
const breakdownValue =
normalized.breakdowns.length > 0 && grouped.name.length > 1
? grouped.name.slice(1).join(' - ')
: undefined;
// Build breakdowns object
const breakdowns: Record<string, string> | undefined =
normalized.breakdowns.length > 0 && grouped.name.length > 1
? {}
: undefined;
if (breakdowns) {
normalized.breakdowns.forEach((breakdown, idx) => {
const breakdownNamePart = grouped.name[idx + 1];
if (breakdownNamePart) {
breakdowns[breakdown.name] = breakdownNamePart;
}
});
}
// Build filters including breakdown value
const filters = [...event.filters];
if (breakdownValue && normalized.breakdowns.length > 0) {
normalized.breakdowns.forEach((breakdown, idx) => {
const breakdownNamePart = grouped.name[idx + 1];
if (breakdownNamePart) {
filters.push({
id: `breakdown-${idx}`,
name: breakdown.name,
operator: 'is',
value: [breakdownNamePart],
});
}
});
}
// For aggregate charts, grouped.data should have a single data point
// (since we use a constant date in the query)
const concrete: ConcreteSeries = {
id: `${event.name}-${grouped.name.join('-')}-${i}`,
definitionId: definition.id ?? alphabetIds[i] ?? `series-${i}`,
definitionIndex: i,
name: grouped.name,
context: {
event: event.name,
filters,
breakdownValue,
breakdowns,
},
data: grouped.data,
definition,
};
fetchedSeries.push(concrete);
});
}
// Stage 3: Compute formula series from fetched event series
const computedSeries = compute(fetchedSeries, normalized.series);
// Stage 4: Fetch previous period if requested
let previousSeries: ConcreteSeries[] | null = null;
if (input.previous) {
const currentPeriod = {
startDate: normalized.startDate,
endDate: normalized.endDate,
};
const previousPeriod = getChartPrevStartEndDate(currentPeriod);
const previousFetchedSeries: ConcreteSeries[] = [];
for (let i = 0; i < normalized.series.length; i++) {
const definition = normalized.series[i]!;
if (definition.type !== 'event') {
continue;
}
const event = definition as IChartEventItem & { type: 'event' };
const queryInput = {
event: {
id: event.id,
name: event.name,
segment: event.segment,
filters: event.filters,
displayName: event.displayName,
property: event.property,
},
projectId: normalized.projectId,
startDate: previousPeriod.startDate,
endDate: previousPeriod.endDate,
breakdowns: normalized.breakdowns,
limit: normalized.limit,
timezone,
};
let queryResult = await chQuery<ISerieDataItem>(
getAggregateChartSql(queryInput),
{
session_timezone: timezone,
},
);
if (queryResult.length === 0 && normalized.breakdowns.length > 0) {
queryResult = await chQuery<ISerieDataItem>(
getAggregateChartSql({
...queryInput,
breakdowns: [],
}),
{
session_timezone: timezone,
},
);
}
const groupedSeries = groupByLabels(queryResult);
groupedSeries.forEach((grouped) => {
const breakdownValue =
normalized.breakdowns.length > 0 && grouped.name.length > 1
? grouped.name.slice(1).join(' - ')
: undefined;
const breakdowns: Record<string, string> | undefined =
normalized.breakdowns.length > 0 && grouped.name.length > 1
? {}
: undefined;
if (breakdowns) {
normalized.breakdowns.forEach((breakdown, idx) => {
const breakdownNamePart = grouped.name[idx + 1];
if (breakdownNamePart) {
breakdowns[breakdown.name] = breakdownNamePart;
}
});
}
const filters = [...event.filters];
if (breakdownValue && normalized.breakdowns.length > 0) {
normalized.breakdowns.forEach((breakdown, idx) => {
const breakdownNamePart = grouped.name[idx + 1];
if (breakdownNamePart) {
filters.push({
id: `breakdown-${idx}`,
name: breakdown.name,
operator: 'is',
value: [breakdownNamePart],
});
}
});
}
const concrete: ConcreteSeries = {
id: `${event.name}-${grouped.name.join('-')}-${i}`,
definitionId: definition.id ?? alphabetIds[i] ?? `series-${i}`,
definitionIndex: i,
name: grouped.name,
context: {
event: event.name,
filters,
breakdownValue,
breakdowns,
},
data: grouped.data,
definition,
};
previousFetchedSeries.push(concrete);
});
}
// Compute formula series for previous period
previousSeries = compute(previousFetchedSeries, normalized.series);
}
// Stage 5: Format final output with previous period data
const includeAlphaIds = normalized.series.length > 1;
const response = format(
computedSeries,
normalized.series,
includeAlphaIds,
previousSeries,
normalized.limit,
);
return response;
}
// Export as ChartEngine for backward compatibility
export const ChartEngine = {
execute: executeChart,
};
// Export aggregate chart engine
export const AggregateChartEngine = {
execute: executeAggregateChart,
};