From 5516dc6a18a3cb677e73e785414ff1823a4f760b Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 29 Sep 2020 14:01:07 +0200 Subject: [PATCH] [7.x] [CSM] Js errors (#77919) (#78748) Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../step_definitions/csm/csm_filters.ts | 44 ++++-- .../csm/service_name_filter.ts | 8 +- x-pack/plugins/apm/e2e/ingest-data/replay.js | 2 +- .../app/RumDashboard/ClientMetrics/index.tsx | 8 + .../RumDashboard/CsmSharedContext/index.tsx | 45 ++++++ .../ImpactfulMetrics/JSErrors.tsx | 138 ++++++++++++++++++ .../RumDashboard/ImpactfulMetrics/index.tsx | 22 +++ .../app/RumDashboard/RumDashboard.tsx | 4 + .../components/app/RumDashboard/RumHome.tsx | 23 +-- .../app/RumDashboard/translations.ts | 27 ++++ .../LocalUIFilters/Filter/FilterBadgeList.tsx | 5 +- .../shared/LocalUIFilters/Filter/index.tsx | 3 +- .../__snapshots__/queries.test.ts.snap | 91 ++++++++++++ .../server/lib/rum_client/get_js_errors.ts | 99 +++++++++++++ .../apm/server/lib/rum_client/queries.test.ts | 12 ++ .../projections/rum_page_load_transactions.ts | 35 +++++ .../apm/server/routes/create_apm_api.ts | 46 +++--- .../plugins/apm/server/routes/rum_client.ts | 25 ++++ .../apm/typings/elasticsearch/aggregations.ts | 6 + .../apm/typings/elasticsearch/index.ts | 11 ++ .../trial/tests/csm/js_errors.ts | 61 ++++++++ .../apm_api_integration/trial/tests/index.ts | 1 + 22 files changed, 665 insertions(+), 51 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/CsmSharedContext/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx create mode 100644 x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts create mode 100644 x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts index c17845fd61468..75974ef9c202c 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts @@ -14,16 +14,40 @@ When(/^the user filters by "([^"]*)"$/, (filterName) => { cy.get('.euiStat__title-isLoading').should('not.be.visible'); cy.get(`#local-filter-${filterName}`).click(); - if (filterName === 'os') { - cy.get('span.euiSelectableListItem__text', DEFAULT_TIMEOUT) - .contains('Mac OS X') - .click(); - } else { - cy.get('span.euiSelectableListItem__text', DEFAULT_TIMEOUT) - .contains('DE') - .click(); - } - cy.get('[data-cy=applyFilter]').click(); + cy.get(`#local-filter-popover-${filterName}`, DEFAULT_TIMEOUT).within(() => { + if (filterName === 'os') { + const osItem = cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(2); + osItem.should('have.text', 'Mac OS X8 '); + osItem.click(); + + // sometimes click doesn't work as expected so we need to retry here + osItem.invoke('attr', 'aria-selected').then((val) => { + if (val === 'false') { + cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(2).click(); + } + }); + } else { + const deItem = cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(0); + deItem.should('have.text', 'DE28 '); + deItem.click(); + + // sometimes click doesn't work as expected so we need to retry here + deItem.invoke('attr', 'aria-selected').then((val) => { + if (val === 'false') { + cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(0).click(); + } + }); + } + cy.get('[data-cy=applyFilter]').click(); + }); + + cy.get(`div#local-filter-values-${filterName}`, DEFAULT_TIMEOUT).within( + () => { + cy.get('span.euiBadge__content') + .eq(0) + .should('have.text', filterName === 'os' ? 'Mac OS X' : 'DE'); + } + ); }); Then(/^it filters the client metrics "([^"]*)"$/, (filterName) => { diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts index 4169149bc7339..b3899a5649b72 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts @@ -5,15 +5,13 @@ */ import { When, Then } from 'cypress-cucumber-preprocessor/steps'; -import { DEFAULT_TIMEOUT } from '../apm'; import { verifyClientMetrics } from './client_metrics_helper'; +import { DEFAULT_TIMEOUT } from './csm_dashboard'; -When('the user changes the selected service name', (filterName) => { +When('the user changes the selected service name', () => { // wait for all loading to finish cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get(`[data-cy=serviceNameFilter]`, { timeout: DEFAULT_TIMEOUT }).select( - 'client' - ); + cy.get(`[data-cy=serviceNameFilter]`, DEFAULT_TIMEOUT).select('client'); }); Then(`it displays relevant client metrics`, () => { diff --git a/x-pack/plugins/apm/e2e/ingest-data/replay.js b/x-pack/plugins/apm/e2e/ingest-data/replay.js index 74c86b1b09ab4..326cb739e23c6 100644 --- a/x-pack/plugins/apm/e2e/ingest-data/replay.js +++ b/x-pack/plugins/apm/e2e/ingest-data/replay.js @@ -96,7 +96,7 @@ function setRumAgent(item) { if (item.body) { item.body = item.body.replace( '"name":"client"', - '"name":"opbean-client-rum"' + '"name":"elastic-frontend"' ); } } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index a77d27c4bc883..bc1e0a86f17db 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -6,10 +6,12 @@ import * as React from 'react'; import numeral from '@elastic/numeral'; import styled from 'styled-components'; +import { useContext, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiToolTip } from '@elastic/eui'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { I18LABELS } from '../translations'; +import { CsmSharedContext } from '../CsmSharedContext'; const ClFlexGroup = styled(EuiFlexGroup)` flex-direction: row; @@ -45,6 +47,12 @@ export function ClientMetrics() { [start, end, uiFilters, searchTerm] ); + const { setSharedData } = useContext(CsmSharedContext); + + useEffect(() => { + setSharedData({ totalPageViews: data?.pageViews?.value ?? 0 }); + }, [data, setSharedData]); + const STAT_STYLE = { width: '240px' }; return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CsmSharedContext/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CsmSharedContext/index.tsx new file mode 100644 index 0000000000000..3d445104d6d10 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CsmSharedContext/index.tsx @@ -0,0 +1,45 @@ +/* + * 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, { createContext, useMemo, useState } from 'react'; + +interface SharedData { + totalPageViews: number; +} + +interface Index { + sharedData: SharedData; + setSharedData: (data: SharedData) => void; +} + +const defaultContext: Index = { + sharedData: { totalPageViews: 0 }, + setSharedData: (d) => { + throw new Error( + 'setSharedData was not initialized, set it when you invoke the context' + ); + }, +}; + +export const CsmSharedContext = createContext(defaultContext); + +export function CsmSharedContextProvider({ + children, +}: { + children: JSX.Element[]; +}) { + const [newData, setNewData] = useState({ totalPageViews: 0 }); + + const setSharedData = React.useCallback((data: SharedData) => { + setNewData(data); + }, []); + + const value = useMemo(() => { + return { sharedData: newData, setSharedData }; + }, [newData, setSharedData]); + + return ; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx new file mode 100644 index 0000000000000..805b328cb1fb0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx @@ -0,0 +1,138 @@ +/* + * 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, { useContext, useState } from 'react'; +import { + EuiBasicTable, + EuiFlexItem, + EuiFlexGroup, + EuiSpacer, + EuiTitle, + EuiStat, + EuiToolTip, +} from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { I18LABELS } from '../translations'; +import { CsmSharedContext } from '../CsmSharedContext'; + +export function JSErrors() { + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 5 }); + + const { data, status } = useFetcher( + (callApmApi) => { + if (start && end && serviceName) { + return callApmApi({ + pathname: '/api/apm/rum-client/js-errors', + params: { + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + pageSize: String(pagination.pageSize), + pageIndex: String(pagination.pageIndex), + }, + }, + }); + } + return Promise.resolve(null); + }, + [start, end, serviceName, uiFilters, pagination] + ); + + const { + sharedData: { totalPageViews }, + } = useContext(CsmSharedContext); + + const items = (data?.items ?? []).map(({ errorMessage, count }) => ({ + errorMessage, + percent: i18n.translate('xpack.apm.rum.jsErrors.percent', { + defaultMessage: '{pageLoadPercent} %', + values: { pageLoadPercent: ((count / totalPageViews) * 100).toFixed(1) }, + }), + })); + + const cols = [ + { + field: 'errorMessage', + name: I18LABELS.errorMessage, + }, + { + name: I18LABELS.impactedPageLoads, + field: 'percent', + align: 'right' as const, + }, + ]; + + const onTableChange = ({ + page, + }: { + page: { size: number; index: number }; + }) => { + setPagination({ + pageIndex: page.index, + pageSize: page.size, + }); + }; + + return ( + <> + +

{I18LABELS.jsErrors}

+
+ + + + + <>{numeral(data?.totalErrors ?? 0).format('0 a')} + + } + description={I18LABELS.totalErrors} + isLoading={status !== 'success'} + /> + + + + {' '} + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx new file mode 100644 index 0000000000000..34cb6338eb948 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/index.tsx @@ -0,0 +1,22 @@ +/* + * 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 { EuiFlexItem, EuiPanel, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { JSErrors } from './JSErrors'; + +export function ImpactfulMetrics() { + return ( + + + + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index ddef5cd08e521..37522b06970c1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -19,6 +19,7 @@ import { I18LABELS } from './translations'; import { VisitorBreakdown } from './VisitorBreakdown'; import { UXMetrics } from './UXMetrics'; import { VisitorBreakdownMap } from './VisitorBreakdownMap'; +import { ImpactfulMetrics } from './ImpactfulMetrics'; export function RumDashboard() { return ( @@ -66,6 +67,9 @@ export function RumDashboard() { + + + ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx index 9abf792d7a0cf..28bb5307b6e8c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { RumOverview } from '../RumDashboard'; import { RumHeader } from './RumHeader'; +import { CsmSharedContextProvider } from './CsmSharedContext'; export const UX_LABEL = i18n.translate('xpack.apm.ux.title', { defaultMessage: 'User Experience', @@ -17,16 +18,18 @@ export const UX_LABEL = i18n.translate('xpack.apm.ux.title', { export function RumHome() { return (
- - - - -

{UX_LABEL}

-
-
-
-
- + + + + + +

{UX_LABEL}

+
+
+
+
+ +
); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 714788ef468c6..f92a1d5a5945b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -37,6 +37,18 @@ export const I18LABELS = { defaultMessage: 'Page load distribution', } ), + jsErrors: i18n.translate( + 'xpack.apm.rum.dashboard.impactfulMetrics.jsErrors', + { + defaultMessage: 'JavaScript errors', + } + ), + highTrafficPages: i18n.translate( + 'xpack.apm.rum.dashboard.impactfulMetrics.highTrafficPages', + { + defaultMessage: 'High traffic pages', + } + ), resetZoom: i18n.translate('xpack.apm.rum.dashboard.resetZoom.label', { defaultMessage: 'Reset zoom', }), @@ -105,6 +117,21 @@ export const I18LABELS = { noResults: i18n.translate('xpack.apm.rum.filters.url.noResults', { defaultMessage: 'No results available', }), + totalErrors: i18n.translate('xpack.apm.rum.jsErrors.totalErrors', { + defaultMessage: 'Total errors', + }), + errorRate: i18n.translate('xpack.apm.rum.jsErrors.errorRate', { + defaultMessage: 'Error rate', + }), + errorMessage: i18n.translate('xpack.apm.rum.jsErrors.errorMessage', { + defaultMessage: 'Error message', + }), + impactedPageLoads: i18n.translate( + 'xpack.apm.rum.jsErrors.impactedPageLoads', + { + defaultMessage: 'Impacted page loads', + } + ), }; export const VisitorBreakdownLabel = i18n.translate( diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx index ed8d865d2d288..ee240cfef3b21 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx @@ -19,6 +19,7 @@ const BadgeText = styled.div` interface Props { value: string[]; onRemove: (val: string) => void; + name: string; } const removeFilterLabel = i18n.translate( @@ -26,9 +27,9 @@ const removeFilterLabel = i18n.translate( { defaultMessage: 'Remove filter' } ); -function FilterBadgeList({ onRemove, value }: Props) { +function FilterBadgeList({ onRemove, value, name }: Props) { return ( - + {value.map((val) => ( {(list, search) => ( - + @@ -159,6 +159,7 @@ function Filter({ name, title, options, onChange, value, showCount }: Props) { {value.length ? ( <> { onChange(value.filter((v) => val !== v)); }} diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index ceffb4f4d6654..66cfa954965d2 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -72,6 +72,97 @@ Object { } `; +exports[`rum client dashboard queries fetches js errors 1`] = ` +Object { + "apm": Object { + "events": Array [ + "error", + ], + }, + "body": Object { + "aggs": Object { + "errors": Object { + "aggs": Object { + "bucket_truncate": Object { + "bucket_sort": Object { + "from": 0, + "size": 5, + }, + }, + "sample": Object { + "top_hits": Object { + "_source": Array [ + "error.exception.message", + "error.exception.type", + "error.grouping_key", + "@timestamp", + ], + "size": 1, + "sort": Array [ + Object { + "@timestamp": "desc", + }, + ], + }, + }, + }, + "terms": Object { + "field": "error.grouping_key", + "size": 500, + }, + }, + "totalErrorGroups": Object { + "cardinality": Object { + "field": "error.grouping_key", + }, + }, + "totalErrorPages": Object { + "cardinality": Object { + "field": "transaction.id", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "agent.name": "rum-js", + }, + }, + Object { + "term": Object { + "transaction.type": "page-load", + }, + }, + Object { + "term": Object { + "service.language.name": "javascript", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], + }, + }, + "size": 0, + "track_total_hits": true, + }, +} +`; + exports[`rum client dashboard queries fetches long task metrics 1`] = ` Object { "apm": Object { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts new file mode 100644 index 0000000000000..0540ea4bf09dd --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts @@ -0,0 +1,99 @@ +/* + * 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 { mergeProjection } from '../../projections/util/merge_projection'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; +import { getRumErrorsProjection } from '../../projections/rum_page_load_transactions'; +import { + ERROR_EXC_MESSAGE, + ERROR_EXC_TYPE, + ERROR_GROUP_ID, + TRANSACTION_ID, +} from '../../../common/elasticsearch_fieldnames'; + +export async function getJSErrors({ + setup, + pageSize, + pageIndex, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + pageSize: number; + pageIndex: number; +}) { + const projection = getRumErrorsProjection({ + setup, + }); + + const params = mergeProjection(projection, { + body: { + size: 0, + track_total_hits: true, + aggs: { + totalErrorGroups: { + cardinality: { + field: ERROR_GROUP_ID, + }, + }, + totalErrorPages: { + cardinality: { + field: TRANSACTION_ID, + }, + }, + errors: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + }, + aggs: { + bucket_truncate: { + bucket_sort: { + size: pageSize, + from: pageIndex * pageSize, + }, + }, + sample: { + top_hits: { + _source: [ + ERROR_EXC_MESSAGE, + ERROR_EXC_TYPE, + ERROR_GROUP_ID, + '@timestamp', + ], + sort: [{ '@timestamp': 'desc' as const }], + size: 1, + }, + }, + }, + }, + }, + }, + }); + + const { apmEventClient } = setup; + + const response = await apmEventClient.search(params); + + const { totalErrorGroups, totalErrorPages, errors } = + response.aggregations ?? {}; + + return { + totalErrorPages: totalErrorPages?.value ?? 0, + totalErrors: response.hits.total.value ?? 0, + totalErrorGroups: totalErrorGroups?.value ?? 0, + items: errors?.buckets.map(({ sample, doc_count: count }) => { + return { + count, + errorMessage: (sample.hits.hits[0]._source as { + error: { exception: Array<{ message: string }> }; + }).error.exception?.[0].message, + }; + }), + }; +} diff --git a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts index 14cec21cceb79..23d2cb829b8d5 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts @@ -14,6 +14,7 @@ import { getPageLoadDistribution } from './get_page_load_distribution'; import { getRumServices } from './get_rum_services'; import { getLongTaskMetrics } from './get_long_task_metrics'; import { getWebCoreVitals } from './get_web_core_vitals'; +import { getJSErrors } from './get_js_errors'; describe('rum client dashboard queries', () => { let mock: SearchParamsMock; @@ -79,4 +80,15 @@ describe('rum client dashboard queries', () => { ); expect(mock.params).toMatchSnapshot(); }); + + it('fetches js errors', async () => { + mock = await inspectSearchParams((setup) => + getJSErrors({ + setup, + pageSize: 5, + pageIndex: 0, + }) + ); + expect(mock.params).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts index 3c3eaaca7efdb..a8505337e8aec 100644 --- a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts +++ b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @@ -11,7 +11,9 @@ import { } from '../../server/lib/helpers/setup_request'; import { SPAN_TYPE, + AGENT_NAME, TRANSACTION_TYPE, + SERVICE_LANGUAGE_NAME, } from '../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../common/utils/range_filter'; import { ProcessorEvent } from '../../common/processor_event'; @@ -90,3 +92,36 @@ export function getRumLongTasksProjection({ }, }; } + +export function getRumErrorsProjection({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { start, end, uiFiltersES } = setup; + + const bool = { + filter: [ + { range: rangeFilter(start, end) }, + { term: { [AGENT_NAME]: 'rum-js' } }, + { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, + { + term: { + [SERVICE_LANGUAGE_NAME]: 'javascript', + }, + }, + ...uiFiltersES, + ], + }; + + return { + apm: { + events: [ProcessorEvent.error], + }, + body: { + query: { + bool, + }, + }, + }; +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index f975ab177f147..0560b977e708e 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -69,17 +69,6 @@ import { listCustomLinksRoute, customLinkTransactionRoute, } from './settings/custom_link'; -import { - rumClientMetricsRoute, - rumPageViewsTrendRoute, - rumPageLoadDistributionRoute, - rumPageLoadDistBreakdownRoute, - rumServicesRoute, - rumVisitorsBreakdownRoute, - rumWebCoreVitals, - rumUrlSearch, - rumLongTaskMetrics, -} from './rum_client'; import { observabilityOverviewHasDataRoute, observabilityOverviewRoute, @@ -89,6 +78,18 @@ import { createAnomalyDetectionJobsRoute, anomalyDetectionEnvironmentsRoute, } from './settings/anomaly_detection'; +import { + rumClientMetricsRoute, + rumJSErrors, + rumLongTaskMetrics, + rumPageLoadDistBreakdownRoute, + rumPageLoadDistributionRoute, + rumPageViewsTrendRoute, + rumServicesRoute, + rumUrlSearch, + rumVisitorsBreakdownRoute, + rumWebCoreVitals, +} from './rum_client'; const createApmApi = () => { const api = createApi() @@ -165,7 +166,16 @@ const createApmApi = () => { .add(listCustomLinksRoute) .add(customLinkTransactionRoute) - // Rum Overview + // Observability dashboard + .add(observabilityOverviewHasDataRoute) + .add(observabilityOverviewRoute) + + // Anomaly detection + .add(anomalyDetectionJobsRoute) + .add(createAnomalyDetectionJobsRoute) + .add(anomalyDetectionEnvironmentsRoute) + + // User Experience app api routes .add(rumOverviewLocalFiltersRoute) .add(rumPageViewsTrendRoute) .add(rumPageLoadDistributionRoute) @@ -174,17 +184,9 @@ const createApmApi = () => { .add(rumServicesRoute) .add(rumVisitorsBreakdownRoute) .add(rumWebCoreVitals) + .add(rumJSErrors) .add(rumUrlSearch) - .add(rumLongTaskMetrics) - - // Observability dashboard - .add(observabilityOverviewHasDataRoute) - .add(observabilityOverviewRoute) - - // Anomaly detection - .add(anomalyDetectionJobsRoute) - .add(createAnomalyDetectionJobsRoute) - .add(anomalyDetectionEnvironmentsRoute); + .add(rumLongTaskMetrics); return api; }; diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index e3a846f9fb5c7..c0351991e4c0d 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -15,6 +15,7 @@ import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdow import { getRumServices } from '../lib/rum_client/get_rum_services'; import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown'; import { getWebCoreVitals } from '../lib/rum_client/get_web_core_vitals'; +import { getJSErrors } from '../lib/rum_client/get_js_errors'; import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics'; import { getUrlSearch } from '../lib/rum_client/get_url_search'; @@ -191,3 +192,27 @@ export const rumUrlSearch = createRoute(() => ({ return getUrlSearch({ setup, urlQuery }); }, })); + +export const rumJSErrors = createRoute(() => ({ + path: '/api/apm/rum-client/js-errors', + params: { + query: t.intersection([ + uiFiltersRt, + rangeRt, + t.type({ pageSize: t.string, pageIndex: t.string }), + ]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + query: { pageSize, pageIndex }, + } = context.params; + + return getJSErrors({ + setup, + pageSize: Number(pageSize), + pageIndex: Number(pageIndex), + }); + }, +})); diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index 93f8b115256b4..534321201938d 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -153,6 +153,11 @@ export interface AggregationOptionsByType { keyed?: boolean; hdr?: { number_of_significant_value_digits: number }; } & AggregationSourceOptions; + bucket_sort: { + sort?: SortOptions; + from?: number; + size?: number; + }; } type AggregationType = keyof AggregationOptionsByType; @@ -329,6 +334,7 @@ interface AggregationResponsePart< ? Array<{ key: number; value: number }> : Record; }; + bucket_sort: undefined; } // Type for debugging purposes. If you see an error in AggregationResponseMap diff --git a/x-pack/plugins/apm/typings/elasticsearch/index.ts b/x-pack/plugins/apm/typings/elasticsearch/index.ts index 064b684cf9aa6..9a05fe631e888 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/index.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/index.ts @@ -7,11 +7,22 @@ import { SearchParams, SearchResponse } from 'elasticsearch'; import { AggregationResponseMap, AggregationInputMap } from './aggregations'; +interface CollapseQuery { + field: string; + inner_hits: { + name: string; + size?: number; + sort?: [{ date: 'asc' | 'desc' }]; + }; + max_concurrent_group_searches?: number; +} + export interface ESSearchBody { query?: any; size?: number; aggs?: AggregationInputMap; track_total_hits?: boolean | number; + collapse?: CollapseQuery; } export type ESSearchRequest = Omit & { diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts b/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts new file mode 100644 index 0000000000000..0edffe7999a65 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts @@ -0,0 +1,61 @@ +/* + * 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 expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function rumJsErrorsApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('CSM js errors', () => { + describe('when there is no data', () => { + it('returns no js errors', async () => { + const response = await supertest.get( + '/api/apm/rum-client/js-errors?pageSize=5&pageIndex=0&start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatchInline(` + Object { + "totalErrorGroups": 0, + "totalErrorPages": 0, + "totalErrors": 0, + } + `); + }); + }); + + describe('when there is data', () => { + before(async () => { + await esArchiver.load('8.0.0'); + await esArchiver.load('rum_8.0.0'); + }); + after(async () => { + await esArchiver.unload('8.0.0'); + await esArchiver.unload('rum_8.0.0'); + }); + + it('returns js errors', async () => { + const response = await supertest.get( + '/api/apm/rum-client/js-errors?pageSize=5&pageIndex=0&start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [], + "totalErrorGroups": 0, + "totalErrorPages": 0, + "totalErrors": 0, + } + `); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index 69e54ea33c559..a6a031def34ea 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -37,6 +37,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr loadTestFile(require.resolve('./csm/long_task_metrics.ts')); loadTestFile(require.resolve('./csm/url_search.ts')); loadTestFile(require.resolve('./csm/page_views.ts')); + loadTestFile(require.resolve('./csm/js_errors.ts')); }); }); }