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

refactor: add functions to estimate monthly usage from data directly (#9219)

Adds new monthly estimation functions that operate on raw usage data
instead of chart data. This brings those methods in line with the rest
of the traffic calculation functions that we have in that file and means
we can remove other external dependencies.

 This is somewhat inspired by #9218, but not directly linked.
This commit is contained in:
Thomas Heartman 2025-02-05 11:12:17 +01:00 committed by GitHub
parent 543be6dede
commit 17a4099dbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 190 additions and 62 deletions

View File

@ -28,7 +28,10 @@ import type { Theme } from '@mui/material/styles/createTheme';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import { NetworkTrafficUsagePlanSummary } from './NetworkTrafficUsagePlanSummary'; import { NetworkTrafficUsagePlanSummary } from './NetworkTrafficUsagePlanSummary';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin from 'chartjs-plugin-annotation';
import { useTrafficDataEstimation } from 'hooks/useTrafficData'; import {
useTrafficDataEstimation,
calculateEstimatedMonthlyCost as deprecatedCalculateEstimatedMonthlyCost,
} from 'hooks/useTrafficData';
import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin'; import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin';
import { formatTickValue } from 'component/common/Chart/formatTickValue'; import { formatTickValue } from 'component/common/Chart/formatTickValue';
import { useTrafficLimit } from './hooks/useTrafficLimit'; import { useTrafficLimit } from './hooks/useTrafficLimit';
@ -44,7 +47,6 @@ import {
calculateEstimatedMonthlyCost, calculateEstimatedMonthlyCost,
} from 'utils/traffic-calculations'; } from 'utils/traffic-calculations';
import { currentDate, currentMonth } from './dates'; import { currentDate, currentMonth } from './dates';
import { endpointsInfo } from './endpoint-info';
import { type ChartDataSelection, toDateRange } from './chart-data-selection'; import { type ChartDataSelection, toDateRange } from './chart-data-selection';
import { import {
type ChartDatasetType, type ChartDatasetType,
@ -245,10 +247,7 @@ const NewNetworkTrafficUsage: FC = () => {
); );
const estimatedMonthlyCost = calculateEstimatedMonthlyCost( const estimatedMonthlyCost = calculateEstimatedMonthlyCost(
chartDataSelection.grouping === 'daily' traffic.usage?.apiData,
? chartDataSelection.month
: currentMonth,
data.datasets,
includedTraffic, includedTraffic,
currentDate, currentDate,
BILLING_TRAFFIC_BUNDLE_PRICE, BILLING_TRAFFIC_BUNDLE_PRICE,
@ -301,7 +300,6 @@ const NewNetworkTrafficUsage: FC = () => {
chartDataSelection.grouping === 'daily' chartDataSelection.grouping === 'daily'
? usageTotal ? usageTotal
: averageTrafficPreviousMonths( : averageTrafficPreviousMonths(
Object.keys(endpointsInfo),
traffic.usage, traffic.usage,
) )
} }
@ -428,7 +426,7 @@ const OldNetworkTrafficUsage: FC = () => {
setOverageCost(calculatedOverageCost); setOverageCost(calculatedOverageCost);
setEstimatedMonthlyCost( setEstimatedMonthlyCost(
calculateEstimatedMonthlyCost( deprecatedCalculateEstimatedMonthlyCost(
period, period,
data.datasets, data.datasets,
includedTraffic, includedTraffic,

View File

@ -1,8 +1,8 @@
import { differenceInCalendarMonths, format } from 'date-fns'; import { differenceInCalendarMonths } from 'date-fns';
import type { TrafficUsageDataSegmentedCombinedSchema } from 'openapi'; import type { TrafficUsageDataSegmentedCombinedSchema } from 'openapi';
import { currentMonth } from './dates';
export const averageTrafficPreviousMonths = ( export const averageTrafficPreviousMonths = (
endpointData: string[],
traffic: TrafficUsageDataSegmentedCombinedSchema, traffic: TrafficUsageDataSegmentedCombinedSchema,
) => { ) => {
if (!traffic || traffic.grouping === 'daily') { if (!traffic || traffic.grouping === 'daily') {
@ -16,10 +16,7 @@ export const averageTrafficPreviousMonths = (
), ),
); );
const currentMonth = format(new Date(), 'yyyy-MM');
const totalTraffic = traffic.apiData const totalTraffic = traffic.apiData
.filter((endpoint) => endpointData.includes(endpoint.apiPath))
.map((endpoint) => .map((endpoint) =>
endpoint.dataPoints endpoint.dataPoints
.filter(({ period }) => period !== currentMonth) .filter(({ period }) => period !== currentMonth)

View File

@ -1,6 +1,15 @@
import type { ChartDatasetType } from 'component/admin/network/NetworkTrafficUsage/chart-functions'; import type { ChartDatasetType } from 'component/admin/network/NetworkTrafficUsage/chart-functions';
import type { IInstanceTrafficMetricsResponse } from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics'; import type { IInstanceTrafficMetricsResponse } from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
import { useState } from 'react'; import { useState } from 'react';
import {
DEFAULT_TRAFFIC_DATA_UNIT_COST,
DEFAULT_TRAFFIC_DATA_UNIT_SIZE,
calculateOverageCost,
} from 'utils/traffic-calculations';
import {
currentMonth,
daysInCurrentMonth,
} from 'component/admin/network/NetworkTrafficUsage/dates';
export type SelectablePeriod = { export type SelectablePeriod = {
key: string; key: string;
@ -148,6 +157,54 @@ const toTrafficUsageSum = (trafficData: ChartDatasetType[]): number => {
return data; return data;
}; };
export const calculateProjectedUsage = (
today: number,
trafficData: ChartDatasetType[],
daysInPeriod: number,
) => {
if (today < 5) {
return 0;
}
const spliceToYesterday = today - 1;
const trafficDataUpToYesterday = trafficData.map((item) => {
return {
...item,
data: item.data.slice(0, spliceToYesterday),
};
});
const dataUsage = toTrafficUsageSum(trafficDataUpToYesterday);
return (dataUsage / spliceToYesterday) * daysInPeriod;
};
export const calculateEstimatedMonthlyCost = (
period: string,
trafficData: ChartDatasetType[],
includedTraffic: number,
currentDate: Date,
trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST,
trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE,
) => {
if (period !== currentMonth) {
return 0;
}
const today = currentDate.getDate();
const projectedUsage = calculateProjectedUsage(
today,
trafficData,
daysInCurrentMonth,
);
return calculateOverageCost(
projectedUsage,
includedTraffic,
trafficUnitCost,
trafficUnitSize,
);
};
const getDayLabels = (dayCount: number): number[] => { const getDayLabels = (dayCount: number): number[] => {
return [...Array(dayCount).keys()].map((i) => i + 1); return [...Array(dayCount).keys()].map((i) => i + 1);
}; };

View File

@ -1,4 +1,4 @@
import { getDaysInMonth } from 'date-fns'; import { format, getDaysInMonth } from 'date-fns';
import { import {
calculateEstimatedMonthlyCost, calculateEstimatedMonthlyCost,
calculateOverageCost, calculateOverageCost,
@ -7,7 +7,14 @@ import {
cleanTrafficData, cleanTrafficData,
} from './traffic-calculations'; } from './traffic-calculations';
import { toSelectablePeriod } from '../component/admin/network/NetworkTrafficUsage/selectable-periods'; import { toSelectablePeriod } from '../component/admin/network/NetworkTrafficUsage/selectable-periods';
import type { TrafficUsageDataSegmentedCombinedSchema } from 'openapi'; import type {
TrafficUsageDataSegmentedCombinedSchema,
TrafficUsageDataSegmentedCombinedSchemaApiDataItem,
} from 'openapi';
import {
calculateEstimatedMonthlyCost as deprecatedCalculateEstimatedMonthlyCost,
calculateProjectedUsage as deprecatedCalculateProjectedUsage,
} from 'hooks/useTrafficData';
const testData4Days = [ const testData4Days = [
{ {
@ -30,6 +37,32 @@ const testData4Days = [
}, },
]; ];
const dataPoint = (month: Date) => (day: number, count: number) => {
const monthPrefix = format(month, 'yyyy-MM');
return {
period: `${monthPrefix}-${day.toString().padStart(2, '0')}`,
trafficTypes: [{ count, group: 'successful-requests' }],
};
};
const trafficData4Days = (
month: Date,
): TrafficUsageDataSegmentedCombinedSchemaApiDataItem[] => {
const point = dataPoint(month);
const dataPoints = [
point(1, 23_000_000),
point(2, 22_000_000),
point(3, 24_000_000),
point(4, 21_000_000),
];
return [
{ apiPath: '/api/frontend', dataPoints },
{ apiPath: '/api/admin', dataPoints },
{ apiPath: '/api/client', dataPoints },
];
};
describe('traffic overage calculation', () => { describe('traffic overage calculation', () => {
it('should return 0 if there is no overage this month', () => { it('should return 0 if there is no overage this month', () => {
const dataUsage = 52_900_000; const dataUsage = 52_900_000;
@ -63,13 +96,22 @@ describe('traffic overage calculation', () => {
const now = new Date(); const now = new Date();
const period = toSelectablePeriod(now); const period = toSelectablePeriod(now);
const testNow = new Date(now.getFullYear(), now.getMonth(), 4); const testNow = new Date(now.getFullYear(), now.getMonth(), 4);
const result = calculateEstimatedMonthlyCost( const includedTraffic = 53_000_000;
const result = deprecatedCalculateEstimatedMonthlyCost(
period.key, period.key,
testData4Days, testData4Days,
53_000_000, includedTraffic,
testNow, testNow,
); );
expect(result).toBe(0); expect(result).toBe(0);
const rawData = trafficData4Days(now);
const result2 = calculateEstimatedMonthlyCost(
rawData,
includedTraffic,
testNow,
);
expect(result2).toBe(result);
}); });
it('needs 5 days or more to estimate for the month', () => { it('needs 5 days or more to estimate for the month', () => {
@ -80,13 +122,25 @@ describe('traffic overage calculation', () => {
const now = new Date(); const now = new Date();
const period = toSelectablePeriod(now); const period = toSelectablePeriod(now);
const testNow = new Date(now.getFullYear(), now.getMonth(), 5); const testNow = new Date(now.getFullYear(), now.getMonth(), 5);
const result = calculateEstimatedMonthlyCost( const includedTraffic = 53_000_000;
const result = deprecatedCalculateEstimatedMonthlyCost(
period.key, period.key,
testData, testData,
53_000_000, includedTraffic,
testNow, testNow,
); );
expect(result).toBeGreaterThan(1430); expect(result).toBeGreaterThan(1430);
const rawData = trafficData4Days(now);
rawData[0].dataPoints.push(dataPoint(now)(5, 23_000_000));
rawData[1].dataPoints.push(dataPoint(now)(5, 23_000_000));
rawData[2].dataPoints.push(dataPoint(now)(5, 23_000_000));
const result2 = calculateEstimatedMonthlyCost(
rawData,
includedTraffic,
testNow,
);
expect(result2).toBe(result);
}); });
it('estimates projected data usage', () => { it('estimates projected data usage', () => {
@ -97,13 +151,34 @@ describe('traffic overage calculation', () => {
// Testing April 5th of 2024 (30 days) // Testing April 5th of 2024 (30 days)
const now = new Date(2024, 3, 5); const now = new Date(2024, 3, 5);
const period = toSelectablePeriod(now); const period = toSelectablePeriod(now);
const result = calculateProjectedUsage( const result = deprecatedCalculateProjectedUsage(
now.getDate(), now.getDate(),
testData, testData,
period.dayCount, period.dayCount,
); );
// 22_500_000 * 3 * 30 = 2_025_000_000 // 22_500_000 * 3 * 30 = 2_025_000_000
expect(result).toBe(2_025_000_000); expect(result).toBe(2_025_000_000);
const rawData = trafficData4Days(now);
rawData[0].dataPoints.push(dataPoint(now)(5, 22_500_000));
rawData[1].dataPoints.push(dataPoint(now)(5, 22_500_000));
rawData[2].dataPoints.push(dataPoint(now)(5, 22_500_000));
const result2 = calculateProjectedUsage({
dayOfMonth: now.getDate(),
daysInMonth: period.dayCount,
trafficData: rawData,
});
expect(result2).toBe(result);
});
it("doesn't die if traffic is undefined", () => {
expect(
calculateEstimatedMonthlyCost(
undefined,
500_000,
new Date('2024-05-15'),
),
).toBe(0);
}); });
it('supports custom price and unit size', () => { it('supports custom price and unit size', () => {
@ -129,7 +204,7 @@ describe('traffic overage calculation', () => {
const includedTraffic = 53_000_000; const includedTraffic = 53_000_000;
const trafficUnitSize = 500_000; const trafficUnitSize = 500_000;
const trafficUnitCost = 10; const trafficUnitCost = 10;
const result = calculateEstimatedMonthlyCost( const result = deprecatedCalculateEstimatedMonthlyCost(
period.key, period.key,
testData, testData,
includedTraffic, includedTraffic,
@ -143,6 +218,20 @@ describe('traffic overage calculation', () => {
const overageUnits = Math.floor(overage / trafficUnitSize); const overageUnits = Math.floor(overage / trafficUnitSize);
const total = overageUnits * trafficUnitCost; const total = overageUnits * trafficUnitCost;
expect(result).toBe(total); expect(result).toBe(total);
const rawData = trafficData4Days(now);
rawData[0].dataPoints.push(dataPoint(now)(5, 22_500_000));
rawData[1].dataPoints.push(dataPoint(now)(5, 22_500_000));
rawData[2].dataPoints.push(dataPoint(now)(5, 22_500_000));
const result2 = calculateEstimatedMonthlyCost(
rawData,
includedTraffic,
testNow,
trafficUnitCost,
trafficUnitSize,
);
expect(result2).toBe(result);
}); });
}); });

View File

@ -2,15 +2,10 @@ import type {
TrafficUsageDataSegmentedCombinedSchema, TrafficUsageDataSegmentedCombinedSchema,
TrafficUsageDataSegmentedCombinedSchemaApiDataItem, TrafficUsageDataSegmentedCombinedSchemaApiDataItem,
} from 'openapi'; } from 'openapi';
import { import { getDaysInMonth } from 'date-fns';
currentMonth,
daysInCurrentMonth,
} from '../component/admin/network/NetworkTrafficUsage/dates';
import type { ChartDatasetType } from '../component/admin/network/NetworkTrafficUsage/chart-functions';
import { format } from 'date-fns'; import { format } from 'date-fns';
export const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5;
const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5; export const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000;
const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000;
export const TRAFFIC_MEASUREMENT_START_DATE = new Date('2024-05-01'); export const TRAFFIC_MEASUREMENT_START_DATE = new Date('2024-05-01');
@ -112,61 +107,53 @@ export const calculateOverageCost = (
: 0; : 0;
}; };
export const calculateProjectedUsage = ( export const calculateProjectedUsage = ({
today: number, dayOfMonth,
trafficData: ChartDatasetType[], daysInMonth,
daysInPeriod: number, trafficData,
) => { }: {
if (today < 5) { dayOfMonth: number;
daysInMonth: number;
trafficData: TrafficUsageDataSegmentedCombinedSchemaApiDataItem[];
}) => {
if (dayOfMonth < 5) {
return 0; return 0;
} }
const spliceToYesterday = today - 1;
const trafficDataUpToYesterday = trafficData.map((item) => { const trafficDataUpToYesterday = trafficData.map((item) => {
return { return {
...item, ...item,
data: item.data.slice(0, spliceToYesterday), dataPoints: item.dataPoints.filter(
(point) => Number(point.period.slice(-2)) < dayOfMonth,
),
}; };
}); });
const toTrafficUsageSum = (trafficData: ChartDatasetType[]): number => { const dataUsage = dailyTrafficDataToCurrentUsage(trafficDataUpToYesterday);
const data = trafficData.reduce(
(acc: number, current: ChartDatasetType) => {
return (
acc +
current.data.reduce(
(acc_inner, current_inner) => acc_inner + current_inner,
0,
)
);
},
0,
);
return data;
};
const dataUsage = toTrafficUsageSum(trafficDataUpToYesterday); return (dataUsage / (dayOfMonth - 1)) * daysInMonth;
return (dataUsage / spliceToYesterday) * daysInPeriod;
}; };
export const calculateEstimatedMonthlyCost = ( export const calculateEstimatedMonthlyCost = (
period: string, trafficData:
trafficData: ChartDatasetType[], | TrafficUsageDataSegmentedCombinedSchemaApiDataItem[]
| undefined,
includedTraffic: number, includedTraffic: number,
currentDate: Date, currentDate: Date,
trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST, trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST,
trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE, trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE,
) => { ) => {
if (period !== currentMonth) { if (!trafficData) {
return 0; return 0;
} }
const dayOfMonth = currentDate.getDate();
const daysInMonth = getDaysInMonth(currentDate);
const today = currentDate.getDate(); const projectedUsage = calculateProjectedUsage({
const projectedUsage = calculateProjectedUsage( dayOfMonth,
today, daysInMonth,
trafficData, trafficData,
daysInCurrentMonth, });
);
return calculateOverageCost( return calculateOverageCost(
projectedUsage, projectedUsage,