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 { NetworkTrafficUsagePlanSummary } from './NetworkTrafficUsagePlanSummary';
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 { formatTickValue } from 'component/common/Chart/formatTickValue';
import { useTrafficLimit } from './hooks/useTrafficLimit';
@ -44,7 +47,6 @@ import {
calculateEstimatedMonthlyCost,
} from 'utils/traffic-calculations';
import { currentDate, currentMonth } from './dates';
import { endpointsInfo } from './endpoint-info';
import { type ChartDataSelection, toDateRange } from './chart-data-selection';
import {
type ChartDatasetType,
@ -245,10 +247,7 @@ const NewNetworkTrafficUsage: FC = () => {
);
const estimatedMonthlyCost = calculateEstimatedMonthlyCost(
chartDataSelection.grouping === 'daily'
? chartDataSelection.month
: currentMonth,
data.datasets,
traffic.usage?.apiData,
includedTraffic,
currentDate,
BILLING_TRAFFIC_BUNDLE_PRICE,
@ -301,7 +300,6 @@ const NewNetworkTrafficUsage: FC = () => {
chartDataSelection.grouping === 'daily'
? usageTotal
: averageTrafficPreviousMonths(
Object.keys(endpointsInfo),
traffic.usage,
)
}
@ -428,7 +426,7 @@ const OldNetworkTrafficUsage: FC = () => {
setOverageCost(calculatedOverageCost);
setEstimatedMonthlyCost(
calculateEstimatedMonthlyCost(
deprecatedCalculateEstimatedMonthlyCost(
period,
data.datasets,
includedTraffic,

View File

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

View File

@ -1,6 +1,15 @@
import type { ChartDatasetType } from 'component/admin/network/NetworkTrafficUsage/chart-functions';
import type { IInstanceTrafficMetricsResponse } from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
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 = {
key: string;
@ -148,6 +157,54 @@ const toTrafficUsageSum = (trafficData: ChartDatasetType[]): number => {
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[] => {
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 {
calculateEstimatedMonthlyCost,
calculateOverageCost,
@ -7,7 +7,14 @@ import {
cleanTrafficData,
} from './traffic-calculations';
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 = [
{
@ -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', () => {
it('should return 0 if there is no overage this month', () => {
const dataUsage = 52_900_000;
@ -63,13 +96,22 @@ describe('traffic overage calculation', () => {
const now = new Date();
const period = toSelectablePeriod(now);
const testNow = new Date(now.getFullYear(), now.getMonth(), 4);
const result = calculateEstimatedMonthlyCost(
const includedTraffic = 53_000_000;
const result = deprecatedCalculateEstimatedMonthlyCost(
period.key,
testData4Days,
53_000_000,
includedTraffic,
testNow,
);
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', () => {
@ -80,13 +122,25 @@ describe('traffic overage calculation', () => {
const now = new Date();
const period = toSelectablePeriod(now);
const testNow = new Date(now.getFullYear(), now.getMonth(), 5);
const result = calculateEstimatedMonthlyCost(
const includedTraffic = 53_000_000;
const result = deprecatedCalculateEstimatedMonthlyCost(
period.key,
testData,
53_000_000,
includedTraffic,
testNow,
);
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', () => {
@ -97,13 +151,34 @@ describe('traffic overage calculation', () => {
// Testing April 5th of 2024 (30 days)
const now = new Date(2024, 3, 5);
const period = toSelectablePeriod(now);
const result = calculateProjectedUsage(
const result = deprecatedCalculateProjectedUsage(
now.getDate(),
testData,
period.dayCount,
);
// 22_500_000 * 3 * 30 = 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', () => {
@ -129,7 +204,7 @@ describe('traffic overage calculation', () => {
const includedTraffic = 53_000_000;
const trafficUnitSize = 500_000;
const trafficUnitCost = 10;
const result = calculateEstimatedMonthlyCost(
const result = deprecatedCalculateEstimatedMonthlyCost(
period.key,
testData,
includedTraffic,
@ -143,6 +218,20 @@ describe('traffic overage calculation', () => {
const overageUnits = Math.floor(overage / trafficUnitSize);
const total = overageUnits * trafficUnitCost;
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,
TrafficUsageDataSegmentedCombinedSchemaApiDataItem,
} from 'openapi';
import {
currentMonth,
daysInCurrentMonth,
} from '../component/admin/network/NetworkTrafficUsage/dates';
import type { ChartDatasetType } from '../component/admin/network/NetworkTrafficUsage/chart-functions';
import { getDaysInMonth } from 'date-fns';
import { format } from 'date-fns';
const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5;
const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000;
export const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5;
export const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000;
export const TRAFFIC_MEASUREMENT_START_DATE = new Date('2024-05-01');
@ -112,61 +107,53 @@ export const calculateOverageCost = (
: 0;
};
export const calculateProjectedUsage = (
today: number,
trafficData: ChartDatasetType[],
daysInPeriod: number,
) => {
if (today < 5) {
export const calculateProjectedUsage = ({
dayOfMonth,
daysInMonth,
trafficData,
}: {
dayOfMonth: number;
daysInMonth: number;
trafficData: TrafficUsageDataSegmentedCombinedSchemaApiDataItem[];
}) => {
if (dayOfMonth < 5) {
return 0;
}
const spliceToYesterday = today - 1;
const trafficDataUpToYesterday = trafficData.map((item) => {
return {
...item,
data: item.data.slice(0, spliceToYesterday),
dataPoints: item.dataPoints.filter(
(point) => Number(point.period.slice(-2)) < dayOfMonth,
),
};
});
const toTrafficUsageSum = (trafficData: ChartDatasetType[]): number => {
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 = dailyTrafficDataToCurrentUsage(trafficDataUpToYesterday);
const dataUsage = toTrafficUsageSum(trafficDataUpToYesterday);
return (dataUsage / spliceToYesterday) * daysInPeriod;
return (dataUsage / (dayOfMonth - 1)) * daysInMonth;
};
export const calculateEstimatedMonthlyCost = (
period: string,
trafficData: ChartDatasetType[],
trafficData:
| TrafficUsageDataSegmentedCombinedSchemaApiDataItem[]
| undefined,
includedTraffic: number,
currentDate: Date,
trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST,
trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE,
) => {
if (period !== currentMonth) {
if (!trafficData) {
return 0;
}
const dayOfMonth = currentDate.getDate();
const daysInMonth = getDaysInMonth(currentDate);
const today = currentDate.getDate();
const projectedUsage = calculateProjectedUsage(
today,
const projectedUsage = calculateProjectedUsage({
dayOfMonth,
daysInMonth,
trafficData,
daysInCurrentMonth,
);
});
return calculateOverageCost(
projectedUsage,