mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: aggregate metrics for the same timestamp (#5876)
This commit is contained in:
parent
a88763283a
commit
c816ffd49d
@ -20,6 +20,7 @@ import {
|
||||
useQueryParams,
|
||||
withDefault,
|
||||
} from 'use-query-params';
|
||||
import { aggregateFeatureMetrics } from './aggregateFeatureMetrics';
|
||||
|
||||
export const FeatureMetrics = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
@ -53,9 +54,13 @@ export const FeatureMetrics = () => {
|
||||
}, [featureMetrics]);
|
||||
|
||||
const filteredMetrics = useMemo(() => {
|
||||
return cachedMetrics
|
||||
?.filter((metric) => selectedEnvironment === metric.environment)
|
||||
.filter((metric) => selectedApplications.includes(metric.appName));
|
||||
return aggregateFeatureMetrics(
|
||||
cachedMetrics
|
||||
?.filter((metric) => selectedEnvironment === metric.environment)
|
||||
.filter((metric) =>
|
||||
selectedApplications.includes(metric.appName),
|
||||
) || [],
|
||||
);
|
||||
}, [
|
||||
cachedMetrics,
|
||||
selectedEnvironment,
|
||||
|
@ -71,6 +71,6 @@ const createChartPoints = (
|
||||
return metrics.map((metric) => ({
|
||||
x: metric.timestamp,
|
||||
y: y(metric),
|
||||
variants: metric.variants,
|
||||
variants: metric.variants || {},
|
||||
}));
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ test('render hourly metrics stats', async () => {
|
||||
expect(screen.getByText('50%')).toBeInTheDocument();
|
||||
expect(
|
||||
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();
|
||||
});
|
||||
@ -21,7 +21,7 @@ test('render daily metrics stats', async () => {
|
||||
|
||||
expect(
|
||||
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();
|
||||
});
|
||||
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -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,
|
||||
}));
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
export const daysOrHours = (hoursBack: number): string => {
|
||||
if (hoursBack > 48) {
|
||||
return `${Math.floor(hoursBack / 24)} days`;
|
||||
return `${Math.floor(hoursBack / 24)} days (UTC)`;
|
||||
}
|
||||
return `${hoursBack} hours`;
|
||||
return `${hoursBack} hours (local time)`;
|
||||
};
|
||||
|
@ -107,5 +107,5 @@ export interface IFeatureMetricsRaw {
|
||||
timestamp: string;
|
||||
yes: number;
|
||||
no: number;
|
||||
variants: Record<string, number>;
|
||||
variants?: Record<string, number>;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user