From 9775119d695c83a5c5c59b37fbcf074b8a4ea618 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 1 Oct 2020 10:11:30 +0200 Subject: [PATCH] [UX] Add percentile selector (#78562) (#79058) --- x-pack/plugins/apm/common/ui_filter.ts | 98 +++++++++++++++++++ .../cypress/integration/csm_dashboard.feature | 4 + .../step_definitions/csm/percentile_select.ts | 29 ++++++ .../step_definitions/csm/url_search_filter.ts | 2 +- .../support/step_definitions/csm/utils.ts | 14 +++ .../app/RumDashboard/ClientMetrics/index.tsx | 16 +-- .../PageLoadDistribution/index.tsx | 4 +- .../app/RumDashboard/PageViewsTrend/index.tsx | 4 +- .../components/app/RumDashboard/RumHome.tsx | 6 +- .../URLFilter/URLSearch/SelectableUrlList.tsx | 7 +- .../URLFilter/URLSearch/index.tsx | 22 +++-- .../app/RumDashboard/URLFilter/index.tsx | 38 ++++--- .../RumDashboard/UXMetrics/KeyUXMetrics.tsx | 15 +-- .../app/RumDashboard/UXMetrics/index.tsx | 18 +--- .../app/RumDashboard/UserPercentile/index.tsx | 97 ++++++++++++++++++ .../RumDashboard/VisitorBreakdown/index.tsx | 4 +- .../VisitorBreakdownMap/EmbeddedMap.tsx | 4 +- .../VisitorBreakdownMap/useMapFilters.ts | 42 +++++--- .../app/RumDashboard/hooks/useUxQuery.ts | 32 ++++++ .../components/app/RumDashboard/index.tsx | 4 +- .../app/RumDashboard/translations.ts | 18 ++++ .../components/shared/Links/url_helpers.ts | 4 +- .../shared/LocalUIFilters/index.tsx | 3 +- .../public/context/UrlParamsContext/index.tsx | 3 +- .../UrlParamsContext/resolveUrlParams.ts | 2 + .../public/context/UrlParamsContext/types.ts | 4 +- .../apm/public/hooks/useLocalUIFilters.ts | 2 +- .../lib/rum_client/get_client_metrics.ts | 12 ++- .../lib/rum_client/get_page_view_trends.ts | 2 + .../server/lib/rum_client/get_url_search.ts | 18 +++- .../lib/rum_client/get_web_core_vitals.ts | 26 +++-- .../lib/ui_filters/local_ui_filters/config.ts | 92 +---------------- .../get_local_filter_query.ts | 3 +- .../lib/ui_filters/local_ui_filters/index.ts | 3 +- .../plugins/apm/server/routes/rum_client.ts | 70 +++++++------ .../plugins/apm/server/routes/ui_filters.ts | 6 +- x-pack/plugins/apm/typings/ui_filters.ts | 3 +- .../typings/fetch_overview_data/index.ts | 2 +- .../plugins/observability/typings/common.ts | 3 +- .../trial/tests/csm/url_search.ts | 6 +- .../trial/tests/csm/web_core_vitals.ts | 4 +- 41 files changed, 504 insertions(+), 242 deletions(-) create mode 100644 x-pack/plugins/apm/common/ui_filter.ts create mode 100644 x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts create mode 100644 x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/utils.ts create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts diff --git a/x-pack/plugins/apm/common/ui_filter.ts b/x-pack/plugins/apm/common/ui_filter.ts new file mode 100644 index 0000000000000..22463cbdb86d1 --- /dev/null +++ b/x-pack/plugins/apm/common/ui_filter.ts @@ -0,0 +1,98 @@ +/* + * 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'; +import { + AGENT_NAME, + CLIENT_GEO_COUNTRY_ISO_CODE, + CONTAINER_ID, + HOST_NAME, + POD_NAME, + SERVICE_NAME, + SERVICE_VERSION, + TRANSACTION_RESULT, + TRANSACTION_URL, + USER_AGENT_DEVICE, + USER_AGENT_NAME, + USER_AGENT_OS, +} from './elasticsearch_fieldnames'; + +export const filtersByName = { + host: { + title: i18n.translate('xpack.apm.localFilters.titles.host', { + defaultMessage: 'Host', + }), + fieldName: HOST_NAME, + }, + agentName: { + title: i18n.translate('xpack.apm.localFilters.titles.agentName', { + defaultMessage: 'Agent name', + }), + fieldName: AGENT_NAME, + }, + containerId: { + title: i18n.translate('xpack.apm.localFilters.titles.containerId', { + defaultMessage: 'Container ID', + }), + fieldName: CONTAINER_ID, + }, + podName: { + title: i18n.translate('xpack.apm.localFilters.titles.podName', { + defaultMessage: 'Kubernetes pod', + }), + fieldName: POD_NAME, + }, + transactionResult: { + title: i18n.translate('xpack.apm.localFilters.titles.transactionResult', { + defaultMessage: 'Transaction result', + }), + fieldName: TRANSACTION_RESULT, + }, + serviceVersion: { + title: i18n.translate('xpack.apm.localFilters.titles.serviceVersion', { + defaultMessage: 'Service version', + }), + 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_COUNTRY_ISO_CODE, + }, + os: { + title: i18n.translate('xpack.apm.localFilters.titles.os', { + defaultMessage: 'OS', + }), + fieldName: USER_AGENT_OS, + }, + serviceName: { + title: i18n.translate('xpack.apm.localFilters.titles.serviceName', { + defaultMessage: 'Service name', + }), + fieldName: SERVICE_NAME, + }, +}; + +export type LocalUIFilterName = keyof typeof filtersByName; diff --git a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature index 7b894b6ca7aac..5dc1d5da0b75c 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature @@ -9,6 +9,10 @@ Feature: CSM Dashboard When a user browses the APM UI application for RUM Data Then should have correct client metrics + Scenario: Percentile select + When the user changes the selected percentile + Then it displays client metric related to that percentile + Scenario Outline: CSM page filters When the user filters by "" Then it filters the client metrics "" diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts new file mode 100644 index 0000000000000..4d2ba4d01ae6c --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts @@ -0,0 +1,29 @@ +/* + * 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 { When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { verifyClientMetrics } from './client_metrics_helper'; +import { getDataTestSubj } from './utils'; + +When('the user changes the selected percentile', () => { + // wait for all loading to finish + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + getDataTestSubj('uxPercentileSelect').click(); + + getDataTestSubj('p95Percentile').click(); +}); + +Then(`it displays client metric related to that percentile`, () => { + const metrics = ['14 ms', '0.13 s', '55 ']; + + verifyClientMetrics(metrics, false); + + // reset to median + getDataTestSubj('uxPercentileSelect').click(); + + getDataTestSubj('p50Percentile').click(); +}); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts index 3b5dd70065055..b8bfeffb2293c 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts @@ -24,7 +24,7 @@ Then(`it displays top pages in the suggestion popover`, () => { listOfUrls.should('have.length', 5); const actualUrlsText = [ - 'http://opbeans-node:3000/dashboardPage views: 17Page load duration: 109 ms ', + 'http://opbeans-node:3000/dashboardPage views: 17Page load duration: 109 ms', 'http://opbeans-node:3000/ordersPage views: 14Page load duration: 72 ms', ]; diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/utils.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/utils.ts new file mode 100644 index 0000000000000..87b3a1d70d073 --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/utils.ts @@ -0,0 +1,14 @@ +/* + * 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 { DEFAULT_TIMEOUT } from './csm_dashboard'; + +export function getDataTestSubj(dataTestSubj: string) { + // wait for all loading to finish + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + return cy.get(`[data-test-subj=${dataTestSubj}]`, DEFAULT_TIMEOUT); +} 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 bc1e0a86f17db..03f2f31f35817 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 @@ -9,8 +9,8 @@ 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 { useUxQuery } from '../hooks/useUxQuery'; import { CsmSharedContext } from '../CsmSharedContext'; const ClFlexGroup = styled(EuiFlexGroup)` @@ -22,29 +22,23 @@ const ClFlexGroup = styled(EuiFlexGroup)` `; export function ClientMetrics() { - const { urlParams, uiFilters } = useUrlParams(); - - const { start, end, searchTerm } = urlParams; + const uxQuery = useUxQuery(); const { data, status } = useFetcher( (callApmApi) => { - const { serviceName } = uiFilters; - if (start && end && serviceName) { + if (uxQuery) { return callApmApi({ pathname: '/api/apm/rum/client-metrics', params: { query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - urlQuery: searchTerm, + ...uxQuery, }, }, }); } return Promise.resolve(null); }, - [start, end, uiFilters, searchTerm] + [uxQuery] ); const { setSharedData } = useContext(CsmSharedContext); 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 index c8e45d2b2f672..88d14a0213a96 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -33,7 +33,9 @@ export function PageLoadDistribution() { const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + const { serviceName } = uiFilters; + + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/page-load-distribution', params: { 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 index f2da0955412e7..621098b6028cb 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -22,7 +22,9 @@ export function PageViewsTrend() { const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + const { serviceName } = uiFilters; + + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/page-view-trends', params: { 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 28bb5307b6e8c..71a992ae4df82 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 { UserPercentile } from './UserPercentile'; import { CsmSharedContextProvider } from './CsmSharedContext'; export const UX_LABEL = i18n.translate('xpack.apm.ux.title', { @@ -21,11 +22,14 @@ export function RumHome() { - +

{UX_LABEL}

+ + +
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx index 298ec15b8480b..ebca1df17038c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FormEvent, useRef, useState } from 'react'; +import React, { FormEvent, SetStateAction, useRef, useState } from 'react'; import { EuiButtonEmpty, EuiFlexGroup, @@ -33,6 +33,8 @@ interface Props { onChange: (updatedOptions: UrlOption[]) => void; searchValue: string; onClose: () => void; + popoverIsOpen: boolean; + setPopoverIsOpen: React.Dispatch>; } export function SelectableUrlList({ @@ -43,8 +45,9 @@ export function SelectableUrlList({ onChange, searchValue, onClose, + popoverIsOpen, + setPopoverIsOpen, }: Props) { - const [popoverIsOpen, setPopoverIsOpen] = useState(false); const [popoverRef, setPopoverRef] = useState(null); const [searchRef, setSearchRef] = useState(null); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx index b88cf29740dcd..5ad666cd466bc 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx @@ -15,6 +15,7 @@ import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; import { formatToSec } from '../../UXMetrics/KeyUXMetrics'; import { SelectableUrlList } from './SelectableUrlList'; import { UrlOption } from './RenderOption'; +import { useUxQuery } from '../../hooks/useUxQuery'; interface Props { onChange: (value: string[]) => void; @@ -23,9 +24,10 @@ interface Props { export function URLSearch({ onChange: onFilterChange }: Props) { const history = useHistory(); - const { urlParams, uiFilters } = useUrlParams(); + const { uiFilters } = useUrlParams(); + + const [popoverIsOpen, setPopoverIsOpen] = useState(false); - const { start, end, serviceName } = urlParams; const [searchValue, setSearchValue] = useState(''); const [debouncedValue, setDebouncedValue] = useState(''); @@ -54,17 +56,18 @@ export function URLSearch({ onChange: onFilterChange }: Props) { const [checkedUrls, setCheckedUrls] = useState([]); + const uxQuery = useUxQuery(); + const { data, status } = useFetcher( (callApmApi) => { - if (start && end && serviceName) { + if (uxQuery && popoverIsOpen) { const { transactionUrl, ...restFilters } = uiFilters; return callApmApi({ pathname: '/api/apm/rum-client/url-search', params: { query: { - start, - end, + ...uxQuery, uiFilters: JSON.stringify(restFilters), urlQuery: searchValue, }, @@ -73,7 +76,8 @@ export function URLSearch({ onChange: onFilterChange }: Props) { } return Promise.resolve(null); }, - [start, end, serviceName, uiFilters, searchValue] + // eslint-disable-next-line react-hooks/exhaustive-deps + [uxQuery, searchValue, popoverIsOpen] ); useEffect(() => { @@ -110,7 +114,9 @@ export function URLSearch({ onChange: onFilterChange }: Props) { }; const onClose = () => { - onFilterChange(checkedUrls); + if (uiFilters.transactionUrl || checkedUrls.length > 0) { + onFilterChange(checkedUrls); + } }; return ( @@ -126,6 +132,8 @@ export function URLSearch({ onChange: onFilterChange }: Props) { onChange={onChange} onClose={onClose} searchValue={searchValue} + popoverIsOpen={popoverIsOpen} + setPopoverIsOpen={setPopoverIsOpen} /> ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx index 9d3c8d012871f..6456329bdb456 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx @@ -5,16 +5,16 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback } from 'react'; import { EuiSpacer, EuiBadge } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; -import { Projection } from '../../../../../common/projections'; -import { useLocalUIFilters } from '../../../../hooks/useLocalUIFilters'; +import { omit } from 'lodash'; import { URLSearch } from './URLSearch'; -import { LocalUIFilters } from '../../../shared/LocalUIFilters'; import { UrlList } from './UrlList'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { removeUndefinedProps } from '../../../../context/UrlParamsContext/helpers'; +import { LocalUIFilterName } from '../../../../../common/ui_filter'; const removeSearchTermLabel = i18n.translate( 'xpack.apm.uiFilter.url.removeSearchTerm', @@ -28,18 +28,19 @@ export function URLFilter() { urlParams: { searchTerm }, } = useUrlParams(); - const localUIFiltersConfig = useMemo(() => { - const config: React.ComponentProps = { - filterNames: ['transactionUrl'], - projection: Projection.rumOverview, - }; + const setFilterValue = (name: LocalUIFilterName, value: string[]) => { + const search = omit(toQuery(history.location.search), name); - return config; - }, []); - - const { filters, setFilterValue } = useLocalUIFilters({ - ...localUIFiltersConfig, - }); + history.push({ + ...history.location, + search: fromQuery( + removeUndefinedProps({ + ...search, + [name]: value.length ? value.join(',') : undefined, + }) + ), + }); + }; const updateSearchTerm = useCallback( (searchTermN?: string) => { @@ -55,7 +56,12 @@ export function URLFilter() { [history] ); - const { name, value: filterValue } = filters[0]; + const name = 'transactionUrl'; + + const { uiFilters } = useUrlParams(); + const { transactionUrl } = uiFilters; + + const filterValue = transactionUrl ?? []; return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx index 1d8360872afba..37836a2c47d64 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx @@ -14,8 +14,8 @@ import { SUM_LONG_TASKS, TBT_LABEL, } from '../CoreVitals/translations'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUxQuery } from '../hooks/useUxQuery'; export function formatToSec( value?: number | string, @@ -36,28 +36,23 @@ interface Props { } export function KeyUXMetrics({ data, loading }: Props) { - const { urlParams, uiFilters } = useUrlParams(); - - const { start, end, serviceName, searchTerm } = urlParams; + const uxQuery = useUxQuery(); const { data: longTaskData, status } = useFetcher( (callApmApi) => { - if (start && end && serviceName) { + if (uxQuery) { return callApmApi({ pathname: '/api/apm/rum-client/long-task-metrics', params: { query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - urlQuery: searchTerm, + ...uxQuery, }, }, }); } return Promise.resolve(null); }, - [start, end, serviceName, uiFilters, searchTerm] + [uxQuery] ); // Note: FCP value is in ms unit diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx index 1a95ac50587af..910c37c6ccbaa 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx @@ -21,8 +21,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { I18LABELS } from '../translations'; import { CoreVitals } from '../CoreVitals'; import { KeyUXMetrics } from './KeyUXMetrics'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUxQuery } from '../hooks/useUxQuery'; export interface UXMetrics { cls: string; @@ -36,29 +36,21 @@ export interface UXMetrics { } export function UXMetrics() { - const { urlParams, uiFilters } = useUrlParams(); - - const { start, end, searchTerm } = urlParams; + const uxQuery = useUxQuery(); const { data, status } = useFetcher( (callApmApi) => { - const { serviceName } = uiFilters; - if (start && end && serviceName) { + if (uxQuery) { return callApmApi({ pathname: '/api/apm/rum-client/web-core-vitals', params: { - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - urlQuery: searchTerm, - }, + query: uxQuery, }, }); } return Promise.resolve(null); }, - [start, end, uiFilters, searchTerm] + [uxQuery] ); const [isPopoverOpen, setIsPopoverOpen] = useState(false); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx new file mode 100644 index 0000000000000..2ce724e7fec85 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UserPercentile/index.tsx @@ -0,0 +1,97 @@ +/* + * 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, { useCallback, useEffect } from 'react'; + +import { EuiSuperSelect } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import styled from 'styled-components'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { I18LABELS } from '../translations'; + +const DEFAULT_P = 50; + +const StyledSpan = styled.span` + font-weight: 600; +`; + +export function UserPercentile() { + const history = useHistory(); + + const { + urlParams: { percentile }, + } = useUrlParams(); + + const updatePercentile = useCallback( + (percentileN?: number) => { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + percentile: percentileN, + }), + }; + history.push(newLocation); + }, + [history] + ); + + useEffect(() => { + if (!percentile) { + updatePercentile(DEFAULT_P); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const options = [ + { + value: '50', + inputDisplay: I18LABELS.percentile50thMedian, + dropdownDisplay: I18LABELS.percentile50thMedian, + 'data-test-subj': 'p50Percentile', + }, + { + value: '75', + inputDisplay: {I18LABELS.percentile75th}, + dropdownDisplay: I18LABELS.percentile75th, + 'data-test-subj': 'p75Percentile', + }, + { + value: '90', + inputDisplay: {I18LABELS.percentile90th}, + dropdownDisplay: I18LABELS.percentile90th, + 'data-test-subj': 'p90Percentile', + }, + { + value: '95', + inputDisplay: {I18LABELS.percentile95th}, + dropdownDisplay: I18LABELS.percentile95th, + 'data-test-subj': 'p95Percentile', + }, + { + value: '99', + inputDisplay: {I18LABELS.percentile99th}, + dropdownDisplay: I18LABELS.percentile99th, + 'data-test-subj': 'p99Percentile', + }, + ]; + + const onChange = (val: string) => { + updatePercentile(Number(val)); + }; + + return ( + onChange(value)} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx index 2db6ef8fa6c06..092c416303bb5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -18,7 +18,9 @@ export function VisitorBreakdown() { const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + const { serviceName } = uiFilters; + + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/visitor-breakdown', params: { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx index 93608a0ccd826..1198c014f5921 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -162,10 +162,10 @@ export function EmbeddedMapComponent() { // We can only render after embeddable has already initialized useEffect(() => { - if (embeddableRoot.current && embeddable) { + if (embeddableRoot.current && embeddable && serviceName) { embeddable.render(embeddableRoot.current); } - }, [embeddable, embeddableRoot]); + }, [embeddable, embeddableRoot, serviceName]); return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts index 357e04c538e68..774ac23d23196 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useMapFilters.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { FieldFilter as Filter } from '../../../../../../../../src/plugins/data/common'; import { CLIENT_GEO_COUNTRY_ISO_CODE, SERVICE_NAME, + TRANSACTION_URL, USER_AGENT_DEVICE, USER_AGENT_NAME, USER_AGENT_OS, @@ -17,6 +18,21 @@ import { import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../src/plugins/apm_oss/public'; +const getWildcardFilter = (field: string, value: string): Filter => { + return { + meta: { + index: APM_STATIC_INDEX_PATTERN_ID, + alias: null, + negate: false, + disabled: false, + type: 'term', + key: field, + params: { query: value }, + }, + query: { wildcard: { [field]: { value: `*${value}*` } } }, + }; +}; + const getMatchFilter = (field: string, value: string): Filter => { return { meta: { @@ -28,7 +44,7 @@ const getMatchFilter = (field: string, value: string): Filter => { key: field, params: { query: value }, }, - query: { match_phrase: { [field]: value } }, + query: { term: { [field]: value } }, }; }; @@ -52,14 +68,13 @@ const getMultiMatchFilter = (field: string, values: string[]): Filter => { }, }; }; + export const useMapFilters = (): Filter[] => { const { urlParams, uiFilters } = useUrlParams(); - const { serviceName } = urlParams; - - const { browser, device, os, location } = uiFilters; + const { serviceName, searchTerm } = urlParams; - const [mapFilters, setMapFilters] = useState([]); + const { browser, device, os, location, transactionUrl } = uiFilters; const existFilter: Filter = { meta: { @@ -76,7 +91,7 @@ export const useMapFilters = (): Filter[] => { }, }; - useEffect(() => { + return useMemo(() => { const filters = [existFilter]; if (serviceName) { filters.push(getMatchFilter(SERVICE_NAME, serviceName)); @@ -93,10 +108,15 @@ export const useMapFilters = (): Filter[] => { if (location) { filters.push(getMultiMatchFilter(CLIENT_GEO_COUNTRY_ISO_CODE, location)); } + if (transactionUrl) { + filters.push(getMultiMatchFilter(TRANSACTION_URL, transactionUrl)); + } + if (searchTerm) { + filters.push(getWildcardFilter(TRANSACTION_URL, searchTerm)); + } - setMapFilters(filters); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [serviceName, browser, device, os, location]); + return filters; - return mapFilters; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serviceName, browser, device, os, location, searchTerm]); }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts new file mode 100644 index 0000000000000..da2ac52602198 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useUxQuery.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; + +export function useUxQuery() { + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end, searchTerm, percentile } = urlParams; + + const queryParams = useMemo(() => { + const { serviceName } = uiFilters; + + if (start && end && serviceName) { + return { + start, + end, + percentile: String(percentile), + urlQuery: searchTerm || undefined, + uiFilters: JSON.stringify(uiFilters), + }; + } + + return null; + }, [start, end, searchTerm, percentile, uiFilters]); + + return queryParams; +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 33a7a23ab086b..a04d145555b19 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -23,8 +23,8 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { ServiceNameFilter } from './URLFilter/ServiceNameFilter'; export function RumOverview() { - useTrackPageview({ app: 'apm', path: 'rum_overview' }); - useTrackPageview({ app: 'apm', path: 'rum_overview', delay: 15000 }); + useTrackPageview({ app: 'ux', path: 'home' }); + useTrackPageview({ app: 'ux', path: 'home', delay: 15000 }); const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { 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 f92a1d5a5945b..dd7721d9f7129 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -132,6 +132,24 @@ export const I18LABELS = { defaultMessage: 'Impacted page loads', } ), + percentile: i18n.translate('xpack.apm.ux.percentile.label', { + defaultMessage: 'Percentile', + }), + percentile50thMedian: i18n.translate('xpack.apm.ux.percentile.50thMedian', { + defaultMessage: '50th (Median)', + }), + percentile75th: i18n.translate('xpack.apm.ux.percentile.75th', { + defaultMessage: '75th', + }), + percentile90th: i18n.translate('xpack.apm.ux.percentile.90th', { + defaultMessage: '90th', + }), + percentile95th: i18n.translate('xpack.apm.ux.percentile.95th', { + defaultMessage: '95th', + }), + percentile99th: i18n.translate('xpack.apm.ux.percentile.99th', { + defaultMessage: '99th', + }), }; export const VisitorBreakdownLabel = i18n.translate( diff --git a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts index 3f98fc449e8c1..991735a450724 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -5,9 +5,8 @@ */ import { parse, stringify } from 'query-string'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { url } from '../../../../../../../src/plugins/kibana_utils/public'; +import { LocalUIFilterName } from '../../../../common/ui_filter'; export function toQuery(search?: string): APMQueryParamsRaw { return search ? parse(search.slice(1), { sort: false }) : {}; @@ -41,6 +40,7 @@ export type APMQueryParams = { refreshPaused?: string | boolean; refreshInterval?: string | number; searchTerm?: string; + percentile?: 50 | 75 | 90 | 95 | 99; } & { [key in LocalUIFilterName]?: string }; // forces every value of T[K] to be type: string diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx index ba700e68b59bc..65164a43bf10e 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -13,11 +13,10 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { Filter } from './Filter'; import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; import { Projection } from '../../../../common/projections'; +import { LocalUIFilterName } from '../../../../common/ui_filter'; interface Props { projection: Projection; diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx b/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx index 6083a216bbf95..9eb4704a2ca29 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx @@ -19,11 +19,12 @@ import { resolveUrlParams } from './resolveUrlParams'; import { UIFilters } from '../../../typings/ui_filters'; import { localUIFilterNames, - LocalUIFilterName, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../server/lib/ui_filters/local_ui_filters/config'; import { pickKeys } from '../../../common/utils/pick_keys'; import { useDeepObjectIdentity } from '../../hooks/useDeepObjectIdentity'; +import { LocalUIFilterName } from '../../../common/ui_filter'; interface TimeRange { rangeFrom: string; diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts index 8feb4ac1858d1..ccc106cc00c9e 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts @@ -47,6 +47,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { rangeTo, environment, searchTerm, + percentile, } = query; const localUIFilters = pickKeys(query, ...localUIFilterNames); @@ -75,6 +76,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { transactionName, transactionType, searchTerm: toString(searchTerm), + percentile: toNumber(percentile), // ui filters environment, diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts index 574eca3b74f70..68ef73e7b7bc6 100644 --- a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { LocalUIFilterName } from '../../../common/ui_filter'; export type IUrlParams = { detailTab?: string; @@ -28,4 +27,5 @@ export type IUrlParams = { page?: number; pageSize?: number; searchTerm?: string; + percentile?: number; } & Partial>; diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index 0ed26fe089487..da1797fd712b1 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -11,7 +11,6 @@ import { pickKeys } from '../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LocalUIFiltersAPIResponse } from '../../server/lib/ui_filters/local_ui_filters'; import { - LocalUIFilterName, localUIFilters, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../server/lib/ui_filters/local_ui_filters/config'; @@ -20,6 +19,7 @@ import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; import { useCallApi } from './useCallApi'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; +import { LocalUIFilterName } from '../../common/ui_filter'; const getInitialData = ( filterNames: LocalUIFilterName[] 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 index cf4a5538a208d..a210c32ceb44e 100644 --- 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 @@ -20,9 +20,11 @@ import { export async function getClientMetrics({ setup, urlQuery, + percentile = 50, }: { setup: Setup & SetupTimeRange & SetupUIFilters; urlQuery?: string; + percentile?: number; }) { const projection = getRumPageLoadTransactionsProjection({ setup, @@ -41,7 +43,7 @@ export async function getClientMetrics({ backEnd: { percentiles: { field: TRANSACTION_TIME_TO_FIRST_BYTE, - percents: [50], + percents: [percentile], hdr: { number_of_significant_value_digits: 3, }, @@ -50,7 +52,7 @@ export async function getClientMetrics({ domInteractive: { percentiles: { field: TRANSACTION_DOM_INTERACTIVE, - percents: [50], + percents: [percentile], hdr: { number_of_significant_value_digits: 3, }, @@ -65,13 +67,15 @@ export async function getClientMetrics({ const response = await apmEventClient.search(params); const { backEnd, domInteractive, pageViews } = response.aggregations!; + const pkey = percentile.toFixed(1); + // Divide by 1000 to convert ms into seconds return { pageViews, - backEnd: { value: (backEnd.values['50.0'] || 0) / 1000 }, + backEnd: { value: (backEnd.values[pkey] || 0) / 1000 }, frontEnd: { value: - ((domInteractive.values['50.0'] || 0) - (backEnd.values['50.0'] || 0)) / + ((domInteractive.values[pkey] || 0) - (backEnd.values[pkey] || 0)) / 1000, }, }; 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 index 352a3ecdc3f12..40f8a8bc58a54 100644 --- 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 @@ -15,6 +15,7 @@ import { BreakdownItem } from '../../../typings/ui_filters'; export async function getPageViewTrends({ setup, breakdowns, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; breakdowns?: string; @@ -22,6 +23,7 @@ export async function getPageViewTrends({ }) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); let breakdownItem: BreakdownItem | null = null; if (breakdowns) { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts index a7117f275c17b..6aa39c7ef961f 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts @@ -11,13 +11,19 @@ import { SetupUIFilters, } from '../helpers/setup_request'; import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; +import { + TRANSACTION_DURATION, + TRANSACTION_URL, +} from '../../../common/elasticsearch_fieldnames'; export async function getUrlSearch({ setup, urlQuery, + percentile, }: { setup: Setup & SetupTimeRange & SetupUIFilters; urlQuery?: string; + percentile: number; }) { const projection = getRumPageLoadTransactionsProjection({ setup, @@ -30,19 +36,19 @@ export async function getUrlSearch({ aggs: { totalUrls: { cardinality: { - field: 'url.full', + field: TRANSACTION_URL, }, }, urls: { terms: { - field: 'url.full', + field: TRANSACTION_URL, size: 10, }, aggs: { medianPLD: { percentiles: { - field: 'transaction.duration.us', - percents: [50], + field: TRANSACTION_DURATION, + percents: [percentile], }, }, }, @@ -56,12 +62,14 @@ export async function getUrlSearch({ const response = await apmEventClient.search(params); const { urls, totalUrls } = response.aggregations ?? {}; + const pkey = percentile.toFixed(1); + return { total: totalUrls?.value || 0, items: (urls?.buckets ?? []).map((bucket) => ({ url: bucket.key as string, count: bucket.doc_count, - pld: bucket.medianPLD.values['50.0'] ?? 0, + pld: bucket.medianPLD.values[pkey] ?? 0, })), }; } diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index 0828d7ab65190..676b3506397a7 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -23,12 +23,15 @@ import { export async function getWebCoreVitals({ setup, urlQuery, + percentile = 50, }: { setup: Setup & SetupTimeRange & SetupUIFilters; urlQuery?: string; + percentile?: number; }) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { @@ -50,31 +53,31 @@ export async function getWebCoreVitals({ lcp: { percentiles: { field: LCP_FIELD, - percents: [50], + percents: [percentile], }, }, fid: { percentiles: { field: FID_FIELD, - percents: [50], + percents: [percentile], }, }, cls: { percentiles: { field: CLS_FIELD, - percents: [50], + percents: [percentile], }, }, tbt: { percentiles: { field: TBT_FIELD, - percents: [50], + percents: [percentile], }, }, fcp: { percentiles: { field: FCP_FIELD, - percents: [50], + percents: [percentile], }, }, lcpRanks: { @@ -124,12 +127,15 @@ export async function getWebCoreVitals({ { value: 0, key: 0 }, ]; + const pkey = percentile.toFixed(1); + + // Divide by 1000 to convert ms into seconds return { - cls: String(cls?.values['50.0']?.toFixed(2) || 0), - fid: fid?.values['50.0'] ?? 0, - lcp: lcp?.values['50.0'] ?? 0, - tbt: tbt?.values['50.0'] ?? 0, - fcp: fcp?.values['50.0'] ?? 0, + cls: String(cls?.values[pkey]?.toFixed(2) || 0), + fid: fid?.values[pkey] ?? 0, + lcp: lcp?.values[pkey] ?? 0, + tbt: tbt?.values[pkey] ?? 0, + fcp: fcp?.values[pkey] ?? 0, lcpRanks: getRanksPercentages(lcpRanks?.values ?? defaultRanks), fidRanks: getRanksPercentages(fidRanks?.values ?? defaultRanks), 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 9f2483ab8a24e..40744202b804c 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 @@ -3,98 +3,8 @@ * 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'; -import { - CONTAINER_ID, - POD_NAME, - AGENT_NAME, - HOST_NAME, - TRANSACTION_RESULT, - SERVICE_VERSION, - TRANSACTION_URL, - USER_AGENT_NAME, - USER_AGENT_DEVICE, - USER_AGENT_OS, - CLIENT_GEO_COUNTRY_ISO_CODE, - SERVICE_NAME, -} from '../../../../common/elasticsearch_fieldnames'; -const filtersByName = { - host: { - title: i18n.translate('xpack.apm.localFilters.titles.host', { - defaultMessage: 'Host', - }), - fieldName: HOST_NAME, - }, - agentName: { - title: i18n.translate('xpack.apm.localFilters.titles.agentName', { - defaultMessage: 'Agent name', - }), - fieldName: AGENT_NAME, - }, - containerId: { - title: i18n.translate('xpack.apm.localFilters.titles.containerId', { - defaultMessage: 'Container ID', - }), - fieldName: CONTAINER_ID, - }, - podName: { - title: i18n.translate('xpack.apm.localFilters.titles.podName', { - defaultMessage: 'Kubernetes pod', - }), - fieldName: POD_NAME, - }, - transactionResult: { - title: i18n.translate('xpack.apm.localFilters.titles.transactionResult', { - defaultMessage: 'Transaction result', - }), - fieldName: TRANSACTION_RESULT, - }, - serviceVersion: { - title: i18n.translate('xpack.apm.localFilters.titles.serviceVersion', { - defaultMessage: 'Service version', - }), - 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_COUNTRY_ISO_CODE, - }, - os: { - title: i18n.translate('xpack.apm.localFilters.titles.os', { - defaultMessage: 'OS', - }), - fieldName: USER_AGENT_OS, - }, - serviceName: { - title: i18n.translate('xpack.apm.localFilters.titles.serviceName', { - defaultMessage: 'Service name', - }), - fieldName: SERVICE_NAME, - }, -}; - -export type LocalUIFilterName = keyof typeof filtersByName; +import { filtersByName, LocalUIFilterName } from '../../../../common/ui_filter'; export interface LocalUIFilter { name: LocalUIFilterName; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts index cfbd79d37c041..10f6e93c1cfc1 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts @@ -9,7 +9,8 @@ import { mergeProjection } from '../../../projections/util/merge_projection'; import { Projection } from '../../../projections/typings'; import { UIFilters } from '../../../../typings/ui_filters'; import { getUiFiltersES } from '../../helpers/convert_ui_filters/get_ui_filters_es'; -import { localUIFilters, LocalUIFilterName } from './config'; +import { localUIFilters } from './config'; +import { LocalUIFilterName } from '../../../../common/ui_filter'; export const getLocalFilterQuery = ({ uiFilters, diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts index 78c4abd75f2ee..20184f1d53ac5 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts @@ -9,7 +9,8 @@ import { Projection } from '../../../projections/typings'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { getLocalFilterQuery } from './get_local_filter_query'; import { Setup } from '../../helpers/setup_request'; -import { localUIFilters, LocalUIFilterName } from './config'; +import { localUIFilters } from './config'; +import { LocalUIFilterName } from '../../../../common/ui_filter'; export type LocalUIFiltersAPIResponse = PromiseReturnType< typeof getLocalUIFilters diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index c0351991e4c0d..d86069a3ec27a 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -24,33 +24,36 @@ export const percentileRangeRt = t.partial({ maxPercentile: t.string, }); -const urlQueryRt = t.partial({ urlQuery: t.string }); +const uxQueryRt = t.intersection([ + uiFiltersRt, + rangeRt, + t.partial({ urlQuery: t.string, percentile: t.string }), +]); export const rumClientMetricsRoute = createRoute(() => ({ path: '/api/apm/rum/client-metrics', params: { - query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), + query: uxQueryRt, }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { - query: { urlQuery }, + query: { urlQuery, percentile }, } = context.params; - return getClientMetrics({ setup, urlQuery }); + return getClientMetrics({ + setup, + urlQuery, + percentile: percentile ? Number(percentile) : undefined, + }); }, })); export const rumPageLoadDistributionRoute = createRoute(() => ({ path: '/api/apm/rum-client/page-load-distribution', params: { - query: t.intersection([ - uiFiltersRt, - rangeRt, - percentileRangeRt, - urlQueryRt, - ]), + query: t.intersection([uxQueryRt, percentileRangeRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -72,10 +75,8 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ path: '/api/apm/rum-client/page-load-distribution/breakdown', params: { query: t.intersection([ - uiFiltersRt, - rangeRt, + uxQueryRt, percentileRangeRt, - urlQueryRt, t.type({ breakdown: t.string }), ]), }, @@ -99,12 +100,7 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ export const rumPageViewsTrendRoute = createRoute(() => ({ path: '/api/apm/rum-client/page-view-trends', params: { - query: t.intersection([ - uiFiltersRt, - rangeRt, - urlQueryRt, - t.partial({ breakdowns: t.string }), - ]), + query: t.intersection([uxQueryRt, t.partial({ breakdowns: t.string })]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -113,7 +109,11 @@ export const rumPageViewsTrendRoute = createRoute(() => ({ query: { breakdowns, urlQuery }, } = context.params; - return getPageViewTrends({ setup, breakdowns, urlQuery }); + return getPageViewTrends({ + setup, + breakdowns, + urlQuery, + }); }, })); @@ -132,7 +132,7 @@ export const rumServicesRoute = createRoute(() => ({ export const rumVisitorsBreakdownRoute = createRoute(() => ({ path: '/api/apm/rum-client/visitor-breakdown', params: { - query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), + query: uxQueryRt, }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -141,30 +141,37 @@ export const rumVisitorsBreakdownRoute = createRoute(() => ({ query: { urlQuery }, } = context.params; - return getVisitorBreakdown({ setup, urlQuery }); + return getVisitorBreakdown({ + setup, + urlQuery, + }); }, })); export const rumWebCoreVitals = createRoute(() => ({ path: '/api/apm/rum-client/web-core-vitals', params: { - query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), + query: uxQueryRt, }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { - query: { urlQuery }, + query: { urlQuery, percentile }, } = context.params; - return getWebCoreVitals({ setup, urlQuery }); + return getWebCoreVitals({ + setup, + urlQuery, + percentile: percentile ? Number(percentile) : undefined, + }); }, })); export const rumLongTaskMetrics = createRoute(() => ({ path: '/api/apm/rum-client/long-task-metrics', params: { - query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), + query: uxQueryRt, }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -173,23 +180,26 @@ export const rumLongTaskMetrics = createRoute(() => ({ query: { urlQuery }, } = context.params; - return getLongTaskMetrics({ setup, urlQuery }); + return getLongTaskMetrics({ + setup, + urlQuery, + }); }, })); export const rumUrlSearch = createRoute(() => ({ path: '/api/apm/rum-client/url-search', params: { - query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), + query: uxQueryRt, }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { - query: { urlQuery }, + query: { urlQuery, percentile }, } = context.params; - return getUrlSearch({ setup, urlQuery }); + return getUrlSearch({ setup, urlQuery, percentile: Number(percentile) }); }, })); diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index 8bdd83a8ddda6..936d460102dce 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -14,10 +14,7 @@ import { } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/ui_filters/get_environments'; import { Projection } from '../projections/typings'; -import { - localUIFilterNames, - LocalUIFilterName, -} from '../lib/ui_filters/local_ui_filters/config'; +import { localUIFilterNames } from '../lib/ui_filters/local_ui_filters/config'; import { getUiFiltersES } from '../lib/helpers/convert_ui_filters/get_ui_filters_es'; import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters'; import { getServicesProjection } from '../projections/services'; @@ -32,6 +29,7 @@ import { getServiceNodesProjection } from '../projections/service_nodes'; import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { APMRequestHandlerContext } from './typings'; +import { LocalUIFilterName } from '../../common/ui_filter'; export const uiFiltersEnvironmentsRoute = createRoute(() => ({ path: '/api/apm/ui_filters/environments', diff --git a/x-pack/plugins/apm/typings/ui_filters.ts b/x-pack/plugins/apm/typings/ui_filters.ts index efba6919778bb..d2b051b63cde2 100644 --- a/x-pack/plugins/apm/typings/ui_filters.ts +++ b/x-pack/plugins/apm/typings/ui_filters.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFilterName } from '../server/lib/ui_filters/local_ui_filters/config'; +import { LocalUIFilterName } from '../common/ui_filter'; export type UIFilters = { kuery?: string; diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index a87ae3fb26159..41330e878035c 100644 --- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -34,7 +34,7 @@ export type HasData = () => Promise; export type ObservabilityFetchDataPlugins = Exclude< ObservabilityApp, - 'observability' | 'stack_monitoring' + 'observability' | 'stack_monitoring' | 'ux' >; export interface DataHandler< diff --git a/x-pack/plugins/observability/typings/common.ts b/x-pack/plugins/observability/typings/common.ts index 845652031a578..c86eb924a051e 100644 --- a/x-pack/plugins/observability/typings/common.ts +++ b/x-pack/plugins/observability/typings/common.ts @@ -10,7 +10,8 @@ export type ObservabilityApp = | 'apm' | 'uptime' | 'observability' - | 'stack_monitoring'; + | 'stack_monitoring' + | 'ux'; export type PromiseReturnType = Func extends (...args: any[]) => Promise ? Value diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts b/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts index 76dc758895e32..c887fa3e77648 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts @@ -16,7 +16,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) describe('when there is no data', () => { it('returns empty list', async () => { const response = await supertest.get( - '/api/apm/rum-client/url-search?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' + '/api/apm/rum-client/url-search?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&percentile=50' ); expect(response.status).to.be(200); @@ -41,7 +41,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) it('returns top urls when no query', async () => { const response = await supertest.get( - '/api/apm/rum-client/url-search?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' + '/api/apm/rum-client/url-search?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&percentile=50' ); expect(response.status).to.be(200); @@ -67,7 +67,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) it('returns specific results against query', async () => { const response = await supertest.get( - '/api/apm/rum-client/url-search?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&urlQuery=csm' + '/api/apm/rum-client/url-search?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&urlQuery=csm&percentile=50' ); expect(response.status).to.be(200); diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts index 5825c8fc49a6b..efbdb75c47cc1 100644 --- a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts +++ b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts @@ -16,7 +16,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) describe('when there is no data', () => { it('returns empty list', async () => { const response = await supertest.get( - '/api/apm/rum-client/web-core-vitals?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' + '/api/apm/rum-client/web-core-vitals?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&percentile=50' ); expect(response.status).to.be(200); @@ -45,7 +45,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) it('returns web core vitals values', async () => { const response = await supertest.get( - '/api/apm/rum-client/web-core-vitals?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' + '/api/apm/rum-client/web-core-vitals?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&percentile=50' ); expect(response.status).to.be(200);