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