mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[UX] Add percentile selector (#78562)
This commit is contained in:
parent
80c004cb01
commit
a31dd64778
41 changed files with 504 additions and 242 deletions
98
x-pack/plugins/apm/common/ui_filter.ts
Normal file
98
x-pack/plugins/apm/common/ui_filter.ts
Normal file
|
@ -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;
|
|
@ -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 "<filterName>"
|
||||
Then it filters the client metrics "<filterName>"
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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',
|
||||
];
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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() {
|
|||
<CsmSharedContextProvider>
|
||||
<RumHeader>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiTitle size="l">
|
||||
<h1>{UX_LABEL}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<UserPercentile />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</RumHeader>
|
||||
<RumOverview />
|
||||
|
|
|
@ -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<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
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<HTMLElement | null>(null);
|
||||
const [searchRef, setSearchRef] = useState<HTMLInputElement | null>(null);
|
||||
|
||||
|
|
|
@ -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<string[]>([]);
|
||||
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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<typeof LocalUIFilters> = {
|
||||
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 (
|
||||
<span data-cy="csmUrlFilter">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: <StyledSpan>{I18LABELS.percentile75th}</StyledSpan>,
|
||||
dropdownDisplay: I18LABELS.percentile75th,
|
||||
'data-test-subj': 'p75Percentile',
|
||||
},
|
||||
{
|
||||
value: '90',
|
||||
inputDisplay: <StyledSpan>{I18LABELS.percentile90th}</StyledSpan>,
|
||||
dropdownDisplay: I18LABELS.percentile90th,
|
||||
'data-test-subj': 'p90Percentile',
|
||||
},
|
||||
{
|
||||
value: '95',
|
||||
inputDisplay: <StyledSpan>{I18LABELS.percentile95th}</StyledSpan>,
|
||||
dropdownDisplay: I18LABELS.percentile95th,
|
||||
'data-test-subj': 'p95Percentile',
|
||||
},
|
||||
{
|
||||
value: '99',
|
||||
inputDisplay: <StyledSpan>{I18LABELS.percentile99th}</StyledSpan>,
|
||||
dropdownDisplay: I18LABELS.percentile99th,
|
||||
'data-test-subj': 'p99Percentile',
|
||||
},
|
||||
];
|
||||
|
||||
const onChange = (val: string) => {
|
||||
updatePercentile(Number(val));
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
prepend={I18LABELS.percentile}
|
||||
data-test-subj="uxPercentileSelect"
|
||||
style={{ width: 150 }}
|
||||
options={options}
|
||||
valueOfSelected={String(percentile ?? DEFAULT_P)}
|
||||
onChange={(value) => onChange(value)}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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 (
|
||||
<EmbeddedPanel>
|
||||
|
|
|
@ -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 { serviceName, searchTerm } = urlParams;
|
||||
|
||||
const { browser, device, os, location } = uiFilters;
|
||||
|
||||
const [mapFilters, setMapFilters] = useState<Filter[]>([]);
|
||||
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));
|
||||
}
|
||||
|
||||
return filters;
|
||||
|
||||
setMapFilters(filters);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [serviceName, browser, device, os, location]);
|
||||
|
||||
return mapFilters;
|
||||
}, [serviceName, browser, device, os, location, searchTerm]);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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<typeof LocalUIFilters> = {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Record<LocalUIFilterName, string>>;
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) });
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -34,7 +34,7 @@ export type HasData = () => Promise<boolean>;
|
|||
|
||||
export type ObservabilityFetchDataPlugins = Exclude<
|
||||
ObservabilityApp,
|
||||
'observability' | 'stack_monitoring'
|
||||
'observability' | 'stack_monitoring' | 'ux'
|
||||
>;
|
||||
|
||||
export interface DataHandler<
|
||||
|
|
|
@ -10,7 +10,8 @@ export type ObservabilityApp =
|
|||
| 'apm'
|
||||
| 'uptime'
|
||||
| 'observability'
|
||||
| 'stack_monitoring';
|
||||
| 'stack_monitoring'
|
||||
| 'ux';
|
||||
|
||||
export type PromiseReturnType<Func> = Func extends (...args: any[]) => Promise<infer Value>
|
||||
? Value
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue