diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0bdddddab8de5c..eeeb1cf66338da 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -65,11 +65,12 @@ # Client Side Monitoring (lives in APM directories but owned by Uptime) /x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm @elastic/uptime +/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature @elastic/uptime /x-pack/plugins/apm/public/application/csmApp.tsx @elastic/uptime /x-pack/plugins/apm/public/components/app/RumDashboard @elastic/uptime /x-pack/plugins/apm/server/lib/rum_client @elastic/uptime /x-pack/plugins/apm/server/routes/rum_client.ts @elastic/uptime -/x-pack/plugins/apm/server/projections/rum_overview.ts @elastic/uptime +/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @elastic/uptime # Beats /x-pack/legacy/plugins/beats_management/ @elastic/beats 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 ac4188a598458b..7b894b6ca7aac9 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature @@ -27,3 +27,11 @@ Feature: CSM Dashboard Given a user clicks the page load breakdown filter When the user selected the breakdown Then breakdown series should appear in chart + + Scenario: Search by url filter focus + When a user clicks inside url search field + Then it displays top pages in the suggestion popover + + Scenario: Search by url filter + When a user enters a query in url search field + Then it should filter results based on query 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 new file mode 100644 index 00000000000000..3b5dd70065055d --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts @@ -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 { When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { DEFAULT_TIMEOUT } from './csm_dashboard'; + +When(`a user clicks inside url search field`, () => { + // wait for all loading to finish + cy.get('kbnLoadingIndicator').should('not.be.visible'); + cy.get('.euiStat__title-isLoading').should('not.be.visible'); + cy.get('span[data-cy=csmUrlFilter]', DEFAULT_TIMEOUT).within(() => { + cy.get('input.euiFieldSearch').click(); + }); +}); + +Then(`it displays top pages in the suggestion popover`, () => { + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + cy.get('div.euiPopover__panel-isOpen', DEFAULT_TIMEOUT).within(() => { + const listOfUrls = cy.get('li.euiSelectableListItem'); + listOfUrls.should('have.length', 5); + + const actualUrlsText = [ + 'http://opbeans-node:3000/dashboardPage views: 17Page load duration: 109 ms ', + 'http://opbeans-node:3000/ordersPage views: 14Page load duration: 72 ms', + ]; + + cy.get('li.euiSelectableListItem') + .eq(0) + .should('have.text', actualUrlsText[0]); + cy.get('li.euiSelectableListItem') + .eq(1) + .should('have.text', actualUrlsText[1]); + }); +}); + +When(`a user enters a query in url search field`, () => { + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + cy.get('[data-cy=csmUrlFilter]').within(() => { + cy.get('input.euiSelectableSearch').type('cus'); + }); + + cy.get('kbnLoadingIndicator').should('not.be.visible'); +}); + +Then(`it should filter results based on query`, () => { + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + cy.get('div.euiPopover__panel-isOpen', DEFAULT_TIMEOUT).within(() => { + const listOfUrls = cy.get('li.euiSelectableListItem'); + listOfUrls.should('have.length', 1); + + const actualUrlsText = [ + 'http://opbeans-node:3000/customersPage views: 10Page load duration: 76 ms ', + ]; + + cy.get('li.euiSelectableListItem') + .eq(0) + .should('have.text', actualUrlsText[0]); + }); +}); 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 1edfd724dadd70..a77d27c4bc883a 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 @@ -22,7 +22,7 @@ const ClFlexGroup = styled(EuiFlexGroup)` export function ClientMetrics() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { data, status } = useFetcher( (callApmApi) => { @@ -31,13 +31,18 @@ export function ClientMetrics() { return callApmApi({ pathname: '/api/apm/rum/client-metrics', params: { - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, + }, }, }); } return Promise.resolve(null); }, - [start, end, uiFilters] + [start, end, uiFilters, searchTerm] ); const STAT_STYLE = { width: '240px' }; 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 f97db3b42eecb3..c8e45d2b2f6729 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 @@ -22,7 +22,7 @@ export interface PercentileRange { export function PageLoadDistribution() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const [percentileRange, setPercentileRange] = useState({ min: null, @@ -41,6 +41,7 @@ export function PageLoadDistribution() { start, end, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, ...(percentileRange.min && percentileRange.max ? { minPercentile: String(percentileRange.min), @@ -53,7 +54,14 @@ export function PageLoadDistribution() { } return Promise.resolve(null); }, - [end, start, uiFilters, percentileRange.min, percentileRange.max] + [ + end, + start, + uiFilters, + percentileRange.min, + percentileRange.max, + searchTerm, + ] ); const onPercentileChange = (min: number, max: number) => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index 814cf977c95699..d6a544333531f4 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -17,7 +17,7 @@ interface Props { export const useBreakdowns = ({ percentileRange, field, value }: Props) => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { min: minP, max: maxP } = percentileRange ?? {}; @@ -32,6 +32,7 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { end, breakdown: value, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, ...(minP && maxP ? { minPercentile: String(minP), @@ -43,6 +44,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { }); } }, - [end, start, uiFilters, field, value, minP, maxP] + [end, start, uiFilters, field, value, minP, maxP, searchTerm] ); }; 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 2991f9a15f0859..f2da0955412e76 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 @@ -16,7 +16,7 @@ import { BreakdownItem } from '../../../../../typings/ui_filters'; export function PageViewsTrend() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const [breakdown, setBreakdown] = useState(null); @@ -30,6 +30,7 @@ export function PageViewsTrend() { start, end, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, ...(breakdown ? { breakdowns: JSON.stringify(breakdown), @@ -41,7 +42,7 @@ export function PageViewsTrend() { } return Promise.resolve(undefined); }, - [end, start, uiFilters, breakdown] + [end, start, uiFilters, breakdown, searchTerm] ); return ( diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx index cbf9ba009dce23..f10c9e888a1930 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx @@ -13,8 +13,8 @@ import { import { i18n } from '@kbn/i18n'; import React, { useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { fromQuery, toQuery } from '../../Links/url_helpers'; +import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; interface Props { serviceNames: string[]; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/RenderOption.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/RenderOption.tsx new file mode 100644 index 00000000000000..1a6f4e25fc315f --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/RenderOption.tsx @@ -0,0 +1,68 @@ +/* + * 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, { ReactNode } from 'react'; +import classNames from 'classnames'; +import { EuiHighlight, EuiSelectableOption } from '@elastic/eui'; +import styled from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +const StyledSpan = styled.span` + color: ${euiLightVars.euiColorSecondaryText}; + font-weight: 500; + :not(:last-of-type)::after { + content: '•'; + margin: 0 4px; + } +`; + +const StyledListSpan = styled.span` + display: block; + margin-top: 4px; + font-size: 12px; +`; +export type UrlOption = { + meta?: string[]; +} & EuiSelectableOption; + +export const formatOptions = (options: EuiSelectableOption[]) => { + return options.map((item: EuiSelectableOption) => ({ + title: item.label, + ...item, + className: classNames( + 'euiSelectableTemplateSitewide__listItem', + item.className + ), + })); +}; + +export function selectableRenderOptions( + option: UrlOption, + searchValue: string +) { + return ( + <> + + {option.label} + + {renderOptionMeta(option.meta)} + + ); +} + +function renderOptionMeta(meta?: string[]): ReactNode { + if (!meta || meta.length < 1) return; + return ( + + {meta.map((item: string) => ( + {item} + ))} + + ); +} 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 new file mode 100644 index 00000000000000..298ec15b8480b0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx @@ -0,0 +1,164 @@ +/* + * 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, { FormEvent, useRef, useState } from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPopover, + EuiPopoverTitle, + EuiSelectable, + EuiSelectableMessage, +} from '@elastic/eui'; +import { + formatOptions, + selectableRenderOptions, + UrlOption, +} from './RenderOption'; +import { I18LABELS } from '../../translations'; + +interface Props { + data: { + items: UrlOption[]; + total?: number; + }; + loading: boolean; + onInputChange: (e: FormEvent) => void; + onTermChange: () => void; + onChange: (updatedOptions: UrlOption[]) => void; + searchValue: string; + onClose: () => void; +} + +export function SelectableUrlList({ + data, + loading, + onInputChange, + onTermChange, + onChange, + searchValue, + onClose, +}: Props) { + const [popoverIsOpen, setPopoverIsOpen] = useState(false); + const [popoverRef, setPopoverRef] = useState(null); + const [searchRef, setSearchRef] = useState(null); + + const titleRef = useRef(null); + + const searchOnFocus = (e: React.FocusEvent) => { + setPopoverIsOpen(true); + }; + + const onSearchInput = (e: React.FormEvent) => { + onInputChange(e); + setPopoverIsOpen(true); + }; + + const searchOnBlur = (e: React.FocusEvent) => { + if ( + !popoverRef?.contains(e.relatedTarget as HTMLElement) && + !popoverRef?.contains(titleRef.current as HTMLDivElement) + ) { + setPopoverIsOpen(false); + } + }; + + const formattedOptions = formatOptions(data.items ?? []); + + const closePopover = () => { + setPopoverIsOpen(false); + onClose(); + if (searchRef) { + searchRef.blur(); + } + }; + + const loadingMessage = ( + + +
+

{I18LABELS.loadingResults}

+
+ ); + + const emptyMessage = ( + +

{I18LABELS.noResults}

+
+ ); + + const titleText = searchValue + ? I18LABELS.getSearchResultsLabel(data?.total ?? 0) + : I18LABELS.topPages; + + function PopOverTitle() { + return ( + + + + {loading ? : titleText} + + + { + onTermChange(); + setPopoverIsOpen(false); + }} + > + {I18LABELS.matchThisQuery} + + + + + ); + } + + return ( + + {(list, search) => ( + +
+ + {list} +
+
+ )} +
+ ); +} 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 new file mode 100644 index 00000000000000..b88cf29740dcd5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx @@ -0,0 +1,132 @@ +/* + * 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 { EuiTitle } from '@elastic/eui'; +import useDebounce from 'react-use/lib/useDebounce'; +import React, { useEffect, useState, FormEvent, useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../../hooks/useFetcher'; +import { I18LABELS } from '../../translations'; +import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; +import { formatToSec } from '../../UXMetrics/KeyUXMetrics'; +import { SelectableUrlList } from './SelectableUrlList'; +import { UrlOption } from './RenderOption'; + +interface Props { + onChange: (value: string[]) => void; +} + +export function URLSearch({ onChange: onFilterChange }: Props) { + const history = useHistory(); + + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + const [searchValue, setSearchValue] = useState(''); + + const [debouncedValue, setDebouncedValue] = useState(''); + + useDebounce( + () => { + setSearchValue(debouncedValue); + }, + 250, + [debouncedValue] + ); + + const updateSearchTerm = useCallback( + (searchTermN: string) => { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + searchTerm: searchTermN, + }), + }; + history.push(newLocation); + }, + [history] + ); + + const [checkedUrls, setCheckedUrls] = useState([]); + + const { data, status } = useFetcher( + (callApmApi) => { + if (start && end && serviceName) { + const { transactionUrl, ...restFilters } = uiFilters; + + return callApmApi({ + pathname: '/api/apm/rum-client/url-search', + params: { + query: { + start, + end, + uiFilters: JSON.stringify(restFilters), + urlQuery: searchValue, + }, + }, + }); + } + return Promise.resolve(null); + }, + [start, end, serviceName, uiFilters, searchValue] + ); + + useEffect(() => { + setCheckedUrls(uiFilters.transactionUrl || []); + }, [uiFilters]); + + const onChange = (updatedOptions: UrlOption[]) => { + const clickedItems = updatedOptions.filter( + (option) => option.checked === 'on' + ); + + setCheckedUrls(clickedItems.map((item) => item.url)); + }; + + const items: UrlOption[] = (data?.items ?? []).map((item) => ({ + label: item.url, + key: item.url, + meta: [ + I18LABELS.pageViews + ': ' + item.count, + I18LABELS.pageLoadDuration + ': ' + formatToSec(item.pld), + ], + url: item.url, + checked: checkedUrls?.includes(item.url) ? 'on' : undefined, + })); + + const onInputChange = (e: FormEvent) => { + setDebouncedValue(e.currentTarget.value); + }; + + const isLoading = status !== 'success'; + + const onTermChange = () => { + updateSearchTerm(searchValue); + }; + + const onClose = () => { + onFilterChange(checkedUrls); + }; + + return ( + <> + +

{I18LABELS.url}

+
+ + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/UrlList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/UrlList.tsx new file mode 100644 index 00000000000000..437c005db37b00 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/UrlList.tsx @@ -0,0 +1,74 @@ +/* + * 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 { EuiFlexGrid, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { px, truncate, unit } from '../../../../style/variables'; + +const BadgeText = styled.div` + display: inline-block; + ${truncate(px(unit * 12))}; + vertical-align: middle; +`; + +interface Props { + value: string[]; + onRemove: (val: string) => void; +} + +const formatUrlValue = (val: string) => { + const maxUrlToDisplay = 30; + const urlLength = val.length; + if (urlLength < maxUrlToDisplay) { + return val; + } + const urlObj = new URL(val); + if (urlObj.pathname === '/') { + return val; + } + const domainVal = urlObj.hostname; + const extraLength = urlLength - maxUrlToDisplay; + const extraDomain = domainVal.substring(0, extraLength); + + if (urlObj.pathname.length + 7 > maxUrlToDisplay) { + return val.replace(domainVal, '..'); + } + + return val.replace(extraDomain, '..'); +}; + +const removeFilterLabel = i18n.translate( + 'xpack.apm.uifilter.badge.removeFilter', + { defaultMessage: 'Remove filter' } +); + +export function UrlList({ onRemove, value }: Props) { + return ( + + {value.map((val) => ( + + { + onRemove(val); + }} + onClickAriaLabel={removeFilterLabel} + iconOnClick={() => { + onRemove(val); + }} + iconOnClickAriaLabel={removeFilterLabel} + iconType="cross" + iconSide="right" + > + {formatUrlValue(val)} + + + ))} + + ); +} 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 new file mode 100644 index 00000000000000..9d3c8d012871f5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx @@ -0,0 +1,102 @@ +/* + * 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 React, { useCallback, useMemo } 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 { 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'; + +const removeSearchTermLabel = i18n.translate( + 'xpack.apm.uiFilter.url.removeSearchTerm', + { defaultMessage: 'Clear url query' } +); + +export function URLFilter() { + const history = useHistory(); + + const { + urlParams: { searchTerm }, + } = useUrlParams(); + + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps = { + filterNames: ['transactionUrl'], + projection: Projection.rumOverview, + }; + + return config; + }, []); + + const { filters, setFilterValue } = useLocalUIFilters({ + ...localUIFiltersConfig, + }); + + const updateSearchTerm = useCallback( + (searchTermN?: string) => { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + searchTerm: searchTermN, + }), + }; + history.push(newLocation); + }, + [history] + ); + + const { name, value: filterValue } = filters[0]; + + return ( + + + { + setFilterValue('transactionUrl', value); + }} + /> + + {searchTerm && ( + <> + { + updateSearchTerm(); + }} + onClickAriaLabel={removeSearchTermLabel} + iconOnClick={() => { + updateSearchTerm(); + }} + iconOnClickAriaLabel={removeSearchTermLabel} + iconType="cross" + iconSide="right" + > + *{searchTerm}* + + + + )} + {filterValue.length > 0 && ( + { + setFilterValue( + name, + filterValue.filter((v) => val !== v) + ); + }} + value={filterValue} + /> + )} + + + ); +} 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 5c9a636adec8ff..1d8360872afba2 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 @@ -38,7 +38,7 @@ interface Props { export function KeyUXMetrics({ data, loading }: Props) { const { urlParams, uiFilters } = useUrlParams(); - const { start, end, serviceName } = urlParams; + const { start, end, serviceName, searchTerm } = urlParams; const { data: longTaskData, status } = useFetcher( (callApmApi) => { @@ -46,13 +46,18 @@ export function KeyUXMetrics({ data, loading }: Props) { return callApmApi({ pathname: '/api/apm/rum-client/long-task-metrics', params: { - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, + }, }, }); } return Promise.resolve(null); }, - [start, end, serviceName, uiFilters] + [start, end, serviceName, uiFilters, searchTerm] ); // 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 94c3acfaa97274..3c7b4e39401de5 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 @@ -33,7 +33,7 @@ export interface UXMetrics { export function UXMetrics() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { data, status } = useFetcher( (callApmApi) => { @@ -42,13 +42,18 @@ export function UXMetrics() { return callApmApi({ pathname: '/api/apm/rum-client/web-core-vitals', params: { - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, + }, }, }); } return Promise.resolve(null); }, - [start, end, uiFilters] + [start, end, uiFilters, searchTerm] ); return ( 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 245f58370d3d7f..2db6ef8fa6c067 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 @@ -14,7 +14,7 @@ import { useUrlParams } from '../../../../hooks/useUrlParams'; export function VisitorBreakdown() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { data, status } = useFetcher( (callApmApi) => { @@ -26,13 +26,14 @@ export function VisitorBreakdown() { start, end, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, }, }, }); } return Promise.resolve(null); }, - [end, start, uiFilters] + [end, start, uiFilters, searchTerm] ); return ( 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 fa0551252b6a11..588831d55771dc 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -12,14 +12,15 @@ import { EuiSpacer, } from '@elastic/eui'; import { useTrackPageview } from '../../../../../observability/public'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { Projection } from '../../../../common/projections'; import { RumDashboard } from './RumDashboard'; -import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useFetcher } from '../../../hooks/useFetcher'; import { RUM_AGENTS } from '../../../../common/agent_name'; import { EnvironmentFilter } from '../../shared/EnvironmentFilter'; +import { URLFilter } from './URLFilter'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { ServiceNameFilter } from './URLFilter/ServiceNameFilter'; export function RumOverview() { useTrackPageview({ app: 'apm', path: 'rum_overview' }); @@ -27,7 +28,7 @@ export function RumOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { - filterNames: ['transactionUrl', 'location', 'device', 'os', 'browser'], + filterNames: ['location', 'device', 'os', 'browser'], projection: Projection.rumOverview, }; @@ -63,6 +64,7 @@ export function RumOverview() { + <> + {' '} 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 1fafb7d1ed4d01..714788ef468c65 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -79,6 +79,32 @@ export const I18LABELS = { defaultMessage: 'Page load duration by region', } ), + searchByUrl: i18n.translate('xpack.apm.rum.filters.searchByUrl', { + defaultMessage: 'Search by url', + }), + getSearchResultsLabel: (total: number) => + i18n.translate('xpack.apm.rum.filters.searchResults', { + defaultMessage: '{total} Search results', + values: { total }, + }), + topPages: i18n.translate('xpack.apm.rum.filters.topPages', { + defaultMessage: 'Top pages', + }), + select: i18n.translate('xpack.apm.rum.filters.select', { + defaultMessage: 'Select', + }), + url: i18n.translate('xpack.apm.rum.filters.url', { + defaultMessage: 'Url', + }), + matchThisQuery: i18n.translate('xpack.apm.rum.filters.url.matchThisQuery', { + defaultMessage: 'Match this query', + }), + loadingResults: i18n.translate('xpack.apm.rum.filters.url.loadingResults', { + defaultMessage: 'Loading results', + }), + noResults: i18n.translate('xpack.apm.rum.filters.url.noResults', { + defaultMessage: 'No results available', + }), }; export const VisitorBreakdownLabel = i18n.translate( 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 b3f9646f640295..cf4a5538a208da 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 @@ -19,11 +19,14 @@ import { export async function getClientMetrics({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts index 1faee52034580b..812cf9865bda83 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts @@ -14,12 +14,17 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { SPAN_DURATION } from '../../../common/elasticsearch_fieldnames'; +import { + SPAN_DURATION, + TRANSACTION_ID, +} from '../../../common/elasticsearch_fieldnames'; export async function getLongTaskMetrics({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumLongTasksProjection({ setup, @@ -28,9 +33,6 @@ export async function getLongTaskMetrics({ const params = mergeProjection(projection, { body: { size: 0, - query: { - bool: projection.body.query.bool, - }, aggs: { transIds: { terms: { @@ -59,10 +61,13 @@ export async function getLongTaskMetrics({ const response = await apmEventClient.search(params); const { transIds } = response.aggregations ?? {}; - const validTransactions: string[] = await filterPageLoadTransactions( + const validTransactions: string[] = await filterPageLoadTransactions({ setup, - (transIds?.buckets ?? []).map((bucket) => bucket.key as string) - ); + urlQuery, + transactionIds: (transIds?.buckets ?? []).map( + (bucket) => bucket.key as string + ), + }); let noOfLongTasks = 0; let sumOfLongTasks = 0; let longestLongTask = 0; @@ -83,12 +88,18 @@ export async function getLongTaskMetrics({ }; } -async function filterPageLoadTransactions( - setup: Setup & SetupTimeRange & SetupUIFilters, - transactionIds: string[] -) { +async function filterPageLoadTransactions({ + setup, + urlQuery, + transactionIds, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; + transactionIds: string[]; +}) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { @@ -99,14 +110,14 @@ async function filterPageLoadTransactions( must: [ { terms: { - 'transaction.id': transactionIds, + [TRANSACTION_ID]: transactionIds, }, }, ], filter: [...projection.body.query.bool.filter], }, }, - _source: ['transaction.id'], + _source: [TRANSACTION_ID], }, }); 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 index 3d8ab7a72654d0..25de9f06fefc41 100644 --- 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 @@ -40,13 +40,16 @@ export async function getPageLoadDistribution({ setup, minPercentile, maxPercentile, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; minPercentile?: string; maxPercentile?: string; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { 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 543aa911b0b1fd..ef4f8b16e0e7b5 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 @@ -18,6 +18,7 @@ export async function getPageViewTrends({ }: { setup: Setup & SetupTimeRange & SetupUIFilters; breakdowns?: string; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index 1945140e357772..d59817cc682a94 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -44,11 +44,13 @@ export const getPageLoadDistBreakdown = async ({ minDuration, maxDuration, breakdown, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; minDuration: number; maxDuration: number; breakdown: string; + urlQuery?: string; }) => { // convert secs to micros const stepValues = getPLDChartSteps({ @@ -58,6 +60,7 @@ export const getPageLoadDistBreakdown = async ({ const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { 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 new file mode 100644 index 00000000000000..a7117f275c17b4 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mergeProjection } from '../../projections/util/merge_projection'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; +import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; + +export async function getUrlSearch({ + setup, + urlQuery, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; +}) { + const projection = getRumPageLoadTransactionsProjection({ + setup, + urlQuery, + }); + + const params = mergeProjection(projection, { + body: { + size: 0, + aggs: { + totalUrls: { + cardinality: { + field: 'url.full', + }, + }, + urls: { + terms: { + field: 'url.full', + size: 10, + }, + aggs: { + medianPLD: { + percentiles: { + field: 'transaction.duration.us', + percents: [50], + }, + }, + }, + }, + }, + }, + }); + + const { apmEventClient } = setup; + + const response = await apmEventClient.search(params); + const { urls, totalUrls } = response.aggregations ?? {}; + + 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, + })), + }; +} diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index 3493307929f42a..1b4388afd7c5d1 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -19,11 +19,14 @@ import { export async function getVisitorBreakdown({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { 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 2ff0173b9ac12a..fa34c2e25fecd9 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 @@ -22,8 +22,10 @@ import { export async function getWebCoreVitals({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, diff --git a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts index 27cd9b53f83492..3c3eaaca7efdb9 100644 --- a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts +++ b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @@ -19,8 +19,10 @@ import { TRANSACTION_PAGE_LOAD } from '../../common/transaction_types'; export function getRumPageLoadTransactionsProjection({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const { start, end, uiFiltersES } = setup; @@ -35,6 +37,17 @@ export function getRumPageLoadTransactionsProjection({ field: 'transaction.marks.navigationTiming.fetchStart', }, }, + ...(urlQuery + ? [ + { + wildcard: { + 'url.full': { + value: `*${urlQuery}*`, + }, + }, + }, + ] + : []), ...uiFiltersES, ], }; 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 7d9a9ccc167e0a..f975ab177f1475 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -77,6 +77,7 @@ import { rumServicesRoute, rumVisitorsBreakdownRoute, rumWebCoreVitals, + rumUrlSearch, rumLongTaskMetrics, } from './rum_client'; import { @@ -173,6 +174,7 @@ const createApmApi = () => { .add(rumServicesRoute) .add(rumVisitorsBreakdownRoute) .add(rumWebCoreVitals) + .add(rumUrlSearch) .add(rumLongTaskMetrics) // Observability dashboard diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index 179279b6f2d8a5..e3a846f9fb5c77 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -16,37 +16,54 @@ import { getRumServices } from '../lib/rum_client/get_rum_services'; import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown'; import { getWebCoreVitals } from '../lib/rum_client/get_web_core_vitals'; import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics'; +import { getUrlSearch } from '../lib/rum_client/get_url_search'; export const percentileRangeRt = t.partial({ minPercentile: t.string, maxPercentile: t.string, }); +const urlQueryRt = t.partial({ urlQuery: t.string }); + export const rumClientMetricsRoute = createRoute(() => ({ path: '/api/apm/rum/client-metrics', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getClientMetrics({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getClientMetrics({ setup, urlQuery }); }, })); export const rumPageLoadDistributionRoute = createRoute(() => ({ path: '/api/apm/rum-client/page-load-distribution', params: { - query: t.intersection([uiFiltersRt, rangeRt, percentileRangeRt]), + query: t.intersection([ + uiFiltersRt, + rangeRt, + percentileRangeRt, + urlQueryRt, + ]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { - query: { minPercentile, maxPercentile }, + query: { minPercentile, maxPercentile, urlQuery }, } = context.params; - return getPageLoadDistribution({ setup, minPercentile, maxPercentile }); + return getPageLoadDistribution({ + setup, + minPercentile, + maxPercentile, + urlQuery, + }); }, })); @@ -57,6 +74,7 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ uiFiltersRt, rangeRt, percentileRangeRt, + urlQueryRt, t.type({ breakdown: t.string }), ]), }, @@ -64,7 +82,7 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ const setup = await setupRequest(context, request); const { - query: { minPercentile, maxPercentile, breakdown }, + query: { minPercentile, maxPercentile, breakdown, urlQuery }, } = context.params; return getPageLoadDistBreakdown({ @@ -72,6 +90,7 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ minDuration: Number(minPercentile), maxDuration: Number(maxPercentile), breakdown, + urlQuery, }); }, })); @@ -82,6 +101,7 @@ export const rumPageViewsTrendRoute = createRoute(() => ({ query: t.intersection([ uiFiltersRt, rangeRt, + urlQueryRt, t.partial({ breakdowns: t.string }), ]), }, @@ -89,10 +109,10 @@ export const rumPageViewsTrendRoute = createRoute(() => ({ const setup = await setupRequest(context, request); const { - query: { breakdowns }, + query: { breakdowns, urlQuery }, } = context.params; - return getPageViewTrends({ setup, breakdowns }); + return getPageViewTrends({ setup, breakdowns, urlQuery }); }, })); @@ -111,35 +131,63 @@ export const rumServicesRoute = createRoute(() => ({ export const rumVisitorsBreakdownRoute = createRoute(() => ({ path: '/api/apm/rum-client/visitor-breakdown', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getVisitorBreakdown({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getVisitorBreakdown({ setup, urlQuery }); }, })); export const rumWebCoreVitals = createRoute(() => ({ path: '/api/apm/rum-client/web-core-vitals', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getWebCoreVitals({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getWebCoreVitals({ setup, urlQuery }); }, })); export const rumLongTaskMetrics = createRoute(() => ({ path: '/api/apm/rum-client/long-task-metrics', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getLongTaskMetrics({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getLongTaskMetrics({ setup, urlQuery }); + }, +})); + +export const rumUrlSearch = createRoute(() => ({ + path: '/api/apm/rum-client/url-search', + params: { + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + query: { urlQuery }, + } = context.params; + + return getUrlSearch({ setup, urlQuery }); }, })); 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 new file mode 100644 index 00000000000000..76dc758895e32f --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('CSM url search api', () => { + 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' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [], + "total": 0, + } + `); + }); + }); + + describe('when there is data', () => { + before(async () => { + await esArchiver.load('8.0.0'); + await esArchiver.load('rum_8.0.0'); + }); + after(async () => { + await esArchiver.unload('8.0.0'); + await esArchiver.unload('rum_8.0.0'); + }); + + it('returns 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' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [ + Object { + "count": 5, + "pld": 4924000, + "url": "http://localhost:5601/nfw/app/csm?rangeFrom=now-15m&rangeTo=now&serviceName=kibana-frontend-8_0_0", + }, + Object { + "count": 1, + "pld": 2760000, + "url": "http://localhost:5601/nfw/app/home", + }, + ], + "total": 2, + } + `); + }); + + 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' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [ + Object { + "count": 5, + "pld": 4924000, + "url": "http://localhost:5601/nfw/app/csm?rangeFrom=now-15m&rangeTo=now&serviceName=kibana-frontend-8_0_0", + }, + ], + "total": 1, + } + `); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index a026f91a02cd77..69e54ea33c5593 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -35,6 +35,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr loadTestFile(require.resolve('./csm/csm_services.ts')); loadTestFile(require.resolve('./csm/web_core_vitals.ts')); loadTestFile(require.resolve('./csm/long_task_metrics.ts')); + loadTestFile(require.resolve('./csm/url_search.ts')); loadTestFile(require.resolve('./csm/page_views.ts')); }); });