1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-09 11:14:29 +02:00
unleash.unleash/frontend/src/utils/traffic-calculations.ts
Thomas Heartman dadda7b648
fix: Data usage graphs don't work in UTC-n time zones (#9530)
Fixes a number of issues that would surface in UTC-n (where n > 1)
timezones. I've not found a way to check this with tests (and it looks
like [we weren't able to last time
either](https://github.com/Unleash/unleash/pull/9110/files#r1919746328)),
so all the testing's been done manually by adjusting my system time and
zone. (From what I understand, you can't generate a Date with a specific
TZ offset in JS: it's only utc or local time)

Resolved:
- [x] Selecting "Jan" in the dropdown results in the selection being
"December" (off by one in the selector)

- [x] Selecting a month view only gives you one data point (and it's
probably empty). Wrong date parsing on the way out resulted in sending
`{ from: "2025-02-28", to: "2025-02-28"}` instead of `{ from:
"2025-03-01", to: "2025-03-31"}`

- [x] The dates we create when making "daysRec" need to be adjusted.
They showed the wrong month, so the dates were off.

- [x] Make sure the labels are correct when hovering over. Again: we
used the wrong month for generating these.

- [x] The available months are wrong. Incorrect month parsing again.

- [x] The request summary month is wrong. You guessed it: incorrect
month parsing
2025-03-13 15:44:10 +01:00

158 lines
4.6 KiB
TypeScript

import type {
TrafficUsageDataSegmentedCombinedSchema,
TrafficUsageDataSegmentedCombinedSchemaApiDataItem,
} from 'openapi';
import { getDaysInMonth } from 'date-fns';
import { format } from 'date-fns';
import { parseDateString } from 'component/admin/network/NetworkTrafficUsage/dates';
export const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5;
export const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000;
export const TRAFFIC_MEASUREMENT_START_DATE = parseDateString('2024-05-01');
export const METERED_TRAFFIC_ENDPOINTS = [
'/api/admin',
'/api/frontend',
'/api/client',
];
export const cleanTrafficData = (
data: TrafficUsageDataSegmentedCombinedSchema,
): TrafficUsageDataSegmentedCombinedSchema => {
const { apiData, ...rest } = data;
const cleanedApiData = apiData
.filter((item) => METERED_TRAFFIC_ENDPOINTS.includes(item.apiPath))
.map((item) => {
item.dataPoints = item.dataPoints.filter(
({ period }) =>
new Date(period) >= TRAFFIC_MEASUREMENT_START_DATE,
);
return item;
});
return { apiData: cleanedApiData, ...rest };
};
const monthlyTrafficDataToCurrentUsage = (
apiData: TrafficUsageDataSegmentedCombinedSchemaApiDataItem[],
latestMonth: string,
) => {
return apiData.reduce((acc, current) => {
const currentPoint = current.dataPoints.find(
({ period }) => period === latestMonth,
);
const pointUsage =
currentPoint?.trafficTypes.reduce(
(acc, next) => acc + next.count,
0,
) ?? 0;
return acc + pointUsage;
}, 0);
};
const dailyTrafficDataToCurrentUsage = (
apiData: TrafficUsageDataSegmentedCombinedSchemaApiDataItem[],
) => {
return apiData
.flatMap((endpoint) =>
endpoint.dataPoints.flatMap((dataPoint) =>
dataPoint.trafficTypes.map((trafficType) => trafficType.count),
),
)
.reduce((acc, count) => acc + count, 0);
};
// Return the total number of requests for the selected month if showing daily
// data, or the total for the most recent month if showing monthly data
export const calculateTotalUsage = (
data: TrafficUsageDataSegmentedCombinedSchema,
): number => {
const { grouping, apiData } = data;
if (grouping === 'monthly') {
const latestMonth = format(new Date(data.dateRange.to), 'yyyy-MM');
return monthlyTrafficDataToCurrentUsage(apiData, latestMonth);
} else {
return dailyTrafficDataToCurrentUsage(apiData);
}
};
const calculateTrafficDataCost = (
trafficData: number,
trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST,
trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE,
) => {
const unitCount = Math.ceil(trafficData / trafficUnitSize);
return unitCount * trafficUnitCost;
};
export const calculateOverageCost = (
dataUsage: number,
includedTraffic: number,
trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST,
trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE,
): number => {
if (dataUsage === 0) {
return 0;
}
const overage =
Math.floor((dataUsage - includedTraffic) / trafficUnitSize) *
trafficUnitSize;
return overage > 0
? calculateTrafficDataCost(overage, trafficUnitCost, trafficUnitSize)
: 0;
};
export const calculateProjectedUsage = ({
dayOfMonth,
daysInMonth,
trafficData,
}: {
dayOfMonth: number;
daysInMonth: number;
trafficData: TrafficUsageDataSegmentedCombinedSchemaApiDataItem[];
}) => {
if (dayOfMonth < 5) {
return 0;
}
const trafficDataUpToYesterday = trafficData.map((item) => {
return {
...item,
dataPoints: item.dataPoints.filter(
(point) => Number(point.period.slice(-2)) < dayOfMonth,
),
};
});
const dataUsage = dailyTrafficDataToCurrentUsage(trafficDataUpToYesterday);
return (dataUsage / (dayOfMonth - 1)) * daysInMonth;
};
export const calculateEstimatedMonthlyCost = (
trafficData: TrafficUsageDataSegmentedCombinedSchemaApiDataItem[],
includedTraffic: number,
currentDate: Date,
trafficUnitCost = DEFAULT_TRAFFIC_DATA_UNIT_COST,
trafficUnitSize = DEFAULT_TRAFFIC_DATA_UNIT_SIZE,
) => {
if (!trafficData) {
return 0;
}
const dayOfMonth = currentDate.getDate();
const daysInMonth = getDaysInMonth(currentDate);
const projectedUsage = calculateProjectedUsage({
dayOfMonth,
daysInMonth,
trafficData,
});
return calculateOverageCost(
projectedUsage,
includedTraffic,
trafficUnitCost,
trafficUnitSize,
);
};