diff --git a/frontend/src/component/admin/network/Network.tsx b/frontend/src/component/admin/network/Network.tsx
index bc91d48426..6a7ae1ccb0 100644
--- a/frontend/src/component/admin/network/Network.tsx
+++ b/frontend/src/component/admin/network/Network.tsx
@@ -7,6 +7,9 @@ import { PageContent } from 'component/common/PageContent/PageContent';
const NetworkOverview = lazy(() => import('./NetworkOverview/NetworkOverview'));
const NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic'));
+const NetworkTrafficUsage = lazy(
+ () => import('./NetworkTrafficUsage/NetworkTrafficUsage'),
+);
const tabs = [
{
@@ -17,6 +20,10 @@ const tabs = [
label: 'Traffic',
path: '/admin/network/traffic',
},
+ {
+ label: 'Data Usage',
+ path: '/admin/network/data-usage',
+ },
];
export const Network = () => {
@@ -52,6 +59,10 @@ export const Network = () => {
} />
} />
+ }
+ />
diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx
new file mode 100644
index 0000000000..b7172e819b
--- /dev/null
+++ b/frontend/src/component/admin/network/NetworkTrafficUsage/NetworkTrafficUsage.tsx
@@ -0,0 +1,392 @@
+import { useMemo, type VFC, useState, useEffect } from 'react';
+import useTheme from '@mui/material/styles/useTheme';
+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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
+import {
+ Chart as ChartJS,
+ type ChartOptions,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ type ChartDataset,
+ Title,
+ Tooltip,
+ Legend,
+ type Chart,
+ type Tick,
+} from 'chart.js';
+
+import { Bar } from 'react-chartjs-2';
+import {
+ type IInstanceTrafficMetricsResponse,
+ 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';
+
+type ChartDatasetType = ChartDataset<'bar'>;
+
+type SelectablePeriod = {
+ key: string;
+ dayCount: number;
+ label: string;
+ year: number;
+ month: number;
+};
+
+type EndpointInfo = {
+ label: string;
+ color: string;
+ order: number;
+};
+
+const StyledHeader = styled('h3')(({ theme }) => ({
+ display: 'flex',
+ gap: theme.spacing(1),
+ alignItems: 'center',
+ fontSize: theme.fontSizes.bodySize,
+ margin: 0,
+ marginTop: theme.spacing(1),
+ fontWeight: theme.fontWeight.bold,
+}));
+
+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,
+ );
+ selectablePeriods.push(toSelectablePeriod(date));
+ }
+ return selectablePeriods;
+};
+
+const toPeriodsRecord = (
+ periods: SelectablePeriod[],
+): Record => {
+ return periods.reduce(
+ (acc, period) => {
+ acc[period.key] = period;
+ return acc;
+ },
+ {} as Record,
+ );
+};
+
+const getDayLabels = (dayCount: number): number[] => {
+ return [...Array(dayCount).keys()].map((i) => i + 1);
+};
+
+const toChartData = (
+ days: number[],
+ traffic: IInstanceTrafficMetricsResponse,
+ endpointsInfo: Record,
+): 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,
+ );
+
+ 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) => {
+ const width = 36;
+ if (chart.tooltip?.opacity && chart.tooltip.x) {
+ const x = chart.tooltip.caretX;
+ const yAxis = chart.scales.y;
+ const ctx = chart.ctx;
+ ctx.save();
+ const gradient = ctx.createLinearGradient(
+ x,
+ yAxis.top,
+ x,
+ yAxis.bottom + 34,
+ );
+ gradient.addColorStop(0, 'rgba(129, 122, 254, 0)');
+ gradient.addColorStop(1, 'rgba(129, 122, 254, 0.12)');
+ ctx.fillStyle = gradient;
+ ctx.roundRect(
+ x - width / 2,
+ yAxis.top,
+ width,
+ yAxis.bottom - yAxis.top + 34,
+ 5,
+ );
+ ctx.fill();
+ ctx.restore();
+ }
+ },
+};
+
+const createBarChartOptions = (
+ theme: Theme,
+ tooltipTitleCallback: (tooltipItems: any) => string,
+): ChartOptions<'bar'> => ({
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ color: theme.palette.text.primary,
+ pointStyle: 'circle',
+ usePointStyle: true,
+ boxHeight: 6,
+ padding: 15,
+ boxPadding: 5,
+ },
+ },
+ tooltip: {
+ backgroundColor: theme.palette.background.paper,
+ titleColor: theme.palette.text.primary,
+ bodyColor: theme.palette.text.primary,
+ bodySpacing: 6,
+ padding: {
+ top: 20,
+ bottom: 20,
+ left: 30,
+ right: 30,
+ },
+ borderColor: 'rgba(0, 0, 0, 0.05)',
+ borderWidth: 3,
+ usePointStyle: true,
+ caretSize: 0,
+ boxPadding: 10,
+ callbacks: {
+ title: tooltipTitleCallback,
+ },
+ },
+ },
+ responsive: true,
+ scales: {
+ x: {
+ stacked: true,
+ ticks: {
+ color: theme.palette.text.secondary,
+ },
+ grid: {
+ display: false,
+ },
+ },
+ y: {
+ stacked: true,
+ ticks: {
+ color: theme.palette.text.secondary,
+ maxTicksLimit: 5,
+ callback: (
+ tickValue: string | number,
+ index: number,
+ ticks: Tick[],
+ ) => {
+ if (typeof tickValue === 'string') {
+ return tickValue;
+ }
+ const value = Number.parseInt(tickValue.toString());
+ if (value > 999999) {
+ return `${value / 1000000}M`;
+ }
+ return value > 999 ? `${value / 1000}k` : value;
+ },
+ },
+ grid: {
+ drawBorder: false,
+ },
+ },
+ },
+ elements: {
+ bar: {
+ borderRadius: 5,
+ },
+ },
+ interaction: {
+ mode: 'index',
+ intersect: false,
+ },
+});
+
+export const NetworkTrafficUsage: VFC = () => {
+ usePageTitle('Network - Data Usage');
+ const theme = useTheme();
+
+ const endpointsInfo: Record = {
+ '/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 selectablePeriods = getSelectablePeriods();
+ const record = toPeriodsRecord(selectablePeriods);
+ const [period, setPeriod] = useState(selectablePeriods[0].key);
+
+ const options = useMemo(() => {
+ return createBarChartOptions(theme, (tooltipItems: any) => {
+ const periodItem = record[period];
+ const tooltipDate = new Date(
+ periodItem.year,
+ periodItem.month,
+ Number.parseInt(tooltipItems[0].label),
+ );
+ return tooltipDate.toLocaleDateString('en-US', {
+ month: 'long',
+ day: 'numeric',
+ year: 'numeric',
+ });
+ });
+ }, [theme, period]);
+
+ const traffic = useInstanceTrafficMetrics(period);
+
+ const [labels, setLabels] = useState([]);
+
+ const [datasets, setDatasets] = useState([]);
+
+ const data = {
+ labels,
+ datasets,
+ };
+
+ const { isOss } = useUiConfig();
+ const flagEnabled = useUiFlag('collectTrafficDataUsage');
+
+ useEffect(() => {
+ setDatasets(toChartData(labels, traffic, endpointsInfo));
+ }, [labels, traffic]);
+
+ useEffect(() => {
+ if (record && period) {
+ const periodData = record[period];
+ setLabels(getDayLabels(periodData.dayCount));
+ }
+ }, [period]);
+
+ return (
+ No data available.}
+ elseShow={
+ <>
+
+
+
+ Number of requests to Unleash
+
+
+
+
+
+
+
+
+
+
+ >
+ }
+ />
+ );
+};
+
+// Register dependencies that we need to draw the chart.
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend,
+);
+
+// Use a default export to lazy-load the charting library.
+export default NetworkTrafficUsage;
diff --git a/frontend/src/hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics.ts b/frontend/src/hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics.ts
new file mode 100644
index 0000000000..b95d82d68c
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics.ts
@@ -0,0 +1,40 @@
+import useSWR from 'swr';
+import { useMemo } from 'react';
+import { formatApiPath } from 'utils/formatPath';
+import handleErrorResponses from '../httpErrorResponseHandler';
+import type { TrafficUsageDataSegmentedSchema } from 'openapi';
+
+export interface IInstanceTrafficMetricsResponse {
+ usage: TrafficUsageDataSegmentedSchema;
+
+ refetch: () => void;
+
+ loading: boolean;
+
+ error?: Error;
+}
+
+export const useInstanceTrafficMetrics = (
+ period: string,
+): IInstanceTrafficMetricsResponse => {
+ const { data, error, mutate } = useSWR(
+ formatApiPath(`api/admin/metrics/traffic/${period}`),
+ fetcher,
+ );
+
+ return useMemo(
+ () => ({
+ usage: data,
+ loading: !error && !data,
+ refetch: () => mutate(),
+ error,
+ }),
+ [data, error, mutate],
+ );
+};
+
+const fetcher = (path: string) => {
+ return fetch(path)
+ .then(handleErrorResponses('Instance Metrics'))
+ .then((res) => res.json());
+};
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index 022dcf8e2b..55d500d815 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -80,6 +80,7 @@ export type UiFlags = {
sdkReporting?: boolean;
outdatedSdksBanner?: boolean;
projectOverviewRefactor?: string;
+ collectTrafficDataUsage?: boolean;
};
export interface IVersionInfo {
diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts
index 139bb95c06..98adb64b0f 100644
--- a/frontend/src/openapi/models/index.ts
+++ b/frontend/src/openapi/models/index.ts
@@ -1076,6 +1076,10 @@ export * from './toggleMaintenance403';
export * from './toggleMaintenanceSchema';
export * from './tokenStringListSchema';
export * from './tokenUserSchema';
+export * from './trafficUsageApiDataSchema';
+export * from './trafficUsageApiDataSchemaDaysItem';
+export * from './trafficUsageApiDataSchemaDaysItemTrafficTypesItem';
+export * from './trafficUsageDataSegmentedSchema';
export * from './uiConfigSchema';
export * from './uiConfigSchemaAuthenticationType';
export * from './uiConfigSchemaFlags';
diff --git a/frontend/src/openapi/models/trafficUsageApiDataSchema.ts b/frontend/src/openapi/models/trafficUsageApiDataSchema.ts
new file mode 100644
index 0000000000..93f5eee511
--- /dev/null
+++ b/frontend/src/openapi/models/trafficUsageApiDataSchema.ts
@@ -0,0 +1,16 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+import type { TrafficUsageApiDataSchemaDaysItem } from './trafficUsageApiDataSchemaDaysItem';
+
+/**
+ * Contains the recorded data usage for each API path, segmented by day and type of traffic
+ */
+export interface TrafficUsageApiDataSchema {
+ /** The path of the API that the recorded data usage is for */
+ apiPath: string;
+ /** An array containing each day in the selected period that has data usage recorded */
+ days: TrafficUsageApiDataSchemaDaysItem[];
+}
diff --git a/frontend/src/openapi/models/trafficUsageApiDataSchemaDaysItem.ts b/frontend/src/openapi/models/trafficUsageApiDataSchemaDaysItem.ts
new file mode 100644
index 0000000000..211081939f
--- /dev/null
+++ b/frontend/src/openapi/models/trafficUsageApiDataSchemaDaysItem.ts
@@ -0,0 +1,13 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+import type { TrafficUsageApiDataSchemaDaysItemTrafficTypesItem } from './trafficUsageApiDataSchemaDaysItemTrafficTypesItem';
+
+export type TrafficUsageApiDataSchemaDaysItem = {
+ /** The day of the period for which the usage is recorded */
+ day: string;
+ /** Contains the recorded data usage for each type of traffic group */
+ trafficTypes: TrafficUsageApiDataSchemaDaysItemTrafficTypesItem[];
+};
diff --git a/frontend/src/openapi/models/trafficUsageApiDataSchemaDaysItemTrafficTypesItem.ts b/frontend/src/openapi/models/trafficUsageApiDataSchemaDaysItemTrafficTypesItem.ts
new file mode 100644
index 0000000000..584ced8038
--- /dev/null
+++ b/frontend/src/openapi/models/trafficUsageApiDataSchemaDaysItemTrafficTypesItem.ts
@@ -0,0 +1,12 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+
+export type TrafficUsageApiDataSchemaDaysItemTrafficTypesItem = {
+ /** The number of requests */
+ count: number;
+ /** The traffic group */
+ group: string;
+};
diff --git a/frontend/src/openapi/models/trafficUsageDataSegmentedSchema.ts b/frontend/src/openapi/models/trafficUsageDataSegmentedSchema.ts
new file mode 100644
index 0000000000..10e01e86a2
--- /dev/null
+++ b/frontend/src/openapi/models/trafficUsageDataSegmentedSchema.ts
@@ -0,0 +1,16 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+import type { TrafficUsageApiDataSchema } from './trafficUsageApiDataSchema';
+
+/**
+ * Contains the recorded data usage for each API path, segmented by day and type of traffic
+ */
+export interface TrafficUsageDataSegmentedSchema {
+ /** Contains the recorded daily data usage for each API path */
+ apiData: TrafficUsageApiDataSchema[];
+ /** The year-month period for which the data usage is counted */
+ period: string;
+}
diff --git a/src/lib/features/traffic-data-usage/fake-traffic-data-usage-store.ts b/src/lib/features/traffic-data-usage/fake-traffic-data-usage-store.ts
index 00c038ab23..bb4f555e64 100644
--- a/src/lib/features/traffic-data-usage/fake-traffic-data-usage-store.ts
+++ b/src/lib/features/traffic-data-usage/fake-traffic-data-usage-store.ts
@@ -26,4 +26,7 @@ export class FakeTrafficDataUsageStore implements ITrafficDataUsageStore {
upsert(trafficDataUsage: IStatTrafficUsage): Promise {
throw new Error('Method not implemented.');
}
+ getTrafficDataUsageForPeriod(period: string): Promise {
+ throw new Error('Method not implemented.');
+ }
}
diff --git a/src/lib/features/traffic-data-usage/traffic-data-usage-store-type.ts b/src/lib/features/traffic-data-usage/traffic-data-usage-store-type.ts
index dd39dba9c7..f92a1640d3 100644
--- a/src/lib/features/traffic-data-usage/traffic-data-usage-store-type.ts
+++ b/src/lib/features/traffic-data-usage/traffic-data-usage-store-type.ts
@@ -4,7 +4,7 @@ export type IStatTrafficUsage = {
day: Date;
trafficGroup: string;
statusCodeSeries: number;
- count: number | string;
+ count: number;
};
export interface IStatTrafficUsageKey {
@@ -16,4 +16,5 @@ export interface IStatTrafficUsageKey {
export interface ITrafficDataUsageStore
extends Store {
upsert(trafficDataUsage: IStatTrafficUsage): Promise;
+ getTrafficDataUsageForPeriod(period: string): Promise;
}
diff --git a/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts b/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts
index cbe6bb3629..771a3cff0c 100644
--- a/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts
+++ b/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts
@@ -25,7 +25,7 @@ test('upsert stores new entries', async () => {
day: new Date(),
trafficGroup: 'default',
statusCodeSeries: 200,
- count: '1',
+ count: 1,
};
await trafficDataUsageStore.upsert(data);
const data2 = await trafficDataUsageStore.get({
@@ -34,7 +34,7 @@ test('upsert stores new entries', async () => {
statusCodeSeries: data.statusCodeSeries,
});
expect(data2).toBeDefined();
- expect(data2.count).toBe('1');
+ expect(data2.count).toBe(1);
});
test('upsert upserts', async () => {
@@ -42,13 +42,13 @@ test('upsert upserts', async () => {
day: new Date(),
trafficGroup: 'default2',
statusCodeSeries: 200,
- count: '1',
+ count: 1,
};
const dataSecondTime = {
day: new Date(),
trafficGroup: 'default2',
statusCodeSeries: 200,
- count: '3',
+ count: 3,
};
await trafficDataUsageStore.upsert(data);
await trafficDataUsageStore.upsert(dataSecondTime);
@@ -58,7 +58,7 @@ test('upsert upserts', async () => {
statusCodeSeries: data.statusCodeSeries,
});
expect(data2).toBeDefined();
- expect(data2.count).toBe('4');
+ expect(data2.count).toBe(4);
});
test('getAll returns all', async () => {
@@ -67,13 +67,13 @@ test('getAll returns all', async () => {
day: new Date(),
trafficGroup: 'default3',
statusCodeSeries: 200,
- count: '1',
+ count: 1,
};
const data2 = {
day: new Date(),
trafficGroup: 'default4',
statusCodeSeries: 200,
- count: '3',
+ count: 3,
};
await trafficDataUsageStore.upsert(data1);
await trafficDataUsageStore.upsert(data2);
@@ -88,13 +88,13 @@ test('delete deletes the specified item', async () => {
day: new Date(),
trafficGroup: 'default3',
statusCodeSeries: 200,
- count: '1',
+ count: 1,
};
const data2 = {
day: new Date(),
trafficGroup: 'default4',
statusCodeSeries: 200,
- count: '3',
+ count: 3,
};
await trafficDataUsageStore.upsert(data1);
await trafficDataUsageStore.upsert(data2);
@@ -115,19 +115,19 @@ test('can query for specific items', async () => {
day: new Date(),
trafficGroup: 'default3',
statusCodeSeries: 200,
- count: '1',
+ count: 1,
};
const data2 = {
day: new Date(),
trafficGroup: 'default4',
statusCodeSeries: 200,
- count: '3',
+ count: 3,
};
const data3 = {
day: new Date(),
trafficGroup: 'default5',
statusCodeSeries: 200,
- count: '2',
+ count: 2,
};
await trafficDataUsageStore.upsert(data1);
await trafficDataUsageStore.upsert(data2);
@@ -152,3 +152,46 @@ test('can query for specific items', async () => {
expect(results_status_code).toBeDefined();
expect(results_status_code.length).toBe(3);
});
+
+test('can query for data from specific periods', async () => {
+ await trafficDataUsageStore.deleteAll();
+ const data1 = {
+ day: new Date(2024, 2, 12),
+ trafficGroup: 'default-period-query',
+ statusCodeSeries: 200,
+ count: 1,
+ };
+ const data2 = {
+ day: new Date(2024, 2, 13),
+ trafficGroup: 'default-period-query',
+ statusCodeSeries: 200,
+ count: 3,
+ };
+ const data3 = {
+ day: new Date(2024, 1, 12),
+ trafficGroup: 'default-period-query',
+ statusCodeSeries: 200,
+ count: 2,
+ };
+ const data4 = {
+ day: new Date(2023, 9, 6),
+ trafficGroup: 'default-period-query',
+ statusCodeSeries: 200,
+ count: 12,
+ };
+ await trafficDataUsageStore.upsert(data1);
+ await trafficDataUsageStore.upsert(data2);
+ await trafficDataUsageStore.upsert(data3);
+ await trafficDataUsageStore.upsert(data4);
+
+ const traffic_period_usage =
+ await trafficDataUsageStore.getTrafficDataUsageForPeriod('2024-03');
+ expect(traffic_period_usage).toBeDefined();
+ expect(traffic_period_usage.length).toBe(2);
+
+ const traffic_period_usage_older =
+ await trafficDataUsageStore.getTrafficDataUsageForPeriod('2023-10');
+ expect(traffic_period_usage_older).toBeDefined();
+ expect(traffic_period_usage_older.length).toBe(1);
+ expect(traffic_period_usage_older[0].count).toBe(12);
+});
diff --git a/src/lib/features/traffic-data-usage/traffic-data-usage-store.ts b/src/lib/features/traffic-data-usage/traffic-data-usage-store.ts
index 5025160365..5d288089ba 100644
--- a/src/lib/features/traffic-data-usage/traffic-data-usage-store.ts
+++ b/src/lib/features/traffic-data-usage/traffic-data-usage-store.ts
@@ -23,7 +23,7 @@ const mapRow = (row: any): IStatTrafficUsage => {
day: row.day,
trafficGroup: row.traffic_group,
statusCodeSeries: row.status_code_series,
- count: row.count,
+ count: Number.parseInt(row.count),
};
};
@@ -87,4 +87,14 @@ export class TrafficDataUsageStore implements ITrafficDataUsageStore {
count: this.db.raw('stat_traffic_usage.count + EXCLUDED.count'),
});
}
+
+ async getTrafficDataUsageForPeriod(
+ period: string,
+ ): Promise {
+ const rows = await this.db(TABLE).whereRaw(
+ `to_char(day, 'YYYY-MM') = ?`,
+ [period],
+ );
+ return rows.map(mapRow);
+ }
}