1
0
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:
andreas-unleash 2024-04-10 14:38:45 +03:00 committed by GitHub
parent 7f1c46a576
commit 68a1ba3dec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 153 additions and 43 deletions

View File

@ -49,6 +49,7 @@ interface IChartsProps {
}; };
loading: boolean; loading: boolean;
projects: string[]; projects: string[];
allMetricsDatapoints: string[];
} }
const StyledGrid = styled(Box)(({ theme }) => ({ const StyledGrid = styled(Box)(({ theme }) => ({
@ -79,6 +80,7 @@ export const InsightsCharts: VFC<IChartsProps> = ({
flagTrends, flagTrends,
groupedMetricsData, groupedMetricsData,
environmentTypeTrends, environmentTypeTrends,
allMetricsDatapoints,
loading, loading,
}) => { }) => {
const showAllProjects = projects[0] === allOption.id; const showAllProjects = projects[0] === allOption.id;
@ -203,6 +205,7 @@ export const InsightsCharts: VFC<IChartsProps> = ({
> >
<MetricsSummaryChart <MetricsSummaryChart
metricsSummaryTrends={groupedMetricsData} metricsSummaryTrends={groupedMetricsData}
allDatapointsSorted={allMetricsDatapoints}
isAggregate={showAllProjects} isAggregate={showAllProjects}
/> />
</Widget> </Widget>

View File

@ -8,25 +8,30 @@ import {
NotEnoughData, NotEnoughData,
} from 'component/insights/components/LineChart/LineChart'; } from 'component/insights/components/LineChart/LineChart';
import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip'; import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip';
import { useMetricsSummary } from 'component/insights/hooks/useMetricsSummary';
import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData'; import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData';
import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends'; import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends';
import { useTheme } from '@mui/material'; 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 { interface IMetricsSummaryChartProps {
metricsSummaryTrends: GroupedDataByProject< metricsSummaryTrends: GroupedDataByProject<
InstanceInsightsSchema['metricsSummaryTrends'] InstanceInsightsSchema['metricsSummaryTrends']
>; >;
isAggregate?: boolean; isAggregate?: boolean;
allDatapointsSorted: string[];
} }
export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({ export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
metricsSummaryTrends, metricsSummaryTrends,
isAggregate, isAggregate,
allDatapointsSorted,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const metricsSummary = useMetricsSummary(metricsSummaryTrends); const metricsSummary = useFilledMetricsSummary(
metricsSummaryTrends,
allDatapointsSorted,
);
const notEnoughData = useMemo( const notEnoughData = useMemo(
() => !metricsSummary.datasets.some((d) => d.data.length > 1), () => !metricsSummary.datasets.some((d) => d.data.length > 1),
[metricsSummary], [metricsSummary],

View 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]);

View File

@ -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
});
});

View File

@ -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;
};

View File

@ -3,42 +3,47 @@ import type { InstanceInsightsSchema } from 'openapi';
import { useFilteredTrends } from './useFilteredTrends'; import { useFilteredTrends } from './useFilteredTrends';
import { useGroupedProjectTrends } from './useGroupedProjectTrends'; import { useGroupedProjectTrends } from './useGroupedProjectTrends';
import { useFilteredFlagsSummary } from './useFilteredFlagsSummary'; import { useFilteredFlagsSummary } from './useFilteredFlagsSummary';
import { useAllDatapoints } from './useAllDatapoints';
export const useInsightsData = ( export const useInsightsData = (
executiveDashboardData: InstanceInsightsSchema, instanceInsights: InstanceInsightsSchema,
projects: string[], projects: string[],
) => { ) => {
const allMetricsDatapoints = useAllDatapoints(
instanceInsights.metricsSummaryTrends,
);
const projectsData = useFilteredTrends( const projectsData = useFilteredTrends(
executiveDashboardData.projectFlagTrends, instanceInsights.projectFlagTrends,
projects, projects,
); );
const groupedProjectsData = useGroupedProjectTrends(projectsData); const groupedProjectsData = useGroupedProjectTrends(projectsData);
const metricsData = useFilteredTrends( const metricsData = useFilteredTrends(
executiveDashboardData.metricsSummaryTrends, instanceInsights.metricsSummaryTrends,
projects, projects,
); );
const groupedMetricsData = useGroupedProjectTrends(metricsData); const groupedMetricsData = useGroupedProjectTrends(metricsData);
const summary = useFilteredFlagsSummary( const summary = useFilteredFlagsSummary(
projectsData, projectsData,
executiveDashboardData.users, instanceInsights.users,
); );
return useMemo( return useMemo(
() => ({ () => ({
...executiveDashboardData, ...instanceInsights,
projectsData, projectsData,
groupedProjectsData, groupedProjectsData,
metricsData, metricsData,
groupedMetricsData, groupedMetricsData,
users: executiveDashboardData.users, users: instanceInsights.users,
environmentTypeTrends: executiveDashboardData.environmentTypeTrends, environmentTypeTrends: instanceInsights.environmentTypeTrends,
summary, summary,
allMetricsDatapoints,
}), }),
[ [
executiveDashboardData, instanceInsights,
projects, projects,
projectsData, projectsData,
groupedProjectsData, groupedProjectsData,

View File

@ -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;
};