diff --git a/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetrics.tsx b/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetrics.tsx index 0e624426fa..44c10c5164 100644 --- a/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetrics.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetrics.tsx @@ -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, diff --git a/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsChart/createChartData.ts b/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsChart/createChartData.ts index 0ab709b7ad..362e3828e7 100644 --- a/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsChart/createChartData.ts +++ b/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsChart/createChartData.ts @@ -71,6 +71,6 @@ const createChartPoints = ( return metrics.map((metric) => ({ x: metric.timestamp, y: y(metric), - variants: metric.variants, + variants: metric.variants || {}, })); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats.test.tsx b/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats.test.tsx index ab76caaac4..9fd51244d1 100644 --- a/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats.test.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats.test.tsx @@ -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(); }); diff --git a/frontend/src/component/feature/FeatureView/FeatureMetrics/aggregateFeatureMetrics.test.ts b/frontend/src/component/feature/FeatureView/FeatureMetrics/aggregateFeatureMetrics.test.ts new file mode 100644 index 0000000000..a45735fd94 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureMetrics/aggregateFeatureMetrics.test.ts @@ -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, + }, + ]); + }); +}); diff --git a/frontend/src/component/feature/FeatureView/FeatureMetrics/aggregateFeatureMetrics.ts b/frontend/src/component/feature/FeatureView/FeatureMetrics/aggregateFeatureMetrics.ts new file mode 100644 index 0000000000..6ea7079f08 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureMetrics/aggregateFeatureMetrics.ts @@ -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(); + + 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, + })); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureMetrics/daysOrHours.ts b/frontend/src/component/feature/FeatureView/FeatureMetrics/daysOrHours.ts index 9c318f5d10..80837649cb 100644 --- a/frontend/src/component/feature/FeatureView/FeatureMetrics/daysOrHours.ts +++ b/frontend/src/component/feature/FeatureView/FeatureMetrics/daysOrHours.ts @@ -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)`; }; diff --git a/frontend/src/interfaces/featureToggle.ts b/frontend/src/interfaces/featureToggle.ts index 10efd23c53..059b730c67 100644 --- a/frontend/src/interfaces/featureToggle.ts +++ b/frontend/src/interfaces/featureToggle.ts @@ -107,5 +107,5 @@ export interface IFeatureMetricsRaw { timestamp: string; yes: number; no: number; - variants: Record; + variants?: Record; }