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 index bdc6dbf3f6de22..3d9b646931e7d3 100644 --- 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 @@ -32,7 +32,7 @@ export const StepDetailContainer: React.FC = ({ checkGroup, stepIndex }) useEffect(() => { if (checkGroup) { - dispatch(getJourneySteps({ checkGroup })); + dispatch(getJourneySteps({ checkGroup, syntheticEventTypes: ['step/end'] })); } }, [dispatch, checkGroup]); diff --git a/x-pack/plugins/uptime/public/state/actions/journey.ts b/x-pack/plugins/uptime/public/state/actions/journey.ts index 0d35559d97fc38..5931980c569473 100644 --- a/x-pack/plugins/uptime/public/state/actions/journey.ts +++ b/x-pack/plugins/uptime/public/state/actions/journey.ts @@ -9,6 +9,7 @@ import { SyntheticsJourneyApiResponse } from '../../../common/runtime_types'; export interface FetchJourneyStepsParams { checkGroup: string; + syntheticEventTypes?: string[]; } export interface GetJourneyFailPayload { diff --git a/x-pack/plugins/uptime/public/state/api/journey.ts b/x-pack/plugins/uptime/public/state/api/journey.ts index 1aeeb485e481f1..684056b197f938 100644 --- a/x-pack/plugins/uptime/public/state/api/journey.ts +++ b/x-pack/plugins/uptime/public/state/api/journey.ts @@ -16,7 +16,7 @@ export async function fetchJourneySteps( ): Promise { return (await apiService.get( `/api/uptime/journey/${params.checkGroup}`, - undefined, + { syntheticEventTypes: params.syntheticEventTypes }, SyntheticsJourneyApiResponseType )) as SyntheticsJourneyApiResponse; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.test.ts new file mode 100644 index 00000000000000..8c432ff6f1e0fe --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.test.ts @@ -0,0 +1,196 @@ +/* + * 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 { getJourneySteps, formatSyntheticEvents } from './get_journey_steps'; +import { getUptimeESMockClient } from './helper'; + +describe('getJourneySteps request module', () => { + describe('formatStepTypes', () => { + it('returns default steps if none are provided', () => { + expect(formatSyntheticEvents()).toMatchInlineSnapshot(` + Array [ + "step/end", + "stderr", + "cmd/status", + "step/screenshot", + ] + `); + }); + + it('returns provided step array if isArray', () => { + expect(formatSyntheticEvents(['step/end', 'stderr'])).toMatchInlineSnapshot(` + Array [ + "step/end", + "stderr", + ] + `); + }); + + it('returns provided step string in an array', () => { + expect(formatSyntheticEvents('step/end')).toMatchInlineSnapshot(` + Array [ + "step/end", + ] + `); + }); + }); + + describe('getJourneySteps', () => { + let data: any; + beforeEach(() => { + data = { + body: { + hits: { + hits: [ + { + _id: 'o6myXncBFt2V8m6r6z-r', + _source: { + '@timestamp': '2021-02-01T17:45:19.001Z', + synthetics: { + package_version: '0.0.1-alpha.8', + journey: { + name: 'inline', + id: 'inline', + }, + step: { + name: 'load homepage', + index: 1, + }, + type: 'step/end', + }, + monitor: { + name: 'My Monitor', + id: 'my-monitor', + check_group: '2bf952dc-64b5-11eb-8b3b-42010a84000d', + type: 'browser', + }, + }, + }, + { + _id: 'IjqzXncBn2sjqrYxYoCG', + _source: { + '@timestamp': '2021-02-01T17:45:49.944Z', + synthetics: { + package_version: '0.0.1-alpha.8', + journey: { + name: 'inline', + id: 'inline', + }, + step: { + name: 'hover over products menu', + index: 2, + }, + type: 'step/end', + }, + monitor: { + name: 'My Monitor', + timespan: { + lt: '2021-02-01T17:46:49.945Z', + gte: '2021-02-01T17:45:49.945Z', + }, + id: 'my-monitor', + check_group: '2bf952dc-64b5-11eb-8b3b-42010a84000d', + type: 'browser', + }, + }, + }, + ], + }, + }, + }; + }); + + it('formats ES result', async () => { + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); + + mockEsClient.search.mockResolvedValueOnce(data as any); + const result: any = await getJourneySteps({ + uptimeEsClient, + checkGroup: '2bf952dc-64b5-11eb-8b3b-42010a84000d', + }); + expect(mockEsClient.search).toHaveBeenCalledTimes(1); + const call: any = mockEsClient.search.mock.calls[0][0]; + + // check that default `synthetics.type` value is supplied, + expect(call.body.query.bool.filter[0]).toMatchInlineSnapshot(` + Object { + "terms": Object { + "synthetics.type": Array [ + "step/end", + "stderr", + "cmd/status", + "step/screenshot", + ], + }, + } + `); + + // given check group is used for the terms filter + expect(call.body.query.bool.filter[1]).toMatchInlineSnapshot(` + Object { + "term": Object { + "monitor.check_group": "2bf952dc-64b5-11eb-8b3b-42010a84000d", + }, + } + `); + + // should sort by step index, then timestamp + expect(call.body.sort).toMatchInlineSnapshot(` + Array [ + Object { + "synthetics.step.index": Object { + "order": "asc", + }, + }, + Object { + "@timestamp": Object { + "order": "asc", + }, + }, + ] + `); + + expect(result).toHaveLength(2); + // `getJourneySteps` is responsible for formatting these fields, so we need to check them + result.forEach((step: any) => { + expect(['2021-02-01T17:45:19.001Z', '2021-02-01T17:45:49.944Z']).toContain(step.timestamp); + expect(['o6myXncBFt2V8m6r6z-r', 'IjqzXncBn2sjqrYxYoCG']).toContain(step.docId); + expect(step.synthetics.screenshotExists).toBeDefined(); + }); + }); + + it('notes screenshot exists when a document of type step/screenshot is included', async () => { + const { esClient: mockEsClient, uptimeEsClient } = getUptimeESMockClient(); + + data.body.hits.hits[0]._source.synthetics.type = 'step/screenshot'; + data.body.hits.hits[0]._source.synthetics.step.index = 2; + mockEsClient.search.mockResolvedValueOnce(data as any); + + const result: any = await getJourneySteps({ + uptimeEsClient, + checkGroup: '2bf952dc-64b5-11eb-8b3b-42010a84000d', + syntheticEventTypes: ['stderr', 'step/end'], + }); + + const call: any = mockEsClient.search.mock.calls[0][0]; + + // assert that filters for only the provided step types are used + expect(call.body.query.bool.filter[0]).toMatchInlineSnapshot(` + Object { + "terms": Object { + "synthetics.type": Array [ + "stderr", + "step/end", + ], + }, + } + `); + + expect(result).toHaveLength(1); + expect(result[0].synthetics.screenshotExists).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts index c330e1b66fe93c..60d2a97c99f7d1 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts @@ -9,11 +9,23 @@ import { Ping } from '../../../common/runtime_types'; interface GetJourneyStepsParams { checkGroup: string; + syntheticEventTypes?: string | string[]; } +const defaultEventTypes = ['step/end', 'stderr', 'cmd/status', 'step/screenshot']; + +export const formatSyntheticEvents = (eventTypes?: string | string[]) => { + if (!eventTypes) { + return defaultEventTypes; + } else { + return Array.isArray(eventTypes) ? eventTypes : [eventTypes]; + } +}; + export const getJourneySteps: UMElasticsearchQueryFn = async ({ uptimeEsClient, checkGroup, + syntheticEventTypes, }) => { const params = { query: { @@ -21,7 +33,7 @@ export const getJourneySteps: UMElasticsearchQueryFn checkGroup: schema.string(), _debug: schema.maybe(schema.boolean()), }), + query: schema.object({ + // provides a filter for the types of synthetic events to include + // when fetching a journey's data + syntheticEventTypes: schema.maybe( + schema.oneOf([schema.arrayOf(schema.string()), schema.string()]) + ), + }), }, handler: async ({ uptimeEsClient, request }): Promise => { const { checkGroup } = request.params; + const { syntheticEventTypes } = request.query; const result = await libs.requests.getJourneySteps({ uptimeEsClient, checkGroup, + syntheticEventTypes, }); const details = await libs.requests.getJourneyDetails({