[UX] Migrate long task metric to UX (#134711)

* Migrate long task metric query to ux plugin

* Add e2e test

* Clean up integration tests

* Update e2e test user

Co-authored-by: Shahzad <shahzad.muhammad@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Emilio Alvarez Piñeiro 2022-06-24 10:31:53 +02:00 committed by GitHub
parent 2732f26419
commit ce02f07d09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 252 additions and 239 deletions

View file

@ -1,79 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rum client dashboard queries fetches long task metrics 1`] = `
Object {
"apm": Object {
"events": Array [
"transaction",
],
},
"body": Object {
"aggs": Object {
"longTaskCount": Object {
"percentiles": Object {
"field": "transaction.experience.longtask.count",
"hdr": Object {
"number_of_significant_value_digits": 3,
},
"percents": Array [
50,
],
},
},
"longTaskMax": Object {
"percentiles": Object {
"field": "transaction.experience.longtask.max",
"hdr": Object {
"number_of_significant_value_digits": 3,
},
"percents": Array [
50,
],
},
},
"longTaskSum": Object {
"percentiles": Object {
"field": "transaction.experience.longtask.sum",
"hdr": Object {
"number_of_significant_value_digits": 3,
},
"percents": Array [
50,
],
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 0,
"lte": 50000,
},
},
},
Object {
"term": Object {
"transaction.type": "page-load",
},
},
Object {
"exists": Object {
"field": "transaction.marks.navigationTiming.fetchStart",
},
},
],
"must_not": Array [],
},
},
"size": 0,
},
}
`;
exports[`rum client dashboard queries fetches page load distribution 1`] = `
Object {
"apm": Object {

View file

@ -11,7 +11,6 @@ import {
} from '../../utils/test_helpers';
import { getPageViewTrends } from './get_page_view_trends';
import { getPageLoadDistribution } from './get_page_load_distribution';
import { getLongTaskMetrics } from './get_long_task_metrics';
describe('rum client dashboard queries', () => {
let mock: SearchParamsMock;
@ -48,15 +47,4 @@ describe('rum client dashboard queries', () => {
);
expect(mock.params).toMatchSnapshot();
});
it('fetches long task metrics', async () => {
mock = await inspectSearchParams((setup) =>
getLongTaskMetrics({
setup,
start: 0,
end: 50000,
})
);
expect(mock.params).toMatchSnapshot();
});
});

View file

@ -7,7 +7,6 @@
import * as t from 'io-ts';
import { Logger } from '@kbn/core/server';
import { setupRequest, Setup } from '../../lib/helpers/setup_request';
import { getLongTaskMetrics } from './get_long_task_metrics';
import { getPageLoadDistribution } from './get_page_load_distribution';
import { getPageViewTrends } from './get_page_view_trends';
import { getPageLoadDistBreakdown } from './get_pl_dist_breakdown';
@ -180,35 +179,6 @@ const rumVisitorsBreakdownRoute = createApmServerRoute({
},
});
const rumLongTaskMetrics = createApmServerRoute({
endpoint: 'GET /internal/apm/ux/long-task-metrics',
params: t.type({
query: uxQueryRt,
}),
options: { tags: ['access:apm'] },
handler: async (
resources
): Promise<{
noOfLongTasks: number;
sumOfLongTasks: number;
longestLongTask: number;
}> => {
const setup = await setupUXRequest(resources);
const {
query: { urlQuery, percentile, start, end },
} = resources.params;
return getLongTaskMetrics({
setup,
urlQuery,
percentile: percentile ? Number(percentile) : undefined,
start,
end,
});
},
});
function decodeUiFilters(
logger: Logger,
uiFiltersEncoded?: string
@ -243,5 +213,4 @@ export const rumRouteRepository = {
...rumPageLoadDistBreakdownRoute,
...rumPageViewsTrendRoute,
...rumVisitorsBreakdownRoute,
...rumLongTaskMetrics,
};

View file

@ -9,3 +9,4 @@ export * from './core_web_vitals';
export * from './url_ux_query.journey';
export * from './ux_js_errors.journey';
export * from './ux_client_metrics.journey';
export * from './ux_long_task_metric_journey';

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { journey, step, before, expect } from '@elastic/synthetics';
import { UXDashboardDatePicker } from '../page_objects/date_picker';
import { byTestId, loginToKibana, waitForLoadingToFinish } from './utils';
const longestMetric = 'uxLongestTask';
const countMetric = 'uxLongTaskCount';
const sumMetric = 'uxSumLongTask';
const longestMetricValue = `Longest long task duration
237 ms`;
const countMetricValue = `No. of long tasks
3`;
const sumMetricValue = `Total long tasks duration
428 ms`;
journey('UX LongTaskMetrics', async ({ page, params }) => {
before(async () => {
await waitForLoadingToFinish({ page });
});
const queryParams = {
percentile: '50',
rangeFrom: '2020-05-18T11:51:00.000Z',
rangeTo: '2021-10-30T06:37:15.536Z',
};
const queryString = new URLSearchParams(queryParams).toString();
const baseUrl = `${params.kibanaUrl}/app/ux`;
step('Go to UX Dashboard', async () => {
await page.goto(`${baseUrl}?${queryString}`, {
waitUntil: 'networkidle',
});
await loginToKibana({
page,
user: { username: 'viewer', password: 'changeme' },
});
});
step('Set date range', async () => {
const datePickerPage = new UXDashboardDatePicker(page);
await datePickerPage.setDefaultE2eRange();
});
step('Confirm metrics values', async () => {
// Wait until chart data is loaded
page.waitForLoadState('networkidle');
// wait for first metric to be shown
page.waitForSelector(`text="237 ms"`);
let metric = await (
await page.waitForSelector(byTestId(longestMetric))
).innerText();
expect(metric).toBe(longestMetricValue);
metric = await (
await page.waitForSelector(byTestId(countMetric))
).innerText();
expect(metric).toBe(countMetricValue);
metric = await (
await page.waitForSelector(byTestId(sumMetric))
).innerText();
expect(metric).toBe(sumMetricValue);
});
});

View file

@ -7,20 +7,18 @@
import React from 'react';
import { render, Matcher } from '@testing-library/react';
import * as fetcherHook from '../../../../hooks/use_fetcher';
import * as queryHook from '../../../../hooks/use_long_task_metrics_query';
import { KeyUXMetrics } from './key_ux_metrics';
import { FETCH_STATUS } from '@kbn/observability-plugin/public';
describe('KeyUXMetrics', () => {
it('renders metrics with correct formats', () => {
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
jest.spyOn(queryHook, 'useLongTaskMetricsQuery').mockReturnValue({
data: {
noOfLongTasks: 3.0009765625,
sumOfLongTasks: 520.4375,
longestLongTask: 271.4375,
},
status: FETCH_STATUS.SUCCESS,
refetch: jest.fn(),
loading: false,
});
const { getAllByText } = render(
<KeyUXMetrics

View file

@ -9,6 +9,7 @@ import React from 'react';
import { EuiFlexItem, EuiStat, EuiFlexGroup, EuiIconTip } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { UXMetrics } from '@kbn/observability-plugin/public';
import { useLongTaskMetricsQuery } from '../../../../hooks/use_long_task_metrics_query';
import {
DATA_UNDEFINED_LABEL,
FCP_LABEL,
@ -22,8 +23,6 @@ import {
TBT_LABEL,
TBT_TOOLTIP,
} from './translations';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useUxQuery } from '../hooks/use_ux_query';
export function formatToSec(
value?: number | string,
@ -50,23 +49,8 @@ function formatTitle(unit: string, value?: number | null) {
}
export function KeyUXMetrics({ data, loading }: Props) {
const uxQuery = useUxQuery();
const { data: longTaskData, status } = useFetcher(
(callApmApi) => {
if (uxQuery) {
return callApmApi('GET /internal/apm/ux/long-task-metrics', {
params: {
query: {
...uxQuery,
},
},
});
}
return Promise.resolve(null);
},
[uxQuery]
);
const { data: longTaskData, loading: loadingLongTask } =
useLongTaskMetricsQuery();
// Note: FCP value is in ms unit
return (
@ -99,6 +83,7 @@ export function KeyUXMetrics({ data, loading }: Props) {
</EuiFlexItem>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
data-test-subj="uxLongTaskCount"
titleSize="s"
title={
longTaskData?.noOfLongTasks !== undefined
@ -114,11 +99,12 @@ export function KeyUXMetrics({ data, loading }: Props) {
/>
</>
}
isLoading={status !== 'success'}
isLoading={!!loadingLongTask}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
data-test-subj="uxLongestTask"
titleSize="s"
title={formatTitle('ms', longTaskData?.longestLongTask)}
description={
@ -130,11 +116,12 @@ export function KeyUXMetrics({ data, loading }: Props) {
/>
</>
}
isLoading={status !== 'success'}
isLoading={!!loadingLongTask}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={STAT_STYLE}>
<EuiStat
data-test-subj="uxSumLongTask"
titleSize="s"
title={formatTitle('ms', longTaskData?.sumOfLongTasks)}
description={
@ -146,7 +133,7 @@ export function KeyUXMetrics({ data, loading }: Props) {
/>
</>
}
isLoading={status !== 'success'}
isLoading={!!loadingLongTask}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEsSearch } from '@kbn/observability-plugin/public';
import { useMemo } from 'react';
import { useDataView } from '../components/app/rum_dashboard/local_uifilters/use_data_view';
import { longTaskMetricsQuery } from '../services/data/long_task_metrics_query';
import { callDateMath } from '../services/data/call_date_math';
import { useLegacyUrlParams } from '../context/url_params_context/use_url_params';
export function useLongTaskMetricsQuery() {
const {
rangeId,
urlParams: { start, end, searchTerm, percentile },
uxUiFilters,
} = useLegacyUrlParams();
const { dataViewTitle } = useDataView();
const { data: esQueryResponse, loading } = useEsSearch(
{
index: dataViewTitle,
...longTaskMetricsQuery(
callDateMath(start),
callDateMath(end),
percentile,
searchTerm,
uxUiFilters
),
},
[start, end, percentile, searchTerm, uxUiFilters, rangeId, dataViewTitle],
{ name: 'UxLongTaskMetrics' }
);
const data = useMemo(() => {
if (!esQueryResponse) return {};
const pkey = Number(percentile).toFixed(1);
const { longTaskSum, longTaskCount, longTaskMax } =
esQueryResponse.aggregations ?? {};
return {
noOfLongTasks: longTaskCount?.values[pkey] ?? 0,
sumOfLongTasks: longTaskSum?.values[pkey] ?? 0,
longestLongTask: longTaskMax?.values[pkey] ?? 0,
};
}, [esQueryResponse, percentile]);
return { data, loading };
}

View file

@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`longTaskMetricsQuery fetches long task metrics 1`] = `
Object {
"body": Object {
"aggs": Object {
"longTaskCount": Object {
"percentiles": Object {
"field": "transaction.experience.longtask.count",
"hdr": Object {
"number_of_significant_value_digits": 3,
},
"percents": Array [
50,
],
},
},
"longTaskMax": Object {
"percentiles": Object {
"field": "transaction.experience.longtask.max",
"hdr": Object {
"number_of_significant_value_digits": 3,
},
"percents": Array [
50,
],
},
},
"longTaskSum": Object {
"percentiles": Object {
"field": "transaction.experience.longtask.sum",
"hdr": Object {
"number_of_significant_value_digits": 3,
},
"percents": Array [
50,
],
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 0,
"lte": 50000,
},
},
},
Object {
"term": Object {
"transaction.type": "page-load",
},
},
Object {
"terms": Object {
"processor.event": Array [
"transaction",
],
},
},
Object {
"exists": Object {
"field": "transaction.marks.navigationTiming.fetchStart",
},
},
],
"must_not": Array [],
},
},
"size": 0,
},
}
`;

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { longTaskMetricsQuery } from './long_task_metrics_query';
describe('longTaskMetricsQuery', () => {
it('fetches long task metrics', () => {
const query = longTaskMetricsQuery(0, 50000, 50, '', {
environment: 'ENVIRONMENT_ALL',
});
expect(query).toMatchSnapshot();
});
});

View file

@ -5,27 +5,23 @@
* 2.0.
*/
import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions';
import { mergeProjection } from '../../projections/util/merge_projection';
import { SetupUX } from './route';
import { mergeProjection } from '../../../common/utils/merge_projection';
import { SetupUX, UxUIFilters } from '../../../typings/ui_filters';
import { PERCENTILE_DEFAULT } from './core_web_vitals_query';
import { getRumPageLoadTransactionsProjection } from './projections';
const LONG_TASK_SUM_FIELD = 'transaction.experience.longtask.sum';
const LONG_TASK_COUNT_FIELD = 'transaction.experience.longtask.count';
const LONG_TASK_MAX_FIELD = 'transaction.experience.longtask.max';
export async function getLongTaskMetrics({
setup,
urlQuery,
percentile = 50,
start,
end,
}: {
setup: SetupUX;
urlQuery?: string;
percentile?: number;
start: number;
end: number;
}) {
export function longTaskMetricsQuery(
start: number,
end: number,
percentile: number = PERCENTILE_DEFAULT,
urlQuery?: string,
uiFilters?: UxUIFilters
) {
const setup: SetupUX = { uiFilters: uiFilters ? uiFilters : {} };
const projection = getRumPageLoadTransactionsProjection({
setup,
urlQuery,
@ -68,18 +64,5 @@ export async function getLongTaskMetrics({
},
});
const { apmEventClient } = setup;
const response = await apmEventClient.search('get_long_task_metrics', params);
const pkey = percentile.toFixed(1);
const { longTaskSum, longTaskCount, longTaskMax } =
response.aggregations ?? {};
return {
noOfLongTasks: longTaskCount?.values[pkey] ?? 0,
sumOfLongTasks: longTaskSum?.values[pkey] ?? 0,
longestLongTask: longTaskMax?.values[pkey] ?? 0,
};
return params;
}

View file

@ -1,65 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../common/ftr_provider_context';
export default function rumServicesApiTests({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
registry.when('CSM long task metrics without data', { config: 'trial', archives: [] }, () => {
it('returns empty list', async () => {
const response = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/ux/long-task-metrics',
params: {
query: {
start: '2020-09-07T20:35:54.654Z',
end: '2020-09-14T20:35:54.654Z',
uiFilters: '{"serviceName":["elastic-co-rum-test"]}',
},
},
});
expect(response.status).to.be(200);
expect(response.body).to.eql({
longestLongTask: 0,
noOfLongTasks: 0,
sumOfLongTasks: 0,
});
});
});
registry.when(
'CSM long task metrics with data',
{ config: 'trial', archives: ['8.0.0', 'rum_8.0.0'] },
() => {
it('returns web core vitals values', async () => {
const response = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/ux/long-task-metrics',
params: {
query: {
start: '2020-09-07T20:35:54.654Z',
end: '2020-09-16T20:35:54.654Z',
uiFilters: '{"serviceName":["kibana-frontend-8_0_0"]}',
},
},
});
expect(response.status).to.be(200);
expectSnapshot(response.body).toMatchInline(`
Object {
"longestLongTask": 0,
"noOfLongTasks": 0,
"sumOfLongTasks": 0,
}
`);
});
}
);
}