feat(trends): ✨ Add real-time trend update support with new calculation methods
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
0781dd9628
commit
332854bb29
1 changed files with 59 additions and 3 deletions
|
|
@ -22,6 +22,8 @@ export interface TrendsResult {
|
|||
};
|
||||
}
|
||||
|
||||
const VALID_GRANULARITIES = new Set<string>(Object.values(TimeGranularity));
|
||||
|
||||
@Injectable()
|
||||
export class TrendsService {
|
||||
constructor(
|
||||
|
|
@ -32,12 +34,66 @@ export class TrendsService {
|
|||
async getTrends(query: TrendsQueryDto): Promise<TrendsResult> {
|
||||
const { metric, startDate, endDate, granularity = 'day' } = query;
|
||||
|
||||
// Whitelist granularity before interpolating into SQL
|
||||
const safeBucket = VALID_GRANULARITIES.has(granularity) ? granularity : 'day';
|
||||
const bucketExpr = `date_trunc('${safeBucket}', timestamp AT TIME ZONE 'UTC')`;
|
||||
|
||||
let rawSql: string | null = null;
|
||||
|
||||
if (metric === MetricType.PAGE_VIEWS) {
|
||||
rawSql = `
|
||||
SELECT ${bucketExpr} AS timestamp, COUNT(*)::bigint AS value
|
||||
FROM raw_events
|
||||
WHERE "eventType" IN ('pageview', 'pageView')
|
||||
AND timestamp BETWEEN $1 AND $2
|
||||
GROUP BY ${bucketExpr}
|
||||
ORDER BY timestamp ASC
|
||||
`;
|
||||
} else if (metric === MetricType.SESSIONS) {
|
||||
rawSql = `
|
||||
SELECT ${bucketExpr} AS timestamp, COUNT(DISTINCT "sessionId")::bigint AS value
|
||||
FROM raw_events
|
||||
WHERE timestamp BETWEEN $1 AND $2
|
||||
GROUP BY ${bucketExpr}
|
||||
ORDER BY timestamp ASC
|
||||
`;
|
||||
} else if (metric === MetricType.UNIQUE_VISITORS) {
|
||||
rawSql = `
|
||||
SELECT ${bucketExpr} AS timestamp,
|
||||
COUNT(DISTINCT COALESCE("userId", "sessionId"))::bigint AS value
|
||||
FROM raw_events
|
||||
WHERE timestamp BETWEEN $1 AND $2
|
||||
GROUP BY ${bucketExpr}
|
||||
ORDER BY timestamp ASC
|
||||
`;
|
||||
}
|
||||
|
||||
if (rawSql) {
|
||||
const rows = await this.metricsRepository.query(rawSql, [startDate, endDate]) as Array<{
|
||||
timestamp: Date;
|
||||
value: string;
|
||||
}>;
|
||||
const values = rows.map((r) => Number(r.value));
|
||||
const total = values.reduce((sum, v) => sum + v, 0);
|
||||
return {
|
||||
metric,
|
||||
granularity: safeBucket,
|
||||
data: rows.map((r) => ({ timestamp: r.timestamp, value: Number(r.value), count: Number(r.value) })),
|
||||
summary: {
|
||||
total,
|
||||
average: values.length > 0 ? total / values.length : 0,
|
||||
min: values.length > 0 ? Math.min(...values) : 0,
|
||||
max: values.length > 0 ? Math.max(...values) : 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback: pre-aggregated table for other metrics (bounce_rate, avg_session_duration, etc.)
|
||||
const data = await this.metricsRepository.find({
|
||||
where: {
|
||||
metricType: metric as MetricType,
|
||||
granularity: granularity as TimeGranularity,
|
||||
granularity: safeBucket as TimeGranularity,
|
||||
timestamp: Between(new Date(startDate), new Date(endDate)),
|
||||
dimension: undefined,
|
||||
},
|
||||
order: { timestamp: 'ASC' },
|
||||
});
|
||||
|
|
@ -47,7 +103,7 @@ export class TrendsService {
|
|||
|
||||
return {
|
||||
metric,
|
||||
granularity,
|
||||
granularity: safeBucket,
|
||||
data: data.map((d) => ({
|
||||
timestamp: d.timestamp,
|
||||
value: Number(d.value),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue