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:
parent
a88763283a
commit
c816ffd49d
@ -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(
|
||||||
|
cachedMetrics
|
||||||
?.filter((metric) => selectedEnvironment === metric.environment)
|
?.filter((metric) => selectedEnvironment === metric.environment)
|
||||||
.filter((metric) => selectedApplications.includes(metric.appName));
|
.filter((metric) =>
|
||||||
|
selectedApplications.includes(metric.appName),
|
||||||
|
) || [],
|
||||||
|
);
|
||||||
}, [
|
}, [
|
||||||
cachedMetrics,
|
cachedMetrics,
|
||||||
selectedEnvironment,
|
selectedEnvironment,
|
||||||
|
@ -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 || {},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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 => {
|
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)`;
|
||||||
};
|
};
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user