1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +02:00

feat(1-3260): store support for data traffic from a range (#9127)

Add support for querying the traffic data usage store for the aggregated data for an arbitrary number of months back.

Adds a new `getTrafficDataForMonthRange(monthsBack: number)` method to the store that aggregates data on a monthly basis by status code and traffic group. Returns a new type with month data instead of day data.
This commit is contained in:
Thomas Heartman 2025-01-22 09:48:51 +01:00 committed by GitHub
parent 293b1ea434
commit 4bbff0c554
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 144 additions and 6 deletions

View File

@ -1,9 +1,15 @@
import type {
IStatTrafficUsageKey,
IStatTrafficUsage,
IStatMonthlyTrafficUsage,
} from './traffic-data-usage-store-type';
import type { ITrafficDataUsageStore } from '../../types';
import { isSameMonth, parse } from 'date-fns';
import {
differenceInCalendarMonths,
format,
isSameMonth,
parse,
} from 'date-fns';
export class FakeTrafficDataUsageStore implements ITrafficDataUsageStore {
private trafficData: IStatTrafficUsage[] = [];
@ -50,4 +56,37 @@ export class FakeTrafficDataUsageStore implements ITrafficDataUsageStore {
isSameMonth(data.day, periodDate),
);
}
async getTrafficDataForMonthRange(
monthsBack: number,
): Promise<IStatMonthlyTrafficUsage[]> {
const now = new Date();
const data: { [key: string]: IStatMonthlyTrafficUsage } =
this.trafficData
.filter(
(entry) =>
differenceInCalendarMonths(now, entry.day) <=
monthsBack,
)
.reduce((acc, entry) => {
const month = format(entry.day, 'yyyy-MM');
const key = `${month}-${entry.trafficGroup}-${entry.statusCodeSeries}`;
if (acc[key]) {
acc[key].count += entry.count;
} else {
acc[key] = {
month,
trafficGroup: entry.trafficGroup,
statusCodeSeries: entry.statusCodeSeries,
count: entry.count,
};
}
return acc;
}, {});
return Object.values(data);
}
}

View File

@ -7,6 +7,13 @@ export type IStatTrafficUsage = {
count: number;
};
export type IStatMonthlyTrafficUsage = {
month: string;
trafficGroup: string;
statusCodeSeries: number;
count: number;
};
export interface IStatTrafficUsageKey {
day: Date;
trafficGroup: string;
@ -17,4 +24,7 @@ export interface ITrafficDataUsageStore
extends Store<IStatTrafficUsage, IStatTrafficUsageKey> {
upsert(trafficDataUsage: IStatTrafficUsage): Promise<void>;
getTrafficDataUsageForPeriod(period: string): Promise<IStatTrafficUsage[]>;
getTrafficDataForMonthRange(
monthsBack: number,
): Promise<IStatMonthlyTrafficUsage[]>;
}

View File

@ -1,3 +1,4 @@
import { differenceInCalendarMonths, subMonths } from 'date-fns';
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
import getLogger from '../../../test/fixtures/no-logger';
import type { ITrafficDataUsageStore, IUnleashStores } from '../../types';
@ -20,6 +21,10 @@ afterAll(async () => {
await db.destroy();
});
beforeEach(async () => {
await trafficDataUsageStore.deleteAll();
});
test('upsert stores new entries', async () => {
const data = {
day: new Date(),
@ -62,7 +67,6 @@ test('upsert upserts', async () => {
});
test('getAll returns all', async () => {
await trafficDataUsageStore.deleteAll();
const data1 = {
day: new Date(),
trafficGroup: 'default3',
@ -83,7 +87,6 @@ test('getAll returns all', async () => {
});
test('delete deletes the specified item', async () => {
await trafficDataUsageStore.deleteAll();
const data1 = {
day: new Date(),
trafficGroup: 'default3',
@ -110,7 +113,6 @@ test('delete deletes the specified item', async () => {
});
test('can query for specific items', async () => {
await trafficDataUsageStore.deleteAll();
const data1 = {
day: new Date(),
trafficGroup: 'default3',
@ -154,7 +156,6 @@ test('can query for specific items', async () => {
});
test('can query for data from specific periods', async () => {
await trafficDataUsageStore.deleteAll();
const data1 = {
day: new Date(2024, 2, 12),
trafficGroup: 'default-period-query',
@ -195,3 +196,57 @@ test('can query for data from specific periods', async () => {
expect(traffic_period_usage_older.length).toBe(1);
expect(traffic_period_usage_older[0].count).toBe(12);
});
test('can query for monthly aggregation of data for a specified range', async () => {
const now = new Date();
const expectedValues: { groupA: number; groupB: number }[] = [];
// fill in with data for the last 13 months
for (let i = 0; i <= 12; i++) {
const then = subMonths(now, i);
let monthAggregateA = 0;
let monthAggregateB = 0;
for (let day = 1; day <= 5; day++) {
const dayValue = i + day;
const dayValueB = dayValue * 2;
monthAggregateA += dayValue;
monthAggregateB += dayValueB;
const dataA = {
day: new Date(then.getFullYear(), then.getMonth(), day),
trafficGroup: 'groupA',
statusCodeSeries: 200,
count: dayValue,
};
await trafficDataUsageStore.upsert(dataA);
const dataB = {
day: new Date(then.getFullYear(), then.getMonth(), day),
trafficGroup: 'groupB',
statusCodeSeries: 200,
count: dayValueB,
};
await trafficDataUsageStore.upsert(dataB);
}
expectedValues.push({
groupA: monthAggregateA,
groupB: monthAggregateB,
});
}
for (const monthsBack of [3, 6, 12]) {
const result =
await trafficDataUsageStore.getTrafficDataForMonthRange(monthsBack);
// should have the current month and the preceding n months (one entry per group)
expect(result.length).toBe((monthsBack + 1) * 2);
for (const entry of result) {
const index = differenceInCalendarMonths(
now,
new Date(entry.month),
);
const expectedCount = expectedValues[index];
expect(entry.count).toBe(expectedCount[entry.trafficGroup]);
}
}
});

View File

@ -1,6 +1,7 @@
import type { Db } from '../../db/db';
import type { Logger, LogProvider } from '../../logger';
import type {
IStatMonthlyTrafficUsage,
IStatTrafficUsage,
IStatTrafficUsageKey,
ITrafficDataUsageStore,
@ -55,7 +56,7 @@ export class TrafficDataUsageStore implements ITrafficDataUsageStore {
}
async exists(key: IStatTrafficUsageKey): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE
day = ? AND
traffic_group = ? AND
status_code_series ?) AS present`,
@ -97,4 +98,37 @@ export class TrafficDataUsageStore implements ITrafficDataUsageStore {
);
return rows.map(mapRow);
}
async getTrafficDataForMonthRange(
monthsBack: number,
): Promise<IStatMonthlyTrafficUsage[]> {
const rows = await this.db(TABLE)
.select(
'traffic_group',
'status_code_series',
this.db.raw(`to_char(day, 'YYYY-MM') AS month`),
this.db.raw(`SUM(count) AS count`),
)
.whereRaw(
`day >= date_trunc('month', CURRENT_DATE) - make_interval(months := ?)`,
[monthsBack],
)
.groupBy([
'traffic_group',
this.db.raw(`to_char(day, 'YYYY-MM')`),
'status_code_series',
])
.orderBy([
{ column: 'month', order: 'desc' },
{ column: 'traffic_group', order: 'asc' },
]);
return rows.map(
({ traffic_group, status_code_series, month, count }) => ({
trafficGroup: traffic_group,
statusCodeSeries: status_code_series,
month,
count: Number.parseInt(count),
}),
);
}
}