mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: fill metrics summary missing datapoints with 0 (#6820)
Fills missing datapoints with 0s so that all metrics chart lines have data for all datapoints. Closes # [1-2278](https://linear.app/unleash/issue/1-2278/fill-the-metrics-data-with-0s-when-not-enough-data-to-fill-the-chart) Before: <img width="1547" alt="Screenshot 2024-04-10 at 12 48 22" src="https://github.com/Unleash/unleash/assets/104830839/35885852-d986-4760-84e2-9e8ef61bedf0"> <img width="1550" alt="Screenshot 2024-04-10 at 12 48 44" src="https://github.com/Unleash/unleash/assets/104830839/3385b8eb-08e2-4cc9-86b2-7b31b9fe81ef"> After: <img width="1582" alt="Screenshot 2024-04-10 at 13 43 10" src="https://github.com/Unleash/unleash/assets/104830839/d3713df3-869b-48ba-b2ab-095027b37506"> <img width="1545" alt="Screenshot 2024-04-10 at 13 42 49" src="https://github.com/Unleash/unleash/assets/104830839/44a6e662-2e9f-4fe8-8299-c15ab8f8e261"> --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
7f1c46a576
commit
68a1ba3dec
@ -49,6 +49,7 @@ interface IChartsProps {
|
||||
};
|
||||
loading: boolean;
|
||||
projects: string[];
|
||||
allMetricsDatapoints: string[];
|
||||
}
|
||||
|
||||
const StyledGrid = styled(Box)(({ theme }) => ({
|
||||
@ -79,6 +80,7 @@ export const InsightsCharts: VFC<IChartsProps> = ({
|
||||
flagTrends,
|
||||
groupedMetricsData,
|
||||
environmentTypeTrends,
|
||||
allMetricsDatapoints,
|
||||
loading,
|
||||
}) => {
|
||||
const showAllProjects = projects[0] === allOption.id;
|
||||
@ -203,6 +205,7 @@ export const InsightsCharts: VFC<IChartsProps> = ({
|
||||
>
|
||||
<MetricsSummaryChart
|
||||
metricsSummaryTrends={groupedMetricsData}
|
||||
allDatapointsSorted={allMetricsDatapoints}
|
||||
isAggregate={showAllProjects}
|
||||
/>
|
||||
</Widget>
|
||||
|
@ -8,25 +8,30 @@ import {
|
||||
NotEnoughData,
|
||||
} from 'component/insights/components/LineChart/LineChart';
|
||||
import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip';
|
||||
import { useMetricsSummary } from 'component/insights/hooks/useMetricsSummary';
|
||||
import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData';
|
||||
import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { aggregateDataPerDate } from './MetricsChartTooltip/aggregate-metrics-by-day';
|
||||
import { aggregateDataPerDate } from './aggregate-metrics-by-day';
|
||||
import { useFilledMetricsSummary } from '../../hooks/useFilledMetricsSummary';
|
||||
|
||||
interface IMetricsSummaryChartProps {
|
||||
metricsSummaryTrends: GroupedDataByProject<
|
||||
InstanceInsightsSchema['metricsSummaryTrends']
|
||||
>;
|
||||
isAggregate?: boolean;
|
||||
allDatapointsSorted: string[];
|
||||
}
|
||||
|
||||
export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
|
||||
metricsSummaryTrends,
|
||||
isAggregate,
|
||||
allDatapointsSorted,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const metricsSummary = useMetricsSummary(metricsSummaryTrends);
|
||||
const metricsSummary = useFilledMetricsSummary(
|
||||
metricsSummaryTrends,
|
||||
allDatapointsSorted,
|
||||
);
|
||||
const notEnoughData = useMemo(
|
||||
() => !metricsSummary.datasets.some((d) => d.data.length > 1),
|
||||
[metricsSummary],
|
||||
|
12
frontend/src/component/insights/hooks/useAllDatapoints.ts
Normal file
12
frontend/src/component/insights/hooks/useAllDatapoints.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useAllDatapoints = <T extends { date: string }>(data: T[]) =>
|
||||
useMemo(() => {
|
||||
const allDataPoints = new Set<string>();
|
||||
|
||||
data.forEach((item) => {
|
||||
allDataPoints.add(item.date);
|
||||
});
|
||||
|
||||
return Array.from(allDataPoints).sort();
|
||||
}, [data]);
|
@ -0,0 +1,54 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useProjectColor } from './useProjectColor';
|
||||
import { useFilledMetricsSummary } from './useFilledMetricsSummary';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
import type { InstanceInsightsSchema } from '../../../openapi';
|
||||
import type { GroupedDataByProject } from './useGroupedProjectTrends';
|
||||
import { vi, type Mock } from 'vitest';
|
||||
|
||||
vi.mock('@mui/material', () => ({
|
||||
useTheme: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./useProjectColor', () => ({
|
||||
useProjectColor: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock data
|
||||
const mockTheme: Partial<Theme> = {};
|
||||
const mockGetProjectColor = (project: string) => `color-${project}`;
|
||||
const mockFilteredMetricsSummaryTrends = {
|
||||
Project1: [{ date: '2024-01-01', totalRequests: 5 }],
|
||||
Project2: [{ date: '2024-01-02', totalRequests: 10 }],
|
||||
} as unknown as GroupedDataByProject<
|
||||
InstanceInsightsSchema['metricsSummaryTrends']
|
||||
>;
|
||||
const mockAllDataPointsSorted = ['2024-01-01', '2024-01-02'];
|
||||
|
||||
beforeEach(() => {
|
||||
(useTheme as Mock).mockReturnValue(mockTheme);
|
||||
(useProjectColor as Mock).mockImplementation(() => mockGetProjectColor);
|
||||
});
|
||||
|
||||
describe('useFilledMetricsSummary', () => {
|
||||
it('returns datasets with normalized data for each project', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilledMetricsSummary(
|
||||
mockFilteredMetricsSummaryTrends,
|
||||
mockAllDataPointsSorted,
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current.datasets).toHaveLength(2); // Expect two projects
|
||||
expect(result.current.datasets[0].data).toHaveLength(2); // Each dataset should have data for both dates
|
||||
|
||||
// Check for normalized missing data
|
||||
const project1DataFor20240102 = result.current.datasets
|
||||
.find((dataset) => dataset.label === 'Project1')
|
||||
?.data.find((data) => data.date === '2024-01-02');
|
||||
expect(project1DataFor20240102).toEqual(
|
||||
expect.objectContaining({ totalRequests: 0 }),
|
||||
); // Missing data should be filled with 0
|
||||
});
|
||||
});
|
@ -0,0 +1,63 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTheme } from '@mui/material';
|
||||
import type {
|
||||
InstanceInsightsSchema,
|
||||
InstanceInsightsSchemaMetricsSummaryTrendsItem,
|
||||
} from 'openapi';
|
||||
import { useProjectColor } from './useProjectColor';
|
||||
import type { GroupedDataByProject } from './useGroupedProjectTrends';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
type MetricsSummaryTrends = InstanceInsightsSchema['metricsSummaryTrends'];
|
||||
|
||||
const weekIdFromDate = (dateString: string) => {
|
||||
return format(new Date(dateString), 'yyyy-ww');
|
||||
};
|
||||
|
||||
export const useFilledMetricsSummary = (
|
||||
filteredMetricsSummaryTrends: GroupedDataByProject<MetricsSummaryTrends>,
|
||||
allDataPointsSorted: string[],
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const getProjectColor = useProjectColor();
|
||||
|
||||
const data = useMemo(() => {
|
||||
const datasets = Object.entries(filteredMetricsSummaryTrends).map(
|
||||
([project, trends]) => {
|
||||
const trendsMap = new Map<
|
||||
string,
|
||||
InstanceInsightsSchemaMetricsSummaryTrendsItem
|
||||
>(trends.map((trend) => [trend.date, trend]));
|
||||
|
||||
const normalizedData = allDataPointsSorted.map((date) => {
|
||||
return (
|
||||
trendsMap.get(date) || {
|
||||
date,
|
||||
totalRequests: 0,
|
||||
totalNo: 0,
|
||||
project,
|
||||
totalApps: 0,
|
||||
totalYes: 0,
|
||||
totalEnvironments: 0,
|
||||
totalFlags: 0,
|
||||
week: weekIdFromDate(date),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const color = getProjectColor(project);
|
||||
return {
|
||||
label: project,
|
||||
data: normalizedData,
|
||||
borderColor: color,
|
||||
backgroundColor: color,
|
||||
fill: false,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return { datasets };
|
||||
}, [theme, filteredMetricsSummaryTrends, getProjectColor]);
|
||||
|
||||
return data;
|
||||
};
|
@ -3,42 +3,47 @@ import type { InstanceInsightsSchema } from 'openapi';
|
||||
import { useFilteredTrends } from './useFilteredTrends';
|
||||
import { useGroupedProjectTrends } from './useGroupedProjectTrends';
|
||||
import { useFilteredFlagsSummary } from './useFilteredFlagsSummary';
|
||||
import { useAllDatapoints } from './useAllDatapoints';
|
||||
|
||||
export const useInsightsData = (
|
||||
executiveDashboardData: InstanceInsightsSchema,
|
||||
instanceInsights: InstanceInsightsSchema,
|
||||
projects: string[],
|
||||
) => {
|
||||
const allMetricsDatapoints = useAllDatapoints(
|
||||
instanceInsights.metricsSummaryTrends,
|
||||
);
|
||||
const projectsData = useFilteredTrends(
|
||||
executiveDashboardData.projectFlagTrends,
|
||||
instanceInsights.projectFlagTrends,
|
||||
projects,
|
||||
);
|
||||
|
||||
const groupedProjectsData = useGroupedProjectTrends(projectsData);
|
||||
|
||||
const metricsData = useFilteredTrends(
|
||||
executiveDashboardData.metricsSummaryTrends,
|
||||
instanceInsights.metricsSummaryTrends,
|
||||
projects,
|
||||
);
|
||||
const groupedMetricsData = useGroupedProjectTrends(metricsData);
|
||||
|
||||
const summary = useFilteredFlagsSummary(
|
||||
projectsData,
|
||||
executiveDashboardData.users,
|
||||
instanceInsights.users,
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
...executiveDashboardData,
|
||||
...instanceInsights,
|
||||
projectsData,
|
||||
groupedProjectsData,
|
||||
metricsData,
|
||||
groupedMetricsData,
|
||||
users: executiveDashboardData.users,
|
||||
environmentTypeTrends: executiveDashboardData.environmentTypeTrends,
|
||||
users: instanceInsights.users,
|
||||
environmentTypeTrends: instanceInsights.environmentTypeTrends,
|
||||
summary,
|
||||
allMetricsDatapoints,
|
||||
}),
|
||||
[
|
||||
executiveDashboardData,
|
||||
instanceInsights,
|
||||
projects,
|
||||
projectsData,
|
||||
groupedProjectsData,
|
||||
|
@ -1,32 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTheme } from '@mui/material';
|
||||
import type { InstanceInsightsSchema } from 'openapi';
|
||||
import { useProjectColor } from './useProjectColor';
|
||||
import type { GroupedDataByProject } from './useGroupedProjectTrends';
|
||||
|
||||
type MetricsSummaryTrends = InstanceInsightsSchema['metricsSummaryTrends'];
|
||||
|
||||
export const useMetricsSummary = (
|
||||
metricsSummaryTrends: GroupedDataByProject<MetricsSummaryTrends>,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const getProjectColor = useProjectColor();
|
||||
|
||||
const data = useMemo(() => {
|
||||
const datasets = Object.entries(metricsSummaryTrends).map(
|
||||
([project, trends]) => {
|
||||
const color = getProjectColor(project);
|
||||
return {
|
||||
label: project,
|
||||
data: trends,
|
||||
borderColor: color,
|
||||
backgroundColor: color,
|
||||
fill: false,
|
||||
};
|
||||
},
|
||||
);
|
||||
return { datasets };
|
||||
}, [theme, metricsSummaryTrends]);
|
||||
|
||||
return data;
|
||||
};
|
Loading…
Reference in New Issue
Block a user