From 72111702e95214594d5d56bc856e1d5c78e6d802 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 18 Jun 2020 17:13:28 +0200 Subject: [PATCH] [RUM Dashboard] Initial Version (#68778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Casper Hübertz --- .../elasticsearch_fieldnames.test.ts.snap | 24 +++ .../apm/common/elasticsearch_fieldnames.ts | 6 + .../plugins/apm/common/projections/errors.ts | 2 +- .../plugins/apm/common/projections/metrics.ts | 2 +- .../apm/common/projections/rum_overview.ts | 46 +++++ .../apm/common/projections/services.ts | 2 +- .../apm/common/projections/transactions.ts | 2 +- .../plugins/apm/common/projections/typings.ts | 1 + .../helpers => common/utils}/range_filter.ts | 0 .../apm/e2e/cypress/integration/helpers.ts | 20 +- .../cypress/integration/rum_dashboard.feature | 7 + .../apm/e2e/cypress/integration/snapshots.js | 23 ++- .../support/step_definitions/rum_dashboard.ts | 43 ++++ x-pack/plugins/apm/e2e/ingest-data/replay.js | 6 +- x-pack/plugins/apm/e2e/run-e2e.sh | 2 +- x-pack/plugins/apm/e2e/yarn.lock | 8 +- .../plugins/apm/public/application/index.tsx | 3 +- .../apm/public/components/app/Home/index.tsx | 24 ++- .../app/Main/route_config/index.tsx | 9 + .../app/Main/route_config/route_names.tsx | 1 + .../app/RumDashboard/ChartWrapper/index.tsx | 63 ++++++ .../app/RumDashboard/ClientMetrics/index.tsx | 82 ++++++++ .../PercentileAnnotations.tsx | 60 ++++++ .../PageLoadDistribution/index.tsx | 144 +++++++++++++ .../app/RumDashboard/PageViewsTrend/index.tsx | 111 ++++++++++ .../app/RumDashboard/RumDashboard.tsx | 65 ++++++ .../components/app/RumDashboard/index.tsx | 40 ++++ .../app/RumDashboard/translations.ts | 73 +++++++ .../app/ServiceDetails/ServiceDetailTabs.tsx | 16 ++ .../components/shared/KueryBar/index.tsx | 4 +- .../shared/Links/apm/RumOverviewLink.tsx | 31 +++ .../lib/errors/distribution/get_buckets.ts | 2 +- .../apm/server/lib/errors/get_error_group.ts | 2 +- .../__snapshots__/queries.test.ts.snap | 194 ++++++++++++++++++ .../lib/rum_client/get_client_metrics.ts | 61 ++++++ .../rum_client/get_page_load_distribution.ts | 140 +++++++++++++ .../lib/rum_client/get_page_view_trends.ts | 57 +++++ .../apm/server/lib/rum_client/queries.test.ts | 52 +++++ .../get_service_map_service_node_info.ts | 2 +- .../lib/service_map/get_trace_sample_ids.ts | 2 +- .../get_derived_service_annotations.ts | 2 +- .../lib/services/get_service_agent_name.ts | 2 +- .../services/get_service_transaction_types.ts | 2 +- .../apm/server/lib/traces/get_trace_items.ts | 2 +- .../avg_duration_by_browser/fetcher.ts | 2 +- .../avg_duration_by_country/index.ts | 2 +- .../lib/transactions/breakdown/index.ts | 2 +- .../charts/get_timeseries_data/fetcher.ts | 2 +- .../distribution/get_buckets/fetcher.ts | 2 +- .../lib/transactions/get_transaction/index.ts | 2 +- .../server/lib/ui_filters/get_environments.ts | 2 +- .../lib/ui_filters/local_ui_filters/config.ts | 35 ++++ .../apm/server/routes/create_apm_api.ts | 14 +- .../plugins/apm/server/routes/rum_client.ts | 57 +++++ .../plugins/apm/server/routes/ui_filters.ts | 11 + .../apm/typings/elasticsearch/aggregations.ts | 26 +++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 58 files changed, 1552 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/apm/common/projections/rum_overview.ts rename x-pack/plugins/apm/{server/lib/helpers => common/utils}/range_filter.ts (100%) create mode 100644 x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature create mode 100644 x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum_dashboard.ts create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts create mode 100644 x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx create mode 100644 x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap create mode 100644 x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts create mode 100644 x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts create mode 100644 x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts create mode 100644 x-pack/plugins/apm/server/lib/rum_client/queries.test.ts create mode 100644 x-pack/plugins/apm/server/routes/rum_client.ts diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 54dd4704edfc0..f3dc7abcf8239 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -4,6 +4,8 @@ exports[`Error AGENT_NAME 1`] = `"java"`; exports[`Error AGENT_VERSION 1`] = `"agent version"`; +exports[`Error CLIENT_GEO 1`] = `undefined`; + exports[`Error CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`; exports[`Error CONTAINER_ID 1`] = `undefined`; @@ -122,18 +124,26 @@ exports[`Error TRANSACTION_SAMPLED 1`] = `undefined`; exports[`Error TRANSACTION_TYPE 1`] = `"request"`; +exports[`Error TRANSACTION_URL 1`] = `undefined`; + exports[`Error URL_FULL 1`] = `undefined`; +exports[`Error USER_AGENT_DEVICE 1`] = `undefined`; + exports[`Error USER_AGENT_NAME 1`] = `undefined`; exports[`Error USER_AGENT_ORIGINAL 1`] = `undefined`; +exports[`Error USER_AGENT_OS 1`] = `undefined`; + exports[`Error USER_ID 1`] = `undefined`; exports[`Span AGENT_NAME 1`] = `"java"`; exports[`Span AGENT_VERSION 1`] = `"agent version"`; +exports[`Span CLIENT_GEO 1`] = `undefined`; + exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`; exports[`Span CONTAINER_ID 1`] = `undefined`; @@ -252,18 +262,26 @@ exports[`Span TRANSACTION_SAMPLED 1`] = `undefined`; exports[`Span TRANSACTION_TYPE 1`] = `undefined`; +exports[`Span TRANSACTION_URL 1`] = `undefined`; + exports[`Span URL_FULL 1`] = `undefined`; +exports[`Span USER_AGENT_DEVICE 1`] = `undefined`; + exports[`Span USER_AGENT_NAME 1`] = `undefined`; exports[`Span USER_AGENT_ORIGINAL 1`] = `undefined`; +exports[`Span USER_AGENT_OS 1`] = `undefined`; + exports[`Span USER_ID 1`] = `undefined`; exports[`Transaction AGENT_NAME 1`] = `"java"`; exports[`Transaction AGENT_VERSION 1`] = `"agent version"`; +exports[`Transaction CLIENT_GEO 1`] = `undefined`; + exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`; exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`; @@ -382,10 +400,16 @@ exports[`Transaction TRANSACTION_SAMPLED 1`] = `true`; exports[`Transaction TRANSACTION_TYPE 1`] = `"transaction type"`; +exports[`Transaction TRANSACTION_URL 1`] = `undefined`; + exports[`Transaction URL_FULL 1`] = `"http://www.elastic.co"`; +exports[`Transaction USER_AGENT_DEVICE 1`] = `undefined`; + exports[`Transaction USER_AGENT_NAME 1`] = `"Other"`; exports[`Transaction USER_AGENT_ORIGINAL 1`] = `"test original"`; +exports[`Transaction USER_AGENT_OS 1`] = `undefined`; + exports[`Transaction USER_ID 1`] = `"1337"`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index d5c3f91eb9247..7537dba7f8411 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -87,3 +87,9 @@ export const CONTAINER_ID = 'container.id'; export const POD_NAME = 'kubernetes.pod.name'; export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code'; + +// RUM Labels +export const TRANSACTION_URL = 'transaction.page.url'; +export const CLIENT_GEO = 'client.geo'; +export const USER_AGENT_DEVICE = 'user_agent.device.name'; +export const USER_AGENT_OS = 'user_agent.os.name'; diff --git a/x-pack/plugins/apm/common/projections/errors.ts b/x-pack/plugins/apm/common/projections/errors.ts index bd397afae2243..390a8a0968102 100644 --- a/x-pack/plugins/apm/common/projections/errors.ts +++ b/x-pack/plugins/apm/common/projections/errors.ts @@ -16,7 +16,7 @@ import { ERROR_GROUP_ID, } from '../elasticsearch_fieldnames'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../../server/lib/helpers/range_filter'; +import { rangeFilter } from '../utils/range_filter'; export function getErrorGroupsProjection({ setup, diff --git a/x-pack/plugins/apm/common/projections/metrics.ts b/x-pack/plugins/apm/common/projections/metrics.ts index b05ec5f2ba876..45998bfe82e96 100644 --- a/x-pack/plugins/apm/common/projections/metrics.ts +++ b/x-pack/plugins/apm/common/projections/metrics.ts @@ -16,7 +16,7 @@ import { SERVICE_NODE_NAME, } from '../elasticsearch_fieldnames'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../../server/lib/helpers/range_filter'; +import { rangeFilter } from '../utils/range_filter'; import { SERVICE_NODE_NAME_MISSING } from '../service_nodes'; function getServiceNodeNameFilters(serviceNodeName?: string) { diff --git a/x-pack/plugins/apm/common/projections/rum_overview.ts b/x-pack/plugins/apm/common/projections/rum_overview.ts new file mode 100644 index 0000000000000..b1218546d09ff --- /dev/null +++ b/x-pack/plugins/apm/common/projections/rum_overview.ts @@ -0,0 +1,46 @@ +/* + * 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 { + Setup, + SetupTimeRange, + SetupUIFilters, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../server/lib/helpers/setup_request'; +import { PROCESSOR_EVENT, TRANSACTION_TYPE } from '../elasticsearch_fieldnames'; +import { rangeFilter } from '../utils/range_filter'; + +export function getRumOverviewProjection({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { start, end, uiFiltersES, indices } = setup; + + const bool = { + filter: [ + { range: rangeFilter(start, end) }, + { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [TRANSACTION_TYPE]: 'page-load' } }, + { + // Adding this filter to cater for some inconsistent rum data + exists: { + field: 'transaction.marks.navigationTiming.fetchStart', + }, + }, + ...uiFiltersES, + ], + }; + + return { + index: indices['apm_oss.transactionIndices'], + body: { + query: { + bool, + }, + }, + }; +} diff --git a/x-pack/plugins/apm/common/projections/services.ts b/x-pack/plugins/apm/common/projections/services.ts index bcfc27d720ba9..80a3471e9c30d 100644 --- a/x-pack/plugins/apm/common/projections/services.ts +++ b/x-pack/plugins/apm/common/projections/services.ts @@ -12,7 +12,7 @@ import { } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../../server/lib/helpers/range_filter'; +import { rangeFilter } from '../utils/range_filter'; export function getServicesProjection({ setup, diff --git a/x-pack/plugins/apm/common/projections/transactions.ts b/x-pack/plugins/apm/common/projections/transactions.ts index 99d5a04c1e722..b6cd73ca9aaad 100644 --- a/x-pack/plugins/apm/common/projections/transactions.ts +++ b/x-pack/plugins/apm/common/projections/transactions.ts @@ -17,7 +17,7 @@ import { TRANSACTION_NAME, } from '../elasticsearch_fieldnames'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { rangeFilter } from '../../server/lib/helpers/range_filter'; +import { rangeFilter } from '../utils/range_filter'; export function getTransactionsProjection({ setup, diff --git a/x-pack/plugins/apm/common/projections/typings.ts b/x-pack/plugins/apm/common/projections/typings.ts index 3361770336dde..693795b09e1d0 100644 --- a/x-pack/plugins/apm/common/projections/typings.ts +++ b/x-pack/plugins/apm/common/projections/typings.ts @@ -29,4 +29,5 @@ export enum PROJECTION { METRICS = 'metrics', ERROR_GROUPS = 'errorGroups', SERVICE_NODES = 'serviceNodes', + RUM_OVERVIEW = 'rumOverview', } diff --git a/x-pack/plugins/apm/server/lib/helpers/range_filter.ts b/x-pack/plugins/apm/common/utils/range_filter.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/helpers/range_filter.ts rename to x-pack/plugins/apm/common/utils/range_filter.ts diff --git a/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts b/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts index 90d5c9eda632d..689b88390810f 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts +++ b/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts @@ -6,20 +6,30 @@ /* eslint-disable import/no-extraneous-dependencies */ -const RANGE_FROM = '2020-03-04T12:30:00.000Z'; -const RANGE_TO = '2020-03-04T13:00:00.000Z'; +const RANGE_FROM = '2020-06-01T14:59:32.686Z'; +const RANGE_TO = '2020-06-16T16:59:36.219Z'; + const BASE_URL = Cypress.config().baseUrl; /** The default time in ms to wait for a Cypress command to complete */ export const DEFAULT_TIMEOUT = 60 * 1000; -export function loginAndWaitForPage(url: string) { +export function loginAndWaitForPage( + url: string, + dateRange?: { to: string; from: string } +) { const username = Cypress.env('elasticsearch_username'); const password = Cypress.env('elasticsearch_password'); cy.log(`Authenticating via ${username} / ${password}`); - - const fullUrl = `${BASE_URL}${url}?rangeFrom=${RANGE_FROM}&rangeTo=${RANGE_TO}`; + let rangeFrom = RANGE_FROM; + let rangeTo = RANGE_TO; + if (dateRange) { + rangeFrom = dateRange.from; + rangeTo = dateRange.to; + } + + const fullUrl = `${BASE_URL}${url}?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`; cy.visit(fullUrl, { auth: { username, password } }); cy.viewport('macbook-15'); diff --git a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature new file mode 100644 index 0000000000000..eabfaf096731b --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature @@ -0,0 +1,7 @@ +Feature: RUM Dashboard + + Scenario: Client metrics + Given a user browses the APM UI application for RUM Data + When the user inspects the real user monitoring tab + Then should redirect to rum dashboard + And should have correct client metrics diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js index d4c8ba4910850..dd96a57ef8c45 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js +++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js @@ -1,10 +1,17 @@ module.exports = { - APM: { - 'Transaction duration charts': { - '1': '350 ms', - '2': '175 ms', - '3': '0 ms', - }, + "__version": "4.5.0", + "APM": { + "Transaction duration charts": { + "1": "55 ms", + "2": "28 ms", + "3": "0 ms" + } }, - __version: '4.5.0', -}; + "RUM Dashboard": { + "Client metrics": { + "1": "62", + "2": "0.07 sec", + "3": "0.01 sec" + } + } +} diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum_dashboard.ts new file mode 100644 index 0000000000000..38eadbf513032 --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum_dashboard.ts @@ -0,0 +1,43 @@ +/* + * 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 { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { loginAndWaitForPage } from '../../integration/helpers'; + +/** The default time in ms to wait for a Cypress command to complete */ +export const DEFAULT_TIMEOUT = 60 * 1000; + +Given(`a user browses the APM UI application for RUM Data`, () => { + // open service overview page + const RANGE_FROM = 'now-24h'; + const RANGE_TO = 'now'; + loginAndWaitForPage(`/app/apm#/services`, { from: RANGE_FROM, to: RANGE_TO }); +}); + +When(`the user inspects the real user monitoring tab`, () => { + // click rum tab + cy.get(':contains(Real User Monitoring)', { timeout: DEFAULT_TIMEOUT }) + .last() + .click({ force: true }); +}); + +Then(`should redirect to rum dashboard`, () => { + cy.url().should('contain', `/app/apm#/rum-overview`); +}); + +Then(`should have correct client metrics`, () => { + const clientMetrics = '[data-cy=client-metrics] .euiStat__title'; + + // wait for all loading to finish + cy.get('kbnLoadingIndicator').should('not.be.visible'); + cy.get('.euiStat__title-isLoading').should('not.be.visible'); + + cy.get(clientMetrics).eq(2).invoke('text').snapshot(); + + cy.get(clientMetrics).eq(1).invoke('text').snapshot(); + + cy.get(clientMetrics).eq(0).invoke('text').snapshot(); +}); diff --git a/x-pack/plugins/apm/e2e/ingest-data/replay.js b/x-pack/plugins/apm/e2e/ingest-data/replay.js index ae3f62894afc0..3478039f39b50 100644 --- a/x-pack/plugins/apm/e2e/ingest-data/replay.js +++ b/x-pack/plugins/apm/e2e/ingest-data/replay.js @@ -99,7 +99,11 @@ async function init() { .split('\n') .filter((item) => item) .map((item) => JSON.parse(item)) - .filter((item) => item.url === '/intake/v2/events'); + .filter((item) => { + return ( + item.url === '/intake/v2/events' || item.url === '/intake/v2/rum/events' + ); + }); spinner.start(); requestProgress.total = items.length; diff --git a/x-pack/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh index aa7c0e21425ad..43cc74a197f42 100755 --- a/x-pack/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/plugins/apm/e2e/run-e2e.sh @@ -109,7 +109,7 @@ echo "${bold}Static mock data (logs: ${E2E_DIR}${TMP_DIR}/ingest-data.log)${norm # Download static data if not already done if [ ! -e "${TMP_DIR}/events.json" ]; then echo 'Downloading events.json...' - curl --silent https://storage.googleapis.com/apm-ui-e2e-static-data/events.json --output ${TMP_DIR}/events.json + curl --silent https://storage.googleapis.com/apm-ui-e2e-static-data/2020-06-12.json --output ${TMP_DIR}/events.json fi # echo "Deleting existing indices (apm* and .apm*)" diff --git a/x-pack/plugins/apm/e2e/yarn.lock b/x-pack/plugins/apm/e2e/yarn.lock index a6729c56ecb09..975154d71b85d 100644 --- a/x-pack/plugins/apm/e2e/yarn.lock +++ b/x-pack/plugins/apm/e2e/yarn.lock @@ -5561,10 +5561,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.9.2: - version "3.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9" - integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw== +typescript@3.9.5: + version "3.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" + integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== umd@^3.0.0: version "3.0.3" diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 56c427e67ad4c..8800b2fd492bf 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -23,7 +23,7 @@ import { KibanaContextProvider, useUiSetting$, } from '../../../../../src/plugins/kibana_react/public'; -import { px, unit, units } from '../style/variables'; +import { px, units } from '../style/variables'; import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; import { APMIndicesPermission } from '../components/app/APMIndicesPermission'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; @@ -33,7 +33,6 @@ import { ConfigSchema } from '..'; import 'react-vis/dist/style.css'; const MainContainer = styled.div` - min-width: ${px(unit * 50)}; padding: ${px(units.plus)}; height: 100%; `; diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx index 74cbc00b17889..c325a72375359 100644 --- a/x-pack/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx @@ -25,6 +25,9 @@ import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink' import { ServiceMap } from '../ServiceMap'; import { ServiceOverview } from '../ServiceOverview'; import { TraceOverview } from '../TraceOverview'; +import { RumOverview } from '../RumDashboard'; +import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink'; +import { EndUserExperienceLabel } from '../RumDashboard/translations'; function getHomeTabs({ serviceMapEnabled = true, @@ -70,14 +73,27 @@ function getHomeTabs({ }); } + homeTabs.push({ + link: ( + + {i18n.translate('xpack.apm.home.rumTabLabel', { + defaultMessage: 'Real User Monitoring', + })} + + ), + render: () => , + name: 'rum-overview', + }); + return homeTabs; } + const SETTINGS_LINK_LABEL = i18n.translate('xpack.apm.settingsLinkLabel', { defaultMessage: 'Settings', }); interface Props { - tab: 'traces' | 'services' | 'service-map'; + tab: 'traces' | 'services' | 'service-map' | 'rum-overview'; } export function Home({ tab }: Props) { @@ -93,7 +109,11 @@ export function Home({ tab }: Props) { -

APM

+

+ {selectedTab.name === 'rum-overview' + ? EndUserExperienceLabel + : 'APM'} +

diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 577af75e92d9e..295f343b411a9 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -250,4 +250,13 @@ export const routes: BreadcrumbRoute[] = [ }), name: RouteName.CUSTOMIZE_UI, }, + { + exact: true, + path: '/rum-overview', + component: () => , + breadcrumb: i18n.translate('xpack.apm.home.rumOverview.title', { + defaultMessage: 'Real User Monitoring', + }), + name: RouteName.RUM_OVERVIEW, + }, ]; diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx index 167de1a37f427..4965aa9db8760 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx @@ -26,4 +26,5 @@ export enum RouteName { SERVICE_NODES = 'nodes', LINK_TO_TRACE = 'link_to_trace', CUSTOMIZE_UI = 'customize_ui', + RUM_OVERVIEW = 'rum_overview', } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx new file mode 100644 index 0000000000000..a3cfbb28abee2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx @@ -0,0 +1,63 @@ +/* + * 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, HTMLAttributes } from 'react'; +import { + EuiErrorBoundary, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingChart, +} from '@elastic/eui'; + +interface Props { + /** + * Height for the chart + */ + height?: string; + /** + * if chart data source is still loading + */ + loading?: boolean; + /** + * aria-label for accessibility + */ + 'aria-label'?: string; +} + +export const ChartWrapper: FC = ({ + loading = false, + height = '100%', + children, + ...rest +}) => { + const opacity = loading === true ? 0.3 : 1; + + return ( + +
)} + > + {children} +
+ {loading === true && ( + + + + + + )} +
+ ); +}; 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 new file mode 100644 index 0000000000000..8c0a7c6a91f67 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -0,0 +1,82 @@ +/* + * 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. + */ +// @flow +import * as React from 'react'; +import styled from 'styled-components'; +import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { BackEndLabel, FrontEndLabel, PageViewsLabel } from '../translations'; + +export const formatBigValue = (val?: number | null, fixed?: number): string => { + if (val && val >= 1000) { + const result = val / 1000; + if (fixed) { + return result.toFixed(fixed) + 'k'; + } + return result + 'k'; + } + return val + ''; +}; + +const ClFlexGroup = styled(EuiFlexGroup)` + flex-direction: row; + @media only screen and (max-width: 768px) { + flex-direction: row; + justify-content: space-between; + } +`; + +export const ClientMetrics = () => { + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end } = urlParams; + + const { data, status } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + pathname: '/api/apm/rum/client-metrics', + params: { + query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + }, + }); + } + }, + [start, end, uiFilters] + ); + + const STAT_STYLE = { width: '240px' }; + + return ( + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx new file mode 100644 index 0000000000000..9c89b8bc161b7 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx @@ -0,0 +1,60 @@ +/* + * 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 React from 'react'; +import { + AnnotationDomainTypes, + LineAnnotation, + LineAnnotationDatum, + LineAnnotationStyle, +} from '@elastic/charts'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import styled from 'styled-components'; + +interface Props { + percentiles?: Record; +} + +function generateAnnotationData( + values?: Record +): LineAnnotationDatum[] { + return Object.entries(values ?? {}).map((value, index) => ({ + dataValue: value[1], + details: `${(+value[0]).toFixed(0)}`, + })); +} + +const PercentileMarker = styled.span` + position: relative; + bottom: 140px; +`; + +export const PercentileAnnotations = ({ percentiles }: Props) => { + const dataValues = generateAnnotationData(percentiles) ?? []; + + const style: Partial = { + line: { + strokeWidth: 1, + stroke: euiLightVars.euiColorSecondary, + opacity: 1, + }, + }; + + return ( + <> + {dataValues.map((annotation, index) => ( + {annotation.details}th} + /> + ))} + + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx new file mode 100644 index 0000000000000..c7a0b64f6a8b8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -0,0 +1,144 @@ +/* + * 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, { useState } from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { + Axis, + Chart, + ScaleType, + LineSeries, + CurveType, + BrushEndListener, + Settings, + TooltipValueFormatter, + TooltipValue, +} from '@elastic/charts'; +import { Position } from '@elastic/charts/dist/utils/commons'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { ChartWrapper } from '../ChartWrapper'; +import { PercentileAnnotations } from './PercentileAnnotations'; +import { + PageLoadDistLabel, + PageLoadTimeLabel, + PercPageLoadedLabel, + ResetZoomLabel, +} from '../translations'; + +export const PageLoadDistribution = () => { + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end } = urlParams; + + const [percentileRange, setPercentileRange] = useState<{ + min: string | null; + max: string | null; + }>({ + min: null, + max: null, + }); + + const { data, status } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + pathname: '/api/apm/rum-client/page-load-distribution', + params: { + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + ...(percentileRange.min && percentileRange.max + ? { + minPercentile: percentileRange.min, + maxPercentile: percentileRange.max, + } + : {}), + }, + }, + }); + } + }, + [end, start, uiFilters, percentileRange.min, percentileRange.max] + ); + + const onBrushEnd: BrushEndListener = ({ x }) => { + if (!x) { + return; + } + const [minX, maxX] = x; + setPercentileRange({ min: String(minX), max: String(maxX) }); + }; + + const headerFormatter: TooltipValueFormatter = (tooltip: TooltipValue) => { + return ( +
+

{tooltip.value} seconds

+
+ ); + }; + + const tooltipProps = { + headerFormatter, + }; + + return ( +
+ + + +

{PageLoadDistLabel}

+
+
+ + { + setPercentileRange({ min: null, max: null }); + }} + fill={percentileRange.min !== null && percentileRange.max !== null} + > + {ResetZoomLabel} + + +
+ + + + + + + Number(d).toFixed(1) + ' %'} + /> + + + +
+ ); +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx new file mode 100644 index 0000000000000..cc41bd4352947 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -0,0 +1,111 @@ +/* + * 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 React from 'react'; +import { EuiTitle } from '@elastic/eui'; +import { + Axis, + BarSeries, + BrushEndListener, + Chart, + niceTimeFormatByDay, + ScaleType, + Settings, + timeFormatter, +} from '@elastic/charts'; +import moment from 'moment'; +import { Position } from '@elastic/charts/dist/utils/commons'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { ChartWrapper } from '../ChartWrapper'; +import { DateTimeLabel, PageViewsLabel } from '../translations'; +import { history } from '../../../../utils/history'; +import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { formatBigValue } from '../ClientMetrics'; + +export const PageViewsTrend = () => { + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end } = urlParams; + + const { data, status } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + pathname: '/api/apm/rum-client/page-view-trends', + params: { + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, + [end, start, uiFilters] + ); + const formatter = timeFormatter(niceTimeFormatByDay(2)); + + const onBrushEnd: BrushEndListener = ({ x }) => { + if (!x) { + return; + } + const [minX, maxX] = x; + + const rangeFrom = moment(minX).toISOString(); + const rangeTo = moment(maxX).toISOString(); + + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + rangeFrom, + rangeTo, + }), + }); + }; + + return ( +
+ +

{PageViewsLabel}

+
+ + + + + formatBigValue(Number(d))} + /> + + + +
+ ); +}; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx new file mode 100644 index 0000000000000..e3fa7374afb38 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -0,0 +1,65 @@ +/* + * 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, + EuiTitle, + EuiSpacer, + EuiPanel, +} from '@elastic/eui'; +import React from 'react'; +import { ClientMetrics } from './ClientMetrics'; +import { PageViewsTrend } from './PageViewsTrend'; +import { PageLoadDistribution } from './PageLoadDistribution'; +import { getWhatIsGoingOnLabel } from './translations'; +import { useUrlParams } from '../../../hooks/useUrlParams'; + +export function RumDashboard() { + const { urlParams } = useUrlParams(); + + const { environment } = urlParams; + + let environmentLabel = environment || 'all environments'; + + if (environment === 'ENVIRONMENT_NOT_DEFINED') { + environmentLabel = 'undefined environment'; + } + + return ( + <> + +

{getWhatIsGoingOnLabel(environmentLabel)}

+
+ + + + + + + +

Page load times

+
+ + +
+
+
+
+ + + + + + + + + + +
+ + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx new file mode 100644 index 0000000000000..8f21065b0dab0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -0,0 +1,40 @@ +/* + * 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 } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { useTrackPageview } from '../../../../../observability/public'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { RumDashboard } from './RumDashboard'; + +export function RumOverview() { + useTrackPageview({ app: 'apm', path: 'rum_overview' }); + useTrackPageview({ app: 'apm', path: 'rum_overview', delay: 15000 }); + + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps = { + filterNames: ['transactionUrl', 'location', 'device', 'os', 'browser'], + projection: PROJECTION.RUM_OVERVIEW, + }; + + return config; + }, []); + + return ( + <> + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts new file mode 100644 index 0000000000000..c2aed41a55c7d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -0,0 +1,73 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const EndUserExperienceLabel = i18n.translate( + 'xpack.apm.rum.dashboard.title', + { + defaultMessage: 'End User Experience', + } +); + +export const getWhatIsGoingOnLabel = (environmentVal: string) => + i18n.translate('xpack.apm.rum.dashboard.environment.title', { + defaultMessage: `What's going on in {environmentVal}?`, + values: { environmentVal }, + }); + +export const BackEndLabel = i18n.translate('xpack.apm.rum.dashboard.backend', { + defaultMessage: 'Backend', +}); + +export const FrontEndLabel = i18n.translate( + 'xpack.apm.rum.dashboard.frontend', + { + defaultMessage: 'Frontend', + } +); + +export const PageViewsLabel = i18n.translate( + 'xpack.apm.rum.dashboard.pageViews', + { + defaultMessage: 'Page views', + } +); + +export const DateTimeLabel = i18n.translate( + 'xpack.apm.rum.dashboard.dateTime.label', + { + defaultMessage: 'Date / Time', + } +); + +export const PercPageLoadedLabel = i18n.translate( + 'xpack.apm.rum.dashboard.pagesLoaded.label', + { + defaultMessage: 'Pages loaded', + } +); + +export const PageLoadTimeLabel = i18n.translate( + 'xpack.apm.rum.dashboard.pageLoadTime.label', + { + defaultMessage: 'Page load time (seconds)', + } +); + +export const PageLoadDistLabel = i18n.translate( + 'xpack.apm.rum.dashboard.pageLoadDistribution.label', + { + defaultMessage: 'Page load distribution', + } +); + +export const ResetZoomLabel = i18n.translate( + 'xpack.apm.rum.dashboard.resetZoom.label', + { + defaultMessage: 'Reset zoom', + } +); diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index 2f35e329720de..81bdbdad805d6 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -22,6 +22,8 @@ import { ServiceMap } from '../ServiceMap'; import { ServiceMetrics } from '../ServiceMetrics'; import { ServiceNodeOverview } from '../ServiceNodeOverview'; import { TransactionOverview } from '../TransactionOverview'; +import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink'; +import { RumOverview } from '../RumDashboard'; interface Props { tab: 'transactions' | 'errors' | 'metrics' | 'nodes' | 'service-map'; @@ -110,6 +112,20 @@ export function ServiceDetailTabs({ tab }: Props) { tabs.push(serviceMapTab); } + if (isRumAgentName(agentName)) { + tabs.push({ + link: ( + + {i18n.translate('xpack.apm.home.rumTabLabel', { + defaultMessage: 'Real User Monitoring', + })} + + ), + render: () => , + name: 'rum-overview', + }); + } + const selectedTab = tabs.find((serviceTab) => serviceTab.name === tab); return ( diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index d01deb8160858..eab685a4c1ab4 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -76,10 +76,10 @@ export function KueryBar() { }); // The bar should be disabled when viewing the service map - const disabled = /\/service-map$/.test(location.pathname); + const disabled = /\/(service-map|rum-overview)$/.test(location.pathname); const disabledPlaceholder = i18n.translate( 'xpack.apm.kueryBar.disabledPlaceholder', - { defaultMessage: 'Search is not available for service map' } + { defaultMessage: 'Search is not available here' } ); async function onChange(inputValue: string, selectionStart: number) { diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx new file mode 100644 index 0000000000000..abca9817bd69d --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx @@ -0,0 +1,31 @@ +/* + * 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. + */ + +/* + * 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 { APMLink, APMLinkExtendProps } from './APMLink'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; + +const RumOverviewLink = (props: APMLinkExtendProps) => { + const { urlParams } = useUrlParams(); + + const persistedFilters = pickKeys( + urlParams, + 'transactionResult', + 'host', + 'containerId', + 'podName' + ); + + return ; +}; + +export { RumOverviewLink }; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index 7d96c490fcd70..db36ad1ede91c 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -10,7 +10,7 @@ import { PROCESSOR_EVENT, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../helpers/range_filter'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, SetupTimeRange, diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts index b157abd0b7e76..3d20f84ccfbc2 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts @@ -12,7 +12,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../typings/common'; import { APMError } from '../../../typings/es_schemas/ui/apm_error'; -import { rangeFilter } from '../helpers/range_filter'; +import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange, 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 new file mode 100644 index 0000000000000..7d8f31aaeca7f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -0,0 +1,194 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`rum client dashboard queries fetches client metrics 1`] = ` +Object { + "body": Object { + "aggs": Object { + "backEnd": Object { + "avg": Object { + "field": "transaction.marks.agent.timeToFirstByte", + "missing": 0, + }, + }, + "domInteractive": Object { + "avg": Object { + "field": "transaction.marks.agent.domInteractive", + "missing": 0, + }, + }, + "pageViews": Object { + "value_count": Object { + "field": "transaction.type", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "page-load", + }, + }, + Object { + "exists": Object { + "field": "transaction.marks.navigationTiming.fetchStart", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`rum client dashboard queries fetches page load distribution 1`] = ` +Object { + "body": Object { + "aggs": Object { + "durationMinMax": Object { + "min": Object { + "field": "transaction.duration.us", + "missing": 0, + }, + }, + "durationPercentiles": Object { + "percentiles": Object { + "field": "transaction.duration.us", + "percents": Array [ + 50, + 75, + 90, + 95, + 99, + ], + "script": Object { + "lang": "painless", + "params": Object { + "timeUnit": 1000, + }, + "source": "doc['transaction.duration.us'].value / params.timeUnit", + }, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "page-load", + }, + }, + Object { + "exists": Object { + "field": "transaction.marks.navigationTiming.fetchStart", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`rum client dashboard queries fetches page view trends 1`] = ` +Object { + "body": Object { + "aggs": Object { + "pageViews": Object { + "aggs": Object { + "trans_count": Object { + "value_count": Object { + "field": "transaction.type", + }, + }, + }, + "auto_date_histogram": Object { + "buckets": 50, + "field": "@timestamp", + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "page-load", + }, + }, + Object { + "exists": Object { + "field": "transaction.marks.navigationTiming.fetchStart", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts new file mode 100644 index 0000000000000..8b3f733fc402a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.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 { getRumOverviewProjection } from '../../../common/projections/rum_overview'; +import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; + +export async function getClientMetrics({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const projection = getRumOverviewProjection({ + setup, + }); + + const params = mergeProjection(projection, { + body: { + size: 0, + query: { + bool: projection.body.query.bool, + }, + aggs: { + pageViews: { value_count: { field: 'transaction.type' } }, + backEnd: { + avg: { + field: 'transaction.marks.agent.timeToFirstByte', + missing: 0, + }, + }, + domInteractive: { + avg: { + field: 'transaction.marks.agent.domInteractive', + missing: 0, + }, + }, + }, + }, + }); + + const { client } = setup; + + const response = await client.search(params); + const { backEnd, domInteractive, pageViews } = response.aggregations!; + + // Divide by 1000 to convert ms into seconds + return { + pageViews, + backEnd: { value: (backEnd.value || 0) / 1000 }, + frontEnd: { + value: ((domInteractive.value || 0) - (backEnd.value || 0)) / 1000, + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts new file mode 100644 index 0000000000000..3c563946e4052 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -0,0 +1,140 @@ +/* + * 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 { getRumOverviewProjection } from '../../../common/projections/rum_overview'; +import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; + +export async function getPageLoadDistribution({ + setup, + minPercentile, + maxPercentile, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + minPercentile?: string; + maxPercentile?: string; +}) { + const projection = getRumOverviewProjection({ + setup, + }); + + const params = mergeProjection(projection, { + body: { + size: 0, + query: { + bool: projection.body.query.bool, + }, + aggs: { + durationMinMax: { + min: { + field: 'transaction.duration.us', + missing: 0, + }, + }, + durationPercentiles: { + percentiles: { + field: 'transaction.duration.us', + percents: [50, 75, 90, 95, 99], + script: { + lang: 'painless', + source: "doc['transaction.duration.us'].value / params.timeUnit", + params: { + timeUnit: 1000, + }, + }, + }, + }, + }, + }, + }); + + const { client } = setup; + + const { + aggregations, + hits: { total }, + } = await client.search(params); + + if (total.value === 0) { + return null; + } + + const minDuration = (aggregations?.durationMinMax.value ?? 0) / 1000; + + const minPerc = minPercentile ? +minPercentile : minDuration; + + const maxPercentileQuery = + aggregations?.durationPercentiles.values['99.0'] ?? 100; + + const maxPerc = maxPercentile ? +maxPercentile : maxPercentileQuery; + + const pageDist = await getPercentilesDistribution(setup, minPerc, maxPerc); + return { + pageLoadDistribution: pageDist, + percentiles: aggregations?.durationPercentiles.values, + }; +} + +const getPercentilesDistribution = async ( + setup: Setup & SetupTimeRange & SetupUIFilters, + minPercentiles: number, + maxPercentile: number +) => { + const stepValue = (maxPercentile - minPercentiles) / 50; + const stepValues = []; + for (let i = 1; i < 50; i++) { + stepValues.push((stepValue * i + minPercentiles).toFixed(2)); + } + + const projection = getRumOverviewProjection({ + setup, + }); + + const params = mergeProjection(projection, { + body: { + size: 0, + query: { + bool: projection.body.query.bool, + }, + aggs: { + loadDistribution: { + percentile_ranks: { + field: 'transaction.duration.us', + values: stepValues, + keyed: false, + script: { + lang: 'painless', + source: "doc['transaction.duration.us'].value / params.timeUnit", + params: { + timeUnit: 1000, + }, + }, + }, + }, + }, + }, + }); + + const { client } = setup; + + const { aggregations } = await client.search(params); + + const pageDist = (aggregations?.loadDistribution.values ?? []) as Array<{ + key: number; + value: number; + }>; + + return pageDist.map(({ key, value }, index: number, arr) => { + return { + x: key, + y: index === 0 ? value : value - arr[index - 1].value, + }; + }); +}; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts new file mode 100644 index 0000000000000..126605206d299 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -0,0 +1,57 @@ +/* + * 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 { getRumOverviewProjection } from '../../../common/projections/rum_overview'; +import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; + +export async function getPageViewTrends({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const projection = getRumOverviewProjection({ + setup, + }); + + const params = mergeProjection(projection, { + body: { + size: 0, + query: { + bool: projection.body.query.bool, + }, + aggs: { + pageViews: { + auto_date_histogram: { + field: '@timestamp', + buckets: 50, + }, + aggs: { + trans_count: { + value_count: { + field: 'transaction.type', + }, + }, + }, + }, + }, + }, + }); + + const { client } = setup; + + const response = await client.search(params); + + const result = response.aggregations?.pageViews.buckets ?? []; + return result.map(({ key, trans_count }) => ({ + x: key, + y: trans_count.value, + })); +} 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 new file mode 100644 index 0000000000000..5f5a48eced746 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { + SearchParamsMock, + inspectSearchParams, +} from '../../../public/utils/testHelpers'; +import { getClientMetrics } from './get_client_metrics'; +import { getPageViewTrends } from './get_page_view_trends'; +import { getPageLoadDistribution } from './get_page_load_distribution'; + +describe('rum client dashboard queries', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + it('fetches client metrics', async () => { + mock = await inspectSearchParams((setup) => + getClientMetrics({ + setup, + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches page view trends', async () => { + mock = await inspectSearchParams((setup) => + getPageViewTrends({ + setup, + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches page load distribution', async () => { + mock = await inspectSearchParams((setup) => + getPageLoadDistribution({ + setup, + minPercentile: '0', + maxPercentile: '99', + }) + ); + expect(mock.params).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index d069e93397611..e521efa687388 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -6,7 +6,7 @@ import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ESFilter } from '../../../typings/elasticsearch'; -import { rangeFilter } from '../helpers/range_filter'; +import { rangeFilter } from '../../../common/utils/range_filter'; import { PROCESSOR_EVENT, SERVICE_ENVIRONMENT, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 6eba84f2205a1..11c3a00f32980 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -5,7 +5,7 @@ */ import { uniq, take, sortBy } from 'lodash'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { rangeFilter } from '../helpers/range_filter'; +import { rangeFilter } from '../../../common/utils/range_filter'; import { ESFilter } from '../../../typings/elasticsearch'; import { PROCESSOR_EVENT, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 9829c5cb25182..6da5d195cf194 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -7,7 +7,7 @@ import { isNumber } from 'lodash'; import { Annotation, AnnotationType } from '../../../../common/annotations'; import { SetupTimeRange, Setup } from '../../helpers/setup_request'; import { ESFilter } from '../../../../typings/elasticsearch'; -import { rangeFilter } from '../../helpers/range_filter'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { PROCESSOR_EVENT, SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts index 0b016828d5f00..8d75d746c7fca 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -8,7 +8,7 @@ import { AGENT_NAME, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../helpers/range_filter'; +import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export async function getServiceAgentName( diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts index 963dea4d8322c..d88be4055dc21 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -8,7 +8,7 @@ import { SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../helpers/range_filter'; +import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export async function getServiceTransactionTypes( diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index e96b323958fd7..f9374558dfeeb 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -16,7 +16,7 @@ import { import { Span } from '../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { APMError } from '../../../typings/es_schemas/ui/apm_error'; -import { rangeFilter } from '../helpers/range_filter'; +import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { PromiseValueType } from '../../../typings/common'; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts index 90dd41cb9b0c8..e3d688b694380 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -13,7 +13,7 @@ import { USER_AGENT_NAME, TRANSACTION_DURATION, } from '../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../helpers/range_filter'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Options } from '.'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts index cc23055e34672..ea6213f64ee36 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts @@ -17,7 +17,7 @@ import { SetupTimeRange, SetupUIFilters, } from '../../helpers/setup_request'; -import { rangeFilter } from '../../helpers/range_filter'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; export async function getTransactionAvgDurationByCountry({ diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 713423f8953d5..5af8b9f78cec1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -20,7 +20,7 @@ import { SetupTimeRange, SetupUIFilters, } from '../../helpers/setup_request'; -import { rangeFilter } from '../../helpers/range_filter'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { getMetricsDateHistogramParams } from '../../helpers/metrics'; import { MAX_KPIS } from './constants'; import { getVizColorForIndex } from '../../../../common/viz_colors'; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 71c40010a2a3f..8e19af926ce02 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -15,7 +15,7 @@ import { } from '../../../../../common/elasticsearch_fieldnames'; import { PromiseReturnType } from '../../../../../../observability/typings/common'; import { getBucketSize } from '../../../helpers/get_bucket_size'; -import { rangeFilter } from '../../../helpers/range_filter'; +import { rangeFilter } from '../../../../../common/utils/range_filter'; import { Setup, SetupTimeRange, diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts index 920552d1c1aeb..3f8bf635712be 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts @@ -15,7 +15,7 @@ import { TRANSACTION_SAMPLED, TRANSACTION_TYPE, } from '../../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../helpers/range_filter'; +import { rangeFilter } from '../../../../../common/utils/range_filter'; import { Setup, SetupTimeRange, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index 60dc16b6a546c..a7de93a3bf650 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -10,7 +10,7 @@ import { TRANSACTION_ID, } from '../../../../common/elasticsearch_fieldnames'; import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; -import { rangeFilter } from '../../helpers/range_filter'; +import { rangeFilter } from '../../../../common/utils/range_filter'; import { Setup, SetupTimeRange, diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts index ccbe7a19d2f82..3fca30634be6a 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -9,7 +9,7 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../helpers/range_filter'; +import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; import { ESFilter } from '../../../typings/elasticsearch'; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts index 8f35664c2599c..25a559cb07a3d 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts @@ -11,6 +11,11 @@ import { HOST_NAME, TRANSACTION_RESULT, SERVICE_VERSION, + TRANSACTION_URL, + USER_AGENT_NAME, + USER_AGENT_DEVICE, + CLIENT_GEO, + USER_AGENT_OS, } from '../../../../common/elasticsearch_fieldnames'; const filtersByName = { @@ -50,6 +55,36 @@ const filtersByName = { }), fieldName: SERVICE_VERSION, }, + transactionUrl: { + title: i18n.translate('xpack.apm.localFilters.titles.transactionUrl', { + defaultMessage: 'Url', + }), + fieldName: TRANSACTION_URL, + }, + browser: { + title: i18n.translate('xpack.apm.localFilters.titles.browser', { + defaultMessage: 'Browser', + }), + fieldName: USER_AGENT_NAME, + }, + device: { + title: i18n.translate('xpack.apm.localFilters.titles.device', { + defaultMessage: 'Device', + }), + fieldName: USER_AGENT_DEVICE, + }, + location: { + title: i18n.translate('xpack.apm.localFilters.titles.location', { + defaultMessage: 'Location', + }), + fieldName: CLIENT_GEO, + }, + os: { + title: i18n.translate('xpack.apm.localFilters.titles.os', { + defaultMessage: 'OS', + }), + fieldName: USER_AGENT_OS, + }, }; export type LocalUIFilterName = keyof typeof filtersByName; 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 bdfb49fa30828..a34690aff43b4 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -59,6 +59,7 @@ import { transactionsLocalFiltersRoute, serviceNodesLocalFiltersRoute, uiFiltersEnvironmentsRoute, + rumOverviewLocalFiltersRoute, } from './ui_filters'; import { createApi } from './create_api'; import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map'; @@ -70,6 +71,11 @@ import { listCustomLinksRoute, customLinkTransactionRoute, } from './settings/custom_link'; +import { + rumClientMetricsRoute, + rumPageViewsTrendRoute, + rumPageLoadDistributionRoute, +} from './rum_client'; const createApmApi = () => { const api = createApi() @@ -148,7 +154,13 @@ const createApmApi = () => { .add(updateCustomLinkRoute) .add(deleteCustomLinkRoute) .add(listCustomLinksRoute) - .add(customLinkTransactionRoute); + .add(customLinkTransactionRoute) + + // Rum Overview + .add(rumOverviewLocalFiltersRoute) + .add(rumPageViewsTrendRoute) + .add(rumPageLoadDistributionRoute) + .add(rumClientMetricsRoute); return api; }; diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts new file mode 100644 index 0000000000000..9b5f6529b1783 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -0,0 +1,57 @@ +/* + * 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'; +import { createRoute } from './create_route'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { getClientMetrics } from '../lib/rum_client/get_client_metrics'; +import { rangeRt, uiFiltersRt } from './default_api_types'; +import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends'; +import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distribution'; + +export const percentileRangeRt = t.partial({ + minPercentile: t.string, + maxPercentile: t.string, +}); + +export const rumClientMetricsRoute = createRoute(() => ({ + path: '/api/apm/rum/client-metrics', + params: { + query: t.intersection([uiFiltersRt, rangeRt]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + return getClientMetrics({ setup }); + }, +})); + +export const rumPageLoadDistributionRoute = createRoute(() => ({ + path: '/api/apm/rum-client/page-load-distribution', + params: { + query: t.intersection([uiFiltersRt, rangeRt, percentileRangeRt]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + query: { minPercentile, maxPercentile }, + } = context.params; + + return getPageLoadDistribution({ setup, minPercentile, maxPercentile }); + }, +})); + +export const rumPageViewsTrendRoute = createRoute(() => ({ + path: '/api/apm/rum-client/page-view-trends', + params: { + query: t.intersection([uiFiltersRt, rangeRt]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + return getPageViewTrends({ setup }); + }, +})); diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index 8f4ef94b86ac5..280645d4de8d0 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -29,6 +29,7 @@ import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { jsonRt } from '../../common/runtime_types/json_rt'; import { getServiceNodesProjection } from '../../common/projections/service_nodes'; +import { getRumOverviewProjection } from '../../common/projections/rum_overview'; export const uiFiltersEnvironmentsRoute = createRoute(() => ({ path: '/api/apm/ui_filters/environments', @@ -221,6 +222,16 @@ export const serviceNodesLocalFiltersRoute = createLocalFiltersRoute({ }), }); +export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ + path: '/api/apm/ui_filters/local_filters/rumOverview', + getProjection: ({ setup }) => { + return getRumOverviewProjection({ + setup, + }); + }, + queryRt: t.type({}), +}); + type BaseQueryType = typeof localUiBaseQueryRt; type GetProjection< diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index 0739e8e6120bf..6ee26caa4ef7c 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -137,6 +137,15 @@ export interface AggregationOptionsByType { >; keyed?: boolean; }; + auto_date_histogram: { + field: string; + buckets: number; + }; + percentile_ranks: { + field: string; + values: string[]; + keyed?: boolean; + }; } type AggregationType = keyof AggregationOptionsByType; @@ -301,6 +310,23 @@ interface AggregationResponsePart< ? Record : { buckets: DateRangeBucket[] }; }; + auto_date_histogram: { + buckets: Array< + { + doc_count: number; + key: number; + key_as_string: string; + } & BucketSubAggregationResponse< + TAggregationOptionsMap['aggs'], + TDocument + > + >; + interval: string; + }; + + percentile_ranks: { + values: Record | Array<{ key: number; value: number }>; + }; } // Type for debugging purposes. If you see an error in AggregationResponseMap diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 37cdbf5c0d8a9..d119ddb5a1a1f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4229,7 +4229,6 @@ "xpack.apm.jvmsTable.noJvmsLabel": "JVM が見つかりませんでした", "xpack.apm.jvmsTable.nonHeapMemoryColumnLabel": "非ヒープ領域の平均", "xpack.apm.jvmsTable.threadCountColumnLabel": "最大スレッド数", - "xpack.apm.kueryBar.disabledPlaceholder": "サービスマップの検索は利用できません", "xpack.apm.kueryBar.placeholder": "検索 {event, select,\n transaction {トランザクション}\n metric {メトリック}\n error {エラー}\n other {その他}\n } (E.g. {queryExample})", "xpack.apm.license.betaBadge": "ベータ", "xpack.apm.license.betaTooltipMessage": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2dfa0d40b9a8a..240baa3fe7744 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4232,7 +4232,6 @@ "xpack.apm.jvmsTable.noJvmsLabel": "未找到任何 JVM", "xpack.apm.jvmsTable.nonHeapMemoryColumnLabel": "非堆内存平均值", "xpack.apm.jvmsTable.threadCountColumnLabel": "线程计数最大值", - "xpack.apm.kueryBar.disabledPlaceholder": "搜索不适用于服务地图", "xpack.apm.kueryBar.placeholder": "搜索{event, select,\n transaction {事务}\n metric {指标}\n error {错误}\n other {事务、错误和指标}\n }(例如 {queryExample})", "xpack.apm.license.betaBadge": "公测版", "xpack.apm.license.betaTooltipMessage": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。",