From c9c3e898d97d11b81ffe07e0d67adda54d600b6d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 18 Sep 2024 00:00:27 +1000 Subject: [PATCH] [8.x] [Synthetics] Fix issue where heatmap UI crashes on undefined histogram data (#192508) (#193154) # Backport This will backport the following commits from `main` to `8.x`: - [[Synthetics] Fix issue where heatmap UI crashes on undefined histogram data (#192508)](https://github.com/elastic/kibana/pull/192508) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Justin Kambic --- .../monitor_status_data.test.ts | 87 +++++++++++++++++++ .../monitor_status/monitor_status_data.ts | 13 ++- .../monitor_status/use_monitor_status_data.ts | 2 +- .../synthetics/state/status_heatmap/index.ts | 2 +- 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.test.ts diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.test.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.test.ts new file mode 100644 index 0000000000000..488db9c2a112b --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createStatusTimeBins, getStatusEffectiveValue } from './monitor_status_data'; + +describe('createStatusTimeBins', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return default values when `heatmapData` is `undefined`', () => { + const timeBuckets = [ + { start: 1000, end: 2000 }, + { start: 2000, end: 3000 }, + ]; + + const result = createStatusTimeBins(timeBuckets, undefined); + + expect(result).toEqual([ + { start: 1000, end: 2000, ups: 0, downs: 0, value: 0 }, + { start: 2000, end: 3000, ups: 0, downs: 0, value: 0 }, + ]); + }); + + it('should calculate `ups` and `downs` correctly from `heatmapData`', () => { + const timeBuckets = [ + { start: 1000, end: 2000 }, + { start: 2000, end: 3000 }, + ]; + + const heatmapData = [ + { key: 1500, key_as_string: '1500', up: { value: 1 }, down: { value: 2 }, doc_count: 3 }, + { key: 2500, key_as_string: '2500', up: { value: 3 }, down: { value: 1 }, doc_count: 4 }, + ]; + + const result = createStatusTimeBins(timeBuckets, heatmapData); + + expect(result).toEqual([ + { start: 1000, end: 2000, ups: 1, downs: 2, value: getStatusEffectiveValue(1, 2) }, + { start: 2000, end: 3000, ups: 3, downs: 1, value: getStatusEffectiveValue(3, 1) }, + ]); + }); + + it('should return value 0 when ups + downs is 0', () => { + const timeBuckets = [ + { start: 1000, end: 2000 }, + { start: 2000, end: 3000 }, + ]; + + const heatmapData = [ + { key: 1500, key_as_string: '1500', up: { value: 0 }, down: { value: 0 }, doc_count: 0 }, + { key: 2500, key_as_string: '2500', up: { value: 0 }, down: { value: 0 }, doc_count: 0 }, + ]; + + const result = createStatusTimeBins(timeBuckets, heatmapData); + + expect(result).toEqual([ + { start: 1000, end: 2000, ups: 0, downs: 0, value: 0 }, + { start: 2000, end: 3000, ups: 0, downs: 0, value: 0 }, + ]); + }); + + it('should filter heatmapData correctly based on start and end values', () => { + const timeBuckets = [ + { start: 1000, end: 2000 }, + { start: 2000, end: 3000 }, + ]; + + const heatmapData = [ + { key: 500, key_as_string: '500', doc_count: 2, up: { value: 1 }, down: { value: 1 } }, + { key: 1500, key_as_string: '1500', doc_count: 5, up: { value: 2 }, down: { value: 3 } }, + { key: 2500, key_as_string: '2500', doc_count: 9, up: { value: 4 }, down: { value: 5 } }, + { key: 3500, key_as_string: '3500', doc_count: 1, up: { value: 6 }, down: { value: 7 } }, + ]; + + const result = createStatusTimeBins(timeBuckets, heatmapData); + + expect(result).toEqual([ + { start: 1000, end: 2000, ups: 2, downs: 3, value: getStatusEffectiveValue(2, 3) }, + { start: 2000, end: 3000, ups: 4, downs: 5, value: getStatusEffectiveValue(4, 5) }, + ]); + }); +}); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.ts index e5ee43aa04f8d..0a76badc574ab 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.ts @@ -114,9 +114,18 @@ export function createTimeBuckets(intervalMinutes: number, from: number, to: num export function createStatusTimeBins( timeBuckets: MonitorStatusTimeBucket[], - heatmapData: MonitorStatusHeatmapBucket[] + heatmapData?: MonitorStatusHeatmapBucket[] ): MonitorStatusTimeBin[] { return timeBuckets.map(({ start, end }) => { + if (!Array.isArray(heatmapData)) { + return { + start, + end, + ups: 0, + downs: 0, + value: 0, + }; + } const { ups, downs } = heatmapData .filter(({ key }) => key >= start && key <= end) .reduce( @@ -163,7 +172,7 @@ export function getBrushData(e: BrushEvent) { return { from, to, fromUtc, toUtc }; } -function getStatusEffectiveValue(ups: number, downs: number): number { +export function getStatusEffectiveValue(ups: number, downs: number): number { if (ups === downs) { return -0.1; } diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts index 59d807cb3bef8..efb001f1776b7 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts @@ -59,7 +59,7 @@ export const useMonitorStatusData = ({ from, to, initialSizeRef }: Props) => { }, [binsAvailableByWidth, initialSizeRef]); useEffect(() => { - if (monitor?.id && location?.label && debouncedBinsCount !== null && minsPerBin !== null) { + if (monitor?.id && location?.label && debouncedBinsCount !== null && !!minsPerBin) { dispatch( quietGetMonitorStatusHeatmapAction.get({ monitorId: monitor.id, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/index.ts index 29f8a1ba87345..b01b7d0e0b918 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/index.ts @@ -18,7 +18,7 @@ import { } from './actions'; export interface MonitorStatusHeatmap { - heatmap: MonitorStatusHeatmapBucket[]; + heatmap?: MonitorStatusHeatmapBucket[]; loading: boolean; error: IHttpSerializedFetchError | null; }