[RUM Dashboard] User experience metrics (#77384)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2020-09-16 15:34:28 +02:00 committed by GitHub
parent 6c5258a8c3
commit 10b192b5b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 8395 additions and 412 deletions

View file

@ -5,11 +5,9 @@
*/
import * as React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useFetcher } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { CLS_LABEL, FID_LABEL, LCP_LABEL } from './translations';
import { CoreVitalItem } from './CoreVitalItem';
import { UXMetrics } from '../UXMetrics';
const CoreVitalsThresholds = {
LCP: { good: '2.5s', bad: '4.0s' },
@ -17,27 +15,12 @@ const CoreVitalsThresholds = {
CLS: { good: '0.1', bad: '0.25' },
};
export function CoreVitals() {
const { urlParams, uiFilters } = useUrlParams();
const { start, end } = urlParams;
const { data, status } = useFetcher(
(callApmApi) => {
const { serviceName } = uiFilters;
if (start && end && serviceName) {
return callApmApi({
pathname: '/api/apm/rum-client/web-core-vitals',
params: {
query: { start, end, uiFilters: JSON.stringify(uiFilters) },
},
});
}
return Promise.resolve(null);
},
[start, end, uiFilters]
);
interface Props {
data?: UXMetrics | null;
loading: boolean;
}
export function CoreVitals({ data, loading }: Props) {
const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks } = data || {};
return (
@ -47,7 +30,7 @@ export function CoreVitals() {
title={LCP_LABEL}
value={lcp ? lcp + 's' : '0'}
ranks={lcpRanks}
loading={status !== 'success'}
loading={loading}
thresholds={CoreVitalsThresholds.LCP}
/>
</EuiFlexItem>
@ -56,7 +39,7 @@ export function CoreVitals() {
title={FID_LABEL}
value={fid ? fid + 's' : '0'}
ranks={fidRanks}
loading={status !== 'success'}
loading={loading}
thresholds={CoreVitalsThresholds.FID}
/>
</EuiFlexItem>
@ -65,7 +48,7 @@ export function CoreVitals() {
title={CLS_LABEL}
value={cls ?? '0'}
ranks={clsRanks}
loading={status !== 'success'}
loading={loading}
thresholds={CoreVitalsThresholds.CLS}
/>
</EuiFlexItem>

View file

@ -26,6 +26,27 @@ export const TBT_LABEL = i18n.translate('xpack.apm.rum.coreVitals.tbt', {
defaultMessage: 'Total blocking time',
});
export const NO_OF_LONG_TASK = i18n.translate(
'xpack.apm.rum.uxMetrics.noOfLongTasks',
{
defaultMessage: 'No. of long tasks',
}
);
export const LONGEST_LONG_TASK = i18n.translate(
'xpack.apm.rum.uxMetrics.longestLongTasks',
{
defaultMessage: 'Longest long task duration',
}
);
export const SUM_LONG_TASKS = i18n.translate(
'xpack.apm.rum.uxMetrics.sumLongTasks',
{
defaultMessage: 'Total long tasks duration',
}
);
export const POOR_LABEL = i18n.translate('xpack.apm.rum.coreVitals.poor', {
defaultMessage: 'a poor',
});

View file

@ -17,7 +17,7 @@ import { PageViewsTrend } from './PageViewsTrend';
import { PageLoadDistribution } from './PageLoadDistribution';
import { I18LABELS } from './translations';
import { VisitorBreakdown } from './VisitorBreakdown';
import { CoreVitals } from './CoreVitals';
import { UXMetrics } from './UXMetrics';
import { VisitorBreakdownMap } from './VisitorBreakdownMap';
export function RumDashboard() {
@ -37,17 +37,7 @@ export function RumDashboard() {
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={1} data-cy={`client-metrics`}>
<EuiTitle size="xs">
<h3>{I18LABELS.coreWebVitals}</h3>
</EuiTitle>
<EuiSpacer size="s" />
<CoreVitals />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<UXMetrics />
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s" wrap>

View file

@ -0,0 +1,103 @@
/*
* 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 { EuiFlexItem, EuiStat, EuiFlexGroup } from '@elastic/eui';
import { UXMetrics } from './index';
import {
FCP_LABEL,
LONGEST_LONG_TASK,
NO_OF_LONG_TASK,
SUM_LONG_TASKS,
TBT_LABEL,
} from '../CoreVitals/translations';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useFetcher } from '../../../../hooks/useFetcher';
export function formatToSec(
value?: number | string,
fromUnit = 'MicroSec'
): string {
const valueInMs = Number(value ?? 0) / (fromUnit === 'MicroSec' ? 1000 : 1);
if (valueInMs < 1000) {
return valueInMs + ' ms';
}
return (valueInMs / 1000).toFixed(2) + ' s';
}
const STAT_STYLE = { width: '240px' };
interface Props {
data?: UXMetrics | null;
loading: boolean;
}
export function KeyUXMetrics({ data, loading }: Props) {
const { urlParams, uiFilters } = useUrlParams();
const { start, end, serviceName } = urlParams;
const { data: longTaskData, status } = useFetcher(
(callApmApi) => {
if (start && end && serviceName) {
return callApmApi({
pathname: '/api/apm/rum-client/long-task-metrics',
params: {
query: { start, end, uiFilters: JSON.stringify(uiFilters) },
},
});
}
return Promise.resolve(null);
},
[start, end, serviceName, uiFilters]
);
// Note: FCP value is in ms unit
return (
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
titleSize="s"
title={formatToSec(data?.fcp, 'ms')}
description={FCP_LABEL}
isLoading={loading}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
titleSize="s"
title={formatToSec(data?.tbt)}
description={TBT_LABEL}
isLoading={loading}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
titleSize="s"
title={longTaskData?.noOfLongTasks ?? 0}
description={NO_OF_LONG_TASK}
isLoading={status !== 'success'}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
titleSize="s"
title={formatToSec(longTaskData?.longestLongTask)}
description={LONGEST_LONG_TASK}
isLoading={status !== 'success'}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
titleSize="s"
title={formatToSec(longTaskData?.sumOfLongTasks)}
description={SUM_LONG_TASKS}
isLoading={status !== 'success'}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,19 @@
/*
* 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 { formatToSec } from '../KeyUXMetrics';
describe('FormatToSec', () => {
test('it returns the expected value', () => {
expect(formatToSec(3413000)).toStrictEqual('3.41 s');
expect(formatToSec(15548000)).toStrictEqual('15.55 s');
expect(formatToSec(1147.5, 'ms')).toStrictEqual('1.15 s');
expect(formatToSec(114, 'ms')).toStrictEqual('114 ms');
expect(formatToSec(undefined, 'ms')).toStrictEqual('0 ms');
expect(formatToSec(undefined)).toStrictEqual('0 ms');
expect(formatToSec('1123232')).toStrictEqual('1.12 s');
});
});

View file

@ -0,0 +1,78 @@
/*
* 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 {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { I18LABELS } from '../translations';
import { CoreVitals } from '../CoreVitals';
import { KeyUXMetrics } from './KeyUXMetrics';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useFetcher } from '../../../../hooks/useFetcher';
export interface UXMetrics {
cls: string;
fid: string;
lcp: string;
tbt: string;
fcp: number;
lcpRanks: number[];
fidRanks: number[];
clsRanks: number[];
}
export function UXMetrics() {
const { urlParams, uiFilters } = useUrlParams();
const { start, end } = urlParams;
const { data, status } = useFetcher(
(callApmApi) => {
const { serviceName } = uiFilters;
if (start && end && serviceName) {
return callApmApi({
pathname: '/api/apm/rum-client/web-core-vitals',
params: {
query: { start, end, uiFilters: JSON.stringify(uiFilters) },
},
});
}
return Promise.resolve(null);
},
[start, end, uiFilters]
);
return (
<EuiPanel>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={1} data-cy={`client-metrics`}>
<EuiTitle size="s">
<h2>{I18LABELS.userExperienceMetrics}</h2>
</EuiTitle>
<EuiSpacer size="s" />
<KeyUXMetrics data={data} loading={status !== 'success'} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule />
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={1} data-cy={`client-metrics`}>
<EuiTitle size="xs">
<h3>{I18LABELS.coreWebVitals}</h3>
</EuiTitle>
<EuiSpacer size="s" />
<CoreVitals data={data} loading={status !== 'success'} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -64,6 +64,9 @@ export const I18LABELS = {
defaultMessage: 'Operating system',
}
),
userExperienceMetrics: i18n.translate('xpack.apm.rum.userExperienceMetrics', {
defaultMessage: 'User experience metrics',
}),
avgPageLoadDuration: i18n.translate(
'xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration',
{

View file

@ -72,6 +72,64 @@ Object {
}
`;
exports[`rum client dashboard queries fetches long task metrics 1`] = `
Object {
"apm": Object {
"events": Array [
"span",
],
},
"body": Object {
"aggs": Object {
"transIds": Object {
"aggs": Object {
"longestLongTask": Object {
"max": Object {
"field": "span.duration.us",
},
},
"sumLongTask": Object {
"sum": Object {
"field": "span.duration.us",
},
},
},
"terms": Object {
"field": "transaction.id",
"size": 1000,
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
Object {
"term": Object {
"span.type": "longtask",
},
},
Object {
"term": Object {
"my.custom.ui.filter": "foo-bar",
},
},
],
},
},
"size": 0,
},
}
`;
exports[`rum client dashboard queries fetches page load distribution 1`] = `
Object {
"apm": Object {
@ -190,6 +248,126 @@ Object {
}
`;
exports[`rum client dashboard queries fetches rum core vitals 1`] = `
Object {
"apm": Object {
"events": Array [
"transaction",
],
},
"body": Object {
"aggs": Object {
"cls": Object {
"percentiles": Object {
"field": "transaction.experience.cls",
"percents": Array [
50,
],
},
},
"clsRanks": Object {
"percentile_ranks": Object {
"field": "transaction.experience.cls",
"keyed": false,
"values": Array [
0.1,
0.25,
],
},
},
"fcp": Object {
"percentiles": Object {
"field": "transaction.marks.agent.firstContentfulPaint",
"percents": Array [
50,
],
},
},
"fid": Object {
"percentiles": Object {
"field": "transaction.experience.fid",
"percents": Array [
50,
],
},
},
"fidRanks": Object {
"percentile_ranks": Object {
"field": "transaction.experience.fid",
"keyed": false,
"values": Array [
100,
300,
],
},
},
"lcp": Object {
"percentiles": Object {
"field": "transaction.marks.agent.largestContentfulPaint",
"percents": Array [
50,
],
},
},
"lcpRanks": Object {
"percentile_ranks": Object {
"field": "transaction.marks.agent.largestContentfulPaint",
"keyed": false,
"values": Array [
2500,
4000,
],
},
},
"tbt": Object {
"percentiles": Object {
"field": "transaction.experience.tbt",
"percents": Array [
50,
],
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 1528113600000,
"lte": 1528977600000,
},
},
},
Object {
"term": Object {
"transaction.type": "page-load",
},
},
Object {
"exists": Object {
"field": "transaction.marks.navigationTiming.fetchStart",
},
},
Object {
"term": Object {
"my.custom.ui.filter": "foo-bar",
},
},
Object {
"term": Object {
"user_agent.name": "Chrome",
},
},
],
},
},
"size": 0,
},
}
`;
exports[`rum client dashboard queries fetches rum services 1`] = `
Object {
"apm": Object {

View file

@ -0,0 +1,117 @@
/*
* 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 {
getRumLongTasksProjection,
getRumOverviewProjection,
} from '../../projections/rum_overview';
import { mergeProjection } from '../../projections/util/merge_projection';
import {
Setup,
SetupTimeRange,
SetupUIFilters,
} from '../helpers/setup_request';
import { SPAN_DURATION } from '../../../common/elasticsearch_fieldnames';
export async function getLongTaskMetrics({
setup,
}: {
setup: Setup & SetupTimeRange & SetupUIFilters;
}) {
const projection = getRumLongTasksProjection({
setup,
});
const params = mergeProjection(projection, {
body: {
size: 0,
query: {
bool: projection.body.query.bool,
},
aggs: {
transIds: {
terms: {
field: 'transaction.id',
size: 1000,
},
aggs: {
sumLongTask: {
sum: {
field: SPAN_DURATION,
},
},
longestLongTask: {
max: {
field: SPAN_DURATION,
},
},
},
},
},
},
});
const { apmEventClient } = setup;
const response = await apmEventClient.search(params);
const { transIds } = response.aggregations ?? {};
const validTransactions: string[] = await filterPageLoadTransactions(
setup,
(transIds?.buckets ?? []).map((bucket) => bucket.key as string)
);
let noOfLongTasks = 0;
let sumOfLongTasks = 0;
let longestLongTask = 0;
(transIds?.buckets ?? []).forEach((bucket) => {
if (validTransactions.includes(bucket.key as string)) {
noOfLongTasks += bucket.doc_count;
sumOfLongTasks += bucket.sumLongTask.value ?? 0;
if ((bucket.longestLongTask.value ?? 0) > longestLongTask) {
longestLongTask = bucket.longestLongTask.value!;
}
}
});
return {
noOfLongTasks,
sumOfLongTasks,
longestLongTask,
};
}
async function filterPageLoadTransactions(
setup: Setup & SetupTimeRange & SetupUIFilters,
transactionIds: string[]
) {
const projection = getRumOverviewProjection({
setup,
});
const params = mergeProjection(projection, {
body: {
size: transactionIds.length,
query: {
bool: {
must: [
{
terms: {
'transaction.id': transactionIds,
},
},
],
filter: [...projection.body.query.bool.filter],
},
},
_source: ['transaction.id'],
},
});
const { apmEventClient } = setup;
const response = await apmEventClient.search(params);
return response.hits.hits.map((hit) => (hit._source as any).transaction.id)!;
}

View file

@ -13,8 +13,10 @@ import {
} from '../helpers/setup_request';
import {
CLS_FIELD,
FCP_FIELD,
FID_FIELD,
LCP_FIELD,
TBT_FIELD,
} from '../../../common/elasticsearch_fieldnames';
export async function getWebCoreVitals({
@ -60,6 +62,18 @@ export async function getWebCoreVitals({
percents: [50],
},
},
tbt: {
percentiles: {
field: TBT_FIELD,
percents: [50],
},
},
fcp: {
percentiles: {
field: FCP_FIELD,
percents: [50],
},
},
lcpRanks: {
percentile_ranks: {
field: LCP_FIELD,
@ -88,21 +102,13 @@ export async function getWebCoreVitals({
const { apmEventClient } = setup;
const response = await apmEventClient.search(params);
const {
lcp,
cls,
fid,
lcpRanks,
fidRanks,
clsRanks,
} = response.aggregations!;
const { lcp, cls, fid, tbt, fcp, lcpRanks, fidRanks, clsRanks } =
response.aggregations ?? {};
const getRanksPercentages = (
ranks: Array<{ key: number; value: number }>
) => {
const ranksVal = (ranks ?? [0, 0]).map(
({ value }) => value?.toFixed(0) ?? 0
);
const ranksVal = ranks.map(({ value }) => value?.toFixed(0) ?? 0);
return [
Number(ranksVal?.[0]),
Number(ranksVal?.[1]) - Number(ranksVal?.[0]),
@ -110,14 +116,21 @@ export async function getWebCoreVitals({
];
};
const defaultRanks = [
{ value: 0, key: 0 },
{ value: 0, key: 0 },
];
// Divide by 1000 to convert ms into seconds
return {
cls: String(cls.values['50.0'] || 0),
fid: ((fid.values['50.0'] || 0) / 1000).toFixed(2),
lcp: ((lcp.values['50.0'] || 0) / 1000).toFixed(2),
cls: String(cls?.values['50.0'] || 0),
fid: ((fid?.values['50.0'] || 0) / 1000).toFixed(2),
lcp: ((lcp?.values['50.0'] || 0) / 1000).toFixed(2),
tbt: ((tbt?.values['50.0'] || 0) / 1000).toFixed(2),
fcp: fcp?.values['50.0'] || 0,
lcpRanks: getRanksPercentages(lcpRanks.values),
fidRanks: getRanksPercentages(fidRanks.values),
clsRanks: getRanksPercentages(clsRanks.values),
lcpRanks: getRanksPercentages(lcpRanks?.values ?? defaultRanks),
fidRanks: getRanksPercentages(fidRanks?.values ?? defaultRanks),
clsRanks: getRanksPercentages(clsRanks?.values ?? defaultRanks),
};
}

View file

@ -12,6 +12,8 @@ import { getClientMetrics } from './get_client_metrics';
import { getPageViewTrends } from './get_page_view_trends';
import { getPageLoadDistribution } from './get_page_load_distribution';
import { getRumServices } from './get_rum_services';
import { getLongTaskMetrics } from './get_long_task_metrics';
import { getWebCoreVitals } from './get_web_core_vitals';
describe('rum client dashboard queries', () => {
let mock: SearchParamsMock;
@ -59,4 +61,22 @@ describe('rum client dashboard queries', () => {
);
expect(mock.params).toMatchSnapshot();
});
it('fetches rum core vitals', async () => {
mock = await inspectSearchParams((setup) =>
getWebCoreVitals({
setup,
})
);
expect(mock.params).toMatchSnapshot();
});
it('fetches long task metrics', async () => {
mock = await inspectSearchParams((setup) =>
getLongTaskMetrics({
setup,
})
);
expect(mock.params).toMatchSnapshot();
});
});

View file

@ -9,7 +9,10 @@ import {
SetupTimeRange,
SetupUIFilters,
} from '../../server/lib/helpers/setup_request';
import { TRANSACTION_TYPE } from '../../common/elasticsearch_fieldnames';
import {
SPAN_TYPE,
TRANSACTION_TYPE,
} from '../../common/elasticsearch_fieldnames';
import { rangeFilter } from '../../common/utils/range_filter';
import { ProcessorEvent } from '../../common/processor_event';
@ -45,3 +48,30 @@ export function getRumOverviewProjection({
},
};
}
export function getRumLongTasksProjection({
setup,
}: {
setup: Setup & SetupTimeRange & SetupUIFilters;
}) {
const { start, end, uiFiltersES } = setup;
const bool = {
filter: [
{ range: rangeFilter(start, end) },
{ term: { [SPAN_TYPE]: 'longtask' } },
...uiFiltersES,
],
};
return {
apm: {
events: [ProcessorEvent.span],
},
body: {
query: {
bool,
},
},
};
}

View file

@ -78,6 +78,7 @@ import {
rumServicesRoute,
rumVisitorsBreakdownRoute,
rumWebCoreVitals,
rumLongTaskMetrics,
} from './rum_client';
import {
observabilityOverviewHasDataRoute,
@ -174,6 +175,7 @@ const createApmApi = () => {
.add(rumServicesRoute)
.add(rumVisitorsBreakdownRoute)
.add(rumWebCoreVitals)
.add(rumLongTaskMetrics)
// Observability dashboard
.add(observabilityOverviewHasDataRoute)

View file

@ -15,6 +15,7 @@ import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdow
import { getRumServices } from '../lib/rum_client/get_rum_services';
import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown';
import { getWebCoreVitals } from '../lib/rum_client/get_web_core_vitals';
import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics';
export const percentileRangeRt = t.partial({
minPercentile: t.string,
@ -130,3 +131,15 @@ export const rumWebCoreVitals = createRoute(() => ({
return getWebCoreVitals({ setup });
},
}));
export const rumLongTaskMetrics = createRoute(() => ({
path: '/api/apm/rum-client/long-task-metrics',
params: {
query: t.intersection([uiFiltersRt, rangeRt]),
},
handler: async ({ context, request }) => {
const setup = await setupRequest(context, request);
return getLongTaskMetrics({ setup });
},
}));

View file

@ -94,6 +94,9 @@ export interface AggregationOptionsByType {
percents?: number[];
hdr?: { number_of_significant_value_digits: number };
} & AggregationSourceOptions;
stats: {
field: string;
};
extended_stats: {
field: string;
};
@ -223,6 +226,13 @@ interface AggregationResponsePart<
percentiles: {
values: Record<string, number | null>;
};
stats: {
count: number;
min: number | null;
max: number | null;
avg: number | null;
sum: number | null;
};
extended_stats: {
count: number;
min: number | null;

View file

@ -12,7 +12,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext)
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('RUM Services', () => {
describe('CSM Services', () => {
describe('when there is no data', () => {
it('returns empty list', async () => {
const response = await supertest.get(
@ -41,12 +41,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext)
expect(response.status).to.be(200);
expectSnapshot(response.body).toMatchInline(`
Array [
"client",
"opbean-client-rum",
]
`);
expectSnapshot(response.body).toMatchInline(`Array []`);
});
});
});

View file

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { expectSnapshot } from '../../../common/match_snapshot';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
export default function rumServicesApiTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('CSM long task metrics', () => {
describe('when there is no data', () => {
it('returns empty list', async () => {
const response = await supertest.get(
'/api/apm/rum-client/long-task-metrics?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D'
);
expect(response.status).to.be(200);
expect(response.body).to.eql({
longestLongTask: 0,
noOfLongTasks: 0,
sumOfLongTasks: 0,
});
});
});
describe('when there is data', () => {
before(async () => {
await esArchiver.load('8.0.0');
await esArchiver.load('rum_8.0.0');
});
after(async () => {
await esArchiver.unload('8.0.0');
await esArchiver.unload('rum_8.0.0');
});
it('returns web core vitals values', async () => {
const response = await supertest.get(
'/api/apm/rum-client/long-task-metrics?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D'
);
expect(response.status).to.be(200);
expectSnapshot(response.body).toMatchInline(`
Object {
"longestLongTask": 109000,
"noOfLongTasks": 2,
"sumOfLongTasks": 168000,
}
`);
});
});
});
}

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { expectSnapshot } from '../../../common/match_snapshot';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
export default function rumServicesApiTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('CSM web core vitals', () => {
describe('when there is no data', () => {
it('returns empty list', async () => {
const response = await supertest.get(
'/api/apm/rum-client/web-core-vitals?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D'
);
expect(response.status).to.be(200);
expect(response.body).to.eql({
cls: '0',
fid: '0.00',
lcp: '0.00',
tbt: '0.00',
fcp: 0,
lcpRanks: [0, 0, 100],
fidRanks: [0, 0, 100],
clsRanks: [0, 0, 100],
});
});
});
describe('when there is data', () => {
before(async () => {
await esArchiver.load('8.0.0');
await esArchiver.load('rum_8.0.0');
});
after(async () => {
await esArchiver.unload('8.0.0');
await esArchiver.unload('rum_8.0.0');
});
it('returns web core vitals values', async () => {
const response = await supertest.get(
'/api/apm/rum-client/web-core-vitals?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D'
);
expect(response.status).to.be(200);
expectSnapshot(response.body).toMatchInline(`
Object {
"cls": "0",
"clsRanks": Array [
100,
0,
0,
],
"fcp": 1072,
"fid": "1.35",
"fidRanks": Array [
0,
0,
100,
],
"lcp": "1.27",
"lcpRanks": Array [
100,
0,
0,
],
"tbt": "0.00",
}
`);
});
});
});
}

View file

@ -15,7 +15,6 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr
describe('Services', function () {
loadTestFile(require.resolve('./services/annotations'));
loadTestFile(require.resolve('./services/rum_services.ts'));
loadTestFile(require.resolve('./services/top_services.ts'));
});
@ -30,5 +29,11 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr
describe('Service Maps', function () {
loadTestFile(require.resolve('./service_maps/service_maps'));
});
describe('CSM', function () {
loadTestFile(require.resolve('./csm/csm_services.ts'));
loadTestFile(require.resolve('./csm/web_core_vitals.ts'));
loadTestFile(require.resolve('./csm/long_task_metrics.ts'));
});
});
}