[RUM Dashboard] Initial Version (#68778)
Co-authored-by: Casper Hübertz <casper@formgeist.com>
This commit is contained in:
parent
e552a96121
commit
72111702e9
|
@ -4,6 +4,8 @@ exports[`Error AGENT_NAME 1`] = `"java"`;
|
|||
|
||||
exports[`Error AGENT_VERSION 1`] = `"agent version"`;
|
||||
|
||||
exports[`Error CLIENT_GEO 1`] = `undefined`;
|
||||
|
||||
exports[`Error CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Error CONTAINER_ID 1`] = `undefined`;
|
||||
|
@ -122,18 +124,26 @@ exports[`Error TRANSACTION_SAMPLED 1`] = `undefined`;
|
|||
|
||||
exports[`Error TRANSACTION_TYPE 1`] = `"request"`;
|
||||
|
||||
exports[`Error TRANSACTION_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Error URL_FULL 1`] = `undefined`;
|
||||
|
||||
exports[`Error USER_AGENT_DEVICE 1`] = `undefined`;
|
||||
|
||||
exports[`Error USER_AGENT_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error USER_AGENT_ORIGINAL 1`] = `undefined`;
|
||||
|
||||
exports[`Error USER_AGENT_OS 1`] = `undefined`;
|
||||
|
||||
exports[`Error USER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span AGENT_NAME 1`] = `"java"`;
|
||||
|
||||
exports[`Span AGENT_VERSION 1`] = `"agent version"`;
|
||||
|
||||
exports[`Span CLIENT_GEO 1`] = `undefined`;
|
||||
|
||||
exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Span CONTAINER_ID 1`] = `undefined`;
|
||||
|
@ -252,18 +262,26 @@ exports[`Span TRANSACTION_SAMPLED 1`] = `undefined`;
|
|||
|
||||
exports[`Span TRANSACTION_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Span TRANSACTION_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Span URL_FULL 1`] = `undefined`;
|
||||
|
||||
exports[`Span USER_AGENT_DEVICE 1`] = `undefined`;
|
||||
|
||||
exports[`Span USER_AGENT_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span USER_AGENT_ORIGINAL 1`] = `undefined`;
|
||||
|
||||
exports[`Span USER_AGENT_OS 1`] = `undefined`;
|
||||
|
||||
exports[`Span USER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction AGENT_NAME 1`] = `"java"`;
|
||||
|
||||
exports[`Transaction AGENT_VERSION 1`] = `"agent version"`;
|
||||
|
||||
exports[`Transaction CLIENT_GEO 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`;
|
||||
|
@ -382,10 +400,16 @@ exports[`Transaction TRANSACTION_SAMPLED 1`] = `true`;
|
|||
|
||||
exports[`Transaction TRANSACTION_TYPE 1`] = `"transaction type"`;
|
||||
|
||||
exports[`Transaction TRANSACTION_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction URL_FULL 1`] = `"http://www.elastic.co"`;
|
||||
|
||||
exports[`Transaction USER_AGENT_DEVICE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction USER_AGENT_NAME 1`] = `"Other"`;
|
||||
|
||||
exports[`Transaction USER_AGENT_ORIGINAL 1`] = `"test original"`;
|
||||
|
||||
exports[`Transaction USER_AGENT_OS 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction USER_ID 1`] = `"1337"`;
|
||||
|
|
|
@ -87,3 +87,9 @@ export const CONTAINER_ID = 'container.id';
|
|||
export const POD_NAME = 'kubernetes.pod.name';
|
||||
|
||||
export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code';
|
||||
|
||||
// RUM Labels
|
||||
export const TRANSACTION_URL = 'transaction.page.url';
|
||||
export const CLIENT_GEO = 'client.geo';
|
||||
export const USER_AGENT_DEVICE = 'user_agent.device.name';
|
||||
export const USER_AGENT_OS = 'user_agent.os.name';
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
ERROR_GROUP_ID,
|
||||
} from '../elasticsearch_fieldnames';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { rangeFilter } from '../../server/lib/helpers/range_filter';
|
||||
import { rangeFilter } from '../utils/range_filter';
|
||||
|
||||
export function getErrorGroupsProjection({
|
||||
setup,
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
SERVICE_NODE_NAME,
|
||||
} from '../elasticsearch_fieldnames';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { rangeFilter } from '../../server/lib/helpers/range_filter';
|
||||
import { rangeFilter } from '../utils/range_filter';
|
||||
import { SERVICE_NODE_NAME_MISSING } from '../service_nodes';
|
||||
|
||||
function getServiceNodeNameFilters(serviceNodeName?: string) {
|
||||
|
|
46
x-pack/plugins/apm/common/projections/rum_overview.ts
Normal file
46
x-pack/plugins/apm/common/projections/rum_overview.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Setup,
|
||||
SetupTimeRange,
|
||||
SetupUIFilters,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../server/lib/helpers/setup_request';
|
||||
import { PROCESSOR_EVENT, TRANSACTION_TYPE } from '../elasticsearch_fieldnames';
|
||||
import { rangeFilter } from '../utils/range_filter';
|
||||
|
||||
export function getRumOverviewProjection({
|
||||
setup,
|
||||
}: {
|
||||
setup: Setup & SetupTimeRange & SetupUIFilters;
|
||||
}) {
|
||||
const { start, end, uiFiltersES, indices } = setup;
|
||||
|
||||
const bool = {
|
||||
filter: [
|
||||
{ range: rangeFilter(start, end) },
|
||||
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
|
||||
{ term: { [TRANSACTION_TYPE]: 'page-load' } },
|
||||
{
|
||||
// Adding this filter to cater for some inconsistent rum data
|
||||
exists: {
|
||||
field: 'transaction.marks.navigationTiming.fetchStart',
|
||||
},
|
||||
},
|
||||
...uiFiltersES,
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
index: indices['apm_oss.transactionIndices'],
|
||||
body: {
|
||||
query: {
|
||||
bool,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '../../server/lib/helpers/setup_request';
|
||||
import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { rangeFilter } from '../../server/lib/helpers/range_filter';
|
||||
import { rangeFilter } from '../utils/range_filter';
|
||||
|
||||
export function getServicesProjection({
|
||||
setup,
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
TRANSACTION_NAME,
|
||||
} from '../elasticsearch_fieldnames';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { rangeFilter } from '../../server/lib/helpers/range_filter';
|
||||
import { rangeFilter } from '../utils/range_filter';
|
||||
|
||||
export function getTransactionsProjection({
|
||||
setup,
|
||||
|
|
|
@ -29,4 +29,5 @@ export enum PROJECTION {
|
|||
METRICS = 'metrics',
|
||||
ERROR_GROUPS = 'errorGroups',
|
||||
SERVICE_NODES = 'serviceNodes',
|
||||
RUM_OVERVIEW = 'rumOverview',
|
||||
}
|
||||
|
|
|
@ -6,20 +6,30 @@
|
|||
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
|
||||
const RANGE_FROM = '2020-03-04T12:30:00.000Z';
|
||||
const RANGE_TO = '2020-03-04T13:00:00.000Z';
|
||||
const RANGE_FROM = '2020-06-01T14:59:32.686Z';
|
||||
const RANGE_TO = '2020-06-16T16:59:36.219Z';
|
||||
|
||||
const BASE_URL = Cypress.config().baseUrl;
|
||||
|
||||
/** The default time in ms to wait for a Cypress command to complete */
|
||||
export const DEFAULT_TIMEOUT = 60 * 1000;
|
||||
|
||||
export function loginAndWaitForPage(url: string) {
|
||||
export function loginAndWaitForPage(
|
||||
url: string,
|
||||
dateRange?: { to: string; from: string }
|
||||
) {
|
||||
const username = Cypress.env('elasticsearch_username');
|
||||
const password = Cypress.env('elasticsearch_password');
|
||||
|
||||
cy.log(`Authenticating via ${username} / ${password}`);
|
||||
let rangeFrom = RANGE_FROM;
|
||||
let rangeTo = RANGE_TO;
|
||||
if (dateRange) {
|
||||
rangeFrom = dateRange.from;
|
||||
rangeTo = dateRange.to;
|
||||
}
|
||||
|
||||
const fullUrl = `${BASE_URL}${url}?rangeFrom=${RANGE_FROM}&rangeTo=${RANGE_TO}`;
|
||||
const fullUrl = `${BASE_URL}${url}?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`;
|
||||
cy.visit(fullUrl, { auth: { username, password } });
|
||||
|
||||
cy.viewport('macbook-15');
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
Feature: RUM Dashboard
|
||||
|
||||
Scenario: Client metrics
|
||||
Given a user browses the APM UI application for RUM Data
|
||||
When the user inspects the real user monitoring tab
|
||||
Then should redirect to rum dashboard
|
||||
And should have correct client metrics
|
|
@ -1,10 +1,17 @@
|
|||
module.exports = {
|
||||
APM: {
|
||||
'Transaction duration charts': {
|
||||
'1': '350 ms',
|
||||
'2': '175 ms',
|
||||
'3': '0 ms',
|
||||
},
|
||||
"__version": "4.5.0",
|
||||
"APM": {
|
||||
"Transaction duration charts": {
|
||||
"1": "55 ms",
|
||||
"2": "28 ms",
|
||||
"3": "0 ms"
|
||||
}
|
||||
},
|
||||
__version: '4.5.0',
|
||||
};
|
||||
"RUM Dashboard": {
|
||||
"Client metrics": {
|
||||
"1": "62",
|
||||
"2": "0.07 sec",
|
||||
"3": "0.01 sec"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps';
|
||||
import { loginAndWaitForPage } from '../../integration/helpers';
|
||||
|
||||
/** The default time in ms to wait for a Cypress command to complete */
|
||||
export const DEFAULT_TIMEOUT = 60 * 1000;
|
||||
|
||||
Given(`a user browses the APM UI application for RUM Data`, () => {
|
||||
// open service overview page
|
||||
const RANGE_FROM = 'now-24h';
|
||||
const RANGE_TO = 'now';
|
||||
loginAndWaitForPage(`/app/apm#/services`, { from: RANGE_FROM, to: RANGE_TO });
|
||||
});
|
||||
|
||||
When(`the user inspects the real user monitoring tab`, () => {
|
||||
// click rum tab
|
||||
cy.get(':contains(Real User Monitoring)', { timeout: DEFAULT_TIMEOUT })
|
||||
.last()
|
||||
.click({ force: true });
|
||||
});
|
||||
|
||||
Then(`should redirect to rum dashboard`, () => {
|
||||
cy.url().should('contain', `/app/apm#/rum-overview`);
|
||||
});
|
||||
|
||||
Then(`should have correct client metrics`, () => {
|
||||
const clientMetrics = '[data-cy=client-metrics] .euiStat__title';
|
||||
|
||||
// wait for all loading to finish
|
||||
cy.get('kbnLoadingIndicator').should('not.be.visible');
|
||||
cy.get('.euiStat__title-isLoading').should('not.be.visible');
|
||||
|
||||
cy.get(clientMetrics).eq(2).invoke('text').snapshot();
|
||||
|
||||
cy.get(clientMetrics).eq(1).invoke('text').snapshot();
|
||||
|
||||
cy.get(clientMetrics).eq(0).invoke('text').snapshot();
|
||||
});
|
|
@ -99,7 +99,11 @@ async function init() {
|
|||
.split('\n')
|
||||
.filter((item) => item)
|
||||
.map((item) => JSON.parse(item))
|
||||
.filter((item) => item.url === '/intake/v2/events');
|
||||
.filter((item) => {
|
||||
return (
|
||||
item.url === '/intake/v2/events' || item.url === '/intake/v2/rum/events'
|
||||
);
|
||||
});
|
||||
|
||||
spinner.start();
|
||||
requestProgress.total = items.length;
|
||||
|
|
|
@ -109,7 +109,7 @@ echo "${bold}Static mock data (logs: ${E2E_DIR}${TMP_DIR}/ingest-data.log)${norm
|
|||
# Download static data if not already done
|
||||
if [ ! -e "${TMP_DIR}/events.json" ]; then
|
||||
echo 'Downloading events.json...'
|
||||
curl --silent https://storage.googleapis.com/apm-ui-e2e-static-data/events.json --output ${TMP_DIR}/events.json
|
||||
curl --silent https://storage.googleapis.com/apm-ui-e2e-static-data/2020-06-12.json --output ${TMP_DIR}/events.json
|
||||
fi
|
||||
|
||||
# echo "Deleting existing indices (apm* and .apm*)"
|
||||
|
|
|
@ -5561,10 +5561,10 @@ typedarray@^0.0.6:
|
|||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||
|
||||
typescript@3.9.2:
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9"
|
||||
integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==
|
||||
typescript@3.9.5:
|
||||
version "3.9.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36"
|
||||
integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==
|
||||
|
||||
umd@^3.0.0:
|
||||
version "3.0.3"
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
KibanaContextProvider,
|
||||
useUiSetting$,
|
||||
} from '../../../../../src/plugins/kibana_react/public';
|
||||
import { px, unit, units } from '../style/variables';
|
||||
import { px, units } from '../style/variables';
|
||||
import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
|
||||
import { APMIndicesPermission } from '../components/app/APMIndicesPermission';
|
||||
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
|
||||
|
@ -33,7 +33,6 @@ import { ConfigSchema } from '..';
|
|||
import 'react-vis/dist/style.css';
|
||||
|
||||
const MainContainer = styled.div`
|
||||
min-width: ${px(unit * 50)};
|
||||
padding: ${px(units.plus)};
|
||||
height: 100%;
|
||||
`;
|
||||
|
|
|
@ -25,6 +25,9 @@ import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'
|
|||
import { ServiceMap } from '../ServiceMap';
|
||||
import { ServiceOverview } from '../ServiceOverview';
|
||||
import { TraceOverview } from '../TraceOverview';
|
||||
import { RumOverview } from '../RumDashboard';
|
||||
import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink';
|
||||
import { EndUserExperienceLabel } from '../RumDashboard/translations';
|
||||
|
||||
function getHomeTabs({
|
||||
serviceMapEnabled = true,
|
||||
|
@ -70,14 +73,27 @@ function getHomeTabs({
|
|||
});
|
||||
}
|
||||
|
||||
homeTabs.push({
|
||||
link: (
|
||||
<RumOverviewLink>
|
||||
{i18n.translate('xpack.apm.home.rumTabLabel', {
|
||||
defaultMessage: 'Real User Monitoring',
|
||||
})}
|
||||
</RumOverviewLink>
|
||||
),
|
||||
render: () => <RumOverview />,
|
||||
name: 'rum-overview',
|
||||
});
|
||||
|
||||
return homeTabs;
|
||||
}
|
||||
|
||||
const SETTINGS_LINK_LABEL = i18n.translate('xpack.apm.settingsLinkLabel', {
|
||||
defaultMessage: 'Settings',
|
||||
});
|
||||
|
||||
interface Props {
|
||||
tab: 'traces' | 'services' | 'service-map';
|
||||
tab: 'traces' | 'services' | 'service-map' | 'rum-overview';
|
||||
}
|
||||
|
||||
export function Home({ tab }: Props) {
|
||||
|
@ -93,7 +109,11 @@ export function Home({ tab }: Props) {
|
|||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="l">
|
||||
<h1>APM</h1>
|
||||
<h1>
|
||||
{selectedTab.name === 'rum-overview'
|
||||
? EndUserExperienceLabel
|
||||
: 'APM'}
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -250,4 +250,13 @@ export const routes: BreadcrumbRoute[] = [
|
|||
}),
|
||||
name: RouteName.CUSTOMIZE_UI,
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
path: '/rum-overview',
|
||||
component: () => <Home tab="rum-overview" />,
|
||||
breadcrumb: i18n.translate('xpack.apm.home.rumOverview.title', {
|
||||
defaultMessage: 'Real User Monitoring',
|
||||
}),
|
||||
name: RouteName.RUM_OVERVIEW,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -26,4 +26,5 @@ export enum RouteName {
|
|||
SERVICE_NODES = 'nodes',
|
||||
LINK_TO_TRACE = 'link_to_trace',
|
||||
CUSTOMIZE_UI = 'customize_ui',
|
||||
RUM_OVERVIEW = 'rum_overview',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FC, HTMLAttributes } from 'react';
|
||||
import {
|
||||
EuiErrorBoundary,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingChart,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Height for the chart
|
||||
*/
|
||||
height?: string;
|
||||
/**
|
||||
* if chart data source is still loading
|
||||
*/
|
||||
loading?: boolean;
|
||||
/**
|
||||
* aria-label for accessibility
|
||||
*/
|
||||
'aria-label'?: string;
|
||||
}
|
||||
|
||||
export const ChartWrapper: FC<Props> = ({
|
||||
loading = false,
|
||||
height = '100%',
|
||||
children,
|
||||
...rest
|
||||
}) => {
|
||||
const opacity = loading === true ? 0.3 : 1;
|
||||
|
||||
return (
|
||||
<EuiErrorBoundary>
|
||||
<div
|
||||
style={{
|
||||
height,
|
||||
opacity,
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
{...(rest as HTMLAttributes<HTMLDivElement>)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{loading === true && (
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceAround"
|
||||
alignItems="center"
|
||||
style={{ height, marginTop: `-${height}`, marginBottom: 0 }}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingChart size="xl" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui';
|
||||
import { useFetcher } from '../../../../hooks/useFetcher';
|
||||
import { useUrlParams } from '../../../../hooks/useUrlParams';
|
||||
import { BackEndLabel, FrontEndLabel, PageViewsLabel } from '../translations';
|
||||
|
||||
export const formatBigValue = (val?: number | null, fixed?: number): string => {
|
||||
if (val && val >= 1000) {
|
||||
const result = val / 1000;
|
||||
if (fixed) {
|
||||
return result.toFixed(fixed) + 'k';
|
||||
}
|
||||
return result + 'k';
|
||||
}
|
||||
return val + '';
|
||||
};
|
||||
|
||||
const ClFlexGroup = styled(EuiFlexGroup)`
|
||||
flex-direction: row;
|
||||
@media only screen and (max-width: 768px) {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ClientMetrics = () => {
|
||||
const { urlParams, uiFilters } = useUrlParams();
|
||||
|
||||
const { start, end } = urlParams;
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi({
|
||||
pathname: '/api/apm/rum/client-metrics',
|
||||
params: {
|
||||
query: { start, end, uiFilters: JSON.stringify(uiFilters) },
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[start, end, uiFilters]
|
||||
);
|
||||
|
||||
const STAT_STYLE = { width: '240px' };
|
||||
|
||||
return (
|
||||
<ClFlexGroup responsive={false}>
|
||||
<EuiFlexItem grow={false} style={STAT_STYLE}>
|
||||
<EuiStat
|
||||
titleSize="s"
|
||||
title={(data?.backEnd?.value?.toFixed(2) ?? '-') + ' sec'}
|
||||
description={BackEndLabel}
|
||||
isLoading={status !== 'success'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={STAT_STYLE}>
|
||||
<EuiStat
|
||||
titleSize="s"
|
||||
title={(data?.frontEnd?.value?.toFixed(2) ?? '-') + ' sec'}
|
||||
description={FrontEndLabel}
|
||||
isLoading={status !== 'success'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={STAT_STYLE}>
|
||||
<EuiStat
|
||||
titleSize="s"
|
||||
title={formatBigValue(data?.pageViews?.value, 2) ?? '-'}
|
||||
description={PageViewsLabel}
|
||||
isLoading={status !== 'success'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</ClFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
AnnotationDomainTypes,
|
||||
LineAnnotation,
|
||||
LineAnnotationDatum,
|
||||
LineAnnotationStyle,
|
||||
} from '@elastic/charts';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
percentiles?: Record<string, number>;
|
||||
}
|
||||
|
||||
function generateAnnotationData(
|
||||
values?: Record<string, number>
|
||||
): LineAnnotationDatum[] {
|
||||
return Object.entries(values ?? {}).map((value, index) => ({
|
||||
dataValue: value[1],
|
||||
details: `${(+value[0]).toFixed(0)}`,
|
||||
}));
|
||||
}
|
||||
|
||||
const PercentileMarker = styled.span`
|
||||
position: relative;
|
||||
bottom: 140px;
|
||||
`;
|
||||
|
||||
export const PercentileAnnotations = ({ percentiles }: Props) => {
|
||||
const dataValues = generateAnnotationData(percentiles) ?? [];
|
||||
|
||||
const style: Partial<LineAnnotationStyle> = {
|
||||
line: {
|
||||
strokeWidth: 1,
|
||||
stroke: euiLightVars.euiColorSecondary,
|
||||
opacity: 1,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{dataValues.map((annotation, index) => (
|
||||
<LineAnnotation
|
||||
id={index + 'annotation_' + annotation.dataValue}
|
||||
key={index + 'percentile_' + annotation.dataValue}
|
||||
domainType={AnnotationDomainTypes.XDomain}
|
||||
dataValues={[annotation]}
|
||||
style={style}
|
||||
marker={<PercentileMarker>{annotation.details}th</PercentileMarker>}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
Axis,
|
||||
Chart,
|
||||
ScaleType,
|
||||
LineSeries,
|
||||
CurveType,
|
||||
BrushEndListener,
|
||||
Settings,
|
||||
TooltipValueFormatter,
|
||||
TooltipValue,
|
||||
} from '@elastic/charts';
|
||||
import { Position } from '@elastic/charts/dist/utils/commons';
|
||||
import { useUrlParams } from '../../../../hooks/useUrlParams';
|
||||
import { useFetcher } from '../../../../hooks/useFetcher';
|
||||
import { ChartWrapper } from '../ChartWrapper';
|
||||
import { PercentileAnnotations } from './PercentileAnnotations';
|
||||
import {
|
||||
PageLoadDistLabel,
|
||||
PageLoadTimeLabel,
|
||||
PercPageLoadedLabel,
|
||||
ResetZoomLabel,
|
||||
} from '../translations';
|
||||
|
||||
export const PageLoadDistribution = () => {
|
||||
const { urlParams, uiFilters } = useUrlParams();
|
||||
|
||||
const { start, end } = urlParams;
|
||||
|
||||
const [percentileRange, setPercentileRange] = useState<{
|
||||
min: string | null;
|
||||
max: string | null;
|
||||
}>({
|
||||
min: null,
|
||||
max: null,
|
||||
});
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi({
|
||||
pathname: '/api/apm/rum-client/page-load-distribution',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
uiFilters: JSON.stringify(uiFilters),
|
||||
...(percentileRange.min && percentileRange.max
|
||||
? {
|
||||
minPercentile: percentileRange.min,
|
||||
maxPercentile: percentileRange.max,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[end, start, uiFilters, percentileRange.min, percentileRange.max]
|
||||
);
|
||||
|
||||
const onBrushEnd: BrushEndListener = ({ x }) => {
|
||||
if (!x) {
|
||||
return;
|
||||
}
|
||||
const [minX, maxX] = x;
|
||||
setPercentileRange({ min: String(minX), max: String(maxX) });
|
||||
};
|
||||
|
||||
const headerFormatter: TooltipValueFormatter = (tooltip: TooltipValue) => {
|
||||
return (
|
||||
<div>
|
||||
<p>{tooltip.value} seconds</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const tooltipProps = {
|
||||
headerFormatter,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{PageLoadDistLabel}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
iconType="inspect"
|
||||
size="s"
|
||||
onClick={() => {
|
||||
setPercentileRange({ min: null, max: null });
|
||||
}}
|
||||
fill={percentileRange.min !== null && percentileRange.max !== null}
|
||||
>
|
||||
{ResetZoomLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<ChartWrapper loading={status !== 'success'} height="200px">
|
||||
<Chart className="story-chart">
|
||||
<Settings onBrushEnd={onBrushEnd} tooltip={tooltipProps} />
|
||||
<PercentileAnnotations percentiles={data?.percentiles} />
|
||||
<Axis
|
||||
id="bottom"
|
||||
title={PageLoadTimeLabel}
|
||||
position={Position.Bottom}
|
||||
/>
|
||||
<Axis
|
||||
id="left"
|
||||
title={PercPageLoadedLabel}
|
||||
position={Position.Left}
|
||||
tickFormat={(d) => Number(d).toFixed(1) + ' %'}
|
||||
/>
|
||||
<LineSeries
|
||||
id={'PagesPercentage'}
|
||||
name={PercPageLoadedLabel}
|
||||
xScaleType={ScaleType.Linear}
|
||||
yScaleType={ScaleType.Linear}
|
||||
data={data?.pageLoadDistribution ?? []}
|
||||
curve={CurveType.CURVE_NATURAL}
|
||||
/>
|
||||
</Chart>
|
||||
</ChartWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import {
|
||||
Axis,
|
||||
BarSeries,
|
||||
BrushEndListener,
|
||||
Chart,
|
||||
niceTimeFormatByDay,
|
||||
ScaleType,
|
||||
Settings,
|
||||
timeFormatter,
|
||||
} from '@elastic/charts';
|
||||
import moment from 'moment';
|
||||
import { Position } from '@elastic/charts/dist/utils/commons';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { useUrlParams } from '../../../../hooks/useUrlParams';
|
||||
import { useFetcher } from '../../../../hooks/useFetcher';
|
||||
import { ChartWrapper } from '../ChartWrapper';
|
||||
import { DateTimeLabel, PageViewsLabel } from '../translations';
|
||||
import { history } from '../../../../utils/history';
|
||||
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { formatBigValue } from '../ClientMetrics';
|
||||
|
||||
export const PageViewsTrend = () => {
|
||||
const { urlParams, uiFilters } = useUrlParams();
|
||||
|
||||
const { start, end } = urlParams;
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi({
|
||||
pathname: '/api/apm/rum-client/page-view-trends',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
uiFilters: JSON.stringify(uiFilters),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[end, start, uiFilters]
|
||||
);
|
||||
const formatter = timeFormatter(niceTimeFormatByDay(2));
|
||||
|
||||
const onBrushEnd: BrushEndListener = ({ x }) => {
|
||||
if (!x) {
|
||||
return;
|
||||
}
|
||||
const [minX, maxX] = x;
|
||||
|
||||
const rangeFrom = moment(minX).toISOString();
|
||||
const rangeTo = moment(maxX).toISOString();
|
||||
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{PageViewsLabel}</h3>
|
||||
</EuiTitle>
|
||||
<ChartWrapper loading={status !== 'success'} height="200px">
|
||||
<Chart>
|
||||
<Settings
|
||||
showLegend={false}
|
||||
showLegendExtra
|
||||
legendPosition={Position.Bottom}
|
||||
onBrushEnd={onBrushEnd}
|
||||
/>
|
||||
<Axis
|
||||
id="date_time"
|
||||
position={Position.Bottom}
|
||||
title={DateTimeLabel}
|
||||
tickFormat={formatter}
|
||||
/>
|
||||
<Axis
|
||||
id="page_views"
|
||||
title={PageViewsLabel}
|
||||
position={Position.Left}
|
||||
tickFormat={(d) => formatBigValue(Number(d))}
|
||||
/>
|
||||
<BarSeries
|
||||
id={PageViewsLabel}
|
||||
color={[euiLightVars.euiColorLightShade]}
|
||||
xScaleType={ScaleType.Linear}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor="x"
|
||||
yAccessors={['y']}
|
||||
data={data ?? []}
|
||||
/>
|
||||
</Chart>
|
||||
</ChartWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { ClientMetrics } from './ClientMetrics';
|
||||
import { PageViewsTrend } from './PageViewsTrend';
|
||||
import { PageLoadDistribution } from './PageLoadDistribution';
|
||||
import { getWhatIsGoingOnLabel } from './translations';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
|
||||
export function RumDashboard() {
|
||||
const { urlParams } = useUrlParams();
|
||||
|
||||
const { environment } = urlParams;
|
||||
|
||||
let environmentLabel = environment || 'all environments';
|
||||
|
||||
if (environment === 'ENVIRONMENT_NOT_DEFINED') {
|
||||
environmentLabel = 'undefined environment';
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>{getWhatIsGoingOnLabel(environmentLabel)}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={1} data-cy={`client-metrics`}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>Page load times</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<ClientMetrics />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={3}>
|
||||
<PageLoadDistribution />
|
||||
<PageViewsTrend />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTrackPageview } from '../../../../../observability/public';
|
||||
import { LocalUIFilters } from '../../shared/LocalUIFilters';
|
||||
import { PROJECTION } from '../../../../common/projections/typings';
|
||||
import { RumDashboard } from './RumDashboard';
|
||||
|
||||
export function RumOverview() {
|
||||
useTrackPageview({ app: 'apm', path: 'rum_overview' });
|
||||
useTrackPageview({ app: 'apm', path: 'rum_overview', delay: 15000 });
|
||||
|
||||
const localUIFiltersConfig = useMemo(() => {
|
||||
const config: React.ComponentProps<typeof LocalUIFilters> = {
|
||||
filterNames: ['transactionUrl', 'location', 'device', 'os', 'browser'],
|
||||
projection: PROJECTION.RUM_OVERVIEW,
|
||||
};
|
||||
|
||||
return config;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={1}>
|
||||
<LocalUIFilters {...localUIFiltersConfig} showCount={true} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={7}>
|
||||
<RumDashboard />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const EndUserExperienceLabel = i18n.translate(
|
||||
'xpack.apm.rum.dashboard.title',
|
||||
{
|
||||
defaultMessage: 'End User Experience',
|
||||
}
|
||||
);
|
||||
|
||||
export const getWhatIsGoingOnLabel = (environmentVal: string) =>
|
||||
i18n.translate('xpack.apm.rum.dashboard.environment.title', {
|
||||
defaultMessage: `What's going on in {environmentVal}?`,
|
||||
values: { environmentVal },
|
||||
});
|
||||
|
||||
export const BackEndLabel = i18n.translate('xpack.apm.rum.dashboard.backend', {
|
||||
defaultMessage: 'Backend',
|
||||
});
|
||||
|
||||
export const FrontEndLabel = i18n.translate(
|
||||
'xpack.apm.rum.dashboard.frontend',
|
||||
{
|
||||
defaultMessage: 'Frontend',
|
||||
}
|
||||
);
|
||||
|
||||
export const PageViewsLabel = i18n.translate(
|
||||
'xpack.apm.rum.dashboard.pageViews',
|
||||
{
|
||||
defaultMessage: 'Page views',
|
||||
}
|
||||
);
|
||||
|
||||
export const DateTimeLabel = i18n.translate(
|
||||
'xpack.apm.rum.dashboard.dateTime.label',
|
||||
{
|
||||
defaultMessage: 'Date / Time',
|
||||
}
|
||||
);
|
||||
|
||||
export const PercPageLoadedLabel = i18n.translate(
|
||||
'xpack.apm.rum.dashboard.pagesLoaded.label',
|
||||
{
|
||||
defaultMessage: 'Pages loaded',
|
||||
}
|
||||
);
|
||||
|
||||
export const PageLoadTimeLabel = i18n.translate(
|
||||
'xpack.apm.rum.dashboard.pageLoadTime.label',
|
||||
{
|
||||
defaultMessage: 'Page load time (seconds)',
|
||||
}
|
||||
);
|
||||
|
||||
export const PageLoadDistLabel = i18n.translate(
|
||||
'xpack.apm.rum.dashboard.pageLoadDistribution.label',
|
||||
{
|
||||
defaultMessage: 'Page load distribution',
|
||||
}
|
||||
);
|
||||
|
||||
export const ResetZoomLabel = i18n.translate(
|
||||
'xpack.apm.rum.dashboard.resetZoom.label',
|
||||
{
|
||||
defaultMessage: 'Reset zoom',
|
||||
}
|
||||
);
|
|
@ -22,6 +22,8 @@ import { ServiceMap } from '../ServiceMap';
|
|||
import { ServiceMetrics } from '../ServiceMetrics';
|
||||
import { ServiceNodeOverview } from '../ServiceNodeOverview';
|
||||
import { TransactionOverview } from '../TransactionOverview';
|
||||
import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink';
|
||||
import { RumOverview } from '../RumDashboard';
|
||||
|
||||
interface Props {
|
||||
tab: 'transactions' | 'errors' | 'metrics' | 'nodes' | 'service-map';
|
||||
|
@ -110,6 +112,20 @@ export function ServiceDetailTabs({ tab }: Props) {
|
|||
tabs.push(serviceMapTab);
|
||||
}
|
||||
|
||||
if (isRumAgentName(agentName)) {
|
||||
tabs.push({
|
||||
link: (
|
||||
<RumOverviewLink>
|
||||
{i18n.translate('xpack.apm.home.rumTabLabel', {
|
||||
defaultMessage: 'Real User Monitoring',
|
||||
})}
|
||||
</RumOverviewLink>
|
||||
),
|
||||
render: () => <RumOverview />,
|
||||
name: 'rum-overview',
|
||||
});
|
||||
}
|
||||
|
||||
const selectedTab = tabs.find((serviceTab) => serviceTab.name === tab);
|
||||
|
||||
return (
|
||||
|
|
|
@ -76,10 +76,10 @@ export function KueryBar() {
|
|||
});
|
||||
|
||||
// The bar should be disabled when viewing the service map
|
||||
const disabled = /\/service-map$/.test(location.pathname);
|
||||
const disabled = /\/(service-map|rum-overview)$/.test(location.pathname);
|
||||
const disabledPlaceholder = i18n.translate(
|
||||
'xpack.apm.kueryBar.disabledPlaceholder',
|
||||
{ defaultMessage: 'Search is not available for service map' }
|
||||
{ defaultMessage: 'Search is not available here' }
|
||||
);
|
||||
|
||||
async function onChange(inputValue: string, selectionStart: number) {
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { APMLink, APMLinkExtendProps } from './APMLink';
|
||||
import { useUrlParams } from '../../../../hooks/useUrlParams';
|
||||
import { pickKeys } from '../../../../../common/utils/pick_keys';
|
||||
|
||||
const RumOverviewLink = (props: APMLinkExtendProps) => {
|
||||
const { urlParams } = useUrlParams();
|
||||
|
||||
const persistedFilters = pickKeys(
|
||||
urlParams,
|
||||
'transactionResult',
|
||||
'host',
|
||||
'containerId',
|
||||
'podName'
|
||||
);
|
||||
|
||||
return <APMLink path="/rum-overview" query={persistedFilters} {...props} />;
|
||||
};
|
||||
|
||||
export { RumOverviewLink };
|
|
@ -10,7 +10,7 @@ import {
|
|||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import {
|
||||
Setup,
|
||||
SetupTimeRange,
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { PromiseReturnType } from '../../../typings/common';
|
||||
import { APMError } from '../../../typings/es_schemas/ui/apm_error';
|
||||
import { rangeFilter } from '../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../common/utils/range_filter';
|
||||
import {
|
||||
Setup,
|
||||
SetupTimeRange,
|
||||
|
|
194
x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
generated
Normal file
194
x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
generated
Normal file
|
@ -0,0 +1,194 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`rum client dashboard queries fetches client metrics 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"aggs": Object {
|
||||
"backEnd": Object {
|
||||
"avg": Object {
|
||||
"field": "transaction.marks.agent.timeToFirstByte",
|
||||
"missing": 0,
|
||||
},
|
||||
},
|
||||
"domInteractive": Object {
|
||||
"avg": Object {
|
||||
"field": "transaction.marks.agent.domInteractive",
|
||||
"missing": 0,
|
||||
},
|
||||
},
|
||||
"pageViews": Object {
|
||||
"value_count": Object {
|
||||
"field": "transaction.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"format": "epoch_millis",
|
||||
"gte": 1528113600000,
|
||||
"lte": 1528977600000,
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"processor.event": "transaction",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"transaction.type": "page-load",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "transaction.marks.navigationTiming.fetchStart",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"my.custom.ui.filter": "foo-bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`rum client dashboard queries fetches page load distribution 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"aggs": Object {
|
||||
"durationMinMax": Object {
|
||||
"min": Object {
|
||||
"field": "transaction.duration.us",
|
||||
"missing": 0,
|
||||
},
|
||||
},
|
||||
"durationPercentiles": Object {
|
||||
"percentiles": Object {
|
||||
"field": "transaction.duration.us",
|
||||
"percents": Array [
|
||||
50,
|
||||
75,
|
||||
90,
|
||||
95,
|
||||
99,
|
||||
],
|
||||
"script": Object {
|
||||
"lang": "painless",
|
||||
"params": Object {
|
||||
"timeUnit": 1000,
|
||||
},
|
||||
"source": "doc['transaction.duration.us'].value / params.timeUnit",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"format": "epoch_millis",
|
||||
"gte": 1528113600000,
|
||||
"lte": 1528977600000,
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"processor.event": "transaction",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"transaction.type": "page-load",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "transaction.marks.navigationTiming.fetchStart",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"my.custom.ui.filter": "foo-bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`rum client dashboard queries fetches page view trends 1`] = `
|
||||
Object {
|
||||
"body": Object {
|
||||
"aggs": Object {
|
||||
"pageViews": Object {
|
||||
"aggs": Object {
|
||||
"trans_count": Object {
|
||||
"value_count": Object {
|
||||
"field": "transaction.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
"auto_date_histogram": Object {
|
||||
"buckets": 50,
|
||||
"field": "@timestamp",
|
||||
},
|
||||
},
|
||||
},
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"format": "epoch_millis",
|
||||
"gte": 1528113600000,
|
||||
"lte": 1528977600000,
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"processor.event": "transaction",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"transaction.type": "page-load",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "transaction.marks.navigationTiming.fetchStart",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"my.custom.ui.filter": "foo-bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
},
|
||||
"index": "myIndex",
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getRumOverviewProjection } from '../../../common/projections/rum_overview';
|
||||
import { mergeProjection } from '../../../common/projections/util/merge_projection';
|
||||
import {
|
||||
Setup,
|
||||
SetupTimeRange,
|
||||
SetupUIFilters,
|
||||
} from '../helpers/setup_request';
|
||||
|
||||
export async function getClientMetrics({
|
||||
setup,
|
||||
}: {
|
||||
setup: Setup & SetupTimeRange & SetupUIFilters;
|
||||
}) {
|
||||
const projection = getRumOverviewProjection({
|
||||
setup,
|
||||
});
|
||||
|
||||
const params = mergeProjection(projection, {
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: projection.body.query.bool,
|
||||
},
|
||||
aggs: {
|
||||
pageViews: { value_count: { field: 'transaction.type' } },
|
||||
backEnd: {
|
||||
avg: {
|
||||
field: 'transaction.marks.agent.timeToFirstByte',
|
||||
missing: 0,
|
||||
},
|
||||
},
|
||||
domInteractive: {
|
||||
avg: {
|
||||
field: 'transaction.marks.agent.domInteractive',
|
||||
missing: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { client } = setup;
|
||||
|
||||
const response = await client.search(params);
|
||||
const { backEnd, domInteractive, pageViews } = response.aggregations!;
|
||||
|
||||
// Divide by 1000 to convert ms into seconds
|
||||
return {
|
||||
pageViews,
|
||||
backEnd: { value: (backEnd.value || 0) / 1000 },
|
||||
frontEnd: {
|
||||
value: ((domInteractive.value || 0) - (backEnd.value || 0)) / 1000,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getRumOverviewProjection } from '../../../common/projections/rum_overview';
|
||||
import { mergeProjection } from '../../../common/projections/util/merge_projection';
|
||||
import {
|
||||
Setup,
|
||||
SetupTimeRange,
|
||||
SetupUIFilters,
|
||||
} from '../helpers/setup_request';
|
||||
|
||||
export async function getPageLoadDistribution({
|
||||
setup,
|
||||
minPercentile,
|
||||
maxPercentile,
|
||||
}: {
|
||||
setup: Setup & SetupTimeRange & SetupUIFilters;
|
||||
minPercentile?: string;
|
||||
maxPercentile?: string;
|
||||
}) {
|
||||
const projection = getRumOverviewProjection({
|
||||
setup,
|
||||
});
|
||||
|
||||
const params = mergeProjection(projection, {
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: projection.body.query.bool,
|
||||
},
|
||||
aggs: {
|
||||
durationMinMax: {
|
||||
min: {
|
||||
field: 'transaction.duration.us',
|
||||
missing: 0,
|
||||
},
|
||||
},
|
||||
durationPercentiles: {
|
||||
percentiles: {
|
||||
field: 'transaction.duration.us',
|
||||
percents: [50, 75, 90, 95, 99],
|
||||
script: {
|
||||
lang: 'painless',
|
||||
source: "doc['transaction.duration.us'].value / params.timeUnit",
|
||||
params: {
|
||||
timeUnit: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { client } = setup;
|
||||
|
||||
const {
|
||||
aggregations,
|
||||
hits: { total },
|
||||
} = await client.search(params);
|
||||
|
||||
if (total.value === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const minDuration = (aggregations?.durationMinMax.value ?? 0) / 1000;
|
||||
|
||||
const minPerc = minPercentile ? +minPercentile : minDuration;
|
||||
|
||||
const maxPercentileQuery =
|
||||
aggregations?.durationPercentiles.values['99.0'] ?? 100;
|
||||
|
||||
const maxPerc = maxPercentile ? +maxPercentile : maxPercentileQuery;
|
||||
|
||||
const pageDist = await getPercentilesDistribution(setup, minPerc, maxPerc);
|
||||
return {
|
||||
pageLoadDistribution: pageDist,
|
||||
percentiles: aggregations?.durationPercentiles.values,
|
||||
};
|
||||
}
|
||||
|
||||
const getPercentilesDistribution = async (
|
||||
setup: Setup & SetupTimeRange & SetupUIFilters,
|
||||
minPercentiles: number,
|
||||
maxPercentile: number
|
||||
) => {
|
||||
const stepValue = (maxPercentile - minPercentiles) / 50;
|
||||
const stepValues = [];
|
||||
for (let i = 1; i < 50; i++) {
|
||||
stepValues.push((stepValue * i + minPercentiles).toFixed(2));
|
||||
}
|
||||
|
||||
const projection = getRumOverviewProjection({
|
||||
setup,
|
||||
});
|
||||
|
||||
const params = mergeProjection(projection, {
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: projection.body.query.bool,
|
||||
},
|
||||
aggs: {
|
||||
loadDistribution: {
|
||||
percentile_ranks: {
|
||||
field: 'transaction.duration.us',
|
||||
values: stepValues,
|
||||
keyed: false,
|
||||
script: {
|
||||
lang: 'painless',
|
||||
source: "doc['transaction.duration.us'].value / params.timeUnit",
|
||||
params: {
|
||||
timeUnit: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { client } = setup;
|
||||
|
||||
const { aggregations } = await client.search(params);
|
||||
|
||||
const pageDist = (aggregations?.loadDistribution.values ?? []) as Array<{
|
||||
key: number;
|
||||
value: number;
|
||||
}>;
|
||||
|
||||
return pageDist.map(({ key, value }, index: number, arr) => {
|
||||
return {
|
||||
x: key,
|
||||
y: index === 0 ? value : value - arr[index - 1].value,
|
||||
};
|
||||
});
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getRumOverviewProjection } from '../../../common/projections/rum_overview';
|
||||
import { mergeProjection } from '../../../common/projections/util/merge_projection';
|
||||
import {
|
||||
Setup,
|
||||
SetupTimeRange,
|
||||
SetupUIFilters,
|
||||
} from '../helpers/setup_request';
|
||||
|
||||
export async function getPageViewTrends({
|
||||
setup,
|
||||
}: {
|
||||
setup: Setup & SetupTimeRange & SetupUIFilters;
|
||||
}) {
|
||||
const projection = getRumOverviewProjection({
|
||||
setup,
|
||||
});
|
||||
|
||||
const params = mergeProjection(projection, {
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: projection.body.query.bool,
|
||||
},
|
||||
aggs: {
|
||||
pageViews: {
|
||||
auto_date_histogram: {
|
||||
field: '@timestamp',
|
||||
buckets: 50,
|
||||
},
|
||||
aggs: {
|
||||
trans_count: {
|
||||
value_count: {
|
||||
field: 'transaction.type',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { client } = setup;
|
||||
|
||||
const response = await client.search(params);
|
||||
|
||||
const result = response.aggregations?.pageViews.buckets ?? [];
|
||||
return result.map(({ key, trans_count }) => ({
|
||||
x: key,
|
||||
y: trans_count.value,
|
||||
}));
|
||||
}
|
52
x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
Normal file
52
x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
SearchParamsMock,
|
||||
inspectSearchParams,
|
||||
} from '../../../public/utils/testHelpers';
|
||||
import { getClientMetrics } from './get_client_metrics';
|
||||
import { getPageViewTrends } from './get_page_view_trends';
|
||||
import { getPageLoadDistribution } from './get_page_load_distribution';
|
||||
|
||||
describe('rum client dashboard queries', () => {
|
||||
let mock: SearchParamsMock;
|
||||
|
||||
afterEach(() => {
|
||||
mock.teardown();
|
||||
});
|
||||
|
||||
it('fetches client metrics', async () => {
|
||||
mock = await inspectSearchParams((setup) =>
|
||||
getClientMetrics({
|
||||
setup,
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('fetches page view trends', async () => {
|
||||
mock = await inspectSearchParams((setup) =>
|
||||
getPageViewTrends({
|
||||
setup,
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('fetches page load distribution', async () => {
|
||||
mock = await inspectSearchParams((setup) =>
|
||||
getPageLoadDistribution({
|
||||
setup,
|
||||
minPercentile: '0',
|
||||
maxPercentile: '99',
|
||||
})
|
||||
);
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { Setup, SetupTimeRange } from '../helpers/setup_request';
|
||||
import { ESFilter } from '../../../typings/elasticsearch';
|
||||
import { rangeFilter } from '../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../common/utils/range_filter';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_ENVIRONMENT,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import { uniq, take, sortBy } from 'lodash';
|
||||
import { Setup, SetupTimeRange } from '../helpers/setup_request';
|
||||
import { rangeFilter } from '../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../common/utils/range_filter';
|
||||
import { ESFilter } from '../../../typings/elasticsearch';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
|
|
|
@ -7,7 +7,7 @@ import { isNumber } from 'lodash';
|
|||
import { Annotation, AnnotationType } from '../../../../common/annotations';
|
||||
import { SetupTimeRange, Setup } from '../../helpers/setup_request';
|
||||
import { ESFilter } from '../../../../typings/elasticsearch';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME,
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
AGENT_NAME,
|
||||
SERVICE_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { rangeFilter } from '../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../common/utils/range_filter';
|
||||
import { Setup, SetupTimeRange } from '../helpers/setup_request';
|
||||
|
||||
export async function getServiceAgentName(
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { rangeFilter } from '../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../common/utils/range_filter';
|
||||
import { Setup, SetupTimeRange } from '../helpers/setup_request';
|
||||
|
||||
export async function getServiceTransactionTypes(
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { Span } from '../../../typings/es_schemas/ui/span';
|
||||
import { Transaction } from '../../../typings/es_schemas/ui/transaction';
|
||||
import { APMError } from '../../../typings/es_schemas/ui/apm_error';
|
||||
import { rangeFilter } from '../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../common/utils/range_filter';
|
||||
import { Setup, SetupTimeRange } from '../helpers/setup_request';
|
||||
import { PromiseValueType } from '../../../typings/common';
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
USER_AGENT_NAME,
|
||||
TRANSACTION_DURATION,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import { getBucketSize } from '../../helpers/get_bucket_size';
|
||||
import { Options } from '.';
|
||||
import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
SetupTimeRange,
|
||||
SetupUIFilters,
|
||||
} from '../../helpers/setup_request';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
|
||||
|
||||
export async function getTransactionAvgDurationByCountry({
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
SetupTimeRange,
|
||||
SetupUIFilters,
|
||||
} from '../../helpers/setup_request';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import { getMetricsDateHistogramParams } from '../../helpers/metrics';
|
||||
import { MAX_KPIS } from './constants';
|
||||
import { getVizColorForIndex } from '../../../../common/viz_colors';
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { PromiseReturnType } from '../../../../../../observability/typings/common';
|
||||
import { getBucketSize } from '../../../helpers/get_bucket_size';
|
||||
import { rangeFilter } from '../../../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../../../common/utils/range_filter';
|
||||
import {
|
||||
Setup,
|
||||
SetupTimeRange,
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
TRANSACTION_SAMPLED,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { rangeFilter } from '../../../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../../../common/utils/range_filter';
|
||||
import {
|
||||
Setup,
|
||||
SetupTimeRange,
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
TRANSACTION_ID,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import {
|
||||
Setup,
|
||||
SetupTimeRange,
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { rangeFilter } from '../helpers/range_filter';
|
||||
import { rangeFilter } from '../../../common/utils/range_filter';
|
||||
import { Setup, SetupTimeRange } from '../helpers/setup_request';
|
||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
|
||||
import { ESFilter } from '../../../typings/elasticsearch';
|
||||
|
|
|
@ -11,6 +11,11 @@ import {
|
|||
HOST_NAME,
|
||||
TRANSACTION_RESULT,
|
||||
SERVICE_VERSION,
|
||||
TRANSACTION_URL,
|
||||
USER_AGENT_NAME,
|
||||
USER_AGENT_DEVICE,
|
||||
CLIENT_GEO,
|
||||
USER_AGENT_OS,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
|
||||
const filtersByName = {
|
||||
|
@ -50,6 +55,36 @@ const filtersByName = {
|
|||
}),
|
||||
fieldName: SERVICE_VERSION,
|
||||
},
|
||||
transactionUrl: {
|
||||
title: i18n.translate('xpack.apm.localFilters.titles.transactionUrl', {
|
||||
defaultMessage: 'Url',
|
||||
}),
|
||||
fieldName: TRANSACTION_URL,
|
||||
},
|
||||
browser: {
|
||||
title: i18n.translate('xpack.apm.localFilters.titles.browser', {
|
||||
defaultMessage: 'Browser',
|
||||
}),
|
||||
fieldName: USER_AGENT_NAME,
|
||||
},
|
||||
device: {
|
||||
title: i18n.translate('xpack.apm.localFilters.titles.device', {
|
||||
defaultMessage: 'Device',
|
||||
}),
|
||||
fieldName: USER_AGENT_DEVICE,
|
||||
},
|
||||
location: {
|
||||
title: i18n.translate('xpack.apm.localFilters.titles.location', {
|
||||
defaultMessage: 'Location',
|
||||
}),
|
||||
fieldName: CLIENT_GEO,
|
||||
},
|
||||
os: {
|
||||
title: i18n.translate('xpack.apm.localFilters.titles.os', {
|
||||
defaultMessage: 'OS',
|
||||
}),
|
||||
fieldName: USER_AGENT_OS,
|
||||
},
|
||||
};
|
||||
|
||||
export type LocalUIFilterName = keyof typeof filtersByName;
|
||||
|
|
|
@ -59,6 +59,7 @@ import {
|
|||
transactionsLocalFiltersRoute,
|
||||
serviceNodesLocalFiltersRoute,
|
||||
uiFiltersEnvironmentsRoute,
|
||||
rumOverviewLocalFiltersRoute,
|
||||
} from './ui_filters';
|
||||
import { createApi } from './create_api';
|
||||
import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map';
|
||||
|
@ -70,6 +71,11 @@ import {
|
|||
listCustomLinksRoute,
|
||||
customLinkTransactionRoute,
|
||||
} from './settings/custom_link';
|
||||
import {
|
||||
rumClientMetricsRoute,
|
||||
rumPageViewsTrendRoute,
|
||||
rumPageLoadDistributionRoute,
|
||||
} from './rum_client';
|
||||
|
||||
const createApmApi = () => {
|
||||
const api = createApi()
|
||||
|
@ -148,7 +154,13 @@ const createApmApi = () => {
|
|||
.add(updateCustomLinkRoute)
|
||||
.add(deleteCustomLinkRoute)
|
||||
.add(listCustomLinksRoute)
|
||||
.add(customLinkTransactionRoute);
|
||||
.add(customLinkTransactionRoute)
|
||||
|
||||
// Rum Overview
|
||||
.add(rumOverviewLocalFiltersRoute)
|
||||
.add(rumPageViewsTrendRoute)
|
||||
.add(rumPageLoadDistributionRoute)
|
||||
.add(rumClientMetricsRoute);
|
||||
|
||||
return api;
|
||||
};
|
||||
|
|
57
x-pack/plugins/apm/server/routes/rum_client.ts
Normal file
57
x-pack/plugins/apm/server/routes/rum_client.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { createRoute } from './create_route';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getClientMetrics } from '../lib/rum_client/get_client_metrics';
|
||||
import { rangeRt, uiFiltersRt } from './default_api_types';
|
||||
import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends';
|
||||
import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distribution';
|
||||
|
||||
export const percentileRangeRt = t.partial({
|
||||
minPercentile: t.string,
|
||||
maxPercentile: t.string,
|
||||
});
|
||||
|
||||
export const rumClientMetricsRoute = createRoute(() => ({
|
||||
path: '/api/apm/rum/client-metrics',
|
||||
params: {
|
||||
query: t.intersection([uiFiltersRt, rangeRt]),
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
|
||||
return getClientMetrics({ setup });
|
||||
},
|
||||
}));
|
||||
|
||||
export const rumPageLoadDistributionRoute = createRoute(() => ({
|
||||
path: '/api/apm/rum-client/page-load-distribution',
|
||||
params: {
|
||||
query: t.intersection([uiFiltersRt, rangeRt, percentileRangeRt]),
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
|
||||
const {
|
||||
query: { minPercentile, maxPercentile },
|
||||
} = context.params;
|
||||
|
||||
return getPageLoadDistribution({ setup, minPercentile, maxPercentile });
|
||||
},
|
||||
}));
|
||||
|
||||
export const rumPageViewsTrendRoute = createRoute(() => ({
|
||||
path: '/api/apm/rum-client/page-view-trends',
|
||||
params: {
|
||||
query: t.intersection([uiFiltersRt, rangeRt]),
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
return getPageViewTrends({ setup });
|
||||
},
|
||||
}));
|
|
@ -29,6 +29,7 @@ import { createRoute } from './create_route';
|
|||
import { uiFiltersRt, rangeRt } from './default_api_types';
|
||||
import { jsonRt } from '../../common/runtime_types/json_rt';
|
||||
import { getServiceNodesProjection } from '../../common/projections/service_nodes';
|
||||
import { getRumOverviewProjection } from '../../common/projections/rum_overview';
|
||||
|
||||
export const uiFiltersEnvironmentsRoute = createRoute(() => ({
|
||||
path: '/api/apm/ui_filters/environments',
|
||||
|
@ -221,6 +222,16 @@ export const serviceNodesLocalFiltersRoute = createLocalFiltersRoute({
|
|||
}),
|
||||
});
|
||||
|
||||
export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({
|
||||
path: '/api/apm/ui_filters/local_filters/rumOverview',
|
||||
getProjection: ({ setup }) => {
|
||||
return getRumOverviewProjection({
|
||||
setup,
|
||||
});
|
||||
},
|
||||
queryRt: t.type({}),
|
||||
});
|
||||
|
||||
type BaseQueryType = typeof localUiBaseQueryRt;
|
||||
|
||||
type GetProjection<
|
||||
|
|
|
@ -137,6 +137,15 @@ export interface AggregationOptionsByType {
|
|||
>;
|
||||
keyed?: boolean;
|
||||
};
|
||||
auto_date_histogram: {
|
||||
field: string;
|
||||
buckets: number;
|
||||
};
|
||||
percentile_ranks: {
|
||||
field: string;
|
||||
values: string[];
|
||||
keyed?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
type AggregationType = keyof AggregationOptionsByType;
|
||||
|
@ -301,6 +310,23 @@ interface AggregationResponsePart<
|
|||
? Record<string, DateRangeBucket>
|
||||
: { buckets: DateRangeBucket[] };
|
||||
};
|
||||
auto_date_histogram: {
|
||||
buckets: Array<
|
||||
{
|
||||
doc_count: number;
|
||||
key: number;
|
||||
key_as_string: string;
|
||||
} & BucketSubAggregationResponse<
|
||||
TAggregationOptionsMap['aggs'],
|
||||
TDocument
|
||||
>
|
||||
>;
|
||||
interval: string;
|
||||
};
|
||||
|
||||
percentile_ranks: {
|
||||
values: Record<string, number> | Array<{ key: number; value: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
// Type for debugging purposes. If you see an error in AggregationResponseMap
|
||||
|
|
|
@ -4229,7 +4229,6 @@
|
|||
"xpack.apm.jvmsTable.noJvmsLabel": "JVM が見つかりませんでした",
|
||||
"xpack.apm.jvmsTable.nonHeapMemoryColumnLabel": "非ヒープ領域の平均",
|
||||
"xpack.apm.jvmsTable.threadCountColumnLabel": "最大スレッド数",
|
||||
"xpack.apm.kueryBar.disabledPlaceholder": "サービスマップの検索は利用できません",
|
||||
"xpack.apm.kueryBar.placeholder": "検索 {event, select,\n transaction {トランザクション}\n metric {メトリック}\n error {エラー}\n other {その他}\n } (E.g. {queryExample})",
|
||||
"xpack.apm.license.betaBadge": "ベータ",
|
||||
"xpack.apm.license.betaTooltipMessage": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。",
|
||||
|
|
|
@ -4232,7 +4232,6 @@
|
|||
"xpack.apm.jvmsTable.noJvmsLabel": "未找到任何 JVM",
|
||||
"xpack.apm.jvmsTable.nonHeapMemoryColumnLabel": "非堆内存平均值",
|
||||
"xpack.apm.jvmsTable.threadCountColumnLabel": "线程计数最大值",
|
||||
"xpack.apm.kueryBar.disabledPlaceholder": "搜索不适用于服务地图",
|
||||
"xpack.apm.kueryBar.placeholder": "搜索{event, select,\n transaction {事务}\n metric {指标}\n error {错误}\n other {事务、错误和指标}\n }(例如 {queryExample})",
|
||||
"xpack.apm.license.betaBadge": "公测版",
|
||||
"xpack.apm.license.betaTooltipMessage": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。",
|
||||
|
|
Loading…
Reference in a new issue