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

feat: aggregate metrics for the same timestamp (#5876)

This commit is contained in:
Mateusz Kwasniewski 2024-01-12 12:19:30 +01:00 committed by GitHub
parent a88763283a
commit c816ffd49d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 125 additions and 9 deletions

View File

@ -20,6 +20,7 @@ import {
useQueryParams, useQueryParams,
withDefault, withDefault,
} from 'use-query-params'; } from 'use-query-params';
import { aggregateFeatureMetrics } from './aggregateFeatureMetrics';
export const FeatureMetrics = () => { export const FeatureMetrics = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
@ -53,9 +54,13 @@ export const FeatureMetrics = () => {
}, [featureMetrics]); }, [featureMetrics]);
const filteredMetrics = useMemo(() => { const filteredMetrics = useMemo(() => {
return cachedMetrics return aggregateFeatureMetrics(
?.filter((metric) => selectedEnvironment === metric.environment) cachedMetrics
.filter((metric) => selectedApplications.includes(metric.appName)); ?.filter((metric) => selectedEnvironment === metric.environment)
.filter((metric) =>
selectedApplications.includes(metric.appName),
) || [],
);
}, [ }, [
cachedMetrics, cachedMetrics,
selectedEnvironment, selectedEnvironment,

View File

@ -71,6 +71,6 @@ const createChartPoints = (
return metrics.map((metric) => ({ return metrics.map((metric) => ({
x: metric.timestamp, x: metric.timestamp,
y: y(metric), y: y(metric),
variants: metric.variants, variants: metric.variants || {},
})); }));
}; };

View File

@ -9,7 +9,7 @@ test('render hourly metrics stats', async () => {
expect(screen.getByText('50%')).toBeInTheDocument(); expect(screen.getByText('50%')).toBeInTheDocument();
expect( expect(
screen.getByText( screen.getByText(
'Total requests for the feature in the environment in the last 48 hours.', 'Total requests for the feature in the environment in the last 48 hours (local time).',
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@ -21,7 +21,7 @@ test('render daily metrics stats', async () => {
expect( expect(
screen.getByText( screen.getByText(
'Total requests for the feature in the environment in the last 7 days.', 'Total requests for the feature in the environment in the last 7 days (UTC).',
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });

View File

@ -0,0 +1,76 @@
import { IFeatureMetricsRaw } from 'interfaces/featureToggle';
import { aggregateFeatureMetrics } from './aggregateFeatureMetrics';
describe('aggregateFeatureMetrics', () => {
it('should aggregate yes and no values for identical timestamps', () => {
const data: IFeatureMetricsRaw[] = [
{
featureName: 'Feature1',
appName: 'App1',
environment: 'dev',
timestamp: '2024-01-12T08:00:00.000Z',
yes: 10,
no: 5,
variants: { a: 1, b: 2 },
},
{
featureName: 'Feature1',
appName: 'App1',
environment: 'dev',
timestamp: '2024-01-12T08:00:00.000Z',
yes: 15,
no: 10,
variants: { a: 2, b: 1 },
},
];
const result = aggregateFeatureMetrics(data);
expect(result).toEqual([
{
featureName: 'Feature1',
appName: 'App1',
environment: 'dev',
timestamp: '2024-01-12T08:00:00.000Z',
yes: 25,
no: 15,
variants: { a: 3, b: 3 },
},
]);
});
it('should handle undefined variants correctly', () => {
const data: IFeatureMetricsRaw[] = [
{
featureName: 'Feature2',
appName: 'App2',
environment: 'test',
timestamp: '2024-01-13T09:00:00.000Z',
yes: 20,
no: 10,
variants: undefined,
},
{
featureName: 'Feature2',
appName: 'App2',
environment: 'test',
timestamp: '2024-01-13T09:00:00.000Z',
yes: 30,
no: 15,
variants: undefined,
},
];
const result = aggregateFeatureMetrics(data);
expect(result).toEqual([
{
featureName: 'Feature2',
appName: 'App2',
environment: 'test',
timestamp: '2024-01-13T09:00:00.000Z',
yes: 50,
no: 25,
variants: undefined,
},
]);
});
});

View File

@ -0,0 +1,35 @@
import { IFeatureMetricsRaw } from 'interfaces/featureToggle';
// multiple applications may have metrics for the same timestamp
export const aggregateFeatureMetrics = (
metrics: IFeatureMetricsRaw[],
): IFeatureMetricsRaw[] => {
const resultMap = new Map<string, IFeatureMetricsRaw>();
metrics.forEach((obj) => {
let aggregated = resultMap.get(obj.timestamp);
if (!aggregated) {
aggregated = { ...obj, yes: 0, no: 0, variants: {} };
resultMap.set(obj.timestamp, aggregated);
}
aggregated.yes += obj.yes;
aggregated.no += obj.no;
if (obj.variants) {
aggregated.variants = aggregated.variants || {};
for (const [key, value] of Object.entries(obj.variants)) {
aggregated.variants[key] =
(aggregated.variants[key] || 0) + value;
}
}
});
return Array.from(resultMap.values()).map((item) => ({
...item,
variants:
item.variants && Object.keys(item.variants).length === 0
? undefined
: item.variants,
}));
};

View File

@ -1,6 +1,6 @@
export const daysOrHours = (hoursBack: number): string => { export const daysOrHours = (hoursBack: number): string => {
if (hoursBack > 48) { if (hoursBack > 48) {
return `${Math.floor(hoursBack / 24)} days`; return `${Math.floor(hoursBack / 24)} days (UTC)`;
} }
return `${hoursBack} hours`; return `${hoursBack} hours (local time)`;
}; };

View File

@ -107,5 +107,5 @@ export interface IFeatureMetricsRaw {
timestamp: string; timestamp: string;
yes: number; yes: number;
no: number; no: number;
variants: Record<string, number>; variants?: Record<string, number>;
} }