mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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