diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
index 54dd4704edfc0..f3dc7abcf8239 100644
--- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
+++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
@@ -4,6 +4,8 @@ exports[`Error AGENT_NAME 1`] = `"java"`;
exports[`Error AGENT_VERSION 1`] = `"agent version"`;
+exports[`Error CLIENT_GEO 1`] = `undefined`;
+
exports[`Error CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Error CONTAINER_ID 1`] = `undefined`;
@@ -122,18 +124,26 @@ exports[`Error TRANSACTION_SAMPLED 1`] = `undefined`;
exports[`Error TRANSACTION_TYPE 1`] = `"request"`;
+exports[`Error TRANSACTION_URL 1`] = `undefined`;
+
exports[`Error URL_FULL 1`] = `undefined`;
+exports[`Error USER_AGENT_DEVICE 1`] = `undefined`;
+
exports[`Error USER_AGENT_NAME 1`] = `undefined`;
exports[`Error USER_AGENT_ORIGINAL 1`] = `undefined`;
+exports[`Error USER_AGENT_OS 1`] = `undefined`;
+
exports[`Error USER_ID 1`] = `undefined`;
exports[`Span AGENT_NAME 1`] = `"java"`;
exports[`Span AGENT_VERSION 1`] = `"agent version"`;
+exports[`Span CLIENT_GEO 1`] = `undefined`;
+
exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Span CONTAINER_ID 1`] = `undefined`;
@@ -252,18 +262,26 @@ exports[`Span TRANSACTION_SAMPLED 1`] = `undefined`;
exports[`Span TRANSACTION_TYPE 1`] = `undefined`;
+exports[`Span TRANSACTION_URL 1`] = `undefined`;
+
exports[`Span URL_FULL 1`] = `undefined`;
+exports[`Span USER_AGENT_DEVICE 1`] = `undefined`;
+
exports[`Span USER_AGENT_NAME 1`] = `undefined`;
exports[`Span USER_AGENT_ORIGINAL 1`] = `undefined`;
+exports[`Span USER_AGENT_OS 1`] = `undefined`;
+
exports[`Span USER_ID 1`] = `undefined`;
exports[`Transaction AGENT_NAME 1`] = `"java"`;
exports[`Transaction AGENT_VERSION 1`] = `"agent version"`;
+exports[`Transaction CLIENT_GEO 1`] = `undefined`;
+
exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`;
@@ -382,10 +400,16 @@ exports[`Transaction TRANSACTION_SAMPLED 1`] = `true`;
exports[`Transaction TRANSACTION_TYPE 1`] = `"transaction type"`;
+exports[`Transaction TRANSACTION_URL 1`] = `undefined`;
+
exports[`Transaction URL_FULL 1`] = `"http://www.elastic.co"`;
+exports[`Transaction USER_AGENT_DEVICE 1`] = `undefined`;
+
exports[`Transaction USER_AGENT_NAME 1`] = `"Other"`;
exports[`Transaction USER_AGENT_ORIGINAL 1`] = `"test original"`;
+exports[`Transaction USER_AGENT_OS 1`] = `undefined`;
+
exports[`Transaction USER_ID 1`] = `"1337"`;
diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
index d5c3f91eb9247..7537dba7f8411 100644
--- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
+++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
@@ -87,3 +87,9 @@ export const CONTAINER_ID = 'container.id';
export const POD_NAME = 'kubernetes.pod.name';
export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code';
+
+// RUM Labels
+export const TRANSACTION_URL = 'transaction.page.url';
+export const CLIENT_GEO = 'client.geo';
+export const USER_AGENT_DEVICE = 'user_agent.device.name';
+export const USER_AGENT_OS = 'user_agent.os.name';
diff --git a/x-pack/plugins/apm/common/projections/errors.ts b/x-pack/plugins/apm/common/projections/errors.ts
index bd397afae2243..390a8a0968102 100644
--- a/x-pack/plugins/apm/common/projections/errors.ts
+++ b/x-pack/plugins/apm/common/projections/errors.ts
@@ -16,7 +16,7 @@ import {
ERROR_GROUP_ID,
} from '../elasticsearch_fieldnames';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { rangeFilter } from '../../server/lib/helpers/range_filter';
+import { rangeFilter } from '../utils/range_filter';
export function getErrorGroupsProjection({
setup,
diff --git a/x-pack/plugins/apm/common/projections/metrics.ts b/x-pack/plugins/apm/common/projections/metrics.ts
index b05ec5f2ba876..45998bfe82e96 100644
--- a/x-pack/plugins/apm/common/projections/metrics.ts
+++ b/x-pack/plugins/apm/common/projections/metrics.ts
@@ -16,7 +16,7 @@ import {
SERVICE_NODE_NAME,
} from '../elasticsearch_fieldnames';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { rangeFilter } from '../../server/lib/helpers/range_filter';
+import { rangeFilter } from '../utils/range_filter';
import { SERVICE_NODE_NAME_MISSING } from '../service_nodes';
function getServiceNodeNameFilters(serviceNodeName?: string) {
diff --git a/x-pack/plugins/apm/common/projections/rum_overview.ts b/x-pack/plugins/apm/common/projections/rum_overview.ts
new file mode 100644
index 0000000000000..b1218546d09ff
--- /dev/null
+++ b/x-pack/plugins/apm/common/projections/rum_overview.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ Setup,
+ SetupTimeRange,
+ SetupUIFilters,
+ // eslint-disable-next-line @kbn/eslint/no-restricted-paths
+} from '../../server/lib/helpers/setup_request';
+import { PROCESSOR_EVENT, TRANSACTION_TYPE } from '../elasticsearch_fieldnames';
+import { rangeFilter } from '../utils/range_filter';
+
+export function getRumOverviewProjection({
+ setup,
+}: {
+ setup: Setup & SetupTimeRange & SetupUIFilters;
+}) {
+ const { start, end, uiFiltersES, indices } = setup;
+
+ const bool = {
+ filter: [
+ { range: rangeFilter(start, end) },
+ { term: { [PROCESSOR_EVENT]: 'transaction' } },
+ { term: { [TRANSACTION_TYPE]: 'page-load' } },
+ {
+ // Adding this filter to cater for some inconsistent rum data
+ exists: {
+ field: 'transaction.marks.navigationTiming.fetchStart',
+ },
+ },
+ ...uiFiltersES,
+ ],
+ };
+
+ return {
+ index: indices['apm_oss.transactionIndices'],
+ body: {
+ query: {
+ bool,
+ },
+ },
+ };
+}
diff --git a/x-pack/plugins/apm/common/projections/services.ts b/x-pack/plugins/apm/common/projections/services.ts
index bcfc27d720ba9..80a3471e9c30d 100644
--- a/x-pack/plugins/apm/common/projections/services.ts
+++ b/x-pack/plugins/apm/common/projections/services.ts
@@ -12,7 +12,7 @@ import {
} from '../../server/lib/helpers/setup_request';
import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { rangeFilter } from '../../server/lib/helpers/range_filter';
+import { rangeFilter } from '../utils/range_filter';
export function getServicesProjection({
setup,
diff --git a/x-pack/plugins/apm/common/projections/transactions.ts b/x-pack/plugins/apm/common/projections/transactions.ts
index 99d5a04c1e722..b6cd73ca9aaad 100644
--- a/x-pack/plugins/apm/common/projections/transactions.ts
+++ b/x-pack/plugins/apm/common/projections/transactions.ts
@@ -17,7 +17,7 @@ import {
TRANSACTION_NAME,
} from '../elasticsearch_fieldnames';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { rangeFilter } from '../../server/lib/helpers/range_filter';
+import { rangeFilter } from '../utils/range_filter';
export function getTransactionsProjection({
setup,
diff --git a/x-pack/plugins/apm/common/projections/typings.ts b/x-pack/plugins/apm/common/projections/typings.ts
index 3361770336dde..693795b09e1d0 100644
--- a/x-pack/plugins/apm/common/projections/typings.ts
+++ b/x-pack/plugins/apm/common/projections/typings.ts
@@ -29,4 +29,5 @@ export enum PROJECTION {
METRICS = 'metrics',
ERROR_GROUPS = 'errorGroups',
SERVICE_NODES = 'serviceNodes',
+ RUM_OVERVIEW = 'rumOverview',
}
diff --git a/x-pack/plugins/apm/server/lib/helpers/range_filter.ts b/x-pack/plugins/apm/common/utils/range_filter.ts
similarity index 100%
rename from x-pack/plugins/apm/server/lib/helpers/range_filter.ts
rename to x-pack/plugins/apm/common/utils/range_filter.ts
diff --git a/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts b/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts
index 90d5c9eda632d..689b88390810f 100644
--- a/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts
+++ b/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts
@@ -6,20 +6,30 @@
/* eslint-disable import/no-extraneous-dependencies */
-const RANGE_FROM = '2020-03-04T12:30:00.000Z';
-const RANGE_TO = '2020-03-04T13:00:00.000Z';
+const RANGE_FROM = '2020-06-01T14:59:32.686Z';
+const RANGE_TO = '2020-06-16T16:59:36.219Z';
+
const BASE_URL = Cypress.config().baseUrl;
/** The default time in ms to wait for a Cypress command to complete */
export const DEFAULT_TIMEOUT = 60 * 1000;
-export function loginAndWaitForPage(url: string) {
+export function loginAndWaitForPage(
+ url: string,
+ dateRange?: { to: string; from: string }
+) {
const username = Cypress.env('elasticsearch_username');
const password = Cypress.env('elasticsearch_password');
cy.log(`Authenticating via ${username} / ${password}`);
-
- const fullUrl = `${BASE_URL}${url}?rangeFrom=${RANGE_FROM}&rangeTo=${RANGE_TO}`;
+ let rangeFrom = RANGE_FROM;
+ let rangeTo = RANGE_TO;
+ if (dateRange) {
+ rangeFrom = dateRange.from;
+ rangeTo = dateRange.to;
+ }
+
+ const fullUrl = `${BASE_URL}${url}?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`;
cy.visit(fullUrl, { auth: { username, password } });
cy.viewport('macbook-15');
diff --git a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature
new file mode 100644
index 0000000000000..eabfaf096731b
--- /dev/null
+++ b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature
@@ -0,0 +1,7 @@
+Feature: RUM Dashboard
+
+ Scenario: Client metrics
+ Given a user browses the APM UI application for RUM Data
+ When the user inspects the real user monitoring tab
+ Then should redirect to rum dashboard
+ And should have correct client metrics
diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js
index d4c8ba4910850..dd96a57ef8c45 100644
--- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js
+++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js
@@ -1,10 +1,17 @@
module.exports = {
- APM: {
- 'Transaction duration charts': {
- '1': '350 ms',
- '2': '175 ms',
- '3': '0 ms',
- },
+ "__version": "4.5.0",
+ "APM": {
+ "Transaction duration charts": {
+ "1": "55 ms",
+ "2": "28 ms",
+ "3": "0 ms"
+ }
},
- __version: '4.5.0',
-};
+ "RUM Dashboard": {
+ "Client metrics": {
+ "1": "62",
+ "2": "0.07 sec",
+ "3": "0.01 sec"
+ }
+ }
+}
diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum_dashboard.ts
new file mode 100644
index 0000000000000..38eadbf513032
--- /dev/null
+++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum_dashboard.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps';
+import { loginAndWaitForPage } from '../../integration/helpers';
+
+/** The default time in ms to wait for a Cypress command to complete */
+export const DEFAULT_TIMEOUT = 60 * 1000;
+
+Given(`a user browses the APM UI application for RUM Data`, () => {
+ // open service overview page
+ const RANGE_FROM = 'now-24h';
+ const RANGE_TO = 'now';
+ loginAndWaitForPage(`/app/apm#/services`, { from: RANGE_FROM, to: RANGE_TO });
+});
+
+When(`the user inspects the real user monitoring tab`, () => {
+ // click rum tab
+ cy.get(':contains(Real User Monitoring)', { timeout: DEFAULT_TIMEOUT })
+ .last()
+ .click({ force: true });
+});
+
+Then(`should redirect to rum dashboard`, () => {
+ cy.url().should('contain', `/app/apm#/rum-overview`);
+});
+
+Then(`should have correct client metrics`, () => {
+ const clientMetrics = '[data-cy=client-metrics] .euiStat__title';
+
+ // wait for all loading to finish
+ cy.get('kbnLoadingIndicator').should('not.be.visible');
+ cy.get('.euiStat__title-isLoading').should('not.be.visible');
+
+ cy.get(clientMetrics).eq(2).invoke('text').snapshot();
+
+ cy.get(clientMetrics).eq(1).invoke('text').snapshot();
+
+ cy.get(clientMetrics).eq(0).invoke('text').snapshot();
+});
diff --git a/x-pack/plugins/apm/e2e/ingest-data/replay.js b/x-pack/plugins/apm/e2e/ingest-data/replay.js
index ae3f62894afc0..3478039f39b50 100644
--- a/x-pack/plugins/apm/e2e/ingest-data/replay.js
+++ b/x-pack/plugins/apm/e2e/ingest-data/replay.js
@@ -99,7 +99,11 @@ async function init() {
.split('\n')
.filter((item) => item)
.map((item) => JSON.parse(item))
- .filter((item) => item.url === '/intake/v2/events');
+ .filter((item) => {
+ return (
+ item.url === '/intake/v2/events' || item.url === '/intake/v2/rum/events'
+ );
+ });
spinner.start();
requestProgress.total = items.length;
diff --git a/x-pack/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh
index aa7c0e21425ad..43cc74a197f42 100755
--- a/x-pack/plugins/apm/e2e/run-e2e.sh
+++ b/x-pack/plugins/apm/e2e/run-e2e.sh
@@ -109,7 +109,7 @@ echo "${bold}Static mock data (logs: ${E2E_DIR}${TMP_DIR}/ingest-data.log)${norm
# Download static data if not already done
if [ ! -e "${TMP_DIR}/events.json" ]; then
echo 'Downloading events.json...'
- curl --silent https://storage.googleapis.com/apm-ui-e2e-static-data/events.json --output ${TMP_DIR}/events.json
+ curl --silent https://storage.googleapis.com/apm-ui-e2e-static-data/2020-06-12.json --output ${TMP_DIR}/events.json
fi
# echo "Deleting existing indices (apm* and .apm*)"
diff --git a/x-pack/plugins/apm/e2e/yarn.lock b/x-pack/plugins/apm/e2e/yarn.lock
index a6729c56ecb09..975154d71b85d 100644
--- a/x-pack/plugins/apm/e2e/yarn.lock
+++ b/x-pack/plugins/apm/e2e/yarn.lock
@@ -5561,10 +5561,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-typescript@3.9.2:
- version "3.9.2"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9"
- integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==
+typescript@3.9.5:
+ version "3.9.5"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36"
+ integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==
umd@^3.0.0:
version "3.0.3"
diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx
index e7191caced9db..0a6cca8434a85 100644
--- a/x-pack/plugins/apm/public/application/index.tsx
+++ b/x-pack/plugins/apm/public/application/index.tsx
@@ -24,7 +24,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';
@@ -34,7 +34,6 @@ import { ConfigSchema } from '..';
import 'react-vis/dist/style.css';
const MainContainer = styled.div`
- min-width: ${px(unit * 50)};
padding: ${px(units.plus)};
height: 100%;
`;
diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx
index 74cbc00b17889..c325a72375359 100644
--- a/x-pack/plugins/apm/public/components/app/Home/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx
@@ -25,6 +25,9 @@ import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'
import { ServiceMap } from '../ServiceMap';
import { ServiceOverview } from '../ServiceOverview';
import { TraceOverview } from '../TraceOverview';
+import { RumOverview } from '../RumDashboard';
+import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink';
+import { EndUserExperienceLabel } from '../RumDashboard/translations';
function getHomeTabs({
serviceMapEnabled = true,
@@ -70,14 +73,27 @@ function getHomeTabs({
});
}
+ homeTabs.push({
+ link: (
+
+ {i18n.translate('xpack.apm.home.rumTabLabel', {
+ defaultMessage: 'Real User Monitoring',
+ })}
+
+ ),
+ render: () => ,
+ name: 'rum-overview',
+ });
+
return homeTabs;
}
+
const SETTINGS_LINK_LABEL = i18n.translate('xpack.apm.settingsLinkLabel', {
defaultMessage: 'Settings',
});
interface Props {
- tab: 'traces' | 'services' | 'service-map';
+ tab: 'traces' | 'services' | 'service-map' | 'rum-overview';
}
export function Home({ tab }: Props) {
@@ -93,7 +109,11 @@ export function Home({ tab }: Props) {
- APM
+
+ {selectedTab.name === 'rum-overview'
+ ? EndUserExperienceLabel
+ : 'APM'}
+
diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
index 577af75e92d9e..295f343b411a9 100644
--- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
@@ -250,4 +250,13 @@ export const routes: BreadcrumbRoute[] = [
}),
name: RouteName.CUSTOMIZE_UI,
},
+ {
+ exact: true,
+ path: '/rum-overview',
+ component: () => ,
+ breadcrumb: i18n.translate('xpack.apm.home.rumOverview.title', {
+ defaultMessage: 'Real User Monitoring',
+ }),
+ name: RouteName.RUM_OVERVIEW,
+ },
];
diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx
index 167de1a37f427..4965aa9db8760 100644
--- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx
+++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx
@@ -26,4 +26,5 @@ export enum RouteName {
SERVICE_NODES = 'nodes',
LINK_TO_TRACE = 'link_to_trace',
CUSTOMIZE_UI = 'customize_ui',
+ RUM_OVERVIEW = 'rum_overview',
}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx
new file mode 100644
index 0000000000000..a3cfbb28abee2
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, HTMLAttributes } from 'react';
+import {
+ EuiErrorBoundary,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingChart,
+} from '@elastic/eui';
+
+interface Props {
+ /**
+ * Height for the chart
+ */
+ height?: string;
+ /**
+ * if chart data source is still loading
+ */
+ loading?: boolean;
+ /**
+ * aria-label for accessibility
+ */
+ 'aria-label'?: string;
+}
+
+export const ChartWrapper: FC = ({
+ loading = false,
+ height = '100%',
+ children,
+ ...rest
+}) => {
+ const opacity = loading === true ? 0.3 : 1;
+
+ return (
+
+ )}
+ >
+ {children}
+
+ {loading === true && (
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
new file mode 100644
index 0000000000000..8c0a7c6a91f67
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+// @flow
+import * as React from 'react';
+import styled from 'styled-components';
+import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui';
+import { useFetcher } from '../../../../hooks/useFetcher';
+import { useUrlParams } from '../../../../hooks/useUrlParams';
+import { BackEndLabel, FrontEndLabel, PageViewsLabel } from '../translations';
+
+export const formatBigValue = (val?: number | null, fixed?: number): string => {
+ if (val && val >= 1000) {
+ const result = val / 1000;
+ if (fixed) {
+ return result.toFixed(fixed) + 'k';
+ }
+ return result + 'k';
+ }
+ return val + '';
+};
+
+const ClFlexGroup = styled(EuiFlexGroup)`
+ flex-direction: row;
+ @media only screen and (max-width: 768px) {
+ flex-direction: row;
+ justify-content: space-between;
+ }
+`;
+
+export const ClientMetrics = () => {
+ const { urlParams, uiFilters } = useUrlParams();
+
+ const { start, end } = urlParams;
+
+ const { data, status } = useFetcher(
+ (callApmApi) => {
+ if (start && end) {
+ return callApmApi({
+ pathname: '/api/apm/rum/client-metrics',
+ params: {
+ query: { start, end, uiFilters: JSON.stringify(uiFilters) },
+ },
+ });
+ }
+ },
+ [start, end, uiFilters]
+ );
+
+ const STAT_STYLE = { width: '240px' };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx
new file mode 100644
index 0000000000000..9c89b8bc161b7
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as React from 'react';
+import {
+ AnnotationDomainTypes,
+ LineAnnotation,
+ LineAnnotationDatum,
+ LineAnnotationStyle,
+} from '@elastic/charts';
+import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
+import styled from 'styled-components';
+
+interface Props {
+ percentiles?: Record;
+}
+
+function generateAnnotationData(
+ values?: Record
+): LineAnnotationDatum[] {
+ return Object.entries(values ?? {}).map((value, index) => ({
+ dataValue: value[1],
+ details: `${(+value[0]).toFixed(0)}`,
+ }));
+}
+
+const PercentileMarker = styled.span`
+ position: relative;
+ bottom: 140px;
+`;
+
+export const PercentileAnnotations = ({ percentiles }: Props) => {
+ const dataValues = generateAnnotationData(percentiles) ?? [];
+
+ const style: Partial = {
+ line: {
+ strokeWidth: 1,
+ stroke: euiLightVars.euiColorSecondary,
+ opacity: 1,
+ },
+ };
+
+ return (
+ <>
+ {dataValues.map((annotation, index) => (
+ {annotation.details}th}
+ />
+ ))}
+ >
+ );
+};
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
new file mode 100644
index 0000000000000..c7a0b64f6a8b8
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
@@ -0,0 +1,144 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiTitle,
+} from '@elastic/eui';
+import {
+ Axis,
+ Chart,
+ ScaleType,
+ LineSeries,
+ CurveType,
+ BrushEndListener,
+ Settings,
+ TooltipValueFormatter,
+ TooltipValue,
+} from '@elastic/charts';
+import { Position } from '@elastic/charts/dist/utils/commons';
+import { useUrlParams } from '../../../../hooks/useUrlParams';
+import { useFetcher } from '../../../../hooks/useFetcher';
+import { ChartWrapper } from '../ChartWrapper';
+import { PercentileAnnotations } from './PercentileAnnotations';
+import {
+ PageLoadDistLabel,
+ PageLoadTimeLabel,
+ PercPageLoadedLabel,
+ ResetZoomLabel,
+} from '../translations';
+
+export const PageLoadDistribution = () => {
+ const { urlParams, uiFilters } = useUrlParams();
+
+ const { start, end } = urlParams;
+
+ const [percentileRange, setPercentileRange] = useState<{
+ min: string | null;
+ max: string | null;
+ }>({
+ min: null,
+ max: null,
+ });
+
+ const { data, status } = useFetcher(
+ (callApmApi) => {
+ if (start && end) {
+ return callApmApi({
+ pathname: '/api/apm/rum-client/page-load-distribution',
+ params: {
+ query: {
+ start,
+ end,
+ uiFilters: JSON.stringify(uiFilters),
+ ...(percentileRange.min && percentileRange.max
+ ? {
+ minPercentile: percentileRange.min,
+ maxPercentile: percentileRange.max,
+ }
+ : {}),
+ },
+ },
+ });
+ }
+ },
+ [end, start, uiFilters, percentileRange.min, percentileRange.max]
+ );
+
+ const onBrushEnd: BrushEndListener = ({ x }) => {
+ if (!x) {
+ return;
+ }
+ const [minX, maxX] = x;
+ setPercentileRange({ min: String(minX), max: String(maxX) });
+ };
+
+ const headerFormatter: TooltipValueFormatter = (tooltip: TooltipValue) => {
+ return (
+
+
{tooltip.value} seconds
+
+ );
+ };
+
+ const tooltipProps = {
+ headerFormatter,
+ };
+
+ return (
+
+
+
+
+ {PageLoadDistLabel}
+
+
+
+ {
+ setPercentileRange({ min: null, max: null });
+ }}
+ fill={percentileRange.min !== null && percentileRange.max !== null}
+ >
+ {ResetZoomLabel}
+
+
+
+
+
+
+
+
+
+ Number(d).toFixed(1) + ' %'}
+ />
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
new file mode 100644
index 0000000000000..cc41bd4352947
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
@@ -0,0 +1,111 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as React from 'react';
+import { EuiTitle } from '@elastic/eui';
+import {
+ Axis,
+ BarSeries,
+ BrushEndListener,
+ Chart,
+ niceTimeFormatByDay,
+ ScaleType,
+ Settings,
+ timeFormatter,
+} from '@elastic/charts';
+import moment from 'moment';
+import { Position } from '@elastic/charts/dist/utils/commons';
+import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
+import { useUrlParams } from '../../../../hooks/useUrlParams';
+import { useFetcher } from '../../../../hooks/useFetcher';
+import { ChartWrapper } from '../ChartWrapper';
+import { DateTimeLabel, PageViewsLabel } from '../translations';
+import { history } from '../../../../utils/history';
+import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
+import { formatBigValue } from '../ClientMetrics';
+
+export const PageViewsTrend = () => {
+ const { urlParams, uiFilters } = useUrlParams();
+
+ const { start, end } = urlParams;
+
+ const { data, status } = useFetcher(
+ (callApmApi) => {
+ if (start && end) {
+ return callApmApi({
+ pathname: '/api/apm/rum-client/page-view-trends',
+ params: {
+ query: {
+ start,
+ end,
+ uiFilters: JSON.stringify(uiFilters),
+ },
+ },
+ });
+ }
+ },
+ [end, start, uiFilters]
+ );
+ const formatter = timeFormatter(niceTimeFormatByDay(2));
+
+ const onBrushEnd: BrushEndListener = ({ x }) => {
+ if (!x) {
+ return;
+ }
+ const [minX, maxX] = x;
+
+ const rangeFrom = moment(minX).toISOString();
+ const rangeTo = moment(maxX).toISOString();
+
+ history.push({
+ ...history.location,
+ search: fromQuery({
+ ...toQuery(history.location.search),
+ rangeFrom,
+ rangeTo,
+ }),
+ });
+ };
+
+ return (
+
+
+ {PageViewsLabel}
+
+
+
+
+
+ formatBigValue(Number(d))}
+ />
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx
new file mode 100644
index 0000000000000..e3fa7374afb38
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiSpacer,
+ EuiPanel,
+} from '@elastic/eui';
+import React from 'react';
+import { ClientMetrics } from './ClientMetrics';
+import { PageViewsTrend } from './PageViewsTrend';
+import { PageLoadDistribution } from './PageLoadDistribution';
+import { getWhatIsGoingOnLabel } from './translations';
+import { useUrlParams } from '../../../hooks/useUrlParams';
+
+export function RumDashboard() {
+ const { urlParams } = useUrlParams();
+
+ const { environment } = urlParams;
+
+ let environmentLabel = environment || 'all environments';
+
+ if (environment === 'ENVIRONMENT_NOT_DEFINED') {
+ environmentLabel = 'undefined environment';
+ }
+
+ return (
+ <>
+
+ {getWhatIsGoingOnLabel(environmentLabel)}
+
+
+
+
+
+
+
+
+ Page load times
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx
new file mode 100644
index 0000000000000..8f21065b0dab0
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import React, { useMemo } from 'react';
+import { useTrackPageview } from '../../../../../observability/public';
+import { LocalUIFilters } from '../../shared/LocalUIFilters';
+import { PROJECTION } from '../../../../common/projections/typings';
+import { RumDashboard } from './RumDashboard';
+
+export function RumOverview() {
+ useTrackPageview({ app: 'apm', path: 'rum_overview' });
+ useTrackPageview({ app: 'apm', path: 'rum_overview', delay: 15000 });
+
+ const localUIFiltersConfig = useMemo(() => {
+ const config: React.ComponentProps = {
+ filterNames: ['transactionUrl', 'location', 'device', 'os', 'browser'],
+ projection: PROJECTION.RUM_OVERVIEW,
+ };
+
+ return config;
+ }, []);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts
new file mode 100644
index 0000000000000..c2aed41a55c7d
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts
@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const EndUserExperienceLabel = i18n.translate(
+ 'xpack.apm.rum.dashboard.title',
+ {
+ defaultMessage: 'End User Experience',
+ }
+);
+
+export const getWhatIsGoingOnLabel = (environmentVal: string) =>
+ i18n.translate('xpack.apm.rum.dashboard.environment.title', {
+ defaultMessage: `What's going on in {environmentVal}?`,
+ values: { environmentVal },
+ });
+
+export const BackEndLabel = i18n.translate('xpack.apm.rum.dashboard.backend', {
+ defaultMessage: 'Backend',
+});
+
+export const FrontEndLabel = i18n.translate(
+ 'xpack.apm.rum.dashboard.frontend',
+ {
+ defaultMessage: 'Frontend',
+ }
+);
+
+export const PageViewsLabel = i18n.translate(
+ 'xpack.apm.rum.dashboard.pageViews',
+ {
+ defaultMessage: 'Page views',
+ }
+);
+
+export const DateTimeLabel = i18n.translate(
+ 'xpack.apm.rum.dashboard.dateTime.label',
+ {
+ defaultMessage: 'Date / Time',
+ }
+);
+
+export const PercPageLoadedLabel = i18n.translate(
+ 'xpack.apm.rum.dashboard.pagesLoaded.label',
+ {
+ defaultMessage: 'Pages loaded',
+ }
+);
+
+export const PageLoadTimeLabel = i18n.translate(
+ 'xpack.apm.rum.dashboard.pageLoadTime.label',
+ {
+ defaultMessage: 'Page load time (seconds)',
+ }
+);
+
+export const PageLoadDistLabel = i18n.translate(
+ 'xpack.apm.rum.dashboard.pageLoadDistribution.label',
+ {
+ defaultMessage: 'Page load distribution',
+ }
+);
+
+export const ResetZoomLabel = i18n.translate(
+ 'xpack.apm.rum.dashboard.resetZoom.label',
+ {
+ defaultMessage: 'Reset zoom',
+ }
+);
diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx
index 2f35e329720de..81bdbdad805d6 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx
@@ -22,6 +22,8 @@ import { ServiceMap } from '../ServiceMap';
import { ServiceMetrics } from '../ServiceMetrics';
import { ServiceNodeOverview } from '../ServiceNodeOverview';
import { TransactionOverview } from '../TransactionOverview';
+import { RumOverviewLink } from '../../shared/Links/apm/RumOverviewLink';
+import { RumOverview } from '../RumDashboard';
interface Props {
tab: 'transactions' | 'errors' | 'metrics' | 'nodes' | 'service-map';
@@ -110,6 +112,20 @@ export function ServiceDetailTabs({ tab }: Props) {
tabs.push(serviceMapTab);
}
+ if (isRumAgentName(agentName)) {
+ tabs.push({
+ link: (
+
+ {i18n.translate('xpack.apm.home.rumTabLabel', {
+ defaultMessage: 'Real User Monitoring',
+ })}
+
+ ),
+ render: () => ,
+ name: 'rum-overview',
+ });
+ }
+
const selectedTab = tabs.find((serviceTab) => serviceTab.name === tab);
return (
diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
index d01deb8160858..eab685a4c1ab4 100644
--- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
@@ -76,10 +76,10 @@ export function KueryBar() {
});
// The bar should be disabled when viewing the service map
- const disabled = /\/service-map$/.test(location.pathname);
+ const disabled = /\/(service-map|rum-overview)$/.test(location.pathname);
const disabledPlaceholder = i18n.translate(
'xpack.apm.kueryBar.disabledPlaceholder',
- { defaultMessage: 'Search is not available for service map' }
+ { defaultMessage: 'Search is not available here' }
);
async function onChange(inputValue: string, selectionStart: number) {
diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx
new file mode 100644
index 0000000000000..abca9817bd69d
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/apm/RumOverviewLink.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import { APMLink, APMLinkExtendProps } from './APMLink';
+import { useUrlParams } from '../../../../hooks/useUrlParams';
+import { pickKeys } from '../../../../../common/utils/pick_keys';
+
+const RumOverviewLink = (props: APMLinkExtendProps) => {
+ const { urlParams } = useUrlParams();
+
+ const persistedFilters = pickKeys(
+ urlParams,
+ 'transactionResult',
+ 'host',
+ 'containerId',
+ 'podName'
+ );
+
+ return ;
+};
+
+export { RumOverviewLink };
diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
index 7d96c490fcd70..db36ad1ede91c 100644
--- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
+++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
@@ -10,7 +10,7 @@ import {
PROCESSOR_EVENT,
SERVICE_NAME,
} from '../../../../common/elasticsearch_fieldnames';
-import { rangeFilter } from '../../helpers/range_filter';
+import { rangeFilter } from '../../../../common/utils/range_filter';
import {
Setup,
SetupTimeRange,
diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts
index b157abd0b7e76..3d20f84ccfbc2 100644
--- a/x-pack/plugins/apm/server/lib/errors/get_error_group.ts
+++ b/x-pack/plugins/apm/server/lib/errors/get_error_group.ts
@@ -12,7 +12,7 @@ import {
} from '../../../common/elasticsearch_fieldnames';
import { PromiseReturnType } from '../../../typings/common';
import { APMError } from '../../../typings/es_schemas/ui/apm_error';
-import { rangeFilter } from '../helpers/range_filter';
+import { rangeFilter } from '../../../common/utils/range_filter';
import {
Setup,
SetupTimeRange,
diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts
index d558e3942a42b..e91d3953942d9 100644
--- a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts
+++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts
@@ -10,12 +10,12 @@ import {
} from '../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../common/processor_event';
import { getMetricsDateHistogramParams } from '../helpers/metrics';
-import { rangeFilter } from '../helpers/range_filter';
import {
Setup,
SetupTimeRange,
SetupUIFilters,
} from '../helpers/setup_request';
+import { rangeFilter } from '../../../common/utils/range_filter';
export async function getErrorRate({
serviceName,
diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
new file mode 100644
index 0000000000000..7d8f31aaeca7f
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
@@ -0,0 +1,194 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`rum client dashboard queries fetches client metrics 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "backEnd": Object {
+ "avg": Object {
+ "field": "transaction.marks.agent.timeToFirstByte",
+ "missing": 0,
+ },
+ },
+ "domInteractive": Object {
+ "avg": Object {
+ "field": "transaction.marks.agent.domInteractive",
+ "missing": 0,
+ },
+ },
+ "pageViews": Object {
+ "value_count": Object {
+ "field": "transaction.type",
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "transaction",
+ },
+ },
+ Object {
+ "term": Object {
+ "transaction.type": "page-load",
+ },
+ },
+ Object {
+ "exists": Object {
+ "field": "transaction.marks.navigationTiming.fetchStart",
+ },
+ },
+ Object {
+ "term": Object {
+ "my.custom.ui.filter": "foo-bar",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`rum client dashboard queries fetches page load distribution 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "durationMinMax": Object {
+ "min": Object {
+ "field": "transaction.duration.us",
+ "missing": 0,
+ },
+ },
+ "durationPercentiles": Object {
+ "percentiles": Object {
+ "field": "transaction.duration.us",
+ "percents": Array [
+ 50,
+ 75,
+ 90,
+ 95,
+ 99,
+ ],
+ "script": Object {
+ "lang": "painless",
+ "params": Object {
+ "timeUnit": 1000,
+ },
+ "source": "doc['transaction.duration.us'].value / params.timeUnit",
+ },
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "transaction",
+ },
+ },
+ Object {
+ "term": Object {
+ "transaction.type": "page-load",
+ },
+ },
+ Object {
+ "exists": Object {
+ "field": "transaction.marks.navigationTiming.fetchStart",
+ },
+ },
+ Object {
+ "term": Object {
+ "my.custom.ui.filter": "foo-bar",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`rum client dashboard queries fetches page view trends 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "pageViews": Object {
+ "aggs": Object {
+ "trans_count": Object {
+ "value_count": Object {
+ "field": "transaction.type",
+ },
+ },
+ },
+ "auto_date_histogram": Object {
+ "buckets": 50,
+ "field": "@timestamp",
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "transaction",
+ },
+ },
+ Object {
+ "term": Object {
+ "transaction.type": "page-load",
+ },
+ },
+ Object {
+ "exists": Object {
+ "field": "transaction.marks.navigationTiming.fetchStart",
+ },
+ },
+ Object {
+ "term": Object {
+ "my.custom.ui.filter": "foo-bar",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts
new file mode 100644
index 0000000000000..8b3f733fc402a
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getRumOverviewProjection } from '../../../common/projections/rum_overview';
+import { mergeProjection } from '../../../common/projections/util/merge_projection';
+import {
+ Setup,
+ SetupTimeRange,
+ SetupUIFilters,
+} from '../helpers/setup_request';
+
+export async function getClientMetrics({
+ setup,
+}: {
+ setup: Setup & SetupTimeRange & SetupUIFilters;
+}) {
+ const projection = getRumOverviewProjection({
+ setup,
+ });
+
+ const params = mergeProjection(projection, {
+ body: {
+ size: 0,
+ query: {
+ bool: projection.body.query.bool,
+ },
+ aggs: {
+ pageViews: { value_count: { field: 'transaction.type' } },
+ backEnd: {
+ avg: {
+ field: 'transaction.marks.agent.timeToFirstByte',
+ missing: 0,
+ },
+ },
+ domInteractive: {
+ avg: {
+ field: 'transaction.marks.agent.domInteractive',
+ missing: 0,
+ },
+ },
+ },
+ },
+ });
+
+ const { client } = setup;
+
+ const response = await client.search(params);
+ const { backEnd, domInteractive, pageViews } = response.aggregations!;
+
+ // Divide by 1000 to convert ms into seconds
+ return {
+ pageViews,
+ backEnd: { value: (backEnd.value || 0) / 1000 },
+ frontEnd: {
+ value: ((domInteractive.value || 0) - (backEnd.value || 0)) / 1000,
+ },
+ };
+}
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts
new file mode 100644
index 0000000000000..3c563946e4052
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts
@@ -0,0 +1,140 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getRumOverviewProjection } from '../../../common/projections/rum_overview';
+import { mergeProjection } from '../../../common/projections/util/merge_projection';
+import {
+ Setup,
+ SetupTimeRange,
+ SetupUIFilters,
+} from '../helpers/setup_request';
+
+export async function getPageLoadDistribution({
+ setup,
+ minPercentile,
+ maxPercentile,
+}: {
+ setup: Setup & SetupTimeRange & SetupUIFilters;
+ minPercentile?: string;
+ maxPercentile?: string;
+}) {
+ const projection = getRumOverviewProjection({
+ setup,
+ });
+
+ const params = mergeProjection(projection, {
+ body: {
+ size: 0,
+ query: {
+ bool: projection.body.query.bool,
+ },
+ aggs: {
+ durationMinMax: {
+ min: {
+ field: 'transaction.duration.us',
+ missing: 0,
+ },
+ },
+ durationPercentiles: {
+ percentiles: {
+ field: 'transaction.duration.us',
+ percents: [50, 75, 90, 95, 99],
+ script: {
+ lang: 'painless',
+ source: "doc['transaction.duration.us'].value / params.timeUnit",
+ params: {
+ timeUnit: 1000,
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ const { client } = setup;
+
+ const {
+ aggregations,
+ hits: { total },
+ } = await client.search(params);
+
+ if (total.value === 0) {
+ return null;
+ }
+
+ const minDuration = (aggregations?.durationMinMax.value ?? 0) / 1000;
+
+ const minPerc = minPercentile ? +minPercentile : minDuration;
+
+ const maxPercentileQuery =
+ aggregations?.durationPercentiles.values['99.0'] ?? 100;
+
+ const maxPerc = maxPercentile ? +maxPercentile : maxPercentileQuery;
+
+ const pageDist = await getPercentilesDistribution(setup, minPerc, maxPerc);
+ return {
+ pageLoadDistribution: pageDist,
+ percentiles: aggregations?.durationPercentiles.values,
+ };
+}
+
+const getPercentilesDistribution = async (
+ setup: Setup & SetupTimeRange & SetupUIFilters,
+ minPercentiles: number,
+ maxPercentile: number
+) => {
+ const stepValue = (maxPercentile - minPercentiles) / 50;
+ const stepValues = [];
+ for (let i = 1; i < 50; i++) {
+ stepValues.push((stepValue * i + minPercentiles).toFixed(2));
+ }
+
+ const projection = getRumOverviewProjection({
+ setup,
+ });
+
+ const params = mergeProjection(projection, {
+ body: {
+ size: 0,
+ query: {
+ bool: projection.body.query.bool,
+ },
+ aggs: {
+ loadDistribution: {
+ percentile_ranks: {
+ field: 'transaction.duration.us',
+ values: stepValues,
+ keyed: false,
+ script: {
+ lang: 'painless',
+ source: "doc['transaction.duration.us'].value / params.timeUnit",
+ params: {
+ timeUnit: 1000,
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ const { client } = setup;
+
+ const { aggregations } = await client.search(params);
+
+ const pageDist = (aggregations?.loadDistribution.values ?? []) as Array<{
+ key: number;
+ value: number;
+ }>;
+
+ return pageDist.map(({ key, value }, index: number, arr) => {
+ return {
+ x: key,
+ y: index === 0 ? value : value - arr[index - 1].value,
+ };
+ });
+};
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts
new file mode 100644
index 0000000000000..126605206d299
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getRumOverviewProjection } from '../../../common/projections/rum_overview';
+import { mergeProjection } from '../../../common/projections/util/merge_projection';
+import {
+ Setup,
+ SetupTimeRange,
+ SetupUIFilters,
+} from '../helpers/setup_request';
+
+export async function getPageViewTrends({
+ setup,
+}: {
+ setup: Setup & SetupTimeRange & SetupUIFilters;
+}) {
+ const projection = getRumOverviewProjection({
+ setup,
+ });
+
+ const params = mergeProjection(projection, {
+ body: {
+ size: 0,
+ query: {
+ bool: projection.body.query.bool,
+ },
+ aggs: {
+ pageViews: {
+ auto_date_histogram: {
+ field: '@timestamp',
+ buckets: 50,
+ },
+ aggs: {
+ trans_count: {
+ value_count: {
+ field: 'transaction.type',
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ const { client } = setup;
+
+ const response = await client.search(params);
+
+ const result = response.aggregations?.pageViews.buckets ?? [];
+ return result.map(({ key, trans_count }) => ({
+ x: key,
+ y: trans_count.value,
+ }));
+}
diff --git a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
new file mode 100644
index 0000000000000..5f5a48eced746
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ SearchParamsMock,
+ inspectSearchParams,
+} from '../../../public/utils/testHelpers';
+import { getClientMetrics } from './get_client_metrics';
+import { getPageViewTrends } from './get_page_view_trends';
+import { getPageLoadDistribution } from './get_page_load_distribution';
+
+describe('rum client dashboard queries', () => {
+ let mock: SearchParamsMock;
+
+ afterEach(() => {
+ mock.teardown();
+ });
+
+ it('fetches client metrics', async () => {
+ mock = await inspectSearchParams((setup) =>
+ getClientMetrics({
+ setup,
+ })
+ );
+
+ expect(mock.params).toMatchSnapshot();
+ });
+
+ it('fetches page view trends', async () => {
+ mock = await inspectSearchParams((setup) =>
+ getPageViewTrends({
+ setup,
+ })
+ );
+
+ expect(mock.params).toMatchSnapshot();
+ });
+
+ it('fetches page load distribution', async () => {
+ mock = await inspectSearchParams((setup) =>
+ getPageLoadDistribution({
+ setup,
+ minPercentile: '0',
+ maxPercentile: '99',
+ })
+ );
+ expect(mock.params).toMatchSnapshot();
+ });
+});
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
index d069e93397611..e521efa687388 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
@@ -6,7 +6,7 @@
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { ESFilter } from '../../../typings/elasticsearch';
-import { rangeFilter } from '../helpers/range_filter';
+import { rangeFilter } from '../../../common/utils/range_filter';
import {
PROCESSOR_EVENT,
SERVICE_ENVIRONMENT,
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts
index 6eba84f2205a1..11c3a00f32980 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts
@@ -5,7 +5,7 @@
*/
import { uniq, take, sortBy } from 'lodash';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
-import { rangeFilter } from '../helpers/range_filter';
+import { rangeFilter } from '../../../common/utils/range_filter';
import { ESFilter } from '../../../typings/elasticsearch';
import {
PROCESSOR_EVENT,
diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts
index 9829c5cb25182..6da5d195cf194 100644
--- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts
+++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts
@@ -7,7 +7,7 @@ import { isNumber } from 'lodash';
import { Annotation, AnnotationType } from '../../../../common/annotations';
import { SetupTimeRange, Setup } from '../../helpers/setup_request';
import { ESFilter } from '../../../../typings/elasticsearch';
-import { rangeFilter } from '../../helpers/range_filter';
+import { rangeFilter } from '../../../../common/utils/range_filter';
import {
PROCESSOR_EVENT,
SERVICE_NAME,
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts
index 0b016828d5f00..8d75d746c7fca 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts
@@ -8,7 +8,7 @@ import {
AGENT_NAME,
SERVICE_NAME,
} from '../../../common/elasticsearch_fieldnames';
-import { rangeFilter } from '../helpers/range_filter';
+import { rangeFilter } from '../../../common/utils/range_filter';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
export async function getServiceAgentName(
diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts
index 963dea4d8322c..d88be4055dc21 100644
--- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts
+++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts
@@ -8,7 +8,7 @@ import {
SERVICE_NAME,
TRANSACTION_TYPE,
} from '../../../common/elasticsearch_fieldnames';
-import { rangeFilter } from '../helpers/range_filter';
+import { rangeFilter } from '../../../common/utils/range_filter';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
export async function getServiceTransactionTypes(
diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts
index e96b323958fd7..f9374558dfeeb 100644
--- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts
+++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts
@@ -16,7 +16,7 @@ import {
import { Span } from '../../../typings/es_schemas/ui/span';
import { Transaction } from '../../../typings/es_schemas/ui/transaction';
import { APMError } from '../../../typings/es_schemas/ui/apm_error';
-import { rangeFilter } from '../helpers/range_filter';
+import { rangeFilter } from '../../../common/utils/range_filter';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { PromiseValueType } from '../../../typings/common';
diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts
index 90dd41cb9b0c8..e3d688b694380 100644
--- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts
@@ -13,7 +13,7 @@ import {
USER_AGENT_NAME,
TRANSACTION_DURATION,
} from '../../../../common/elasticsearch_fieldnames';
-import { rangeFilter } from '../../helpers/range_filter';
+import { rangeFilter } from '../../../../common/utils/range_filter';
import { getBucketSize } from '../../helpers/get_bucket_size';
import { Options } from '.';
import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts
index cc23055e34672..ea6213f64ee36 100644
--- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts
@@ -17,7 +17,7 @@ import {
SetupTimeRange,
SetupUIFilters,
} from '../../helpers/setup_request';
-import { rangeFilter } from '../../helpers/range_filter';
+import { rangeFilter } from '../../../../common/utils/range_filter';
import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
export async function getTransactionAvgDurationByCountry({
diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts
index 713423f8953d5..5af8b9f78cec1 100644
--- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts
@@ -20,7 +20,7 @@ import {
SetupTimeRange,
SetupUIFilters,
} from '../../helpers/setup_request';
-import { rangeFilter } from '../../helpers/range_filter';
+import { rangeFilter } from '../../../../common/utils/range_filter';
import { getMetricsDateHistogramParams } from '../../helpers/metrics';
import { MAX_KPIS } from './constants';
import { getVizColorForIndex } from '../../../../common/viz_colors';
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts
index 71c40010a2a3f..8e19af926ce02 100644
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts
@@ -15,7 +15,7 @@ import {
} from '../../../../../common/elasticsearch_fieldnames';
import { PromiseReturnType } from '../../../../../../observability/typings/common';
import { getBucketSize } from '../../../helpers/get_bucket_size';
-import { rangeFilter } from '../../../helpers/range_filter';
+import { rangeFilter } from '../../../../../common/utils/range_filter';
import {
Setup,
SetupTimeRange,
diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts
index 920552d1c1aeb..3f8bf635712be 100644
--- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts
@@ -15,7 +15,7 @@ import {
TRANSACTION_SAMPLED,
TRANSACTION_TYPE,
} from '../../../../../common/elasticsearch_fieldnames';
-import { rangeFilter } from '../../../helpers/range_filter';
+import { rangeFilter } from '../../../../../common/utils/range_filter';
import {
Setup,
SetupTimeRange,
diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts
index 60dc16b6a546c..a7de93a3bf650 100644
--- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts
@@ -10,7 +10,7 @@ import {
TRANSACTION_ID,
} from '../../../../common/elasticsearch_fieldnames';
import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
-import { rangeFilter } from '../../helpers/range_filter';
+import { rangeFilter } from '../../../../common/utils/range_filter';
import {
Setup,
SetupTimeRange,
diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts
index ccbe7a19d2f82..3fca30634be6a 100644
--- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts
+++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts
@@ -9,7 +9,7 @@ import {
SERVICE_ENVIRONMENT,
SERVICE_NAME,
} from '../../../common/elasticsearch_fieldnames';
-import { rangeFilter } from '../helpers/range_filter';
+import { rangeFilter } from '../../../common/utils/range_filter';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
import { ESFilter } from '../../../typings/elasticsearch';
diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts
index 8f35664c2599c..25a559cb07a3d 100644
--- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts
+++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts
@@ -11,6 +11,11 @@ import {
HOST_NAME,
TRANSACTION_RESULT,
SERVICE_VERSION,
+ TRANSACTION_URL,
+ USER_AGENT_NAME,
+ USER_AGENT_DEVICE,
+ CLIENT_GEO,
+ USER_AGENT_OS,
} from '../../../../common/elasticsearch_fieldnames';
const filtersByName = {
@@ -50,6 +55,36 @@ const filtersByName = {
}),
fieldName: SERVICE_VERSION,
},
+ transactionUrl: {
+ title: i18n.translate('xpack.apm.localFilters.titles.transactionUrl', {
+ defaultMessage: 'Url',
+ }),
+ fieldName: TRANSACTION_URL,
+ },
+ browser: {
+ title: i18n.translate('xpack.apm.localFilters.titles.browser', {
+ defaultMessage: 'Browser',
+ }),
+ fieldName: USER_AGENT_NAME,
+ },
+ device: {
+ title: i18n.translate('xpack.apm.localFilters.titles.device', {
+ defaultMessage: 'Device',
+ }),
+ fieldName: USER_AGENT_DEVICE,
+ },
+ location: {
+ title: i18n.translate('xpack.apm.localFilters.titles.location', {
+ defaultMessage: 'Location',
+ }),
+ fieldName: CLIENT_GEO,
+ },
+ os: {
+ title: i18n.translate('xpack.apm.localFilters.titles.os', {
+ defaultMessage: 'OS',
+ }),
+ fieldName: USER_AGENT_OS,
+ },
};
export type LocalUIFilterName = keyof typeof filtersByName;
diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts
index bdfb49fa30828..a34690aff43b4 100644
--- a/x-pack/plugins/apm/server/routes/create_apm_api.ts
+++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts
@@ -59,6 +59,7 @@ import {
transactionsLocalFiltersRoute,
serviceNodesLocalFiltersRoute,
uiFiltersEnvironmentsRoute,
+ rumOverviewLocalFiltersRoute,
} from './ui_filters';
import { createApi } from './create_api';
import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map';
@@ -70,6 +71,11 @@ import {
listCustomLinksRoute,
customLinkTransactionRoute,
} from './settings/custom_link';
+import {
+ rumClientMetricsRoute,
+ rumPageViewsTrendRoute,
+ rumPageLoadDistributionRoute,
+} from './rum_client';
const createApmApi = () => {
const api = createApi()
@@ -148,7 +154,13 @@ const createApmApi = () => {
.add(updateCustomLinkRoute)
.add(deleteCustomLinkRoute)
.add(listCustomLinksRoute)
- .add(customLinkTransactionRoute);
+ .add(customLinkTransactionRoute)
+
+ // Rum Overview
+ .add(rumOverviewLocalFiltersRoute)
+ .add(rumPageViewsTrendRoute)
+ .add(rumPageLoadDistributionRoute)
+ .add(rumClientMetricsRoute);
return api;
};
diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts
new file mode 100644
index 0000000000000..9b5f6529b1783
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/rum_client.ts
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as t from 'io-ts';
+import { createRoute } from './create_route';
+import { setupRequest } from '../lib/helpers/setup_request';
+import { getClientMetrics } from '../lib/rum_client/get_client_metrics';
+import { rangeRt, uiFiltersRt } from './default_api_types';
+import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends';
+import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distribution';
+
+export const percentileRangeRt = t.partial({
+ minPercentile: t.string,
+ maxPercentile: t.string,
+});
+
+export const rumClientMetricsRoute = createRoute(() => ({
+ path: '/api/apm/rum/client-metrics',
+ params: {
+ query: t.intersection([uiFiltersRt, rangeRt]),
+ },
+ handler: async ({ context, request }) => {
+ const setup = await setupRequest(context, request);
+
+ return getClientMetrics({ setup });
+ },
+}));
+
+export const rumPageLoadDistributionRoute = createRoute(() => ({
+ path: '/api/apm/rum-client/page-load-distribution',
+ params: {
+ query: t.intersection([uiFiltersRt, rangeRt, percentileRangeRt]),
+ },
+ handler: async ({ context, request }) => {
+ const setup = await setupRequest(context, request);
+
+ const {
+ query: { minPercentile, maxPercentile },
+ } = context.params;
+
+ return getPageLoadDistribution({ setup, minPercentile, maxPercentile });
+ },
+}));
+
+export const rumPageViewsTrendRoute = createRoute(() => ({
+ path: '/api/apm/rum-client/page-view-trends',
+ params: {
+ query: t.intersection([uiFiltersRt, rangeRt]),
+ },
+ handler: async ({ context, request }) => {
+ const setup = await setupRequest(context, request);
+ return getPageViewTrends({ setup });
+ },
+}));
diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts
index 8f4ef94b86ac5..280645d4de8d0 100644
--- a/x-pack/plugins/apm/server/routes/ui_filters.ts
+++ b/x-pack/plugins/apm/server/routes/ui_filters.ts
@@ -29,6 +29,7 @@ import { createRoute } from './create_route';
import { uiFiltersRt, rangeRt } from './default_api_types';
import { jsonRt } from '../../common/runtime_types/json_rt';
import { getServiceNodesProjection } from '../../common/projections/service_nodes';
+import { getRumOverviewProjection } from '../../common/projections/rum_overview';
export const uiFiltersEnvironmentsRoute = createRoute(() => ({
path: '/api/apm/ui_filters/environments',
@@ -221,6 +222,16 @@ export const serviceNodesLocalFiltersRoute = createLocalFiltersRoute({
}),
});
+export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({
+ path: '/api/apm/ui_filters/local_filters/rumOverview',
+ getProjection: ({ setup }) => {
+ return getRumOverviewProjection({
+ setup,
+ });
+ },
+ queryRt: t.type({}),
+});
+
type BaseQueryType = typeof localUiBaseQueryRt;
type GetProjection<
diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
index 0739e8e6120bf..6ee26caa4ef7c 100644
--- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
+++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
@@ -137,6 +137,15 @@ export interface AggregationOptionsByType {
>;
keyed?: boolean;
};
+ auto_date_histogram: {
+ field: string;
+ buckets: number;
+ };
+ percentile_ranks: {
+ field: string;
+ values: string[];
+ keyed?: boolean;
+ };
}
type AggregationType = keyof AggregationOptionsByType;
@@ -301,6 +310,23 @@ interface AggregationResponsePart<
? Record
: { buckets: DateRangeBucket[] };
};
+ auto_date_histogram: {
+ buckets: Array<
+ {
+ doc_count: number;
+ key: number;
+ key_as_string: string;
+ } & BucketSubAggregationResponse<
+ TAggregationOptionsMap['aggs'],
+ TDocument
+ >
+ >;
+ interval: string;
+ };
+
+ percentile_ranks: {
+ values: Record | Array<{ key: number; value: number }>;
+ };
}
// Type for debugging purposes. If you see an error in AggregationResponseMap
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index c44d08975182b..72ee01cb94c12 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -4233,7 +4233,6 @@
"xpack.apm.jvmsTable.noJvmsLabel": "JVM が見つかりませんでした",
"xpack.apm.jvmsTable.nonHeapMemoryColumnLabel": "非ヒープ領域の平均",
"xpack.apm.jvmsTable.threadCountColumnLabel": "最大スレッド数",
- "xpack.apm.kueryBar.disabledPlaceholder": "サービスマップの検索は利用できません",
"xpack.apm.kueryBar.placeholder": "検索 {event, select,\n transaction {トランザクション}\n metric {メトリック}\n error {エラー}\n other {その他}\n } (E.g. {queryExample})",
"xpack.apm.license.betaBadge": "ベータ",
"xpack.apm.license.betaTooltipMessage": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index fcd2e0bfa5c89..a0359cf59e3d3 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -4236,7 +4236,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": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。",