[RUM Dashboard] Initial Version (#68778)

Co-authored-by: Casper Hübertz <casper@formgeist.com>
This commit is contained in:
Shahzad 2020-06-18 17:13:28 +02:00 committed by GitHub
parent e552a96121
commit 72111702e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1551 additions and 46 deletions

View file

@ -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"`;

View file

@ -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';

View file

@ -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,

View file

@ -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) {

View 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,
},
},
};
}

View file

@ -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,

View file

@ -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,

View file

@ -29,4 +29,5 @@ export enum PROJECTION {
METRICS = 'metrics',
ERROR_GROUPS = 'errorGroups',
SERVICE_NODES = 'serviceNodes',
RUM_OVERVIEW = 'rumOverview',
}

View file

@ -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');

View file

@ -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

View file

@ -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"
}
}
}

View file

@ -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();
});

View file

@ -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;

View file

@ -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*)"

View file

@ -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"

View file

@ -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%;
`;

View file

@ -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}>

View file

@ -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,
},
];

View file

@ -26,4 +26,5 @@ export enum RouteName {
SERVICE_NODES = 'nodes',
LINK_TO_TRACE = 'link_to_trace',
CUSTOMIZE_UI = 'customize_ui',
RUM_OVERVIEW = 'rum_overview',
}

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>}
/>
))}
</>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
</>
);
}

View file

@ -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>
</>
);
}

View file

@ -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',
}
);

View file

@ -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 (

View file

@ -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) {

View file

@ -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 };

View file

@ -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,

View file

@ -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,

View 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",
}
`;

View file

@ -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,
},
};
}

View file

@ -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,
};
});
};

View 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 { 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,
}));
}

View 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();
});
});

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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(

View file

@ -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(

View file

@ -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';

View file

@ -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';

View file

@ -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({

View file

@ -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';

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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';

View file

@ -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;

View file

@ -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;
};

View 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 });
},
}));

View file

@ -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<

View file

@ -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

View file

@ -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": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。",

View file

@ -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": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。",