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

chore: add tests for new traffic usage functions (#9208)

This PR adds tests to all the TODOs created in
https://github.com/Unleash/unleash/pull/9191.

Additionally it finally manages to refactor the `toChartData` function.
This commit is contained in:
Thomas Heartman 2025-02-05 09:47:36 +01:00 committed by GitHub
parent 419d189ddc
commit bd6a90ffd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 362 additions and 100 deletions

View File

@ -233,7 +233,7 @@ const NewNetworkTrafficUsage: FC = () => {
const traffic = useInstanceTrafficMetrics2(
chartDataSelection.grouping,
toDateRange(chartDataSelection),
toDateRange(chartDataSelection, currentDate),
);
const data = newToChartData(traffic.usage);

View File

@ -0,0 +1,30 @@
import { type ChartDataSelection, toDateRange } from './chart-data-selection';
test('daily conversion', () => {
const input: ChartDataSelection = {
grouping: 'daily',
month: '2021-03',
};
const expectedOutput = {
from: '2021-03-01',
to: '2021-03-31',
};
expect(toDateRange(input)).toStrictEqual(expectedOutput);
});
test('monthly conversion', () => {
const now = new Date('2023-06-15');
const input: ChartDataSelection = {
grouping: 'monthly',
monthsBack: 3,
};
const expectedOutput = {
from: '2023-03-01',
to: '2023-06-30',
};
expect(toDateRange(input, now)).toStrictEqual(expectedOutput);
});

View File

@ -10,9 +10,9 @@ export type ChartDataSelection =
monthsBack: number;
};
// todo: write test
export const toDateRange = (
selection: ChartDataSelection,
now = new Date(),
): { from: string; to: string } => {
const fmt = (date: Date) => format(date, 'yyyy-MM-dd');
if (selection.grouping === 'daily') {
@ -21,7 +21,6 @@ export const toDateRange = (
const to = fmt(endOfMonth(month));
return { from, to };
} else {
const now = new Date();
const from = fmt(startOfMonth(subMonths(now, selection.monthsBack)));
const to = fmt(endOfMonth(now));
return { from, to };

View File

@ -0,0 +1,158 @@
import type { TrafficUsageDataSegmentedCombinedSchema } from 'openapi';
import { toChartData } from './chart-functions';
import { endpointsInfo } from './endpoint-info';
describe('toChartData', () => {
const dataPoint = (period: string, count: number) => ({
period,
trafficTypes: [{ count, group: 'successful-requests' }],
});
const fromEndpointInfo = (endpoint: keyof typeof endpointsInfo) => {
const info = endpointsInfo[endpoint];
return {
backgroundColor: info.color,
hoverBackgroundColor: info.color,
label: info.label,
};
};
test('monthly data conversion', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'monthly',
dateRange: {
from: '2025-01-01',
to: '2025-06-30',
},
apiData: [
{
apiPath: '/api/admin',
dataPoints: [
dataPoint('2025-06', 5),
dataPoint('2025-05', 4),
dataPoint('2025-02', 6),
dataPoint('2025-04', 2),
],
},
{
apiPath: '/api/client',
dataPoints: [
dataPoint('2025-06', 10),
dataPoint('2025-01', 7),
dataPoint('2025-03', 11),
dataPoint('2025-04', 13),
],
},
],
};
const expectedOutput = {
datasets: [
{
data: [0, 6, 0, 2, 4, 5],
...fromEndpointInfo('/api/admin'),
},
{
data: [7, 0, 11, 13, 0, 10],
...fromEndpointInfo('/api/client'),
},
],
labels: [
'2025-01',
'2025-02',
'2025-03',
'2025-04',
'2025-05',
'Current month',
],
};
expect(toChartData(input)).toMatchObject(expectedOutput);
});
test('daily data conversion', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'daily',
dateRange: {
from: '2025-01-01',
to: '2025-01-31',
},
apiData: [
{
apiPath: '/api/admin',
dataPoints: [
dataPoint('2025-01-01', 5),
dataPoint('2025-01-15', 4),
dataPoint('2025-01-14', 6),
dataPoint('2025-01-06', 2),
],
},
{
apiPath: '/api/client',
dataPoints: [
dataPoint('2025-01-02', 2),
dataPoint('2025-01-17', 6),
dataPoint('2025-01-19', 4),
dataPoint('2025-01-06', 8),
],
},
],
};
const expectedOutput = {
datasets: [
{
data: [
5, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 6, 4, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
...fromEndpointInfo('/api/admin'),
},
{
data: [
0, 2, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 4,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
...fromEndpointInfo('/api/client'),
},
],
labels: Array.from({ length: 31 }).map((_, index) =>
(index + 1).toString(),
),
};
expect(toChartData(input)).toMatchObject(expectedOutput);
});
test('sorts endpoints according to endpoint data spec', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'daily',
dateRange: {
from: '2025-01-01',
to: '2025-01-31',
},
apiData: [
{ apiPath: '/api/frontend', dataPoints: [] },
{ apiPath: '/api/client', dataPoints: [] },
{ apiPath: '/api/admin', dataPoints: [] },
],
};
const expectedOutput = {
datasets: [
{ label: 'Admin' },
{ label: 'Frontend' },
{ label: 'Server' },
],
};
expect(toChartData(input)).toMatchObject(expectedOutput);
});
test('returns empty data if traffic is undefined', () => {
expect(toChartData(undefined)).toStrictEqual({
labels: [],
datasets: [],
});
});
});

View File

@ -11,113 +11,74 @@ import { formatDay, formatMonth } from './dates';
import type { ChartDataSelection } from './chart-data-selection';
export type ChartDatasetType = ChartDataset<'bar'>;
// todo: test
export const toChartData = (
traffic?: TrafficUsageDataSegmentedCombinedSchema,
): { datasets: ChartDatasetType[]; labels: (string | number)[] } => {
): { datasets: ChartDatasetType[]; labels: string[] } => {
if (!traffic) {
return { labels: [], datasets: [] };
}
if (traffic.grouping === 'monthly') {
return toMonthlyChartData(traffic);
} else {
return toDailyChartData(traffic);
}
};
type SegmentedSchemaApiData =
TrafficUsageDataSegmentedCombinedSchema['apiData'][0];
// todo: integrate filtering `filterData` frontend/src/component/admin/network/NetworkTrafficUsage/util.ts
const prepareApiData = (
apiData: TrafficUsageDataSegmentedCombinedSchema['apiData'],
) =>
apiData
.filter((item) => item.apiPath in endpointsInfo)
const { newRecord, labels } = getLabelsAndRecords(traffic);
const datasets = traffic.apiData
.sort(
(item1: SegmentedSchemaApiData, item2: SegmentedSchemaApiData) =>
(item1, item2) =>
endpointsInfo[item1.apiPath].order -
endpointsInfo[item2.apiPath].order,
);
const toMonthlyChartData = (
traffic: TrafficUsageDataSegmentedCombinedSchema,
): { datasets: ChartDatasetType[]; labels: string[] } => {
const from = new Date(traffic.dateRange.from);
const to = new Date(traffic.dateRange.to);
const numMonths = Math.abs(differenceInCalendarMonths(to, from)) + 1;
const datasets = prepareApiData(traffic.apiData).map(
(item: SegmentedSchemaApiData) => {
const monthsRec: { [month: string]: number } = {};
for (let i = 0; i < numMonths; i++) {
monthsRec[formatMonth(addMonths(from, i))] = 0;
}
for (const month of Object.values(item.dataPoints)) {
monthsRec[month.period] = month.trafficTypes[0].count;
)
.map((item) => {
const record = newRecord();
for (const dataPoint of Object.values(item.dataPoints)) {
record[dataPoint.period] = dataPoint.trafficTypes[0].count;
}
const epInfo = endpointsInfo[item.apiPath];
return {
label: epInfo.label,
data: Object.values(monthsRec),
data: Object.values(record),
backgroundColor: epInfo.color,
hoverBackgroundColor: epInfo.color,
};
},
);
const labels = Array.from({ length: numMonths }).map((_, index) =>
index === numMonths - 1
? 'Current month'
: formatMonth(addMonths(from, index)),
);
});
return { datasets, labels };
};
const toDailyChartData = (
const getLabelsAndRecords = (
traffic: TrafficUsageDataSegmentedCombinedSchema,
): { datasets: ChartDatasetType[]; labels: number[] } => {
const from = new Date(traffic.dateRange.from);
const to = new Date(traffic.dateRange.to);
const numDays = Math.abs(differenceInCalendarDays(to, from)) + 1;
) => {
if (traffic.grouping === 'monthly') {
const from = new Date(traffic.dateRange.from);
const to = new Date(traffic.dateRange.to);
const numMonths = Math.abs(differenceInCalendarMonths(to, from)) + 1;
const monthsRec: { [month: string]: number } = {};
for (let i = 0; i < numMonths; i++) {
monthsRec[formatMonth(addMonths(from, i))] = 0;
}
const daysRec: { [day: string]: number } = {};
for (let i = 0; i < numDays; i++) {
daysRec[formatDay(addDays(from, i))] = 0;
const labels = Array.from({ length: numMonths }).map((_, index) =>
index === numMonths - 1
? 'Current month'
: formatMonth(addMonths(from, index)),
);
return { newRecord: () => ({ ...monthsRec }), labels };
} else {
const from = new Date(traffic.dateRange.from);
const to = new Date(traffic.dateRange.to);
const numDays = Math.abs(differenceInCalendarDays(to, from)) + 1;
const daysRec: { [day: string]: number } = {};
for (let i = 0; i < numDays; i++) {
daysRec[formatDay(addDays(from, i))] = 0;
}
// simplification: the chart only allows for single, full-month views
// when you use a daily chart, so just use the day of the month as the label
const labels = Array.from({ length: numDays }).map((_, index) =>
(index + 1).toString(),
);
return { newRecord: () => ({ ...daysRec }), labels };
}
const getDaysRec = () => ({
...daysRec,
});
const datasets = prepareApiData(traffic.apiData).map(
(item: SegmentedSchemaApiData) => {
const daysRec = getDaysRec();
for (const day of Object.values(item.dataPoints)) {
daysRec[day.period] = day.trafficTypes[0].count;
}
const epInfo = endpointsInfo[item.apiPath];
return {
label: epInfo.label,
data: Object.values(daysRec),
backgroundColor: epInfo.color,
hoverBackgroundColor: epInfo.color,
};
},
);
// simplification: assuming days run in a single month from the 1st onwards
const labels = Array.from({ length: numDays }).map((_, index) => index + 1);
return { datasets, labels };
};
const [lastLabel, ...otherLabels] = Object.values(endpointsInfo)

View File

@ -0,0 +1,31 @@
import { generateSelectablePeriodsFromDate } from './selectable-periods';
test('marks months before May 2024 as unselectable', () => {
const now = new Date('2025-01-01');
const selectablePeriods = generateSelectablePeriodsFromDate(now);
expect(
selectablePeriods.map(({ key, selectable }) => ({ key, selectable })),
).toEqual([
{ key: '2025-01', selectable: true },
{ key: '2024-12', selectable: true },
{ key: '2024-11', selectable: true },
{ key: '2024-10', selectable: true },
{ key: '2024-09', selectable: true },
{ key: '2024-08', selectable: true },
{ key: '2024-07', selectable: true },
{ key: '2024-06', selectable: true },
{ key: '2024-05', selectable: true },
{ key: '2024-04', selectable: false },
{ key: '2024-03', selectable: false },
{ key: '2024-02', selectable: false },
]);
});
test('generates 12 months, including the current month', () => {
const now = new Date('2025-01-01');
const selectablePeriods = generateSelectablePeriodsFromDate(now);
expect(selectablePeriods.length).toBe(12);
expect(selectablePeriods[0].label).toBe('Current month');
});

View File

@ -1,4 +1,4 @@
import { getDaysInMonth, subMonths } from 'date-fns';
import { getDaysInMonth } from 'date-fns';
import { currentDate, formatMonth } from './dates';
import { TRAFFIC_MEASUREMENT_START_DATE } from 'utils/traffic-calculations';
@ -36,20 +36,23 @@ export const toSelectablePeriod = (
};
};
// todo: test
const generateSelectablePeriodsFromDate = (now: Date) => {
export const generateSelectablePeriodsFromDate = (now: Date) => {
const selectablePeriods = [toSelectablePeriod(now, 'Current month')];
for (
let subtractMonthCount = 1;
subtractMonthCount < 12;
subtractMonthCount++
) {
const date = subMonths(now, subtractMonthCount);
// this complicated calc avoids DST issues
const utcYear = now.getUTCFullYear();
const utcMonth = now.getUTCMonth();
const targetMonth = utcMonth - subtractMonthCount;
const targetDate = new Date(Date.UTC(utcYear, targetMonth, 1, 0, 0, 0));
selectablePeriods.push(
toSelectablePeriod(
date,
targetDate,
undefined,
date >= TRAFFIC_MEASUREMENT_START_DATE,
targetDate >= TRAFFIC_MEASUREMENT_START_DATE,
),
);
}

View File

@ -3,6 +3,7 @@ import {
calculateEstimatedMonthlyCost,
calculateOverageCost,
calculateProjectedUsage,
calculateTotalUsage,
cleanTrafficData,
} from './traffic-calculations';
import { toSelectablePeriod } from '../component/admin/network/NetworkTrafficUsage/selectable-periods';
@ -146,7 +147,7 @@ describe('traffic overage calculation', () => {
});
describe('filtering out unwanted data', () => {
test('it removes the /edge endpoint data', () => {
it('removes the /edge endpoint data', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'daily',
dateRange: { from: '2025-02-01', to: '2025-02-28' },
@ -171,7 +172,7 @@ describe('filtering out unwanted data', () => {
expect(cleanTrafficData(input)).toStrictEqual(expected);
});
test('it removes any data from before the traffic measuring was put in place', () => {
it('removes any data from before the traffic measuring was put in place', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'monthly',
dateRange: {
@ -212,3 +213,79 @@ describe('filtering out unwanted data', () => {
expect(cleanTrafficData(input)).toStrictEqual(expected);
});
});
describe('calculateTotalUsage', () => {
const dataPoint = (period: string, count: number) => ({
period,
trafficTypes: [{ count, group: 'successful-requests' }],
});
it('calculates total from daily data', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'daily',
dateRange: { from: '2025-02-01', to: '2025-02-28' },
apiData: [
{
apiPath: '/api/client',
dataPoints: [
dataPoint('2024-02-01', 1),
dataPoint('2024-02-15', 2),
dataPoint('2024-02-07', 3),
],
},
{
apiPath: '/api/admin',
dataPoints: [
dataPoint('2024-02-01', 4),
dataPoint('2024-02-15', 5),
dataPoint('2024-02-07', 6),
],
},
{
apiPath: '/api/frontend',
dataPoints: [
dataPoint('2024-02-01', 7),
dataPoint('2024-02-15', 8),
dataPoint('2024-02-07', 9),
],
},
],
};
expect(calculateTotalUsage(input)).toBe(45);
});
it('calculates total for the most recent month in monthly data', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'monthly',
dateRange: { from: '2024-10-01', to: '2025-01-31' },
apiData: [
{
apiPath: '/api/client',
dataPoints: [
dataPoint('2025-01', 1),
dataPoint('2024-12', 2),
dataPoint('2024-10', 3),
],
},
{
apiPath: '/api/admin',
dataPoints: [
dataPoint('2025-01', 4),
dataPoint('2024-11', 5),
dataPoint('2024-10', 6),
],
},
{
apiPath: '/api/frontend',
dataPoints: [
dataPoint('2024-11', 7),
dataPoint('2024-12', 8),
dataPoint('2024-10', 9),
],
},
],
};
expect(calculateTotalUsage(input)).toBe(5);
});
});

View File

@ -7,6 +7,7 @@ import {
daysInCurrentMonth,
} from '../component/admin/network/NetworkTrafficUsage/dates';
import type { ChartDatasetType } from '../component/admin/network/NetworkTrafficUsage/chart-functions';
import { format } from 'date-fns';
const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5;
const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000;
@ -39,13 +40,13 @@ export const cleanTrafficData = (
return { apiData: cleanedApiData, ...rest };
};
// todo: extract "currentMonth" into a function argument instead
const monthlyTrafficDataToCurrentUsage = (
apiData: TrafficUsageDataSegmentedCombinedSchemaApiDataItem[],
latestMonth: string,
) => {
return apiData.reduce((acc, current) => {
const currentPoint = current.dataPoints.find(
({ period }) => period === currentMonth,
({ period }) => period === latestMonth,
);
const pointUsage =
currentPoint?.trafficTypes.reduce(
@ -68,9 +69,8 @@ const dailyTrafficDataToCurrentUsage = (
.reduce((acc, count) => acc + count, 0);
};
// todo: test
// Return the total number of requests for the selected month if showing daily
// data, or the current month if showing monthly data
// data, or the total for the most recent month if showing monthly data
export const calculateTotalUsage = (
data?: TrafficUsageDataSegmentedCombinedSchema,
): number => {
@ -78,9 +78,12 @@ export const calculateTotalUsage = (
return 0;
}
const { grouping, apiData } = data;
return grouping === 'monthly'
? monthlyTrafficDataToCurrentUsage(apiData)
: dailyTrafficDataToCurrentUsage(apiData);
if (grouping === 'monthly') {
const latestMonth = format(new Date(data.dateRange.to), 'yyyy-MM');
return monthlyTrafficDataToCurrentUsage(apiData, latestMonth);
} else {
return dailyTrafficDataToCurrentUsage(apiData);
}
};
const calculateTrafficDataCost = (