1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: refactor data usage into hooks, estimate monthly added fees (#7048)

- Refactors data processing and overage calculations to separate hooks
- Adds support for estimating traffic costs based on monthly usage up to
current point
- Adds accrued traffic charges to the billing page


![image](https://github.com/Unleash/unleash/assets/707867/39a837c2-5092-49b8-8bbf-46d8757635c0)


![image](https://github.com/Unleash/unleash/assets/707867/55ecfa5e-afe1-4cb6-9aa4-7dd67db4248c)
This commit is contained in:
David Leek 2024-05-17 15:27:32 +02:00 committed by GitHub
parent 5c4b835cb5
commit dfc0c3c63f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 602 additions and 238 deletions

View File

@ -1,4 +1,5 @@
import type { FC } from 'react';
import { useState, useEffect } from 'react';
import { Alert, Divider, Grid, styled, Typography } from '@mui/material';
import { Link } from 'react-router-dom';
import CheckIcon from '@mui/icons-material/Check';
@ -15,6 +16,9 @@ import { GridRow } from 'component/common/GridRow/GridRow';
import { GridCol } from 'component/common/GridCol/GridCol';
import { Badge } from 'component/common/Badge/Badge';
import { GridColLink } from './GridColLink/GridColLink';
import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
import { useTrafficDataEstimation } from 'hooks/useTrafficData';
import { useUiFlag } from 'hooks/useUiFlag';
const StyledPlanBox = styled('aside')(({ theme }) => ({
padding: theme.spacing(2.5),
@ -71,10 +75,21 @@ interface IBillingPlanProps {
instanceStatus: IInstanceStatus;
}
const proPlanIncludedRequests = 53_000_000;
export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
const { users } = useUsers();
const expired = trialHasExpired(instanceStatus);
const { uiConfig } = useUiConfig();
const { uiConfig, isPro } = useUiConfig();
const {
currentPeriod,
toChartData,
toTrafficUsageSum,
endpointsInfo,
getDayLabels,
calculateOverageCost,
} = useTrafficDataEstimation();
const eligibleUsers = users.filter((user: any) => user.email);
@ -94,6 +109,32 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
const paidAssignedPrice = price.user * paidAssigned;
const finalPrice = planPrice + paidAssignedPrice;
const inactive = instanceStatus.state !== InstanceState.ACTIVE;
const [totalCost, setTotalCost] = useState(0);
const flagEnabled = useUiFlag('displayTrafficDataUsage');
const [overageCost, setOverageCost] = useState(0);
const includedTraffic = isPro() ? proPlanIncludedRequests : 0;
const traffic = useInstanceTrafficMetrics(currentPeriod.key);
useEffect(() => {
if (flagEnabled && includedTraffic > 0) {
const trafficData = toChartData(
getDayLabels(currentPeriod.dayCount),
traffic,
endpointsInfo,
);
const totalTraffic = toTrafficUsageSum(trafficData);
const overageCostCalc = calculateOverageCost(
totalTraffic,
includedTraffic,
);
setOverageCost(overageCostCalc);
setTotalCost(finalPrice + overageCostCalc);
} else {
setTotalCost(finalPrice);
}
}, [traffic]);
return (
<Grid item xs={12} md={7}>
@ -185,7 +226,11 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
</Typography>
</GridCol>
</GridRow>
<GridRow>
<GridRow
sx={(theme) => ({
marginBottom: theme.spacing(1.5),
})}
>
<GridCol vertical>
<Typography>
<strong>Paid members</strong>
@ -210,6 +255,40 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
</Typography>
</GridCol>
</GridRow>
<ConditionallyRender
condition={flagEnabled && overageCost > 0}
show={
<GridRow>
<GridCol vertical>
<Typography>
<strong>
Accrued traffic charges
</strong>
<GridColLink>
<Link to='/admin/network/data-usage'>
view details
</Link>
</GridColLink>
</Typography>
<StyledInfoLabel>
$5 dollar per 1 million
started above included data
</StyledInfoLabel>
</GridCol>
<GridCol>
<Typography
sx={(theme) => ({
fontSize:
theme.fontSizes
.mainHeader,
})}
>
${overageCost.toFixed(2)}
</Typography>
</GridCol>
</GridRow>
}
/>
</Grid>
<StyledDivider />
<Grid container>
@ -223,7 +302,7 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
theme.fontSizes.mainHeader,
})}
>
Total per month
Total
</Typography>
</GridCol>
<GridCol>
@ -234,7 +313,7 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
fontSize: '2rem',
})}
>
${finalPrice.toFixed(2)}
${totalCost.toFixed(2)}
</Typography>
</GridCol>
</GridRow>

View File

@ -4,7 +4,8 @@ import styled from '@mui/material/styles/styled';
import { usePageTitle } from 'hooks/usePageTitle';
import Select from 'component/common/select';
import Box from '@mui/system/Box';
import Alert from '@mui/material/Alert';
import { Link as RouterLink } from 'react-router-dom';
import { Alert, Link } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import {
@ -13,7 +14,6 @@ import {
CategoryScale,
LinearScale,
BarElement,
type ChartDataset,
Title,
Tooltip,
Legend,
@ -22,136 +22,22 @@ import {
} from 'chart.js';
import { Bar } from 'react-chartjs-2';
import {
type IInstanceTrafficMetricsResponse,
useInstanceTrafficMetrics,
} from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
import type { Theme } from '@mui/material/styles/createTheme';
import Grid from '@mui/material/Grid';
import { useUiFlag } from 'hooks/useUiFlag';
import { NetworkTrafficUsagePlanSummary } from './NetworkTrafficUsagePlanSummary';
import annotationPlugin from 'chartjs-plugin-annotation';
type ChartDatasetType = ChartDataset<'bar'>;
type SelectablePeriod = {
key: string;
dayCount: number;
label: string;
year: number;
month: number;
};
type EndpointInfo = {
label: string;
color: string;
order: number;
};
import {
type ChartDatasetType,
useTrafficDataEstimation,
} from 'hooks/useTrafficData';
const StyledBox = styled(Box)(({ theme }) => ({
display: 'grid',
gap: theme.spacing(5),
}));
const padMonth = (month: number): string =>
month < 10 ? `0${month}` : `${month}`;
const toSelectablePeriod = (date: Date, label?: string): SelectablePeriod => {
const year = date.getFullYear();
const month = date.getMonth();
const period = `${year}-${padMonth(month + 1)}`;
const dayCount = new Date(year, month + 1, 0).getDate();
return {
key: period,
year,
month,
dayCount,
label:
label ||
date.toLocaleString('en-US', { month: 'long', year: 'numeric' }),
};
};
const getSelectablePeriods = (): SelectablePeriod[] => {
const current = new Date(Date.now());
const selectablePeriods = [toSelectablePeriod(current, 'Current month')];
for (
let subtractMonthCount = 1;
subtractMonthCount < 13;
subtractMonthCount++
) {
// JavaScript wraps around the year, so we don't need to handle that.
const date = new Date(
current.getFullYear(),
current.getMonth() - subtractMonthCount,
1,
);
if (date > new Date('2024-03-31')) {
selectablePeriods.push(toSelectablePeriod(date));
}
}
return selectablePeriods;
};
const toPeriodsRecord = (
periods: SelectablePeriod[],
): Record<string, SelectablePeriod> => {
return periods.reduce(
(acc, period) => {
acc[period.key] = period;
return acc;
},
{} as Record<string, SelectablePeriod>,
);
};
const getDayLabels = (dayCount: number): number[] => {
return [...Array(dayCount).keys()].map((i) => i + 1);
};
const toChartData = (
days: number[],
traffic: IInstanceTrafficMetricsResponse,
endpointsInfo: Record<string, EndpointInfo>,
): ChartDatasetType[] => {
if (!traffic || !traffic.usage || !traffic.usage.apiData) {
return [];
}
const data = traffic.usage.apiData
.filter((item) => !!endpointsInfo[item.apiPath])
.sort(
(item1: any, item2: any) =>
endpointsInfo[item1.apiPath].order -
endpointsInfo[item2.apiPath].order,
)
.map((item: any) => {
const daysRec = days.reduce(
(acc, day: number) => {
acc[`d${day}`] = 0;
return acc;
},
{} as Record<string, number>,
);
for (const dayKey in item.days) {
const day = item.days[dayKey];
const dayNum = new Date(Date.parse(day.day)).getDate();
daysRec[`d${dayNum}`] = day.trafficTypes[0].count;
}
const epInfo = endpointsInfo[item.apiPath];
return {
label: epInfo.label,
data: Object.values(daysRec),
backgroundColor: epInfo.color,
hoverBackgroundColor: epInfo.color,
};
});
return data;
};
const customHighlightPlugin = {
id: 'customLine',
beforeDraw: (chart: Chart) => {
@ -295,36 +181,27 @@ const createBarChartOptions = (
},
});
const endpointsInfo: Record<string, EndpointInfo> = {
'/api/admin': {
label: 'Admin',
color: '#6D66D9',
order: 1,
},
'/api/frontend': {
label: 'Frontend',
color: '#A39EFF',
order: 2,
},
'/api/client': {
label: 'Server',
color: '#D8D6FF',
order: 3,
},
};
const proPlanIncludedRequests = 53_000_000;
export const NetworkTrafficUsage: VFC = () => {
usePageTitle('Network - Data Usage');
const theme = useTheme();
const selectablePeriods = getSelectablePeriods();
const record = toPeriodsRecord(selectablePeriods);
const [period, setPeriod] = useState<string>(selectablePeriods[0].key);
const { isOss, isPro } = useUiConfig();
const {
record,
period,
setPeriod,
selectablePeriods,
getDayLabels,
toChartData,
toTrafficUsageSum,
endpointsInfo,
calculateOverageCost,
calculateEstimatedMonthlyCost,
} = useTrafficDataEstimation();
const includedTraffic = isPro() ? proPlanIncludedRequests : 0;
const options = useMemo(() => {
@ -355,6 +232,10 @@ export const NetworkTrafficUsage: VFC = () => {
const [usageTotal, setUsageTotal] = useState<number>(0);
const [overageCost, setOverageCost] = useState<number>(0);
const [estimatedMonthlyCost, setEstimatedMonthlyCost] = useState<number>(0);
const data = {
labels,
datasets,
@ -375,20 +256,21 @@ export const NetworkTrafficUsage: VFC = () => {
useEffect(() => {
if (data) {
const usage = data.datasets.reduce(
(acc: number, current: ChartDatasetType) => {
return (
acc +
current.data.reduce(
(acc_inner, current_inner) =>
acc_inner + current_inner,
0,
)
);
},
0,
const usage = toTrafficUsageSum(data.datasets);
const calculatedOverageCost = calculateOverageCost(
usage,
includedTraffic,
);
setUsageTotal(usage);
setOverageCost(calculatedOverageCost);
setEstimatedMonthlyCost(
calculateEstimatedMonthlyCost(
period,
data.datasets,
includedTraffic,
new Date(),
),
);
}
}, [data]);
@ -398,15 +280,40 @@ export const NetworkTrafficUsage: VFC = () => {
show={<Alert severity='warning'>Not enabled.</Alert>}
elseShow={
<>
<ConditionallyRender
condition={overageCost > 0}
show={
<Alert severity='warning' sx={{ mb: 4 }}>
<b>Heads up!</b> You are currently consuming
more requests than your plan includes and will
be billed according to our terms. Please see{' '}
<Link
component={RouterLink}
to='https://www.getunleash.io/pricing'
>
this page
</Link>{' '}
for more information. In order to reduce your
traffic consumption, you may configure an{' '}
<Link
component={RouterLink}
to='https://docs.getunleash.io/reference/unleash-edge'
>
Unleash Edge instance
</Link>{' '}
in your own datacenter.
</Alert>
}
/>
<StyledBox>
<Grid container component='header' spacing={2}>
<Grid item xs={12} md={10}>
<Grid item xs={7} md={5.5}>
<NetworkTrafficUsagePlanSummary
usageTotal={usageTotal}
includedTraffic={includedTraffic}
/>
</Grid>
<NetworkTrafficUsagePlanSummary
usageTotal={usageTotal}
includedTraffic={includedTraffic}
overageCost={overageCost}
estimatedMonthlyCost={estimatedMonthlyCost}
/>
</Grid>
<Grid item xs={12} md={2}>
<Select

View File

@ -1,27 +1,38 @@
import styled from '@mui/material/styles/styled';
import Box from '@mui/system/Box';
import Grid from '@mui/material/Grid';
import { flexRow } from 'themes/themeStyles';
import Link from '@mui/material/Link';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Badge } from 'component/common/Badge/Badge';
import { useUiFlag } from 'hooks/useUiFlag';
const StyledContainerGrid = styled(Grid)(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
}));
const StyledGrid = styled(Grid)(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
flex: '1 1',
}));
const StyledColumnGrid = styled(Grid)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
flex: '1 1',
}));
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
flexDirection: 'row',
flex: '1 1',
padding: theme.spacing(3),
border: `2px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusLarge,
}));
const StyledCardTitleRow = styled(Box)(() => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}));
const StyledCardDescription = styled(Box)(({ theme }) => ({
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2.5),
@ -31,13 +42,13 @@ const StyledCardDescription = styled(Box)(({ theme }) => ({
}));
const RowContainer = styled(Box)(({ theme }) => ({
...flexRow,
display: 'flex',
flexDirection: 'row',
}));
const StyledNumbersDiv = styled('div')(({ theme }) => ({
marginLeft: 'auto',
display: 'flex',
justifyContent: 'space-between',
textDecoration: 'none',
color: theme.palette.text.primary,
}));
@ -45,80 +56,112 @@ const StyledNumbersDiv = styled('div')(({ theme }) => ({
interface INetworkTrafficUsagePlanSummary {
usageTotal: number;
includedTraffic: number;
overageCost: number;
estimatedMonthlyCost: number;
}
export const NetworkTrafficUsagePlanSummary = ({
usageTotal,
includedTraffic,
overageCost,
estimatedMonthlyCost,
}: INetworkTrafficUsagePlanSummary) => {
const overages = usageTotal - includedTraffic;
const estimateFlagEnabled = useUiFlag('estimateTrafficDataCost');
return (
<StyledContainer>
<Grid item>
<StyledCardTitleRow>
<b>Number of requests to Unleash</b>
</StyledCardTitleRow>
<StyledCardDescription>
<RowContainer>
Incoming requests selected month{' '}
<StyledNumbersDiv>
<ConditionallyRender
condition={includedTraffic > 0}
show={
<ConditionallyRender
condition={
usageTotal <= includedTraffic
<StyledContainerGrid container spacing={4}>
<StyledGrid item xs={5.5} md={5.5}>
<StyledContainer>
<StyledColumnGrid item>
<Box>
<b>Number of requests to Unleash</b>
</Box>
<StyledCardDescription>
<RowContainer>
Incoming requests selected month{' '}
<StyledNumbersDiv>
<Badge
color={
includedTraffic > 0
? usageTotal <= includedTraffic
? 'success'
: 'error'
: 'neutral'
}
show={
<Badge color='success'>
{usageTotal.toLocaleString()}{' '}
requests
</Badge>
}
elseShow={
<Badge color='error'>
{usageTotal.toLocaleString()}{' '}
requests
</Badge>
}
/>
}
elseShow={
<Badge color='neutral'>
>
{usageTotal.toLocaleString()} requests
</Badge>
}
/>
</StyledNumbersDiv>
</RowContainer>
</StyledCardDescription>
<ConditionallyRender
condition={includedTraffic > 0}
show={
<StyledCardDescription>
<RowContainer>
Included in your plan monthly
<StyledNumbersDiv>
{includedTraffic.toLocaleString()} requests
</StyledNumbersDiv>
</RowContainer>
</StyledCardDescription>
}
/>
<ConditionallyRender
condition={includedTraffic > 0 && overages > 0}
show={
<StyledCardDescription>
<RowContainer>
Requests overages this month
<StyledNumbersDiv>
{overages.toLocaleString()} requests
</StyledNumbersDiv>
</RowContainer>
</StyledCardDescription>
}
/>
</Grid>
</StyledContainer>
<ConditionallyRender
condition={includedTraffic > 0}
show={
<StyledCardDescription>
<RowContainer>
Included in your plan monthly
<StyledNumbersDiv>
{includedTraffic.toLocaleString()}{' '}
requests
</StyledNumbersDiv>
</RowContainer>
</StyledCardDescription>
}
/>
</StyledColumnGrid>
</StyledContainer>
</StyledGrid>
<ConditionallyRender
condition={
estimateFlagEnabled && includedTraffic > 0 && overages > 0
}
show={
<StyledGrid item xs={5.5} md={5.5}>
<StyledContainer>
<StyledColumnGrid item>
<Box>
<b>Accrued traffic charges</b>
</Box>
<StyledCardDescription>
<RowContainer>
Requests overages this month (
<Link href='https://www.getunleash.io/pricing'>
pricing
</Link>
)
<StyledNumbersDiv>
{overages.toLocaleString()} requests
</StyledNumbersDiv>
</RowContainer>
<RowContainer>
Accrued traffic charges
<StyledNumbersDiv>
<Badge color='secondary'>
{overageCost} USD
</Badge>
</StyledNumbersDiv>
</RowContainer>
<ConditionallyRender
condition={estimatedMonthlyCost > 0}
show={
<RowContainer>
Estimated traffic charges based
on current usage
<StyledNumbersDiv>
<Badge color='secondary'>
{estimatedMonthlyCost}{' '}
USD
</Badge>
</StyledNumbersDiv>
</RowContainer>
}
/>
</StyledCardDescription>
</StyledColumnGrid>
</StyledContainer>
</StyledGrid>
}
/>
</StyledContainerGrid>
);
};

View File

@ -0,0 +1,90 @@
import {
toSelectablePeriod,
calculateOverageCost,
calculateEstimatedMonthlyCost,
calculateProjectedUsage,
} from './useTrafficData';
const testData4Days = [
{
label: 'Frontend',
data: [23_000_000, 22_000_000, 24_000_000, 21_000_000],
backgroundColor: 'red',
hoverBackgroundColor: 'red',
},
{
label: 'Admin',
data: [23_000_000, 22_000_000, 24_000_000, 21_000_000],
backgroundColor: 'red',
hoverBackgroundColor: 'red',
},
{
label: 'SDK',
data: [23_000_000, 22_000_000, 24_000_000, 21_000_000],
backgroundColor: 'red',
hoverBackgroundColor: 'red',
},
];
describe('traffic overage calculation', () => {
it('should return 0 if there is no overage this month', () => {
const dataUsage = 52_900_000;
const includedTraffic = 53_000_000;
const result = calculateOverageCost(dataUsage, includedTraffic);
expect(result).toBe(0);
});
it('should return 5 if overage this month is atleast 1 request above included', () => {
const dataUsage = 53_000_001;
const includedTraffic = 53_000_000;
const result = calculateOverageCost(dataUsage, includedTraffic);
expect(result).toBe(5);
});
it('doesnt estimate when having less than 5 days worth of data', () => {
const now = new Date();
const period = toSelectablePeriod(now);
const testNow = new Date(now.getFullYear(), now.getMonth(), 4);
const result = calculateEstimatedMonthlyCost(
period.key,
testData4Days,
53_000_000,
testNow,
);
expect(result).toBe(0);
});
it('needs 5 days or more to estimate for the month', () => {
const testData = testData4Days;
testData[0].data.push(23_000_000);
testData[1].data.push(23_000_000);
testData[2].data.push(23_000_000);
const now = new Date();
const period = toSelectablePeriod(now);
const testNow = new Date(now.getFullYear(), now.getMonth(), 5);
const result = calculateEstimatedMonthlyCost(
period.key,
testData,
53_000_000,
testNow,
);
expect(result).toBeGreaterThan(1430);
});
it('estimates projected data usage', () => {
const testData = testData4Days;
testData[0].data.push(22_500_000);
testData[1].data.push(22_500_000);
testData[2].data.push(22_500_000);
// Testing April 5th of 2024 (30 days)
const now = new Date(2024, 3, 5);
const period = toSelectablePeriod(now);
const result = calculateProjectedUsage(
now.getDate(),
testData,
period.dayCount,
);
// 22_500_000 * 3 * 30 = 2_025_000_000
expect(result).toBe(2_025_000_000);
});
});

View File

@ -0,0 +1,238 @@
import { useState } from 'react';
import type { IInstanceTrafficMetricsResponse } from './api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
import type { ChartDataset } from 'chart.js';
const TRAFFIC_DATA_UNIT_COST = 5;
const TRAFFIC_DATA_UNIT_SIZE = 1_000_000;
export type SelectablePeriod = {
key: string;
dayCount: number;
label: string;
year: number;
month: number;
};
export type EndpointInfo = {
label: string;
color: string;
order: number;
};
export type ChartDatasetType = ChartDataset<'bar'>;
const endpointsInfo: Record<string, EndpointInfo> = {
'/api/admin': {
label: 'Admin',
color: '#6D66D9',
order: 1,
},
'/api/frontend': {
label: 'Frontend',
color: '#A39EFF',
order: 2,
},
'/api/client': {
label: 'Server',
color: '#D8D6FF',
order: 3,
},
};
const calculateTrafficDataCost = (trafficData: number) => {
const unitCount = Math.ceil(trafficData / TRAFFIC_DATA_UNIT_SIZE);
return unitCount * TRAFFIC_DATA_UNIT_COST;
};
const padMonth = (month: number): string =>
month < 10 ? `0${month}` : `${month}`;
export const toSelectablePeriod = (
date: Date,
label?: string,
): SelectablePeriod => {
const year = date.getFullYear();
const month = date.getMonth();
const period = `${year}-${padMonth(month + 1)}`;
const dayCount = new Date(year, month + 1, 0).getDate();
return {
key: period,
year,
month,
dayCount,
label:
label ||
date.toLocaleString('en-US', { month: 'long', year: 'numeric' }),
};
};
const currentDate = new Date(Date.now());
const currentPeriod = toSelectablePeriod(currentDate, 'Current month');
const getSelectablePeriods = (): SelectablePeriod[] => {
const selectablePeriods = [currentPeriod];
for (
let subtractMonthCount = 1;
subtractMonthCount < 13;
subtractMonthCount++
) {
// JavaScript wraps around the year, so we don't need to handle that.
const date = new Date(
currentDate.getFullYear(),
currentDate.getMonth() - subtractMonthCount,
1,
);
if (date > new Date('2024-03-31')) {
selectablePeriods.push(toSelectablePeriod(date));
}
}
return selectablePeriods;
};
const toPeriodsRecord = (
periods: SelectablePeriod[],
): Record<string, SelectablePeriod> => {
return periods.reduce(
(acc, period) => {
acc[period.key] = period;
return acc;
},
{} as Record<string, SelectablePeriod>,
);
};
const toChartData = (
days: number[],
traffic: IInstanceTrafficMetricsResponse,
endpointsInfo: Record<string, EndpointInfo>,
): ChartDatasetType[] => {
if (!traffic || !traffic.usage || !traffic.usage.apiData) {
return [];
}
const data = traffic.usage.apiData
.filter((item) => !!endpointsInfo[item.apiPath])
.sort(
(item1: any, item2: any) =>
endpointsInfo[item1.apiPath].order -
endpointsInfo[item2.apiPath].order,
)
.map((item: any) => {
const daysRec = days.reduce(
(acc, day: number) => {
acc[`d${day}`] = 0;
return acc;
},
{} as Record<string, number>,
);
for (const dayKey in item.days) {
const day = item.days[dayKey];
const dayNum = new Date(Date.parse(day.day)).getDate();
daysRec[`d${dayNum}`] = day.trafficTypes[0].count;
}
const epInfo = endpointsInfo[item.apiPath];
return {
label: epInfo.label,
data: Object.values(daysRec),
backgroundColor: epInfo.color,
hoverBackgroundColor: epInfo.color,
};
});
return data;
};
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 getDayLabels = (dayCount: number): number[] => {
return [...Array(dayCount).keys()].map((i) => i + 1);
};
export const calculateOverageCost = (
dataUsage: number,
includedTraffic: number,
): number => {
if (dataUsage === 0) {
return 0;
}
const overage = dataUsage - includedTraffic;
return overage > 0 ? calculateTrafficDataCost(overage) : 0;
};
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,
) => {
if (period !== currentPeriod.key) {
return 0;
}
const today = currentDate.getDate();
const projectedUsage = calculateProjectedUsage(
today,
trafficData,
currentPeriod.dayCount,
);
return calculateOverageCost(projectedUsage, includedTraffic);
};
export const useTrafficDataEstimation = () => {
const selectablePeriods = getSelectablePeriods();
const record = toPeriodsRecord(selectablePeriods);
const [period, setPeriod] = useState<string>(selectablePeriods[0].key);
return {
calculateTrafficDataCost,
record,
period,
setPeriod,
selectablePeriods,
getDayLabels,
currentPeriod,
toChartData,
toTrafficUsageSum,
endpointsInfo,
calculateOverageCost,
calculateEstimatedMonthlyCost,
};
};

View File

@ -76,6 +76,7 @@ export type UiFlags = {
userAccessUIEnabled?: boolean;
outdatedSdksBanner?: boolean;
displayTrafficDataUsage?: boolean;
estimateTrafficDataCost?: boolean;
disableShowContextFieldSelectionValues?: boolean;
projectOverviewRefactorFeedback?: boolean;
featureLifecycle?: boolean;

View File

@ -97,6 +97,7 @@ exports[`should create default config 1`] = `
"enableLicense": false,
"enableLicenseChecker": false,
"encryptEmails": false,
"estimateTrafficDataCost": false,
"executiveDashboard": false,
"executiveDashboardUI": false,
"extendedUsageMetrics": false,

View File

@ -41,6 +41,7 @@ export type IFlagKey =
| 'killScheduledChangeRequestCache'
| 'collectTrafficDataUsage'
| 'displayTrafficDataUsage'
| 'estimateTrafficDataCost'
| 'useMemoizedActiveTokens'
| 'queryMissingTokens'
| 'checkEdgeValidTokensFromCache'
@ -225,6 +226,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_DISPLAY_TRAFFIC_DATA_USAGE,
false,
),
estimateTrafficDataCost: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_ESTIMATE_TRAFFIC_DATA_COST,
false,
),
userAccessUIEnabled: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_USER_ACCESS_UI_ENABLED,
false,