From a06bfb18d03b7aba0407f57675f102f5468e624f Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Wed, 2 Dec 2020 19:28:47 +0000 Subject: [PATCH 1/8] Add a new synthetics step detail page for displaying waterfall data --- x-pack/plugins/uptime/common/constants/ui.ts | 2 + .../uptime/common/runtime_types/index.ts | 1 + .../common/runtime_types/network_events.ts | 56 ++ .../uptime/common/runtime_types/ping/ping.ts | 18 + .../components/common/step_detail_link.tsx | 32 + .../__tests__/executed_journey.test.tsx | 2 + .../__tests__/executed_step.test.tsx | 352 ++++++++- .../monitor/synthetics/executed_journey.tsx | 2 +- .../monitor/synthetics/executed_step.tsx | 160 ++-- .../synthetics/step_detail/step_detail.tsx | 142 ++++ .../step_detail/step_detail_container.tsx | 114 +++ .../waterfall/data_formatting.test.ts | 153 ++++ .../waterfall}/data_formatting.ts | 87 +-- .../waterfall}/types.ts | 19 +- .../waterfall/waterfall_chart_container.tsx | 66 ++ .../waterfall}/waterfall_chart_wrapper.tsx | 13 +- .../waterfall/components/sidebar.tsx | 6 +- .../waterfall/components/waterfall_chart.tsx | 6 +- .../synthetics/data_formatting.test.ts | 687 ------------------ .../uptime/public/hooks/use_telemetry.ts | 1 + x-pack/plugins/uptime/public/pages/index.ts | 1 + .../uptime/public/pages/step_detail_page.tsx | 20 + x-pack/plugins/uptime/public/routes.tsx | 10 +- .../public/state/actions/network_events.ts | 27 + .../uptime/public/state/api/network_events.ts | 25 + .../uptime/public/state/effects/index.ts | 2 + .../public/state/effects/network_events.ts | 39 + .../uptime/public/state/reducers/index.ts | 2 + .../uptime/public/state/reducers/journey.ts | 4 +- .../public/state/reducers/network_events.ts | 122 ++++ .../state/selectors/__tests__/index.test.ts | 1 + .../uptime/public/state/selectors/index.ts | 2 + .../__tests__/get_network_events.test.ts | 422 +++++++++++ .../lib/requests/get_journey_details.ts | 127 ++++ .../server/lib/requests/get_network_events.ts | 56 ++ .../uptime/server/lib/requests/index.ts | 4 + .../plugins/uptime/server/rest_api/index.ts | 2 + .../network_events/get_network_events.ts | 35 + .../server/rest_api/network_events/index.ts | 7 + .../uptime/server/rest_api/pings/journeys.ts | 6 + 40 files changed, 1991 insertions(+), 842 deletions(-) create mode 100644 x-pack/plugins/uptime/common/runtime_types/network_events.ts create mode 100644 x-pack/plugins/uptime/public/components/common/step_detail_link.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts rename x-pack/plugins/uptime/public/components/monitor/synthetics/{waterfall/consumers/synthetics => step_detail/waterfall}/data_formatting.ts (79%) rename x-pack/plugins/uptime/public/components/monitor/synthetics/{waterfall/consumers/synthetics => step_detail/waterfall}/types.ts (87%) create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx rename x-pack/plugins/uptime/public/components/monitor/synthetics/{waterfall/consumers/synthetics => step_detail/waterfall}/waterfall_chart_wrapper.tsx (91%) delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts create mode 100644 x-pack/plugins/uptime/public/pages/step_detail_page.tsx create mode 100644 x-pack/plugins/uptime/public/state/actions/network_events.ts create mode 100644 x-pack/plugins/uptime/public/state/api/network_events.ts create mode 100644 x-pack/plugins/uptime/public/state/effects/network_events.ts create mode 100644 x-pack/plugins/uptime/public/state/reducers/network_events.ts create mode 100644 x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts create mode 100644 x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts create mode 100644 x-pack/plugins/uptime/server/lib/requests/get_network_events.ts create mode 100644 x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts create mode 100644 x-pack/plugins/uptime/server/rest_api/network_events/index.ts diff --git a/x-pack/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts index 3bf3e3cc0a2cc..f014427d80452 100644 --- a/x-pack/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -12,6 +12,8 @@ export const SETTINGS_ROUTE = '/settings'; export const CERTIFICATES_ROUTE = '/certificates'; +export const STEP_DETAIL_ROUTE = '/journey/:checkGroupId/step/:stepIndex'; + export enum STATUS { UP = 'up', DOWN = 'down', diff --git a/x-pack/plugins/uptime/common/runtime_types/index.ts b/x-pack/plugins/uptime/common/runtime_types/index.ts index e80471bf8b56f..43487eca69e9b 100644 --- a/x-pack/plugins/uptime/common/runtime_types/index.ts +++ b/x-pack/plugins/uptime/common/runtime_types/index.ts @@ -12,3 +12,4 @@ export * from './monitor'; export * from './overview_filters'; export * from './ping'; export * from './snapshot'; +export * from './network_events'; diff --git a/x-pack/plugins/uptime/common/runtime_types/network_events.ts b/x-pack/plugins/uptime/common/runtime_types/network_events.ts new file mode 100644 index 0000000000000..392bd08d75771 --- /dev/null +++ b/x-pack/plugins/uptime/common/runtime_types/network_events.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +const NetworkTimingsType = t.type({ + dns_start: t.number, + push_end: t.number, + worker_fetch_start: t.number, + worker_respond_with_settled: t.number, + proxy_end: t.number, + worker_start: t.number, + worker_ready: t.number, + send_end: t.number, + connect_end: t.number, + connect_start: t.number, + send_start: t.number, + proxy_start: t.number, + push_start: t.number, + ssl_end: t.number, + receive_headers_end: t.number, + ssl_start: t.number, + request_time: t.number, + dns_end: t.number, +}); + +export type NetworkTimings = t.TypeOf; + +const NetworkEventType = t.intersection([ + t.type({ + timestamp: t.string, + requestSentTime: t.number, + loadEndTime: t.number, + }), + t.partial({ + method: t.string, + url: t.string, + status: t.number, + mimeType: t.string, + requestStartTime: t.number, + timings: NetworkTimingsType, + }), +]); + +export type NetworkEvent = t.TypeOf; + +export const SyntheticsNetworkEventsApiResponseType = t.type({ + events: t.array(NetworkEventType), +}); + +export type SyntheticsNetworkEventsApiResponse = t.TypeOf< + typeof SyntheticsNetworkEventsApiResponseType +>; diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index f9dde011b25fe..b63fb365fa3ca 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -241,6 +241,24 @@ export const PingType = t.intersection([ export const SyntheticsJourneyApiResponseType = t.type({ checkGroup: t.string, + details: t.union([ + t.intersection([ + t.type({ + timestamp: t.string, + }), + t.partial({ + next: t.type({ + timestamp: t.string, + checkGroup: t.string, + }), + previous: t.type({ + timestamp: t.string, + checkGroup: t.string, + }), + }), + ]), + t.null, + ]), steps: t.array(PingType), }); diff --git a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx new file mode 100644 index 0000000000000..a8e4c90f2d29a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { Link } from 'react-router-dom'; + +interface StepDetailLinkProps { + /** + * The ID of the check group (the journey run) + */ + checkGroupId: string; + /** + * The index of the step + */ + stepIndex: number; +} + +export const StepDetailLink: FC = ({ children, checkGroupId, stepIndex }) => { + const to = `/journey/${checkGroupId}/step/${stepIndex}`; + + return ( + + + {children} + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx index d6f422b5c7094..030b1a49009ef 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx @@ -206,6 +206,7 @@ describe('ExecutedJourney component', () => { direction="column" > { } /> { let step: Ping; @@ -34,8 +34,11 @@ describe('ExecutedStep', () => { }); it('renders correct step heading', () => { - expect(mountWithIntl().find('EuiText')) - .toMatchInlineSnapshot(` + expect( + mountWithRouter().find( + 'EuiText' + ) + ).toMatchInlineSnapshot(`
{ `); }); + it('renders a link to the step detail view', () => { + expect( + mountWithRouter().find( + 'StepDetailLink' + ) + ).toMatchInlineSnapshot(` + + + + + + `); + }); + it('supplies status badge correct status', () => { step.synthetics = { payload: { status: 'THE_STATUS' }, }; - expect(shallowWithIntl().find('StatusBadge')) - .toMatchInlineSnapshot(` + expect( + mountWithRouter().find( + 'StatusBadge' + ) + ).toMatchInlineSnapshot(` + > + + + + + + + + `); }); @@ -86,8 +170,11 @@ describe('ExecutedStep', () => { }, }; - expect(shallowWithIntl().find('CodeBlockAccordion')) - .toMatchInlineSnapshot(` + expect( + mountWithRouter().find( + 'CodeBlockAccordion' + ) + ).toMatchInlineSnapshot(` Array [ { language="javascript" overflowHeight={360} > - const someVar = "the var" + +
+
+ +
+
+ +
+
+ + +
+
+                              
+                                const someVar = "the var"
+                              
+                            
+
+
+
+
+
+
+
+
+
, { language="html" overflowHeight={360} > - There was an error executing the step. + +
+
+ +
+
+ +
+
+ + +
+
+                              
+                                There was an error executing the step.
+                              
+                            
+
+
+
+
+
+
+
+
+
, { language="html" overflowHeight={360} > - some.stack.trace.string + +
+
+ +
+
+ +
+
+ + +
+
+                              
+                                some.stack.trace.string
+                              
+                            
+
+
+
+
+
+
+
+
+
, ] `); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx index 0c47e4c73e976..a9748524d1bb3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx @@ -78,7 +78,7 @@ export const ExecutedJourney: FC = ({ journey }) => { {journey.steps.filter(isStepEnd).map((step, index) => ( - + ))} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx index 5966851973af2..df2eea4b83b0b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx @@ -12,80 +12,104 @@ import { CodeBlockAccordion } from './code_block_accordion'; import { StepScreenshotDisplay } from './step_screenshot_display'; import { StatusBadge } from './status_badge'; import { Ping } from '../../../../common/runtime_types'; +import { StepDetailLink } from '../../common/step_detail_link'; const CODE_BLOCK_OVERFLOW_HEIGHT = 360; interface ExecutedStepProps { step: Ping; index: number; + checkGroup: string; } -export const ExecutedStep: FC = ({ step, index }) => ( - <> -
-
- - - - - +export const ExecutedStep: FC = ({ step, index, checkGroup }) => { + return ( + <> +
+
+ {step.synthetics?.step?.index && checkGroup ? ( + + + + + + + + ) : ( + + + + + + )} +
+ +
+ +
+ +
+ + + + + + + {step.synthetics?.payload?.source} + + + {step.synthetics?.error?.message} + + + {step.synthetics?.error?.stack} + + + +
- -
- -
- -
- - - - - - - {step.synthetics?.payload?.source} - - - {step.synthetics?.error?.message} - - - {step.synthetics?.error?.stack} - - - -
-
- -); + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx new file mode 100644 index 0000000000000..fd68edef3226b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, + EuiButtonEmpty, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import moment from 'moment'; +import { WaterfallChartContainer } from './waterfall/waterfall_chart_container'; + +export const PREVIOUS_CHECK_BUTTON_TEXT = i18n.translate( + 'xpack.uptime.synthetics.stepDetail.previousCheckButtonText', + { + defaultMessage: 'Previous check', + } +); + +export const NEXT_CHECK_BUTTON_TEXT = i18n.translate( + 'xpack.uptime.synthetics.stepDetail.nextCheckButtonText', + { + defaultMessage: 'Next check', + } +); + +interface Props { + checkGroup: string; + stepName?: string; + stepIndex: number; + totalSteps: number; + hasPreviousStep: boolean; + hasNextStep: boolean; + handlePreviousStep: () => void; + handleNextStep: () => void; + handleNextRun: () => void; + handlePreviousRun: () => void; + previousCheckGroup?: string; + nextCheckGroup?: string; + checkTimestamp?: string; + dateFormat: string; +} + +export const StepDetail: React.FC = ({ + dateFormat, + stepName, + checkGroup, + stepIndex, + totalSteps, + hasPreviousStep, + hasNextStep, + handlePreviousStep, + handleNextStep, + handlePreviousRun, + handleNextRun, + previousCheckGroup, + nextCheckGroup, + checkTimestamp, +}) => { + return ( + <> + + + + + +

{stepName}

+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + {PREVIOUS_CHECK_BUTTON_TEXT} + + + + {moment(checkTimestamp).format(dateFormat)} + + + + {NEXT_CHECK_BUTTON_TEXT} + + + + +
+ + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx new file mode 100644 index 0000000000000..58cf8d6e492da --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiLoadingSpinner } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect, useCallback, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import moment from 'moment'; +import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs'; +import { getJourneySteps } from '../../../../state/actions/journey'; +import { journeySelector } from '../../../../state/selectors'; +import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; +import { StepDetail } from './step_detail'; + +export const NO_STEP_DATA = i18n.translate('xpack.uptime.synthetics.stepDetail.noData', { + defaultMessage: 'No data could be found for this step', +}); + +interface Props { + checkGroup: string; + stepIndex: number; +} + +export const StepDetailContainer: React.FC = ({ checkGroup, stepIndex }) => { + const dispatch = useDispatch(); + const history = useHistory(); + + const [dateFormat] = useUiSetting$('dateFormat'); + + useEffect(() => { + if (checkGroup) { + dispatch(getJourneySteps({ checkGroup })); + } + }, [dispatch, checkGroup]); + + const journeys = useSelector(journeySelector); + const journey = journeys[checkGroup ?? '']; + + const { activeStep, hasPreviousStep, hasNextStep } = useMemo(() => { + return { + hasPreviousStep: stepIndex > 1 ? true : false, + activeStep: journey?.steps?.find((step) => step.synthetics?.step?.index === stepIndex), + hasNextStep: journey && journey.steps && stepIndex < journey.steps.length ? true : false, + }; + }, [stepIndex, journey]); + + useBreadcrumbs([ + ...(activeStep?.monitor?.name ? [{ text: activeStep?.monitor?.name }] : []), + ...(journey?.details?.timestamp + ? [{ text: moment(journey?.details?.timestamp).format(dateFormat) }] + : []), + ]); + + const handleNextStep = useCallback(() => { + history.push(`/journey/${checkGroup}/step/${stepIndex + 1}`); + }, [history, checkGroup, stepIndex]); + + const handlePreviousStep = useCallback(() => { + history.push(`/journey/${checkGroup}/step/${stepIndex - 1}`); + }, [history, checkGroup, stepIndex]); + + const handleNextRun = useCallback(() => { + history.push(`/journey/${journey?.details?.next?.checkGroup}/step/1`); + }, [history, journey?.details?.next?.checkGroup]); + + const handlePreviousRun = useCallback(() => { + history.push(`/journey/${journey?.details?.previous?.checkGroup}/step/1`); + }, [history, journey?.details?.previous?.checkGroup]); + + return ( + <> + + {(!journey || journey.loading) && ( + + + + + + )} + {journey && !activeStep && !journey.loading && ( + + + +

{NO_STEP_DATA}

+
+
+
+ )} + {journey && activeStep && !journey.loading && ( + + )} +
+ + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts new file mode 100644 index 0000000000000..03e025bf54190 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { colourPalette, getTimings, getSeriesAndDomain, extractItems } from './data_formatting'; + +const networkEvent = { + timestamp: '2020-12-06T19:55:01.273Z', + method: 'GET', + url: 'https://www.some-fake-url.css', + status: 200, + mimeType: 'text/css', + requestSentTime: 1723033619.631, + requestStartTime: 1723033621.036, + loadEndTime: 1723033729.635, + timings: { + ssl_start: 32.875, + proxy_end: -1, + send_start: 69.738, + send_end: 69.923, + connect_start: 0.591, + receive_headers_end: 106.076, + dns_end: 0.591, + connect_end: 69.557, + worker_fetch_start: -1, + worker_ready: -1, + push_start: 0, + dns_start: 0.539, + ssl_end: 69.542, + request_time: 1723033.621036, + worker_respond_with_settled: -1, + worker_start: -1, + push_end: 0, + proxy_start: -1, + }, +}; + +describe('getTimings', () => { + it('Calculates timings for network events correctly', () => { + const timings = getTimings( + networkEvent.timings, + networkEvent.requestSentTime, + networkEvent.loadEndTime + ); + expect(timings).toEqual({ + blocked: 1.9439999713897707, + connect: 32.480000000000004, + dns: 0.051999999999999935, + receive: 2.5230000019073486, + send: 0.18500000000000227, + ssl: 36.667, + wait: 36.15299999999999, + }); + }); +}); + +describe('getSeriesAndDomain', () => { + let seriesAndDomain: any; + let NetworkItems: any; + + beforeAll(() => { + NetworkItems = extractItems([networkEvent]); + seriesAndDomain = getSeriesAndDomain(NetworkItems); + }); + + it('Correctly calculates the domain', () => { + expect(seriesAndDomain.domain).toEqual({ max: 110.00399997329711, min: 0 }); + }); + + it('Correctly calculates the series', () => { + expect(seriesAndDomain.series).toEqual([ + { + config: { + colour: '#b9a888', + tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 1.944ms' }, + }, + x: 0, + y: 1.9439999713897707, + y0: 0, + }, + { + config: { colour: '#54b399', tooltipProps: { colour: '#54b399', value: 'DNS: 0.052ms' } }, + x: 0, + y: 1.9959999713897707, + y0: 1.9439999713897707, + }, + { + config: { + colour: '#da8b45', + tooltipProps: { colour: '#da8b45', value: 'Connecting: 32.480ms' }, + }, + x: 0, + y: 34.475999971389776, + y0: 1.9959999713897707, + }, + { + config: { colour: '#edc5a2', tooltipProps: { colour: '#edc5a2', value: 'SSL: 36.667ms' } }, + x: 0, + y: 71.14299997138977, + y0: 34.475999971389776, + }, + { + config: { + colour: '#d36086', + tooltipProps: { colour: '#d36086', value: 'Sending request: 0.185ms' }, + }, + x: 0, + y: 71.32799997138977, + y0: 71.14299997138977, + }, + { + config: { + colour: '#b0c9e0', + tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 36.153ms' }, + }, + x: 0, + y: 107.48099997138976, + y0: 71.32799997138977, + }, + { + config: { + colour: '#ca8eae', + tooltipProps: { colour: '#ca8eae', value: 'Content downloading: 2.523ms' }, + }, + x: 0, + y: 110.00399997329711, + y0: 107.48099997138976, + }, + ]); + }); +}); + +describe('Palettes', () => { + it('A colour palette comprising timing and mime type colours is correctly generated', () => { + expect(colourPalette).toEqual({ + blocked: '#b9a888', + connect: '#da8b45', + dns: '#54b399', + font: '#aa6556', + html: '#f3b3a6', + media: '#d6bf57', + other: '#e7664c', + receive: '#54b399', + script: '#9170b8', + send: '#d36086', + ssl: '#edc5a2', + stylesheet: '#ca8eae', + wait: '#b0c9e0', + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts similarity index 79% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts rename to x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 9c66ea638c942..5badee09e9371 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -10,6 +10,7 @@ import { PayloadTimings, CalculatedTimings, NetworkItems, + NetworkItem, FriendlyTimingLabels, FriendlyMimetypeLabels, MimeType, @@ -19,7 +20,8 @@ import { SidebarItems, LegendItems, } from './types'; -import { WaterfallData } from '../../../waterfall'; +import { WaterfallData } from '../../waterfall'; +import { NetworkEvent } from '../../../../../../common/runtime_types'; const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 1000); @@ -29,12 +31,14 @@ const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 10 // https://chromium.googlesource.com/chromium/blink.git/+/master/Source/devtools/front_end/sdk/HAREntry.js#131 // and // https://github.com/cyrus-and/chrome-har-capturer/blob/master/lib/har.js#L195 +// and +// https://github.com/sitespeedio/chrome-har/blob/4586d2961fe8752982120c3f613b8da42cf3648b/lib/finalizeEntry.js#L7 // Order of events: request_start = 0, [proxy], [dns], [connect [ssl]], [send], receive_headers_end export const getTimings = ( - timings: PayloadTimings, - requestSentTime: number, - responseReceivedTime: number + timings: NetworkEvent['timings'], + requestSentTime: NetworkEvent['requestSentTime'], + loadEndTime: NetworkEvent['loadEndTime'] ): CalculatedTimings => { if (!timings) return { blocked: -1, dns: -1, connect: -1, send: 0, wait: 0, receive: 0, ssl: -1 }; @@ -98,10 +102,11 @@ export const getTimings = ( const send = timings.send_end - timings.send_start; // Wait - const wait = timings.receive_headers_end - timings.send_end; + const waitStart = timings.send_end; + const waitEnd = timings.receive_headers_end; + const wait = waitEnd - waitStart; - // Receive - const receive = responseReceivedTime - (requestStartTime + timings.receive_headers_end); + const receive = loadEndTime - (requestStartTime + timings.receive_headers_end); // SSL connection is a part of the overall connection time if (connect && ssl) { @@ -111,43 +116,19 @@ export const getTimings = ( return { blocked, dns, connect, send, wait, receive, ssl }; }; -// TODO: Switch to real API data, and type data as the payload response (if server response isn't preformatted) -export const extractItems = (data: any): NetworkItems => { - const items = data - .map((entry: any) => { - const requestSentTime = microToMillis(entry.synthetics.payload.start); - const responseReceivedTime = microToMillis(entry.synthetics.payload.end); - const requestStartTime = - entry.synthetics.payload.response && entry.synthetics.payload.response.timing - ? microToMillis(entry.synthetics.payload.response.timing.request_time) - : null; - +export const extractItems = (data: NetworkEvent[]): NetworkItems => { + return data + .map((entry) => { return { - timestamp: entry['@timestamp'], - method: entry.synthetics.payload.method, - url: entry.synthetics.payload.url, - status: entry.synthetics.payload.status, - mimeType: entry.synthetics.payload?.response?.mime_type, - requestSentTime, - responseReceivedTime, - earliestRequestTime: requestStartTime - ? Math.min(requestSentTime, requestStartTime) - : requestSentTime, - timings: - entry.synthetics.payload.response && entry.synthetics.payload.response.timing - ? getTimings( - entry.synthetics.payload.response.timing, - requestSentTime, - responseReceivedTime - ) - : null, + ...entry, + timings: entry.timings + ? getTimings(entry.timings, entry.requestSentTime, entry.loadEndTime) + : undefined, }; }) - .sort((a: any, b: any) => { - return a.earliestRequestTime - b.earliestRequestTime; + .sort((a: NetworkItem, b: NetworkItem) => { + return a.requestSentTime - b.requestSentTime; }); - - return items; }; const formatValueForDisplay = (value: number, points: number = 3) => { @@ -162,29 +143,37 @@ const getColourForMimeType = (mimeType?: string) => { export const getSeriesAndDomain = (items: NetworkItems) => { // The earliest point in time a request is sent or started. This will become our notion of "0". const zeroOffset = items.reduce((acc, item) => { - const { earliestRequestTime } = item; - return earliestRequestTime < acc ? earliestRequestTime : acc; + const { requestSentTime } = item; + return requestSentTime < acc ? requestSentTime : acc; }, Infinity); const series = items.reduce((acc, item, index) => { - const { earliestRequestTime } = item; + const { requestSentTime } = item; // Entries without timings should be handled differently: // https://github.com/ChromeDevTools/devtools-frontend/blob/ed2a064ac194bfae4e25c4748a9fa3513b3e9f7d/front_end/network/RequestTimingView.js#L140 - // If there are no concrete timings just plot one block via start and end + // If there are no concrete timings just plot one block via request start and response end if (!item.timings || item.timings === null) { - const duration = item.responseReceivedTime - item.earliestRequestTime; + const duration = item.loadEndTime - item.requestSentTime; const colour = getColourForMimeType(item.mimeType); return [ ...acc, { x: index, - y0: item.earliestRequestTime - zeroOffset, - y: item.responseReceivedTime - zeroOffset, + y0: item.requestSentTime - zeroOffset, + // NOTE: The loadEndTime can sometimes be "0" + y: + item.loadEndTime && item.loadEndTime > 0 + ? item.loadEndTime - zeroOffset + : item.requestSentTime - zeroOffset, config: { colour, tooltipProps: { - value: `${formatValueForDisplay(duration)}ms`, + // NOTE: The loadEndTime can sometimes be "0" + value: + item.loadEndTime && item.loadEndTime > 0 + ? `${formatValueForDisplay(duration)}ms` + : "Response time couldn't be determined", colour, }, }, @@ -192,7 +181,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { ]; } - let currentOffset = earliestRequestTime - zeroOffset; + let currentOffset = requestSentTime - zeroOffset; TIMING_ORDER.forEach((timing) => { const value = item.timings![timing]; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts similarity index 87% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts rename to x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts index 1dd58b4f86db3..708aca4531513 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { NetworkEvent } from '../../../../../../common/runtime_types'; export enum Timings { Blocked = 'blocked', @@ -144,21 +145,9 @@ export const MimeTypesMap: Record = { 'application/font-sfnt': MimeType.Font, }; -export interface NetworkItem { - timestamp: string; - method: string; - url: string; - status: number; - mimeType?: string; - // NOTE: This is the time the request was actually issued. timing.request_time might be later if the request was queued. - requestSentTime: number; - responseReceivedTime: number; - // NOTE: Denotes the earlier figure out of request sent time and request start time (part of timings). This can vary based on queue times, and - // also whether an entry actually has timings available. - // Ref: https://github.com/ChromeDevTools/devtools-frontend/blob/ed2a064ac194bfae4e25c4748a9fa3513b3e9f7d/front_end/network/RequestTimingView.js#L154 - earliestRequestTime: number; - timings: CalculatedTimings | null; -} +export type NetworkItem = Omit & { + timings?: CalculatedTimings; +}; export type NetworkItems = NetworkItem[]; // NOTE: A number will always be present if the property exists, but that number might be -1, which represents no value. diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx new file mode 100644 index 0000000000000..7657ca7f9c64a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingChart } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { getNetworkEvents } from '../../../../../state/actions/network_events'; +import { networkEventsSelector } from '../../../../../state/selectors'; +import { WaterfallChartWrapper } from './waterfall_chart_wrapper'; +import { extractItems } from './data_formatting'; + +export const NO_DATA_TEXT = i18n.translate('xpack.uptime.synthetics.stepDetail.waterfallNoData', { + defaultMessage: 'No waterfall data could be found for this step', +}); + +interface Props { + checkGroup: string; + stepIndex: number; +} + +export const WaterfallChartContainer: React.FC = ({ checkGroup, stepIndex }) => { + const dispatch = useDispatch(); + + useEffect(() => { + if (checkGroup && stepIndex) { + dispatch( + getNetworkEvents({ + checkGroup, + stepIndex, + }) + ); + } + }, [dispatch, stepIndex, checkGroup]); + + const _networkEvents = useSelector(networkEventsSelector); + const networkEvents = _networkEvents[checkGroup ?? '']?.[stepIndex]; + + return ( + <> + {!networkEvents || + (networkEvents.loading && ( + + + + + + ))} + {networkEvents && !networkEvents.loading && networkEvents.events.length === 0 && ( + + + +

{NO_DATA_TEXT}

+
+
+
+ )} + {networkEvents && !networkEvents.loading && networkEvents.events.length > 0 && ( + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx similarity index 91% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx rename to x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index 434b44a94b79f..b10c3844f3002 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -13,7 +13,7 @@ import { WaterfallChart, MiddleTruncatedText, RenderItem, -} from '../../../waterfall'; +} from '../../waterfall'; const renderSidebarItem: RenderItem = (item, index) => { const { status } = item; @@ -27,7 +27,7 @@ const renderSidebarItem: RenderItem = (item, index) => { return ( <> - {!isErrorStatusCode(status) ? ( + {!status || !isErrorStatusCode(status) ? ( ) : ( @@ -47,9 +47,12 @@ const renderLegendItem: RenderItem = (item) => { return {item.name}; }; -export const WaterfallChartWrapper = () => { - // TODO: Will be sourced via an API - const [networkData] = useState([]); +interface Props { + data: NetworkItems; +} + +export const WaterfallChartWrapper: React.FC = ({ data }) => { + const [networkData] = useState(data); const { series, domain } = useMemo(() => { return getSeriesAndDomain(networkData); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx index 9ff544fc1946b..c551561d5ad4f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx @@ -27,7 +27,11 @@ export const Sidebar: React.FC = ({ items, height, render }) => { - + {items.map((item, index) => { return ( diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index de4be0ea34b2c..f622ad386a4f7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -85,7 +85,7 @@ export const WaterfallChart = ({ barStyleAccessor, renderSidebarItem, renderLegendItem, - maxHeight = 600, + maxHeight = 800, }: WaterfallChartProps) => { const { data, sidebarItems, legendItems } = useWaterfallContext(); @@ -108,7 +108,7 @@ export const WaterfallChart = ({ <> - + {shouldRenderSidebar && ( @@ -149,7 +149,7 @@ export const WaterfallChart = ({ - + {shouldRenderSidebar && ( )} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts deleted file mode 100644 index 698e6b4be0c4c..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts +++ /dev/null @@ -1,687 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { colourPalette } from './data_formatting'; - -// const TEST_DATA = [ -// { -// '@timestamp': '2020-10-29T14:55:01.055Z', -// ecs: { -// version: '1.6.0', -// }, -// agent: { -// type: 'heartbeat', -// version: '7.10.0', -// hostname: 'docker-desktop', -// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', -// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', -// name: 'docker-desktop', -// }, -// synthetics: { -// index: 7, -// payload: { -// request: { -// url: 'https://unpkg.com/director@1.2.8/build/director.js', -// method: 'GET', -// headers: { -// referer: '', -// user_agent: -// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', -// }, -// mixed_content_type: 'none', -// initial_priority: 'High', -// referrer_policy: 'no-referrer-when-downgrade', -// }, -// status: 200, -// method: 'GET', -// end: 13902.944973, -// url: 'https://unpkg.com/director@1.2.8/build/director.js', -// type: 'Script', -// is_navigation_request: false, -// start: 13902.752946, -// response: { -// encoded_data_length: 179, -// protocol: 'h2', -// headers: { -// content_encoding: 'br', -// server: 'cloudflare', -// age: '94838', -// cf_cache_status: 'HIT', -// x_content_type_options: 'nosniff', -// last_modified: 'Wed, 04 Feb 2015 03:25:28 GMT', -// cf_ray: '5e9dbc2bdda2e5a7-MAN', -// content_type: 'application/javascript; charset=utf-8', -// x_cloud_trace_context: 'eec7acc7a6f96b5353ef0d648bf437ac', -// expect_ct: -// 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', -// access_control_allow_origin: '*', -// vary: 'Accept-Encoding', -// cache_control: 'public, max-age=31536000', -// date: 'Thu, 29 Oct 2020 14:55:00 GMT', -// cf_request_id: '061673ef6b0000e5a7cd07a000000001', -// etag: 'W/"4f70-NHpXdyWxnckEaeiXalAnXQ+oh4Q"', -// strict_transport_security: 'max-age=31536000; includeSubDomains; preload', -// }, -// remote_i_p_address: '104.16.125.175', -// connection_reused: true, -// timing: { -// dns_start: -1, -// push_end: 0, -// worker_fetch_start: -1, -// worker_respond_with_settled: -1, -// proxy_end: -1, -// worker_start: -1, -// worker_ready: -1, -// send_end: 158.391, -// connect_end: -1, -// connect_start: -1, -// send_start: 157.876, -// proxy_start: -1, -// push_start: 0, -// ssl_end: -1, -// receive_headers_end: 186.885, -// ssl_start: -1, -// request_time: 13902.757525, -// dns_end: -1, -// }, -// connection_id: 17, -// status_text: '', -// remote_port: 443, -// status: 200, -// security_details: { -// valid_to: 1627905600, -// certificate_id: 0, -// key_exchange_group: 'X25519', -// valid_from: 1596326400, -// protocol: 'TLS 1.3', -// issuer: 'Cloudflare Inc ECC CA-3', -// key_exchange: '', -// san_list: ['unpkg.com', '*.unpkg.com', 'sni.cloudflaressl.com'], -// signed_certificate_timestamp_list: [], -// certificate_transparency_compliance: 'unknown', -// cipher: 'AES_128_GCM', -// subject_name: 'sni.cloudflaressl.com', -// }, -// mime_type: 'application/javascript', -// url: 'https://unpkg.com/director@1.2.8/build/director.js', -// from_prefetch_cache: false, -// from_disk_cache: false, -// security_state: 'secure', -// response_time: 1.603983300513211e12, -// from_service_worker: false, -// }, -// }, -// journey: { -// name: 'check that title is present', -// id: 'check that title is present', -// }, -// type: 'journey/network_info', -// package_version: '0.0.1', -// }, -// monitor: { -// status: 'up', -// duration: { -// us: 24, -// }, -// id: 'check that title is present', -// name: 'check that title is present', -// type: 'browser', -// timespan: { -// gte: '2020-10-29T14:55:01.055Z', -// lt: '2020-10-29T14:56:01.055Z', -// }, -// check_group: '948d3b6b-19f6-11eb-b237-025000000001', -// }, -// event: { -// dataset: 'uptime', -// }, -// }, -// { -// '@timestamp': '2020-10-29T14:55:01.055Z', -// ecs: { -// version: '1.6.0', -// }, -// agent: { -// version: '7.10.0', -// hostname: 'docker-desktop', -// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', -// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', -// name: 'docker-desktop', -// type: 'heartbeat', -// }, -// monitor: { -// check_group: '948d3b6b-19f6-11eb-b237-025000000001', -// status: 'up', -// duration: { -// us: 13, -// }, -// id: 'check that title is present', -// name: 'check that title is present', -// type: 'browser', -// timespan: { -// gte: '2020-10-29T14:55:01.055Z', -// lt: '2020-10-29T14:56:01.055Z', -// }, -// }, -// synthetics: { -// journey: { -// name: 'check that title is present', -// id: 'check that title is present', -// }, -// type: 'journey/network_info', -// package_version: '0.0.1', -// index: 9, -// payload: { -// start: 13902.76168, -// url: 'file:///opt/examples/todos/app/app.js', -// method: 'GET', -// is_navigation_request: false, -// end: 13902.770133, -// request: { -// headers: { -// referer: '', -// user_agent: -// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', -// }, -// mixed_content_type: 'none', -// initial_priority: 'High', -// referrer_policy: 'no-referrer-when-downgrade', -// url: 'file:///opt/examples/todos/app/app.js', -// method: 'GET', -// }, -// status: 0, -// type: 'Script', -// response: { -// protocol: 'file', -// connection_reused: false, -// mime_type: 'text/javascript', -// security_state: 'secure', -// from_disk_cache: false, -// url: 'file:///opt/examples/todos/app/app.js', -// status_text: '', -// connection_id: 0, -// from_prefetch_cache: false, -// encoded_data_length: -1, -// headers: {}, -// status: 0, -// from_service_worker: false, -// }, -// }, -// }, -// event: { -// dataset: 'uptime', -// }, -// }, -// { -// '@timestamp': '2020-10-29T14:55:01.000Z', -// monitor: { -// timespan: { -// lt: '2020-10-29T14:56:01.000Z', -// gte: '2020-10-29T14:55:01.000Z', -// }, -// id: 'check that title is present', -// name: 'check that title is present', -// check_group: '948d3b6b-19f6-11eb-b237-025000000001', -// status: 'up', -// duration: { -// us: 44365, -// }, -// type: 'browser', -// }, -// synthetics: { -// journey: { -// id: 'check that title is present', -// name: 'check that title is present', -// }, -// type: 'journey/network_info', -// package_version: '0.0.1', -// index: 5, -// payload: { -// status: 0, -// url: 'file:///opt/examples/todos/app/index.html', -// end: 13902.730261, -// request: { -// method: 'GET', -// headers: {}, -// mixed_content_type: 'none', -// initial_priority: 'VeryHigh', -// referrer_policy: 'no-referrer-when-downgrade', -// url: 'file:///opt/examples/todos/app/index.html', -// }, -// method: 'GET', -// response: { -// status: 0, -// connection_id: 0, -// from_disk_cache: false, -// headers: {}, -// encoded_data_length: -1, -// status_text: '', -// from_service_worker: false, -// connection_reused: false, -// url: 'file:///opt/examples/todos/app/index.html', -// remote_port: 0, -// security_state: 'secure', -// protocol: 'file', -// mime_type: 'text/html', -// remote_i_p_address: '', -// from_prefetch_cache: false, -// }, -// start: 13902.726626, -// type: 'Document', -// is_navigation_request: true, -// }, -// }, -// event: { -// dataset: 'uptime', -// }, -// ecs: { -// version: '1.6.0', -// }, -// agent: { -// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', -// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', -// name: 'docker-desktop', -// type: 'heartbeat', -// version: '7.10.0', -// hostname: 'docker-desktop', -// }, -// }, -// { -// '@timestamp': '2020-10-29T14:55:01.044Z', -// monitor: { -// type: 'browser', -// timespan: { -// lt: '2020-10-29T14:56:01.044Z', -// gte: '2020-10-29T14:55:01.044Z', -// }, -// check_group: '948d3b6b-19f6-11eb-b237-025000000001', -// status: 'up', -// duration: { -// us: 10524, -// }, -// id: 'check that title is present', -// name: 'check that title is present', -// }, -// synthetics: { -// package_version: '0.0.1', -// index: 6, -// payload: { -// status: 200, -// type: 'Stylesheet', -// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', -// method: 'GET', -// start: 13902.75266, -// is_navigation_request: false, -// end: 13902.943835, -// response: { -// remote_i_p_address: '104.16.125.175', -// response_time: 1.603983300511892e12, -// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', -// mime_type: 'text/css', -// protocol: 'h2', -// security_state: 'secure', -// encoded_data_length: 414, -// remote_port: 443, -// status_text: '', -// timing: { -// proxy_start: -1, -// worker_ready: -1, -// worker_fetch_start: -1, -// receive_headers_end: 189.169, -// worker_respond_with_settled: -1, -// connect_end: 160.311, -// worker_start: -1, -// send_start: 161.275, -// dns_start: 0.528, -// send_end: 161.924, -// ssl_end: 160.267, -// proxy_end: -1, -// ssl_start: 29.726, -// request_time: 13902.753988, -// dns_end: 5.212, -// push_end: 0, -// push_start: 0, -// connect_start: 5.212, -// }, -// connection_reused: false, -// from_service_worker: false, -// security_details: { -// san_list: ['unpkg.com', '*.unpkg.com', 'sni.cloudflaressl.com'], -// valid_from: 1596326400, -// cipher: 'AES_128_GCM', -// protocol: 'TLS 1.3', -// issuer: 'Cloudflare Inc ECC CA-3', -// valid_to: 1627905600, -// certificate_id: 0, -// key_exchange_group: 'X25519', -// certificate_transparency_compliance: 'unknown', -// key_exchange: '', -// subject_name: 'sni.cloudflaressl.com', -// signed_certificate_timestamp_list: [], -// }, -// connection_id: 17, -// status: 200, -// from_disk_cache: false, -// from_prefetch_cache: false, -// headers: { -// date: 'Thu, 29 Oct 2020 14:55:00 GMT', -// x_cloud_trace_context: '76a4f7b8be185f2ac9aa839de3d6f893', -// cache_control: 'public, max-age=31536000', -// expect_ct: -// 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', -// content_type: 'text/css; charset=utf-8', -// age: '627638', -// x_content_type_options: 'nosniff', -// last_modified: 'Sat, 09 Jan 2016 00:57:37 GMT', -// access_control_allow_origin: '*', -// cf_request_id: '061673ef6a0000e5a75a309000000001', -// vary: 'Accept-Encoding', -// strict_transport_security: 'max-age=31536000; includeSubDomains; preload', -// cf_ray: '5e9dbc2bdda1e5a7-MAN', -// content_encoding: 'br', -// etag: 'W/"1921-kYwbQVnRAA2V/L9Gr4SCtUE5LHQ"', -// server: 'cloudflare', -// cf_cache_status: 'HIT', -// }, -// }, -// request: { -// headers: { -// referer: '', -// user_agent: -// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', -// }, -// mixed_content_type: 'none', -// initial_priority: 'VeryHigh', -// referrer_policy: 'no-referrer-when-downgrade', -// url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', -// method: 'GET', -// }, -// }, -// journey: { -// id: 'check that title is present', -// name: 'check that title is present', -// }, -// type: 'journey/network_info', -// }, -// event: { -// dataset: 'uptime', -// }, -// ecs: { -// version: '1.6.0', -// }, -// agent: { -// version: '7.10.0', -// hostname: 'docker-desktop', -// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', -// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', -// name: 'docker-desktop', -// type: 'heartbeat', -// }, -// }, -// { -// '@timestamp': '2020-10-29T14:55:01.055Z', -// agent: { -// ephemeral_id: '34179df8-f97c-46a2-9e73-33976d4ac58d', -// id: '5a03ad5f-cc18-43e8-8f82-6b08b9ceb36a', -// name: 'docker-desktop', -// type: 'heartbeat', -// version: '7.10.0', -// hostname: 'docker-desktop', -// }, -// synthetics: { -// index: 8, -// payload: { -// method: 'GET', -// type: 'Script', -// response: { -// url: 'file:///opt/examples/todos/app/vue.min.js', -// protocol: 'file', -// connection_id: 0, -// headers: {}, -// mime_type: 'text/javascript', -// from_service_worker: false, -// status_text: '', -// connection_reused: false, -// encoded_data_length: -1, -// from_disk_cache: false, -// security_state: 'secure', -// from_prefetch_cache: false, -// status: 0, -// }, -// is_navigation_request: false, -// request: { -// mixed_content_type: 'none', -// initial_priority: 'High', -// referrer_policy: 'no-referrer-when-downgrade', -// url: 'file:///opt/examples/todos/app/vue.min.js', -// method: 'GET', -// headers: { -// referer: '', -// user_agent: -// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4287.0 Safari/537.36', -// }, -// }, -// end: 13902.772783, -// status: 0, -// start: 13902.760644, -// url: 'file:///opt/examples/todos/app/vue.min.js', -// }, -// journey: { -// name: 'check that title is present', -// id: 'check that title is present', -// }, -// type: 'journey/network_info', -// package_version: '0.0.1', -// }, -// monitor: { -// status: 'up', -// duration: { -// us: 82, -// }, -// name: 'check that title is present', -// type: 'browser', -// timespan: { -// gte: '2020-10-29T14:55:01.055Z', -// lt: '2020-10-29T14:56:01.055Z', -// }, -// id: 'check that title is present', -// check_group: '948d3b6b-19f6-11eb-b237-025000000001', -// }, -// event: { -// dataset: 'uptime', -// }, -// ecs: { -// version: '1.6.0', -// }, -// }, -// ]; - -// const toMillis = (seconds: number) => seconds * 1000; - -// describe('getTimings', () => { -// it('Calculates timings for network events correctly', () => { -// // NOTE: Uses these timings as the file protocol events don't have timing information -// const eventOneTimings = getTimings( -// TEST_DATA[0].synthetics.payload.response.timing!, -// toMillis(TEST_DATA[0].synthetics.payload.start), -// toMillis(TEST_DATA[0].synthetics.payload.end) -// ); -// expect(eventOneTimings).toEqual({ -// blocked: 162.4549999999106, -// connect: -1, -// dns: -1, -// receive: 0.5629999989271255, -// send: 0.5149999999999864, -// ssl: undefined, -// wait: 28.494, -// }); - -// const eventFourTimings = getTimings( -// TEST_DATA[3].synthetics.payload.response.timing!, -// toMillis(TEST_DATA[3].synthetics.payload.start), -// toMillis(TEST_DATA[3].synthetics.payload.end) -// ); -// expect(eventFourTimings).toEqual({ -// blocked: 1.8559999997466803, -// connect: 25.52200000000002, -// dns: 4.683999999999999, -// receive: 0.6780000009983667, -// send: 0.6490000000000009, -// ssl: 130.541, -// wait: 27.245000000000005, -// }); -// }); -// }); - -// describe('getSeriesAndDomain', () => { -// let seriesAndDomain: any; -// let NetworkItems: any; - -// beforeAll(() => { -// NetworkItems = extractItems(TEST_DATA); -// seriesAndDomain = getSeriesAndDomain(NetworkItems); -// }); - -// it('Correctly calculates the domain', () => { -// expect(seriesAndDomain.domain).toEqual({ max: 218.34699999913573, min: 0 }); -// }); - -// it('Correctly calculates the series', () => { -// expect(seriesAndDomain.series).toEqual([ -// { -// config: { colour: '#f3b3a6', tooltipProps: { colour: '#f3b3a6', value: '3.635ms' } }, -// x: 0, -// y: 3.6349999997764826, -// y0: 0, -// }, -// { -// config: { -// colour: '#b9a888', -// tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 1.856ms' }, -// }, -// x: 1, -// y: 27.889999999731778, -// y0: 26.0339999999851, -// }, -// { -// config: { colour: '#54b399', tooltipProps: { colour: '#54b399', value: 'DNS: 4.684ms' } }, -// x: 1, -// y: 32.573999999731775, -// y0: 27.889999999731778, -// }, -// { -// config: { -// colour: '#da8b45', -// tooltipProps: { colour: '#da8b45', value: 'Connecting: 25.522ms' }, -// }, -// x: 1, -// y: 58.095999999731795, -// y0: 32.573999999731775, -// }, -// { -// config: { colour: '#edc5a2', tooltipProps: { colour: '#edc5a2', value: 'SSL: 130.541ms' } }, -// x: 1, -// y: 188.63699999973178, -// y0: 58.095999999731795, -// }, -// { -// config: { -// colour: '#d36086', -// tooltipProps: { colour: '#d36086', value: 'Sending request: 0.649ms' }, -// }, -// x: 1, -// y: 189.28599999973179, -// y0: 188.63699999973178, -// }, -// { -// config: { -// colour: '#b0c9e0', -// tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 27.245ms' }, -// }, -// x: 1, -// y: 216.5309999997318, -// y0: 189.28599999973179, -// }, -// { -// config: { -// colour: '#ca8eae', -// tooltipProps: { colour: '#ca8eae', value: 'Content downloading: 0.678ms' }, -// }, -// x: 1, -// y: 217.20900000073016, -// y0: 216.5309999997318, -// }, -// { -// config: { -// colour: '#b9a888', -// tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 162.455ms' }, -// }, -// x: 2, -// y: 188.77500000020862, -// y0: 26.320000000298023, -// }, -// { -// config: { -// colour: '#d36086', -// tooltipProps: { colour: '#d36086', value: 'Sending request: 0.515ms' }, -// }, -// x: 2, -// y: 189.2900000002086, -// y0: 188.77500000020862, -// }, -// { -// config: { -// colour: '#b0c9e0', -// tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 28.494ms' }, -// }, -// x: 2, -// y: 217.7840000002086, -// y0: 189.2900000002086, -// }, -// { -// config: { -// colour: '#9170b8', -// tooltipProps: { colour: '#9170b8', value: 'Content downloading: 0.563ms' }, -// }, -// x: 2, -// y: 218.34699999913573, -// y0: 217.7840000002086, -// }, -// { -// config: { colour: '#9170b8', tooltipProps: { colour: '#9170b8', value: '12.139ms' } }, -// x: 3, -// y: 46.15699999965727, -// y0: 34.01799999922514, -// }, -// { -// config: { colour: '#9170b8', tooltipProps: { colour: '#9170b8', value: '8.453ms' } }, -// x: 4, -// y: 43.506999999284744, -// y0: 35.053999999538064, -// }, -// ]); -// }); -// }); - -describe('Palettes', () => { - it('A colour palette comprising timing and mime type colours is correctly generated', () => { - expect(colourPalette).toEqual({ - blocked: '#b9a888', - connect: '#da8b45', - dns: '#54b399', - font: '#aa6556', - html: '#f3b3a6', - media: '#d6bf57', - other: '#e7664c', - receive: '#54b399', - script: '#9170b8', - send: '#d36086', - ssl: '#edc5a2', - stylesheet: '#ca8eae', - wait: '#b0c9e0', - }); - }); -}); diff --git a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts index 9b4a441fe5ade..21665e93dd274 100644 --- a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts +++ b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts @@ -14,6 +14,7 @@ export enum UptimePage { Monitor = 'Monitor', Settings = 'Settings', Certificates = 'Certificates', + StepDetail = 'StepDetail', NotFound = '__not-found__', } diff --git a/x-pack/plugins/uptime/public/pages/index.ts b/x-pack/plugins/uptime/public/pages/index.ts index cea47d6ccf79c..cb95fb8558cfb 100644 --- a/x-pack/plugins/uptime/public/pages/index.ts +++ b/x-pack/plugins/uptime/public/pages/index.ts @@ -5,5 +5,6 @@ */ export { MonitorPage } from './monitor'; +export { StepDetailPage } from './step_detail_page'; export { SettingsPage } from './settings'; export { NotFoundPage } from './not_found'; diff --git a/x-pack/plugins/uptime/public/pages/step_detail_page.tsx b/x-pack/plugins/uptime/public/pages/step_detail_page.tsx new file mode 100644 index 0000000000000..5bacad7e9a2d2 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/step_detail_page.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { useTrackPageview } from '../../../observability/public'; +import { useInitApp } from '../hooks/use_init_app'; +import { StepDetailContainer } from '../components/monitor/synthetics/step_detail/step_detail_container'; + +export const StepDetailPage: React.FC = () => { + useInitApp(); + const { checkGroupId, stepIndex } = useParams<{ checkGroupId: string; stepIndex: string }>(); + useTrackPageview({ app: 'uptime', path: 'stepDetail' }); + useTrackPageview({ app: 'uptime', path: 'stepDetail', delay: 15000 }); + + return ; +}; diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index 455d5070128f5..bb9caf500c605 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -12,8 +12,9 @@ import { MONITOR_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE, + STEP_DETAIL_ROUTE, } from '../common/constants'; -import { MonitorPage, NotFoundPage, SettingsPage } from './pages'; +import { MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages'; import { CertificatesPage } from './pages/certificates'; import { UptimePage, useUptimeTelemetry } from './hooks'; @@ -49,6 +50,13 @@ const Routes: RouteProps[] = [ dataTestSubj: 'uptimeCertificatesPage', telemetryId: UptimePage.Certificates, }, + { + title: baseTitle, + path: STEP_DETAIL_ROUTE, + component: StepDetailPage, + dataTestSubj: 'uptimeStepDetailPage', + telemetryId: UptimePage.StepDetail, + }, { title: baseTitle, path: OVERVIEW_ROUTE, diff --git a/x-pack/plugins/uptime/public/state/actions/network_events.ts b/x-pack/plugins/uptime/public/state/actions/network_events.ts new file mode 100644 index 0000000000000..e3564689fcd48 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/actions/network_events.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAction } from 'redux-actions'; +import { SyntheticsNetworkEventsApiResponse } from '../../../common/runtime_types'; + +export interface FetchNetworkEventsParams { + checkGroup: string; + stepIndex: number; +} + +export interface FetchNetworkEventsFailPayload { + checkGroup: string; + stepIndex: number; + error: Error; +} + +export const getNetworkEvents = createAction('GET_NETWORK_EVENTS'); +export const getNetworkEventsSuccess = createAction< + Pick & SyntheticsNetworkEventsApiResponse +>('GET_NETWORK_EVENTS_SUCCESS'); +export const getNetworkEventsFail = createAction( + 'GET_NETWORK_EVENTS_FAIL' +); diff --git a/x-pack/plugins/uptime/public/state/api/network_events.ts b/x-pack/plugins/uptime/public/state/api/network_events.ts new file mode 100644 index 0000000000000..a4eceb4812d28 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/network_events.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { apiService } from './utils'; +import { FetchNetworkEventsParams } from '../actions/network_events'; +import { + SyntheticsNetworkEventsApiResponse, + SyntheticsNetworkEventsApiResponseType, +} from '../../../common/runtime_types'; + +export async function fetchNetworkEvents( + params: FetchNetworkEventsParams +): Promise { + return (await apiService.get( + `/api/uptime/network_events`, + { + checkGroup: params.checkGroup, + stepIndex: params.stepIndex, + }, + SyntheticsNetworkEventsApiResponseType + )) as SyntheticsNetworkEventsApiResponse; +} diff --git a/x-pack/plugins/uptime/public/state/effects/index.ts b/x-pack/plugins/uptime/public/state/effects/index.ts index 4951f2102c8a7..3c75e75871882 100644 --- a/x-pack/plugins/uptime/public/state/effects/index.ts +++ b/x-pack/plugins/uptime/public/state/effects/index.ts @@ -19,6 +19,7 @@ import { fetchIndexStatusEffect } from './index_status'; import { fetchCertificatesEffect } from '../certificates/certificates'; import { fetchAlertsEffect } from '../alerts/alerts'; import { fetchJourneyStepsEffect } from './journey'; +import { fetchNetworkEventsEffect } from './network_events'; export function* rootEffect() { yield fork(fetchMonitorDetailsEffect); @@ -37,4 +38,5 @@ export function* rootEffect() { yield fork(fetchCertificatesEffect); yield fork(fetchAlertsEffect); yield fork(fetchJourneyStepsEffect); + yield fork(fetchNetworkEventsEffect); } diff --git a/x-pack/plugins/uptime/public/state/effects/network_events.ts b/x-pack/plugins/uptime/public/state/effects/network_events.ts new file mode 100644 index 0000000000000..95d24fbe37ae2 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/effects/network_events.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux-actions'; +import { call, put, takeLatest } from 'redux-saga/effects'; +import { + getNetworkEvents, + getNetworkEventsSuccess, + getNetworkEventsFail, + FetchNetworkEventsParams, +} from '../actions/network_events'; +import { fetchNetworkEvents } from '../api/network_events'; + +export function* fetchNetworkEventsEffect() { + yield takeLatest(getNetworkEvents, function* (action: Action) { + try { + const response = yield call(fetchNetworkEvents, action.payload); + + yield put( + getNetworkEventsSuccess({ + checkGroup: action.payload.checkGroup, + stepIndex: action.payload.stepIndex, + ...response, + }) + ); + } catch (e) { + yield put( + getNetworkEventsFail({ + checkGroup: action.payload.checkGroup, + stepIndex: action.payload.stepIndex, + error: e, + }) + ); + } + }); +} diff --git a/x-pack/plugins/uptime/public/state/reducers/index.ts b/x-pack/plugins/uptime/public/state/reducers/index.ts index c0bab124d5f9d..661b637802707 100644 --- a/x-pack/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/plugins/uptime/public/state/reducers/index.ts @@ -22,6 +22,7 @@ import { certificatesReducer } from '../certificates/certificates'; import { selectedFiltersReducer } from './selected_filters'; import { alertsReducer } from '../alerts/alerts'; import { journeyReducer } from './journey'; +import { networkEventsReducer } from './network_events'; export const rootReducer = combineReducers({ monitor: monitorReducer, @@ -41,4 +42,5 @@ export const rootReducer = combineReducers({ selectedFilters: selectedFiltersReducer, alerts: alertsReducer, journeys: journeyReducer, + networkEvents: networkEventsReducer, }); diff --git a/x-pack/plugins/uptime/public/state/reducers/journey.ts b/x-pack/plugins/uptime/public/state/reducers/journey.ts index e1c3dc808f1bf..133a5d1edb2c2 100644 --- a/x-pack/plugins/uptime/public/state/reducers/journey.ts +++ b/x-pack/plugins/uptime/public/state/reducers/journey.ts @@ -18,6 +18,7 @@ import { export interface JourneyState { checkGroup: string; steps: Ping[]; + details?: SyntheticsJourneyApiResponse['details']; loading: boolean; error?: Error; } @@ -56,13 +57,14 @@ export const journeyReducer = handleActions( [String(getJourneyStepsSuccess)]: ( state: JourneyKVP, - { payload: { checkGroup, steps } }: Action + { payload: { checkGroup, steps, details } }: Action ) => ({ ...state, [checkGroup]: { loading: false, checkGroup, steps, + details, }, }), diff --git a/x-pack/plugins/uptime/public/state/reducers/network_events.ts b/x-pack/plugins/uptime/public/state/reducers/network_events.ts new file mode 100644 index 0000000000000..44a23b0fa53d7 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/network_events.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { handleActions, Action } from 'redux-actions'; +import { NetworkEvent, SyntheticsNetworkEventsApiResponse } from '../../../common/runtime_types'; +import { + FetchNetworkEventsParams, + FetchNetworkEventsFailPayload, + getNetworkEvents, + getNetworkEventsFail, + getNetworkEventsSuccess, +} from '../actions/network_events'; + +export interface NetworkEventsState { + [checkGroup: string]: { + [stepIndex: number]: { + events: NetworkEvent[]; + loading: boolean; + error?: Error; + }; + }; +} + +const initialState: NetworkEventsState = {}; + +type Payload = FetchNetworkEventsParams & + SyntheticsNetworkEventsApiResponse & + FetchNetworkEventsFailPayload & + string[]; + +export const networkEventsReducer = handleActions( + { + [String(getNetworkEvents)]: ( + state: NetworkEventsState, + { payload: { checkGroup, stepIndex } }: Action + ) => ({ + ...state, + [checkGroup]: state[checkGroup] + ? { + [stepIndex]: state[checkGroup][stepIndex] + ? { + ...state[checkGroup][stepIndex], + loading: true, + events: [], + } + : { + loading: true, + events: [], + }, + } + : { + [stepIndex]: { + loading: true, + events: [], + }, + }, + }), + + [String(getNetworkEventsSuccess)]: ( + state: NetworkEventsState, + { + payload: { events, checkGroup, stepIndex }, + }: Action + ) => { + return { + ...state, + [checkGroup]: state[checkGroup] + ? { + [stepIndex]: state[checkGroup][stepIndex] + ? { + ...state[checkGroup][stepIndex], + loading: false, + events, + } + : { + loading: false, + events, + }, + } + : { + [stepIndex]: { + loading: false, + events, + }, + }, + }; + }, + + [String(getNetworkEventsFail)]: ( + state: NetworkEventsState, + { payload: { checkGroup, stepIndex, error } }: Action + ) => ({ + ...state, + [checkGroup]: state[checkGroup] + ? { + [stepIndex]: state[checkGroup][stepIndex] + ? { + ...state[checkGroup][stepIndex], + loading: false, + events: [], + error, + } + : { + loading: false, + events: [], + error, + }, + } + : { + [stepIndex]: { + loading: false, + events: [], + error, + }, + }, + }), + }, + initialState +); diff --git a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts index a59e0be5cdf3f..608e819848539 100644 --- a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -118,6 +118,7 @@ describe('state selectors', () => { anomalyAlertDeletion: { data: null, loading: false }, }, journeys: {}, + networkEvents: {}, }; it('selects base path from state', () => { diff --git a/x-pack/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts index 6bfe67468aae5..eef53e1100029 100644 --- a/x-pack/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/plugins/uptime/public/state/selectors/index.ts @@ -96,3 +96,5 @@ export const selectedFiltersSelector = ({ selectedFilters }: AppState) => select export const monitorIdSelector = ({ ui: { monitorId } }: AppState) => monitorId; export const journeySelector = ({ journeys }: AppState) => journeys; + +export const networkEventsSelector = ({ networkEvents }: AppState) => networkEvents; diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts new file mode 100644 index 0000000000000..5407baf7e71d4 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts @@ -0,0 +1,422 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getUptimeESMockClient } from './helper'; +import { getNetworkEvents } from '../get_network_events'; + +describe('getNetworkEvents', () => { + let mockHits: any; + + beforeEach(() => { + mockHits = [ + { + _index: 'heartbeat-2020.12.07', + _id: 'o5KvPXYBLREsP7x9Y7Gd', + _score: null, + _source: { + '@timestamp': '2020-12-07T14:51:06.572Z', + monitor: { + type: 'browser', + timespan: { + gte: '2020-12-07T14:51:07.439Z', + lt: '2020-12-07T14:52:07.439Z', + }, + check_group: '7f617f79-389b-11eb-80b5-025000000001', + id: 'BMC-nav', + name: 'BMC', + }, + synthetics: { + journey: { + id: 'inline', + name: 'inline', + }, + type: 'journey/network_info', + package_version: '0.0.1-alpha.7', + payload: { + type: 'Font', + request: { + mixed_content_type: 'none', + initial_priority: 'VeryHigh', + referrer_policy: 'no-referrer-when-downgrade', + url: + 'https://use.typekit.com/af/ce2de8/000000000000000000011f36/27/l?subset_id=2&fvd=n4&v=3', + method: 'GET', + headers: { + origin: 'https://www.thebmc.co.uk', + referer: 'https://www.thebmc.co.uk/volunteers', + user_agent: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4324.0 Safari/537.36', + }, + }, + status: 200, + load_end_time: 11690.378366, + step: { + index: 2, + name: 'Click volunteers link', + }, + response: { + mime_type: 'application/font-woff2', + from_disk_cache: false, + security_state: 'secure', + from_service_worker: false, + url: + 'https://use.typekit.com/af/ce2de8/000000000000000000011f36/27/l?subset_id=2&fvd=n4&v=3', + protocol: 'h2', + connection_reused: true, + response_time: 1.60735266386276e12, + status_text: '', + headers: { + timing_allow_origin: '*', + content_length: '16620', + date: 'Mon, 07 Dec 2020 14:51:03 GMT', + server: 'nginx', + etag: '"cc13c3aaba9f28fe9e0411f0994b936cf4729475"', + content_type: 'application/font-woff2', + access_control_allow_origin: '*', + cache_control: 'public, max-age=31536000', + }, + connection_id: 253, + timing: { + push_start: 0, + worker_respond_with_settled: -1, + dns_end: -1, + worker_start: -1, + ssl_start: -1, + connect_end: -1, + receive_headers_end: 56.15, + worker_fetch_start: -1, + worker_ready: -1, + connect_start: -1, + send_start: 34.293, + proxy_start: -1, + send_end: 35.3, + request_time: 11687.636845, + dns_start: -1, + push_end: 0, + proxy_end: -1, + ssl_end: -1, + }, + remote_i_p_address: '62.252.188.232', + encoded_data_length: 16822, + from_prefetch_cache: false, + remote_port: 443, + status: 200, + security_details: { + key_exchange: '', + san_list: ['use-staging.typekit.net', 'use.typekit.net', 'use.typekit.com'], + issuer: 'DigiCert SHA2 Secure Server CA', + cipher: 'AES_256_GCM', + subject_name: 'use.typekit.net', + protocol: 'TLS 1.3', + valid_from: 1580169600, + key_exchange_group: 'X25519', + certificate_transparency_compliance: 'unknown', + signed_certificate_timestamp_list: [], + valid_to: 1643716800, + certificate_id: 0, + }, + }, + url: + 'https://use.typekit.com/af/ce2de8/000000000000000000011f36/27/l?subset_id=2&fvd=n4&v=3', + request_sent_time: 11690.378291, + is_navigation_request: false, + method: 'GET', + timestamp: 1.6073526665725272e15, + }, + step: { + index: 2, + name: 'Click volunteers link', + }, + }, + event: { + dataset: 'uptime', + }, + ecs: { + version: '1.6.0', + }, + agent: { + ephemeral_id: '5bcee4d8-a1ff-416d-9110-e9d67959fcfa', + id: '4031f8f1-b015-4f2e-8995-42852ff49339', + name: 'docker-desktop', + type: 'heartbeat', + version: '8.0.0', + }, + }, + sort: [1607352666572], + }, + { + _index: 'heartbeat-2020.12.07', + _id: 'pJKvPXYBLREsP7x9Y7Gd', + _score: null, + _source: { + '@timestamp': '2020-12-07T14:51:06.572Z', + synthetics: { + payload: { + load_end_time: 11690.378555, + is_navigation_request: false, + timestamp: 1607352666572586, + step: { + name: 'Click volunteers link', + index: 2, + }, + url: + 'https://use.typekit.com/af/12aedb/000000000000000000011f38/27/l?subset_id=2&fvd=n7&v=3', + request: { + method: 'GET', + headers: { + user_agent: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4324.0 Safari/537.36', + origin: 'https://www.thebmc.co.uk', + referer: 'https://www.thebmc.co.uk/volunteers', + }, + mixed_content_type: 'none', + initial_priority: 'VeryHigh', + referrer_policy: 'no-referrer-when-downgrade', + url: + 'https://use.typekit.com/af/12aedb/000000000000000000011f38/27/l?subset_id=2&fvd=n7&v=3', + }, + status: 200, + response: { + status_text: '', + from_prefetch_cache: false, + timing: { + worker_fetch_start: -1, + send_start: 33.679, + ssl_start: -1, + worker_start: -1, + send_end: 34.725, + dns_start: -1, + connect_start: -1, + worker_ready: -1, + proxy_end: -1, + worker_respond_with_settled: -1, + receive_headers_end: 53.931, + push_start: 0, + connect_end: -1, + dns_end: -1, + push_end: 0, + ssl_end: -1, + proxy_start: -1, + request_time: 11687.637418, + }, + protocol: 'h2', + remote_port: 443, + security_details: { + certificate_transparency_compliance: 'unknown', + signed_certificate_timestamp_list: [], + key_exchange_group: 'X25519', + cipher: 'AES_256_GCM', + protocol: 'TLS 1.3', + valid_from: 1580169600, + key_exchange: '', + subject_name: 'use.typekit.net', + certificate_id: 0, + san_list: ['use-staging.typekit.net', 'use.typekit.net', 'use.typekit.com'], + issuer: 'DigiCert SHA2 Secure Server CA', + valid_to: 1643716800, + }, + connection_id: 253, + status: 200, + response_time: 1.607352663861158e12, + from_disk_cache: false, + connection_reused: true, + security_state: 'secure', + encoded_data_length: 16986, + mime_type: 'application/font-woff2', + from_service_worker: false, + remote_i_p_address: '62.252.188.232', + headers: { + content_type: 'application/font-woff2', + access_control_allow_origin: '*', + cache_control: 'public, max-age=31536000', + timing_allow_origin: '*', + content_length: '16784', + date: 'Mon, 07 Dec 2020 14:51:03 GMT', + server: 'nginx', + etag: '"c0994501e4f56e0b83223f5c4a96d4b3fdcfe17c"', + }, + url: + 'https://use.typekit.com/af/12aedb/000000000000000000011f38/27/l?subset_id=2&fvd=n7&v=3', + }, + method: 'GET', + type: 'Font', + request_sent_time: 11690.378482, + }, + step: { + index: 2, + name: 'Click volunteers link', + }, + journey: { + id: 'inline', + name: 'inline', + }, + type: 'journey/network_info', + package_version: '0.0.1-alpha.7', + }, + monitor: { + timespan: { + gte: '2020-12-07T14:51:07.439Z', + lt: '2020-12-07T14:52:07.439Z', + }, + check_group: '7f617f79-389b-11eb-80b5-025000000001', + id: 'BMC-nav', + name: 'BMC', + type: 'browser', + }, + event: { + dataset: 'uptime', + }, + ecs: { + version: '1.6.0', + }, + agent: { + ephemeral_id: '5bcee4d8-a1ff-416d-9110-e9d67959fcfa', + id: '4031f8f1-b015-4f2e-8995-42852ff49339', + name: 'docker-desktop', + type: 'heartbeat', + version: '8.0.0', + }, + }, + sort: [1607352666572], + }, + ]; + }); + + it('Uses the correct query', async () => { + const { uptimeEsClient, esClient } = getUptimeESMockClient(); + + esClient.search.mockResolvedValueOnce({ + body: { + hits: { + hits: mockHits, + }, + }, + } as any); + + const result = await getNetworkEvents({ + uptimeEsClient, + checkGroup: 'my-fake-group', + stepIndex: '1', + }); + + expect(esClient.search.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "synthetics.type": "journey/network_info", + }, + }, + Object { + "term": Object { + "monitor.check_group": "my-fake-group", + }, + }, + Object { + "term": Object { + "synthetics.step.index": 1, + }, + }, + ], + }, + }, + "size": 1000, + }, + "index": "heartbeat-8*", + }, + ], + ] + `); + }); + + it('Returns the correct result', async () => { + const { esClient, uptimeEsClient } = getUptimeESMockClient(); + + esClient.search.mockResolvedValueOnce({ + body: { + hits: { + hits: mockHits, + }, + }, + } as any); + + const result = await getNetworkEvents({ + uptimeEsClient, + checkGroup: 'my-fake-group', + stepIndex: '1', + }); + + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "loadEndTime": 11690378.366, + "method": "GET", + "mimeType": "application/font-woff2", + "requestSentTime": 11690378.291000001, + "requestStartTime": 11687636.845, + "status": 200, + "timestamp": "2020-12-07T14:51:06.572Z", + "timings": Object { + "connect_end": -1, + "connect_start": -1, + "dns_end": -1, + "dns_start": -1, + "proxy_end": -1, + "proxy_start": -1, + "push_end": 0, + "push_start": 0, + "receive_headers_end": 56.15, + "request_time": 11687.636845, + "send_end": 35.3, + "send_start": 34.293, + "ssl_end": -1, + "ssl_start": -1, + "worker_fetch_start": -1, + "worker_ready": -1, + "worker_respond_with_settled": -1, + "worker_start": -1, + }, + "url": "https://use.typekit.com/af/ce2de8/000000000000000000011f36/27/l?subset_id=2&fvd=n4&v=3", + }, + Object { + "loadEndTime": 11690378.555, + "method": "GET", + "mimeType": "application/font-woff2", + "requestSentTime": 11690378.482, + "requestStartTime": 11687637.418, + "status": 200, + "timestamp": "2020-12-07T14:51:06.572Z", + "timings": Object { + "connect_end": -1, + "connect_start": -1, + "dns_end": -1, + "dns_start": -1, + "proxy_end": -1, + "proxy_start": -1, + "push_end": 0, + "push_start": 0, + "receive_headers_end": 53.931, + "request_time": 11687.637418, + "send_end": 34.725, + "send_start": 33.679, + "ssl_end": -1, + "ssl_start": -1, + "worker_fetch_start": -1, + "worker_ready": -1, + "worker_respond_with_settled": -1, + "worker_start": -1, + }, + "url": "https://use.typekit.com/af/12aedb/000000000000000000011f38/27/l?subset_id=2&fvd=n7&v=3", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts new file mode 100644 index 0000000000000..ef11b00604490 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UMElasticsearchQueryFn } from '../adapters/framework'; +import { SyntheticsJourneyApiResponse } from '../../../common/runtime_types'; + +interface GetJourneyDetails { + checkGroup: string; +} + +export const getJourneyDetails: UMElasticsearchQueryFn< + GetJourneyDetails, + SyntheticsJourneyApiResponse['details'] +> = async ({ uptimeEsClient, checkGroup }) => { + const baseParams = { + query: { + bool: { + filter: [ + { + term: { + 'monitor.check_group': checkGroup, + }, + }, + { + term: { + 'synthetics.type': 'journey/end', + }, + }, + ], + }, + }, + _source: ['@timestamp', 'monitor.id'], + size: 1, + }; + + const { body: thisJourney } = await uptimeEsClient.search({ body: baseParams }); + + if (thisJourney?.hits?.hits.length > 0) { + const thisJourneySource: any = thisJourney.hits.hits[0]._source; + + const baseSiblingParams = { + query: { + bool: { + filter: [ + { + term: { + 'monitor.id': thisJourneySource.monitor.id, + }, + }, + { + term: { + 'synthetics.type': 'journey/end', + }, + }, + ], + }, + }, + _source: ['@timestamp', 'monitor.check_group'], + size: 1, + }; + + const previousParams = { + ...baseSiblingParams, + query: { + bool: { + filter: [ + ...baseSiblingParams.query.bool.filter, + { + range: { + '@timestamp': { + lt: thisJourneySource['@timestamp'], + }, + }, + }, + ], + }, + }, + sort: [{ '@timestamp': { order: 'desc' } }], + }; + + const nextParams = { + ...baseSiblingParams, + query: { + bool: { + filter: [ + ...baseSiblingParams.query.bool.filter, + { + range: { + '@timestamp': { + gt: thisJourneySource['@timestamp'], + }, + }, + }, + ], + }, + }, + sort: [{ '@timestamp': { order: 'asc' } }], + }; + + const { body: previousJourneyResult } = await uptimeEsClient.search({ body: previousParams }); + const { body: nextJourneyResult } = await uptimeEsClient.search({ body: nextParams }); + const previousJourney: any = + previousJourneyResult?.hits?.hits.length > 0 ? previousJourneyResult?.hits?.hits[0] : null; + const nextJourney: any = + nextJourneyResult?.hits?.hits.length > 0 ? nextJourneyResult?.hits?.hits[0] : null; + return { + timestamp: thisJourneySource['@timestamp'], + previous: previousJourney + ? { + checkGroup: previousJourney._source.monitor.check_group, + timestamp: previousJourney._source['@timestamp'], + } + : undefined, + next: nextJourney + ? { + checkGroup: nextJourney._source.monitor.check_group, + timestamp: nextJourney._source['@timestamp'], + } + : undefined, + } as SyntheticsJourneyApiResponse['details']; + } else { + return null; + } +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts new file mode 100644 index 0000000000000..17c50cf15a38a --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UMElasticsearchQueryFn } from '../adapters/framework'; +import { NetworkEvent } from '../../../common/runtime_types'; + +interface GetNetworkEventsParams { + checkGroup: string; + stepIndex: string; +} + +export const getNetworkEvents: UMElasticsearchQueryFn< + GetNetworkEventsParams, + NetworkEvent[] +> = async ({ uptimeEsClient, checkGroup, stepIndex }) => { + const params = { + query: { + bool: { + filter: [ + { term: { 'synthetics.type': 'journey/network_info' } }, + { term: { 'monitor.check_group': checkGroup } }, + { term: { 'synthetics.step.index': Number(stepIndex) } }, + ], + }, + }, + size: 1000, + }; + + const { body: result } = await uptimeEsClient.search({ body: params }); + + const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 1000); + + return result.hits.hits.map((event: any) => { + const requestSentTime = microToMillis(event._source.synthetics.payload.request_sent_time); + const loadEndTime = microToMillis(event._source.synthetics.payload.load_end_time); + const requestStartTime = + event._source.synthetics.payload.response && event._source.synthetics.payload.response.timing + ? microToMillis(event._source.synthetics.payload.response.timing.request_time) + : undefined; + + return { + timestamp: event._source['@timestamp'], + method: event._source.synthetics.payload?.method, + url: event._source.synthetics.payload?.url, + status: event._source.synthetics.payload?.status, + mimeType: event._source.synthetics.payload?.response?.mime_type, + requestSentTime, + requestStartTime, + loadEndTime, + timings: event._source.synthetics.payload.response?.timing, + }; + }); +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/index.ts b/x-pack/plugins/uptime/server/lib/requests/index.ts index 1806495d14cc4..b032b7c6932c0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/index.ts +++ b/x-pack/plugins/uptime/server/lib/requests/index.ts @@ -20,6 +20,8 @@ import { getSnapshotCount } from './get_snapshot_counts'; import { getIndexStatus } from './get_index_status'; import { getJourneySteps } from './get_journey_steps'; import { getJourneyScreenshot } from './get_journey_screenshot'; +import { getJourneyDetails } from './get_journey_details'; +import { getNetworkEvents } from './get_network_events'; export const requests = { getCerts, @@ -38,6 +40,8 @@ export const requests = { getIndexStatus, getJourneySteps, getJourneyScreenshot, + getJourneyDetails, + getNetworkEvents, }; export type UptimeRequests = typeof requests; diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index de44b2565a2f8..0c46abd640b91 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -24,6 +24,7 @@ import { } from './monitors'; import { createGetMonitorDurationRoute } from './monitors/monitors_durations'; import { createGetIndexPatternRoute, createGetIndexStatusRoute } from './index_state'; +import { createNetworkEventsRoute } from './network_events'; export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; @@ -47,4 +48,5 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [ createGetMonitorDurationRoute, createJourneyRoute, createJourneyScreenshotRoute, + createNetworkEventsRoute, ]; diff --git a/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts b/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts new file mode 100644 index 0000000000000..cf3521d7bb40a --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteFactory } from '../types'; + +export const createNetworkEventsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/network_events', + validate: { + query: schema.object({ + checkGroup: schema.string(), + stepIndex: schema.number(), + }), + }, + handler: async ({ uptimeEsClient }, _context, request, response) => { + const { checkGroup, stepIndex } = request.query; + + const result = await libs.requests.getNetworkEvents({ + uptimeEsClient, + checkGroup, + stepIndex, + }); + + return response.ok({ + body: { + events: result, + }, + }); + }, +}); diff --git a/x-pack/plugins/uptime/server/rest_api/network_events/index.ts b/x-pack/plugins/uptime/server/rest_api/network_events/index.ts new file mode 100644 index 0000000000000..3f3c1afe06f99 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/network_events/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createNetworkEventsRoute } from './get_network_events'; diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts index 5b8583ea0322f..35332678f36ac 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts @@ -23,10 +23,16 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => checkGroup, }); + const details = await libs.requests.getJourneyDetails({ + uptimeEsClient, + checkGroup, + }); + return response.ok({ body: { checkGroup, steps: result, + details, }, }); }, From 5d18257f82b4aafa3e25c9bd19a1bc296c23ed66 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 8 Dec 2020 12:16:31 +0000 Subject: [PATCH 2/8] Type and test tweaks --- .../uptime/common/runtime_types/ping/ping.ts | 40 ++++++++++--------- .../__tests__/get_network_events.test.ts | 2 +- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index 8bb716164c91e..bbb6a8b915e05 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -240,28 +240,32 @@ export const PingType = t.intersection([ }), ]); -export const SyntheticsJourneyApiResponseType = t.type({ - checkGroup: t.string, - details: t.union([ - t.intersection([ - t.type({ - timestamp: t.string, - }), - t.partial({ - next: t.type({ +export const SyntheticsJourneyApiResponseType = t.intersection([ + t.type({ + checkGroup: t.string, + steps: t.array(PingType), + }), + t.partial({ + details: t.union([ + t.intersection([ + t.type({ timestamp: t.string, - checkGroup: t.string, }), - previous: t.type({ - timestamp: t.string, - checkGroup: t.string, + t.partial({ + next: t.type({ + timestamp: t.string, + checkGroup: t.string, + }), + previous: t.type({ + timestamp: t.string, + checkGroup: t.string, + }), }), - }), + ]), + t.null, ]), - t.null, - ]), - steps: t.array(PingType), -}); + }), +]); export type SyntheticsJourneyApiResponse = t.TypeOf; diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts index 5407baf7e71d4..a9adfc12fca24 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts @@ -296,7 +296,7 @@ describe('getNetworkEvents', () => { }, } as any); - const result = await getNetworkEvents({ + await getNetworkEvents({ uptimeEsClient, checkGroup: 'my-fake-group', stepIndex: '1', From 9d2f76d621c24138a75147443eade2af35ba9a9a Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 8 Dec 2020 13:16:02 +0000 Subject: [PATCH 3/8] Remove prop from experiments with showing icon --- .../public/components/monitor/synthetics/executed_step.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx index df2eea4b83b0b..01a599f8e8a60 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx @@ -29,7 +29,7 @@ export const ExecutedStep: FC = ({ step, index, checkGroup })
{step.synthetics?.step?.index && checkGroup ? ( - + Date: Thu, 10 Dec 2020 19:18:52 +0000 Subject: [PATCH 4/8] Use timings directly from the Agent: https://github.com/elastic/synthetics/pull/168 --- .../common/runtime_types/network_events.ts | 28 ++- .../waterfall/data_formatting.test.ts | 128 +------------- .../step_detail/waterfall/data_formatting.ts | 166 +++--------------- .../synthetics/step_detail/waterfall/types.ts | 6 +- .../server/lib/requests/get_network_events.ts | 2 +- 5 files changed, 38 insertions(+), 292 deletions(-) diff --git a/x-pack/plugins/uptime/common/runtime_types/network_events.ts b/x-pack/plugins/uptime/common/runtime_types/network_events.ts index 392bd08d75771..6104758f28fd8 100644 --- a/x-pack/plugins/uptime/common/runtime_types/network_events.ts +++ b/x-pack/plugins/uptime/common/runtime_types/network_events.ts @@ -7,24 +7,16 @@ import * as t from 'io-ts'; const NetworkTimingsType = t.type({ - dns_start: t.number, - push_end: t.number, - worker_fetch_start: t.number, - worker_respond_with_settled: t.number, - proxy_end: t.number, - worker_start: t.number, - worker_ready: t.number, - send_end: t.number, - connect_end: t.number, - connect_start: t.number, - send_start: t.number, - proxy_start: t.number, - push_start: t.number, - ssl_end: t.number, - receive_headers_end: t.number, - ssl_start: t.number, - request_time: t.number, - dns_end: t.number, + queueing: t.number, + connect: t.number, + total: t.number, + send: t.number, + blocked: t.number, + receive: t.number, + wait: t.number, + dns: t.number, + proxy: t.number, + ssl: t.number, }); export type NetworkTimings = t.TypeOf; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts index 03e025bf54190..fff14376667b2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -4,133 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { colourPalette, getTimings, getSeriesAndDomain, extractItems } from './data_formatting'; - -const networkEvent = { - timestamp: '2020-12-06T19:55:01.273Z', - method: 'GET', - url: 'https://www.some-fake-url.css', - status: 200, - mimeType: 'text/css', - requestSentTime: 1723033619.631, - requestStartTime: 1723033621.036, - loadEndTime: 1723033729.635, - timings: { - ssl_start: 32.875, - proxy_end: -1, - send_start: 69.738, - send_end: 69.923, - connect_start: 0.591, - receive_headers_end: 106.076, - dns_end: 0.591, - connect_end: 69.557, - worker_fetch_start: -1, - worker_ready: -1, - push_start: 0, - dns_start: 0.539, - ssl_end: 69.542, - request_time: 1723033.621036, - worker_respond_with_settled: -1, - worker_start: -1, - push_end: 0, - proxy_start: -1, - }, -}; - -describe('getTimings', () => { - it('Calculates timings for network events correctly', () => { - const timings = getTimings( - networkEvent.timings, - networkEvent.requestSentTime, - networkEvent.loadEndTime - ); - expect(timings).toEqual({ - blocked: 1.9439999713897707, - connect: 32.480000000000004, - dns: 0.051999999999999935, - receive: 2.5230000019073486, - send: 0.18500000000000227, - ssl: 36.667, - wait: 36.15299999999999, - }); - }); -}); - -describe('getSeriesAndDomain', () => { - let seriesAndDomain: any; - let NetworkItems: any; - - beforeAll(() => { - NetworkItems = extractItems([networkEvent]); - seriesAndDomain = getSeriesAndDomain(NetworkItems); - }); - - it('Correctly calculates the domain', () => { - expect(seriesAndDomain.domain).toEqual({ max: 110.00399997329711, min: 0 }); - }); - - it('Correctly calculates the series', () => { - expect(seriesAndDomain.series).toEqual([ - { - config: { - colour: '#b9a888', - tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 1.944ms' }, - }, - x: 0, - y: 1.9439999713897707, - y0: 0, - }, - { - config: { colour: '#54b399', tooltipProps: { colour: '#54b399', value: 'DNS: 0.052ms' } }, - x: 0, - y: 1.9959999713897707, - y0: 1.9439999713897707, - }, - { - config: { - colour: '#da8b45', - tooltipProps: { colour: '#da8b45', value: 'Connecting: 32.480ms' }, - }, - x: 0, - y: 34.475999971389776, - y0: 1.9959999713897707, - }, - { - config: { colour: '#edc5a2', tooltipProps: { colour: '#edc5a2', value: 'SSL: 36.667ms' } }, - x: 0, - y: 71.14299997138977, - y0: 34.475999971389776, - }, - { - config: { - colour: '#d36086', - tooltipProps: { colour: '#d36086', value: 'Sending request: 0.185ms' }, - }, - x: 0, - y: 71.32799997138977, - y0: 71.14299997138977, - }, - { - config: { - colour: '#b0c9e0', - tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 36.153ms' }, - }, - x: 0, - y: 107.48099997138976, - y0: 71.32799997138977, - }, - { - config: { - colour: '#ca8eae', - tooltipProps: { colour: '#ca8eae', value: 'Content downloading: 2.523ms' }, - }, - x: 0, - y: 110.00399997329711, - y0: 107.48099997138976, - }, - ]); - }); -}); +import { colourPalette } from './data_formatting'; describe('Palettes', () => { it('A colour palette comprising timing and mime type colours is correctly generated', () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 5badee09e9371..94dd8e33e9e91 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -7,8 +7,6 @@ import { euiPaletteColorBlind } from '@elastic/eui'; import { - PayloadTimings, - CalculatedTimings, NetworkItems, NetworkItem, FriendlyTimingLabels, @@ -23,112 +21,10 @@ import { import { WaterfallData } from '../../waterfall'; import { NetworkEvent } from '../../../../../../common/runtime_types'; -const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 1000); - -// The timing calculations here are based off several sources: -// https://github.com/ChromeDevTools/devtools-frontend/blob/2fe91adefb2921b4deb2b4b125370ef9ccdb8d1b/front_end/sdk/HARLog.js#L307 -// and -// https://chromium.googlesource.com/chromium/blink.git/+/master/Source/devtools/front_end/sdk/HAREntry.js#131 -// and -// https://github.com/cyrus-and/chrome-har-capturer/blob/master/lib/har.js#L195 -// and -// https://github.com/sitespeedio/chrome-har/blob/4586d2961fe8752982120c3f613b8da42cf3648b/lib/finalizeEntry.js#L7 -// Order of events: request_start = 0, [proxy], [dns], [connect [ssl]], [send], receive_headers_end - -export const getTimings = ( - timings: NetworkEvent['timings'], - requestSentTime: NetworkEvent['requestSentTime'], - loadEndTime: NetworkEvent['loadEndTime'] -): CalculatedTimings => { - if (!timings) return { blocked: -1, dns: -1, connect: -1, send: 0, wait: 0, receive: 0, ssl: -1 }; - - const getLeastNonNegative = (values: number[]) => - values.reduce((best, value) => (value >= 0 && value < best ? value : best), Infinity); - const getOptionalTiming = (_timings: PayloadTimings, key: keyof PayloadTimings) => - _timings[key] >= 0 ? _timings[key] : -1; - - // NOTE: Request sent and request start can differ due to queue times - const requestStartTime = microToMillis(timings.request_time); - - // Queued - const queuedTime = requestSentTime < requestStartTime ? requestStartTime - requestSentTime : -1; - - // Blocked - // "blocked" represents both queued time + blocked/stalled time + proxy time (ie: anything before the request was actually started). - let blocked = queuedTime; - - const blockedStart = getLeastNonNegative([ - timings.dns_start, - timings.connect_start, - timings.send_start, - ]); - - if (blockedStart !== Infinity) { - blocked += blockedStart; - } - - // Proxy - // Proxy is part of blocked, but it can be quirky in that blocked can be -1 even though there are proxy timings. This can happen with - // protocols like Quic. - if (timings.proxy_end !== -1) { - const blockedProxy = timings.proxy_end - timings.proxy_start; - - if (blockedProxy && blockedProxy > blocked) { - blocked = blockedProxy; - } - } - - // DNS - const dnsStart = timings.dns_end >= 0 ? blockedStart : 0; - const dnsEnd = getOptionalTiming(timings, 'dns_end'); - const dns = dnsEnd - dnsStart; - - // SSL - const sslStart = getOptionalTiming(timings, 'ssl_start'); - const sslEnd = getOptionalTiming(timings, 'ssl_end'); - let ssl; - - if (sslStart >= 0 && sslEnd >= 0) { - ssl = timings.ssl_end - timings.ssl_start; - } - - // Connect - let connect = -1; - if (timings.connect_start >= 0) { - connect = timings.send_start - timings.connect_start; - } - - // Send - const send = timings.send_end - timings.send_start; - - // Wait - const waitStart = timings.send_end; - const waitEnd = timings.receive_headers_end; - const wait = waitEnd - waitStart; - - const receive = loadEndTime - (requestStartTime + timings.receive_headers_end); - - // SSL connection is a part of the overall connection time - if (connect && ssl) { - connect = connect - ssl; - } - - return { blocked, dns, connect, send, wait, receive, ssl }; -}; - export const extractItems = (data: NetworkEvent[]): NetworkItems => { - return data - .map((entry) => { - return { - ...entry, - timings: entry.timings - ? getTimings(entry.timings, entry.requestSentTime, entry.loadEndTime) - : undefined, - }; - }) - .sort((a: NetworkItem, b: NetworkItem) => { - return a.requestSentTime - b.requestSentTime; - }); + return data.sort((a: NetworkItem, b: NetworkItem) => { + return a.requestSentTime - b.requestSentTime; + }); }; const formatValueForDisplay = (value: number, points: number = 3) => { @@ -141,50 +37,36 @@ const getColourForMimeType = (mimeType?: string) => { }; export const getSeriesAndDomain = (items: NetworkItems) => { + const getValueForOffset = (item: NetworkItem) => { + return item.requestSentTime; + }; + // The earliest point in time a request is sent or started. This will become our notion of "0". const zeroOffset = items.reduce((acc, item) => { - const { requestSentTime } = item; - return requestSentTime < acc ? requestSentTime : acc; + const offsetValue = getValueForOffset(item); + return offsetValue < acc ? offsetValue : acc; }, Infinity); - const series = items.reduce((acc, item, index) => { - const { requestSentTime } = item; + const getValue = (timings: NetworkEvent['timings'], timing: Timings) => { + if (!timings) return; - // Entries without timings should be handled differently: - // https://github.com/ChromeDevTools/devtools-frontend/blob/ed2a064ac194bfae4e25c4748a9fa3513b3e9f7d/front_end/network/RequestTimingView.js#L140 - // If there are no concrete timings just plot one block via request start and response end - if (!item.timings || item.timings === null) { - const duration = item.loadEndTime - item.requestSentTime; - const colour = getColourForMimeType(item.mimeType); - return [ - ...acc, - { - x: index, - y0: item.requestSentTime - zeroOffset, - // NOTE: The loadEndTime can sometimes be "0" - y: - item.loadEndTime && item.loadEndTime > 0 - ? item.loadEndTime - zeroOffset - : item.requestSentTime - zeroOffset, - config: { - colour, - tooltipProps: { - // NOTE: The loadEndTime can sometimes be "0" - value: - item.loadEndTime && item.loadEndTime > 0 - ? `${formatValueForDisplay(duration)}ms` - : "Response time couldn't be determined", - colour, - }, - }, - }, - ]; + // SSL is a part of the connect timing + if (timing === Timings.Connect && timings.ssl > 0) { + return timings.connect - timings.ssl; + } else { + return timings[timing]; } + }; + + const series = items.reduce((acc, item, index) => { + if (!item.timings) return acc; + + const offsetValue = getValueForOffset(item); - let currentOffset = requestSentTime - zeroOffset; + let currentOffset = offsetValue - zeroOffset; TIMING_ORDER.forEach((timing) => { - const value = item.timings![timing]; + const value = getValue(item.timings, timing); const colour = timing === Timings.Receive ? getColourForMimeType(item.mimeType) : colourPalette[timing]; if (value && value >= 0) { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts index 708aca4531513..738929741ddaf 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts @@ -34,7 +34,7 @@ export const FriendlyTimingLabels = { } ), [Timings.Ssl]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.ssl', { - defaultMessage: 'SSL', + defaultMessage: 'TLS', }), [Timings.Send]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.send', { defaultMessage: 'Sending request', @@ -145,9 +145,7 @@ export const MimeTypesMap: Record = { 'application/font-sfnt': MimeType.Font, }; -export type NetworkItem = Omit & { - timings?: CalculatedTimings; -}; +export type NetworkItem = NetworkEvent; export type NetworkItems = NetworkItem[]; // NOTE: A number will always be present if the property exists, but that number might be -1, which represents no value. diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index 17c50cf15a38a..81ef6bb65a3df 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -50,7 +50,7 @@ export const getNetworkEvents: UMElasticsearchQueryFn< requestSentTime, requestStartTime, loadEndTime, - timings: event._source.synthetics.payload.response?.timing, + timings: event._source.synthetics.payload.timings, }; }); }; From a2056ca9d8d5b6be1200e8c7e4671bf4636d82a5 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Mon, 14 Dec 2020 11:05:04 +0000 Subject: [PATCH 5/8] Amend test and add comments --- .../step_detail/waterfall/data_formatting.ts | 2 + .../__tests__/get_network_events.test.ts | 397 +++++------------- .../server/lib/requests/get_network_events.ts | 3 + 3 files changed, 118 insertions(+), 284 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 94dd8e33e9e91..7c6e176315b5b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -22,6 +22,8 @@ import { WaterfallData } from '../../waterfall'; import { NetworkEvent } from '../../../../../../common/runtime_types'; export const extractItems = (data: NetworkEvent[]): NetworkItems => { + // NOTE: This happens client side as the "payload" property is mapped + // in such a way it can't be queried (or sorted on) via ES. return data.sort((a: NetworkItem, b: NetworkItem) => { return a.requestSentTime - b.requestSentTime; }); diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts index a9adfc12fca24..bb88911eedfb0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts @@ -13,274 +13,141 @@ describe('getNetworkEvents', () => { beforeEach(() => { mockHits = [ { - _index: 'heartbeat-2020.12.07', - _id: 'o5KvPXYBLREsP7x9Y7Gd', + _index: 'heartbeat-2020.12.14', + _id: 'YMfcYHYBOm8nKLizQt1o', _score: null, _source: { - '@timestamp': '2020-12-07T14:51:06.572Z', - monitor: { - type: 'browser', - timespan: { - gte: '2020-12-07T14:51:07.439Z', - lt: '2020-12-07T14:52:07.439Z', - }, - check_group: '7f617f79-389b-11eb-80b5-025000000001', - id: 'BMC-nav', - name: 'BMC', - }, + '@timestamp': '2020-12-14T10:46:39.183Z', synthetics: { + step: { + name: 'Click next link', + index: 2, + }, journey: { - id: 'inline', name: 'inline', + id: 'inline', }, type: 'journey/network_info', - package_version: '0.0.1-alpha.7', + package_version: '0.0.1-alpha.8', payload: { - type: 'Font', + load_end_time: 3287.298251, + response_received_time: 3287.299074, + method: 'GET', + step: { + index: 2, + name: 'Click next link', + }, + status: 200, + type: 'Image', + request_sent_time: 3287.154973, + url: 'www.test.com', request: { - mixed_content_type: 'none', - initial_priority: 'VeryHigh', + initial_priority: 'Low', referrer_policy: 'no-referrer-when-downgrade', - url: - 'https://use.typekit.com/af/ce2de8/000000000000000000011f36/27/l?subset_id=2&fvd=n4&v=3', + url: 'www.test.com', method: 'GET', headers: { - origin: 'https://www.thebmc.co.uk', - referer: 'https://www.thebmc.co.uk/volunteers', + referer: 'www.test.com', user_agent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4324.0 Safari/537.36', }, - }, - status: 200, - load_end_time: 11690.378366, - step: { - index: 2, - name: 'Click volunteers link', + mixed_content_type: 'none', }, response: { - mime_type: 'application/font-woff2', - from_disk_cache: false, - security_state: 'secure', from_service_worker: false, - url: - 'https://use.typekit.com/af/ce2de8/000000000000000000011f36/27/l?subset_id=2&fvd=n4&v=3', - protocol: 'h2', - connection_reused: true, - response_time: 1.60735266386276e12, - status_text: '', - headers: { - timing_allow_origin: '*', - content_length: '16620', - date: 'Mon, 07 Dec 2020 14:51:03 GMT', - server: 'nginx', - etag: '"cc13c3aaba9f28fe9e0411f0994b936cf4729475"', - content_type: 'application/font-woff2', - access_control_allow_origin: '*', - cache_control: 'public, max-age=31536000', - }, - connection_id: 253, - timing: { - push_start: 0, - worker_respond_with_settled: -1, - dns_end: -1, - worker_start: -1, - ssl_start: -1, - connect_end: -1, - receive_headers_end: 56.15, - worker_fetch_start: -1, - worker_ready: -1, - connect_start: -1, - send_start: 34.293, - proxy_start: -1, - send_end: 35.3, - request_time: 11687.636845, - dns_start: -1, - push_end: 0, - proxy_end: -1, - ssl_end: -1, - }, - remote_i_p_address: '62.252.188.232', - encoded_data_length: 16822, - from_prefetch_cache: false, - remote_port: 443, - status: 200, security_details: { - key_exchange: '', - san_list: ['use-staging.typekit.net', 'use.typekit.net', 'use.typekit.com'], - issuer: 'DigiCert SHA2 Secure Server CA', - cipher: 'AES_256_GCM', - subject_name: 'use.typekit.net', - protocol: 'TLS 1.3', - valid_from: 1580169600, - key_exchange_group: 'X25519', + protocol: 'TLS 1.2', + key_exchange: 'ECDHE_RSA', + valid_to: 1638230399, certificate_transparency_compliance: 'unknown', + cipher: 'AES_128_GCM', + issuer: 'DigiCert TLS RSA SHA256 2020 CA1', + subject_name: 'syndication.twitter.com', + valid_from: 1606694400, signed_certificate_timestamp_list: [], - valid_to: 1643716800, + key_exchange_group: 'P-256', + san_list: [ + 'syndication.twitter.com', + 'syndication.twimg.com', + 'cdn.syndication.twitter.com', + 'cdn.syndication.twimg.com', + 'syndication-o.twitter.com', + 'syndication-o.twimg.com', + ], certificate_id: 0, }, - }, - url: - 'https://use.typekit.com/af/ce2de8/000000000000000000011f36/27/l?subset_id=2&fvd=n4&v=3', - request_sent_time: 11690.378291, - is_navigation_request: false, - method: 'GET', - timestamp: 1.6073526665725272e15, - }, - step: { - index: 2, - name: 'Click volunteers link', - }, - }, - event: { - dataset: 'uptime', - }, - ecs: { - version: '1.6.0', - }, - agent: { - ephemeral_id: '5bcee4d8-a1ff-416d-9110-e9d67959fcfa', - id: '4031f8f1-b015-4f2e-8995-42852ff49339', - name: 'docker-desktop', - type: 'heartbeat', - version: '8.0.0', - }, - }, - sort: [1607352666572], - }, - { - _index: 'heartbeat-2020.12.07', - _id: 'pJKvPXYBLREsP7x9Y7Gd', - _score: null, - _source: { - '@timestamp': '2020-12-07T14:51:06.572Z', - synthetics: { - payload: { - load_end_time: 11690.378555, - is_navigation_request: false, - timestamp: 1607352666572586, - step: { - name: 'Click volunteers link', - index: 2, - }, - url: - 'https://use.typekit.com/af/12aedb/000000000000000000011f38/27/l?subset_id=2&fvd=n7&v=3', - request: { - method: 'GET', - headers: { - user_agent: - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4324.0 Safari/537.36', - origin: 'https://www.thebmc.co.uk', - referer: 'https://www.thebmc.co.uk/volunteers', - }, - mixed_content_type: 'none', - initial_priority: 'VeryHigh', - referrer_policy: 'no-referrer-when-downgrade', - url: - 'https://use.typekit.com/af/12aedb/000000000000000000011f38/27/l?subset_id=2&fvd=n7&v=3', - }, - status: 200, - response: { - status_text: '', - from_prefetch_cache: false, + security_state: 'secure', + connection_reused: true, + remote_port: 443, timing: { - worker_fetch_start: -1, - send_start: 33.679, ssl_start: -1, - worker_start: -1, - send_end: 34.725, - dns_start: -1, + send_start: 0.214, + ssl_end: -1, connect_start: -1, + connect_end: -1, + send_end: 0.402, + dns_start: -1, + request_time: 3287.155502, + push_end: 0, + worker_fetch_start: -1, worker_ready: -1, + worker_start: -1, proxy_end: -1, - worker_respond_with_settled: -1, - receive_headers_end: 53.931, push_start: 0, - connect_end: -1, - dns_end: -1, - push_end: 0, - ssl_end: -1, + worker_respond_with_settled: -1, proxy_start: -1, - request_time: 11687.637418, - }, - protocol: 'h2', - remote_port: 443, - security_details: { - certificate_transparency_compliance: 'unknown', - signed_certificate_timestamp_list: [], - key_exchange_group: 'X25519', - cipher: 'AES_256_GCM', - protocol: 'TLS 1.3', - valid_from: 1580169600, - key_exchange: '', - subject_name: 'use.typekit.net', - certificate_id: 0, - san_list: ['use-staging.typekit.net', 'use.typekit.net', 'use.typekit.com'], - issuer: 'DigiCert SHA2 Secure Server CA', - valid_to: 1643716800, + dns_end: -1, + receive_headers_end: 142.215, }, - connection_id: 253, - status: 200, - response_time: 1.607352663861158e12, + connection_id: 852, + remote_i_p_address: '104.244.42.200', + encoded_data_length: 337, + response_time: 1.60794279932414e12, + from_prefetch_cache: false, + mime_type: 'image/gif', from_disk_cache: false, - connection_reused: true, - security_state: 'secure', - encoded_data_length: 16986, - mime_type: 'application/font-woff2', - from_service_worker: false, - remote_i_p_address: '62.252.188.232', + url: 'www.test.com', + protocol: 'h2', headers: { - content_type: 'application/font-woff2', - access_control_allow_origin: '*', - cache_control: 'public, max-age=31536000', - timing_allow_origin: '*', - content_length: '16784', - date: 'Mon, 07 Dec 2020 14:51:03 GMT', - server: 'nginx', - etag: '"c0994501e4f56e0b83223f5c4a96d4b3fdcfe17c"', + x_frame_options: 'SAMEORIGIN', + cache_control: 'no-cache, no-store, must-revalidate, pre-check=0, post-check=0', + strict_transport_security: 'max-age=631138519', + x_twitter_response_tags: 'BouncerCompliant', + content_type: 'image/gif;charset=utf-8', + expires: 'Tue, 31 Mar 1981 05:00:00 GMT', + date: 'Mon, 14 Dec 2020 10:46:39 GMT', + x_transaction: '008fff3d00a1e64c', + x_connection_hash: 'cb6fe99b8676f4e4b827cc3e6512c90d', + last_modified: 'Mon, 14 Dec 2020 10:46:39 GMT', + x_content_type_options: 'nosniff', + content_encoding: 'gzip', + x_xss_protection: '0', + server: 'tsa_f', + x_response_time: '108', + pragma: 'no-cache', + content_length: '65', + status: '200 OK', }, - url: - 'https://use.typekit.com/af/12aedb/000000000000000000011f38/27/l?subset_id=2&fvd=n7&v=3', + status_text: '', + status: 200, }, - method: 'GET', - type: 'Font', - request_sent_time: 11690.378482, - }, - step: { - index: 2, - name: 'Click volunteers link', - }, - journey: { - id: 'inline', - name: 'inline', - }, - type: 'journey/network_info', - package_version: '0.0.1-alpha.7', - }, - monitor: { - timespan: { - gte: '2020-12-07T14:51:07.439Z', - lt: '2020-12-07T14:52:07.439Z', + timings: { + proxy: -1, + connect: -1, + receive: 0.5340000002433953, + blocked: 0.21400000014182297, + ssl: -1, + send: 0.18799999998009298, + total: 143.27800000000934, + queueing: 0.5289999999149586, + wait: 141.81299999972907, + dns: -1, + }, + is_navigation_request: false, + timestamp: 1607942799183375, }, - check_group: '7f617f79-389b-11eb-80b5-025000000001', - id: 'BMC-nav', - name: 'BMC', - type: 'browser', - }, - event: { - dataset: 'uptime', - }, - ecs: { - version: '1.6.0', - }, - agent: { - ephemeral_id: '5bcee4d8-a1ff-416d-9110-e9d67959fcfa', - id: '4031f8f1-b015-4f2e-8995-42852ff49339', - name: 'docker-desktop', - type: 'heartbeat', - version: '8.0.0', }, }, - sort: [1607352666572], }, ]; }); @@ -357,64 +224,26 @@ describe('getNetworkEvents', () => { expect(result).toMatchInlineSnapshot(` Array [ Object { - "loadEndTime": 11690378.366, - "method": "GET", - "mimeType": "application/font-woff2", - "requestSentTime": 11690378.291000001, - "requestStartTime": 11687636.845, - "status": 200, - "timestamp": "2020-12-07T14:51:06.572Z", - "timings": Object { - "connect_end": -1, - "connect_start": -1, - "dns_end": -1, - "dns_start": -1, - "proxy_end": -1, - "proxy_start": -1, - "push_end": 0, - "push_start": 0, - "receive_headers_end": 56.15, - "request_time": 11687.636845, - "send_end": 35.3, - "send_start": 34.293, - "ssl_end": -1, - "ssl_start": -1, - "worker_fetch_start": -1, - "worker_ready": -1, - "worker_respond_with_settled": -1, - "worker_start": -1, - }, - "url": "https://use.typekit.com/af/ce2de8/000000000000000000011f36/27/l?subset_id=2&fvd=n4&v=3", - }, - Object { - "loadEndTime": 11690378.555, + "loadEndTime": 3287298.251, "method": "GET", - "mimeType": "application/font-woff2", - "requestSentTime": 11690378.482, - "requestStartTime": 11687637.418, + "mimeType": "image/gif", + "requestSentTime": 3287154.973, + "requestStartTime": 3287155.502, "status": 200, - "timestamp": "2020-12-07T14:51:06.572Z", + "timestamp": "2020-12-14T10:46:39.183Z", "timings": Object { - "connect_end": -1, - "connect_start": -1, - "dns_end": -1, - "dns_start": -1, - "proxy_end": -1, - "proxy_start": -1, - "push_end": 0, - "push_start": 0, - "receive_headers_end": 53.931, - "request_time": 11687.637418, - "send_end": 34.725, - "send_start": 33.679, - "ssl_end": -1, - "ssl_start": -1, - "worker_fetch_start": -1, - "worker_ready": -1, - "worker_respond_with_settled": -1, - "worker_start": -1, + "blocked": 0.21400000014182297, + "connect": -1, + "dns": -1, + "proxy": -1, + "queueing": 0.5289999999149586, + "receive": 0.5340000002433953, + "send": 0.18799999998009298, + "ssl": -1, + "total": 143.27800000000934, + "wait": 141.81299999972907, }, - "url": "https://use.typekit.com/af/12aedb/000000000000000000011f38/27/l?subset_id=2&fvd=n7&v=3", + "url": "www.test.com", }, ] `); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index 81ef6bb65a3df..1353175a8f94d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -26,6 +26,9 @@ export const getNetworkEvents: UMElasticsearchQueryFn< ], }, }, + // NOTE: This limit may need tweaking in the future. Users can technically perform multiple + // navigations within one step, and may push up against this limit, however this manner + // of usage isn't advised. size: 1000, }; From ca592f02567d9cac2db90d06f3f09350a993674c Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Mon, 14 Dec 2020 14:37:20 +0000 Subject: [PATCH 6/8] Amend route after addition of debug feature --- .../rest_api/network_events/get_network_events.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts b/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts index cf3521d7bb40a..f24b319baff00 100644 --- a/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts +++ b/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts @@ -17,7 +17,7 @@ export const createNetworkEventsRoute: UMRestApiRouteFactory = (libs: UMServerLi stepIndex: schema.number(), }), }, - handler: async ({ uptimeEsClient }, _context, request, response) => { + handler: async ({ uptimeEsClient, request }): Promise => { const { checkGroup, stepIndex } = request.query; const result = await libs.requests.getNetworkEvents({ @@ -26,10 +26,8 @@ export const createNetworkEventsRoute: UMRestApiRouteFactory = (libs: UMServerLi stepIndex, }); - return response.ok({ - body: { - events: result, - }, - }); + return { + events: result, + }; }, }); From f729bcee00bddeb96265737c4f2e72c08803b0ca Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Mon, 14 Dec 2020 15:15:58 +0000 Subject: [PATCH 7/8] Account for axis height in total height calculation so that smaller totals are at least the BAR_SIZE --- .../monitor/synthetics/waterfall/components/constants.ts | 3 +++ .../monitor/synthetics/waterfall/components/styles.ts | 4 +--- .../synthetics/waterfall/components/waterfall_chart.tsx | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts index ac650c5ef0ddd..d7baaf4245971 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts @@ -10,3 +10,6 @@ export const BAR_HEIGHT = 32; export const MAIN_GROW_SIZE = 8; // Flex grow value export const SIDEBAR_GROW_SIZE = 2; +// Axis height +// NOTE: This isn't a perfect solution - changes in font size etc within charts could change the ideal height here. +export const FIXED_AXIS_HEIGHT = 33; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 25f5e5f8f5cc9..f0e5dbcc33aa3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -6,9 +6,7 @@ import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { euiStyled } from '../../../../../../../observability/public'; - -// NOTE: This isn't a perfect solution - changes in font size etc within charts could change the ideal height here. -const FIXED_AXIS_HEIGHT = 33; +import { FIXED_AXIS_HEIGHT } from './constants'; interface WaterfallChartOuterContainerProps { height?: number; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index f622ad386a4f7..e7bf9b2efe6dc 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -33,7 +33,7 @@ import { WaterfallChartTooltip, } from './styles'; import { WaterfallData } from '../types'; -import { BAR_HEIGHT, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; +import { BAR_HEIGHT, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE, FIXED_AXIS_HEIGHT } from './constants'; import { Sidebar } from './sidebar'; import { Legend } from './legend'; @@ -77,7 +77,8 @@ const getUniqueBars = (data: WaterfallData) => { }, new Set()); }; -const getChartHeight = (data: WaterfallData): number => getUniqueBars(data).size * BAR_HEIGHT; +const getChartHeight = (data: WaterfallData): number => + getUniqueBars(data).size * BAR_HEIGHT + FIXED_AXIS_HEIGHT; export const WaterfallChart = ({ tickFormat, From 8701a949a4cb9d6792a0f6fc42162cf80145c367 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Mon, 14 Dec 2020 17:36:29 +0000 Subject: [PATCH 8/8] Make sidebar and chart border align and remove datepicker --- .../components/common/header/page_header.tsx | 10 ++++++++-- .../waterfall/components/constants.ts | 2 +- .../synthetics/waterfall/components/styles.ts | 1 + .../waterfall/components/waterfall_chart.tsx | 18 ++++++++++++------ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/common/header/page_header.tsx b/x-pack/plugins/uptime/public/components/common/header/page_header.tsx index 63bcb6663619d..7d093efd31be0 100644 --- a/x-pack/plugins/uptime/public/components/common/header/page_header.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/page_header.tsx @@ -11,7 +11,12 @@ import { useRouteMatch } from 'react-router-dom'; import { UptimeDatePicker } from '../uptime_date_picker'; import { SyntheticsCallout } from '../../overview/synthetics_callout'; import { PageTabs } from './page_tabs'; -import { CERTIFICATES_ROUTE, MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../common/constants'; +import { + CERTIFICATES_ROUTE, + MONITOR_ROUTE, + SETTINGS_ROUTE, + STEP_DETAIL_ROUTE, +} from '../../../../common/constants'; import { CertRefreshBtn } from '../../certificates/cert_refresh_btn'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; @@ -34,11 +39,12 @@ const StyledPicker = styled(EuiFlexItem)` export const PageHeader = () => { const isCertRoute = useRouteMatch(CERTIFICATES_ROUTE); const isSettingsRoute = useRouteMatch(SETTINGS_ROUTE); + const isStepDetailRoute = useRouteMatch(STEP_DETAIL_ROUTE); const DatePickerComponent = () => isCertRoute ? ( - ) : ( + ) : isStepDetailRoute ? null : ( diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts index d7baaf4245971..95ec298e2e349 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts @@ -12,4 +12,4 @@ export const MAIN_GROW_SIZE = 8; export const SIDEBAR_GROW_SIZE = 2; // Axis height // NOTE: This isn't a perfect solution - changes in font size etc within charts could change the ideal height here. -export const FIXED_AXIS_HEIGHT = 33; +export const FIXED_AXIS_HEIGHT = 32; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index f0e5dbcc33aa3..320e415585ca3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -22,6 +22,7 @@ export const WaterfallChartFixedTopContainer = euiStyled.div` position: sticky; top: 0; z-index: ${(props) => props.theme.eui.euiZLevel4}; + border-bottom: ${(props) => `1px solid ${props.theme.eui.euiColorLightShade}`}; `; export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index e7bf9b2efe6dc..d92e26335a6bd 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -112,7 +112,7 @@ export const WaterfallChart = ({ {shouldRenderSidebar && ( - + )} @@ -131,10 +131,13 @@ export const WaterfallChart = ({ tickFormat={tickFormat} domain={domain} showGridLines={true} + style={{ + axisLine: { + visible: false, + }, + }} /> - ''} /> - - + {shouldRenderSidebar && ( )} @@ -170,10 +173,13 @@ export const WaterfallChart = ({ tickFormat={tickFormat} domain={domain} showGridLines={true} + style={{ + axisLine: { + visible: false, + }, + }} /> - ''} /> -