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:
parent
293b1ea434
commit
4bbff0c554
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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[]>;
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user