Move UX JS Errors out of APM UI (#133040)

* Migrate Ux-jsErrors query to Ux plugin

* Add untracked files

* Clean up old query

* Clean up jsErrors api from apm

* Remove stale snapshot

* Add e2e test for JsErrors chart
This commit is contained in:
Emilio Alvarez Piñeiro 2022-06-06 18:36:41 +02:00 committed by GitHub
parent a4f2a81163
commit ccea9773e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 350 additions and 313 deletions

View file

@ -73,102 +73,6 @@ Object {
}
`;
exports[`rum client dashboard queries fetches js errors 1`] = `
Object {
"apm": Object {
"events": Array [
"error",
],
},
"body": Object {
"aggs": Object {
"errors": Object {
"aggs": Object {
"bucket_truncate": Object {
"bucket_sort": Object {
"from": 0,
"size": 5,
},
},
"impactedPages": Object {
"aggs": Object {
"pageCount": Object {
"cardinality": Object {
"field": "transaction.id",
},
},
},
"filter": Object {
"term": Object {
"transaction.type": "page-load",
},
},
},
"sample": Object {
"top_hits": Object {
"_source": Array [
"error.exception.message",
"error.exception.type",
"error.grouping_key",
"@timestamp",
],
"size": 1,
"sort": Array [
Object {
"@timestamp": "desc",
},
],
},
},
},
"terms": Object {
"field": "error.grouping_key",
"size": 500,
},
},
"totalErrorGroups": Object {
"cardinality": Object {
"field": "error.grouping_key",
},
},
"totalErrorPages": Object {
"cardinality": Object {
"field": "transaction.id",
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 0,
"lte": 50000,
},
},
},
Object {
"term": Object {
"agent.name": "rum-js",
},
},
Object {
"term": Object {
"service.language.name": "javascript",
},
},
],
"must_not": Array [],
},
},
"size": 0,
"track_total_hits": true,
},
}
`;
exports[`rum client dashboard queries fetches long task metrics 1`] = `
Object {
"apm": Object {

View file

@ -14,7 +14,6 @@ import { getPageViewTrends } from './get_page_view_trends';
import { getPageLoadDistribution } from './get_page_load_distribution';
import { getLongTaskMetrics } from './get_long_task_metrics';
import { getWebCoreVitals } from './get_web_core_vitals';
import { getJSErrors } from './get_js_errors';
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
describe('rum client dashboard queries', () => {
@ -90,17 +89,4 @@ describe('rum client dashboard queries', () => {
);
expect(mock.params).toMatchSnapshot();
});
it('fetches js errors', async () => {
mock = await inspectSearchParams((setup) =>
getJSErrors({
setup,
pageSize: 5,
pageIndex: 0,
start: 0,
end: 50000,
})
);
expect(mock.params).toMatchSnapshot();
});
});

View file

@ -9,7 +9,6 @@ import { Logger } from '@kbn/core/server';
import { isoToEpochRt } from '@kbn/io-ts-utils';
import { setupRequest, Setup } from '../../lib/helpers/setup_request';
import { getClientMetrics } from './get_client_metrics';
import { getJSErrors } from './get_js_errors';
import { getLongTaskMetrics } from './get_long_task_metrics';
import { getPageLoadDistribution } from './get_page_load_distribution';
import { getPageViewTrends } from './get_page_view_trends';
@ -279,48 +278,6 @@ const rumLongTaskMetrics = createApmServerRoute({
},
});
const rumJSErrors = createApmServerRoute({
endpoint: 'GET /internal/apm/ux/js-errors',
params: t.type({
query: t.intersection([
uiFiltersRt,
rangeRt,
t.type({ pageSize: t.string, pageIndex: t.string }),
t.partial({ urlQuery: t.string }),
]),
}),
options: { tags: ['access:apm'] },
handler: async (
resources
): Promise<{
totalErrorPages: number;
totalErrors: number;
totalErrorGroups: number;
items:
| Array<{
count: number;
errorGroupId: string | number;
errorMessage: string;
}>
| undefined;
}> => {
const setup = await setupUXRequest(resources);
const {
query: { pageSize, pageIndex, urlQuery, start, end },
} = resources.params;
return getJSErrors({
setup,
urlQuery,
pageSize: Number(pageSize),
pageIndex: Number(pageIndex),
start,
end,
});
},
});
const rumHasDataRoute = createApmServerRoute({
endpoint: 'GET /api/apm/observability_overview/has_rum_data',
params: t.partial({
@ -383,6 +340,5 @@ export const rumRouteRepository = {
...rumVisitorsBreakdownRoute,
...rumWebCoreVitals,
...rumLongTaskMetrics,
...rumJSErrors,
...rumHasDataRoute,
};

View file

@ -13,9 +13,17 @@ export const AGENT = 'agent';
export const AGENT_NAME = 'agent.name';
export const AGENT_VERSION = 'agent.version';
export const ERROR_EXC_MESSAGE = 'error.exception.message';
export const ERROR_EXC_TYPE = 'error.exception.type';
export const ERROR_GROUP_ID = 'error.grouping_key';
export const PROCESSOR_EVENT = 'processor.event';
export const URL_FULL = 'url.full';
export const USER_AGENT_NAME = 'user_agent.name';
export const SERVICE_LANGUAGE_NAME = 'service.language.name';
export const TRANSACTION_DURATION = 'transaction.duration.us';
export const TRANSACTION_TYPE = 'transaction.type';
export const TRANSACTION_RESULT = 'transaction.result';

View file

@ -6,3 +6,4 @@
*/
export * from './url_ux_query.journey';
export * from './ux_js_errors.journey';

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { journey, step, expect, before } from '@elastic/synthetics';
import { UXDashboardDatePicker } from '../page_objects/date_picker';
import { byTestId, loginToKibana, waitForLoadingToFinish } from './utils';
const jsErrorCount = '3 k';
const jsErrorLabel = `Total errors
${jsErrorCount}`;
journey('UX JsErrors', 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_user', password: 'changeme' },
});
});
step('Set date range', async () => {
const datePickerPage = new UXDashboardDatePicker(page);
await datePickerPage.setDefaultE2eRange();
});
step('Confirm error count', async () => {
// Wait until chart data is loaded
page.waitForLoadState('networkidle');
await page.waitForSelector(`text=${jsErrorCount}`);
const jsErrors = await (
await page.waitForSelector(byTestId('uxJsErrorsTotal'))
).innerText();
expect(jsErrors).toBe(jsErrorLabel);
});
});

View file

@ -19,9 +19,8 @@ import {
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { FETCH_STATUS } from '@kbn/observability-plugin/public';
import { useJsErrorsQuery } from '../../../../hooks/use_js_errors_query';
import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useKibanaServices } from '../../../../hooks/use_kibana_services';
import { I18LABELS } from '../translations';
import { CsmSharedContext } from '../csm_shared_context';
@ -35,34 +34,13 @@ interface JSErrorItem {
export function JSErrors() {
const { http } = useKibanaServices();
const basePath = http.basePath.get();
const { rangeId, urlParams, uxUiFilters } = useLegacyUrlParams();
const { start, end, serviceName, searchTerm } = urlParams;
const {
urlParams: { serviceName },
} = useLegacyUrlParams();
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 5 });
const { data, status } = useFetcher(
(callApmApi) => {
if (start && end && serviceName) {
return callApmApi('GET /internal/apm/ux/js-errors', {
params: {
query: {
start,
end,
urlQuery: searchTerm || undefined,
uiFilters: JSON.stringify(uxUiFilters),
pageSize: String(pagination.pageSize),
pageIndex: String(pagination.pageIndex),
},
},
});
}
return Promise.resolve(null);
},
// `rangeId` acts as a cache buster for stable ranges like "Today"
// eslint-disable-next-line react-hooks/exhaustive-deps
[start, end, serviceName, uxUiFilters, pagination, searchTerm, rangeId]
);
const { data, loading } = useJsErrorsQuery(pagination);
const {
sharedData: { totalPageViews },
@ -130,16 +108,16 @@ export function JSErrors() {
)
}
description={I18LABELS.totalErrors}
isLoading={status === FETCH_STATUS.LOADING}
isLoading={!!loading}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiBasicTable
data-test-subj={'uxJsErrorTable'}
loading={status === FETCH_STATUS.LOADING}
loading={!!loading}
error={
status === FETCH_STATUS.FAILURE
!loading && !data
? i18n.translate('xpack.ux.jsErrorsTable.errorMessage', {
defaultMessage: 'Failed to fetch',
})

View file

@ -0,0 +1,83 @@
/*
* 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 datemath from '@kbn/datemath';
import { useEsSearch } from '@kbn/observability-plugin/public';
import { useMemo } from 'react';
import { useDataView } from '../components/app/rum_dashboard/local_uifilters/use_data_view';
import { jsErrorsQuery } from '../services/data/js_errors_query';
import { useLegacyUrlParams } from '../context/url_params_context/use_url_params';
function callDateMath(value: unknown): number {
const DEFAULT_RETURN_VALUE = 0;
if (typeof value === 'string') {
return datemath.parse(value)?.valueOf() ?? DEFAULT_RETURN_VALUE;
}
return DEFAULT_RETURN_VALUE;
}
export function useJsErrorsQuery(pagination: {
pageIndex: number;
pageSize: number;
}) {
const {
rangeId,
urlParams: { start, end, searchTerm },
uxUiFilters,
} = useLegacyUrlParams();
const { dataViewTitle } = useDataView();
const { data: esQueryResponse, loading } = useEsSearch(
{
index: dataViewTitle,
...jsErrorsQuery(
callDateMath(start),
callDateMath(end),
pagination.pageSize,
pagination.pageIndex,
searchTerm,
uxUiFilters
),
},
[
start,
end,
searchTerm,
uxUiFilters,
dataViewTitle,
pagination.pageSize,
pagination.pageIndex,
rangeId,
],
{ name: 'UxJsErrors' }
);
const data = useMemo(() => {
if (!esQueryResponse) return {};
const { totalErrorGroups, totalErrorPages, errors } =
esQueryResponse?.aggregations ?? {};
return {
totalErrorPages: totalErrorPages?.value ?? 0,
totalErrors: esQueryResponse.hits.total ?? 0,
totalErrorGroups: totalErrorGroups?.value ?? 0,
items: errors?.buckets.map(({ sample, key, impactedPages }: any) => {
return {
count: impactedPages.pageCount.value,
errorGroupId: key,
errorMessage: (
sample.hits.hits[0]._source as {
error: { exception: Array<{ message: string }> };
}
).error.exception?.[0].message,
};
}),
};
}, [esQueryResponse]);
return { data, loading };
}

View file

@ -0,0 +1,100 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`jsErrorsQuery fetches js errors 1`] = `
Object {
"body": Object {
"aggs": Object {
"errors": Object {
"aggs": Object {
"bucket_truncate": Object {
"bucket_sort": Object {
"from": 0,
"size": 5,
},
},
"impactedPages": Object {
"aggs": Object {
"pageCount": Object {
"cardinality": Object {
"field": "transaction.id",
},
},
},
"filter": Object {
"term": Object {
"transaction.type": "page-load",
},
},
},
"sample": Object {
"top_hits": Object {
"_source": Array [
"error.exception.message",
"error.exception.type",
"error.grouping_key",
"@timestamp",
],
"size": 1,
"sort": Array [
Object {
"@timestamp": "desc",
},
],
},
},
},
"terms": Object {
"field": "error.grouping_key",
"size": 500,
},
},
"totalErrorGroups": Object {
"cardinality": Object {
"field": "error.grouping_key",
},
},
"totalErrorPages": Object {
"cardinality": Object {
"field": "transaction.id",
},
},
},
"query": Object {
"bool": Object {
"filter": Array [
Object {
"range": Object {
"@timestamp": Object {
"format": "epoch_millis",
"gte": 0,
"lte": 50000,
},
},
},
Object {
"term": Object {
"agent.name": "rum-js",
},
},
Object {
"term": Object {
"service.language.name": "javascript",
},
},
Object {
"terms": Object {
"processor.event": Array [
"error",
],
},
},
],
"must_not": Array [],
},
},
"size": 0,
"track_total_hits": true,
},
}
`;

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 { jsErrorsQuery } from './js_errors_query';
describe('jsErrorsQuery', () => {
it('fetches js errors', () => {
const query = jsErrorsQuery(0, 50000, 5, 0, '', {
environment: 'ENVIRONMENT_ALL',
});
expect(query).toMatchSnapshot();
});
});

View file

@ -5,9 +5,8 @@
* 2.0.
*/
import { mergeProjection } from '../../projections/util/merge_projection';
import { SetupUX } from './route';
import { getRumErrorsProjection } from '../../projections/rum_page_load_transactions';
import { mergeProjection } from '../../../common/utils/merge_projection';
import { SetupUX, UxUIFilters } from '../../../typings/ui_filters';
import {
ERROR_EXC_MESSAGE,
ERROR_EXC_TYPE,
@ -16,22 +15,17 @@ import {
TRANSACTION_TYPE,
} from '../../../common/elasticsearch_fieldnames';
import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types';
import { getRumErrorsProjection } from './projections';
export async function getJSErrors({
setup,
pageSize,
pageIndex,
urlQuery,
start,
end,
}: {
setup: SetupUX;
pageSize: number;
pageIndex: number;
urlQuery?: string;
start: number;
end: number;
}) {
export function jsErrorsQuery(
start: number,
end: number,
pageSize: number,
pageIndex: number,
urlQuery?: string,
uiFilters?: UxUIFilters
) {
const setup: SetupUX = { uiFilters: uiFilters ? uiFilters : {} };
const projection = getRumErrorsProjection({
setup,
urlQuery,
@ -98,27 +92,5 @@ export async function getJSErrors({
},
});
const { apmEventClient } = setup;
const response = await apmEventClient.search('get_js_errors', params);
const { totalErrorGroups, totalErrorPages, errors } =
response.aggregations ?? {};
return {
totalErrorPages: totalErrorPages?.value ?? 0,
totalErrors: response.hits.total.value ?? 0,
totalErrorGroups: totalErrorGroups?.value ?? 0,
items: errors?.buckets.map(({ sample, key, impactedPages }) => {
return {
count: impactedPages.pageCount.value,
errorGroupId: key,
errorMessage: (
sample.hits.hits[0]._source as {
error: { exception: Array<{ message: string }> };
}
).error.exception?.[0].message,
};
}),
};
return params;
}

View file

@ -4,12 +4,18 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../common/processor_event';
import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types';
import { SetupUX } from '../../../typings/ui_filters';
import { getEsFilter } from './get_es_filter';
import { rangeQuery } from './range_query';
import {
AGENT_NAME,
SERVICE_LANGUAGE_NAME,
PROCESSOR_EVENT,
} from '../../../common/elasticsearch_fieldnames';
export function getRumPageLoadTransactionsProjection({
setup,
@ -66,3 +72,60 @@ export function getRumPageLoadTransactionsProjection({
},
};
}
export interface RumErrorsProjection {
body: {
query: {
bool: {
filter: QueryDslQueryContainer[];
must_not: QueryDslQueryContainer[];
};
};
};
}
export function getRumErrorsProjection({
setup,
urlQuery,
start,
end,
}: {
setup: SetupUX;
urlQuery?: string;
start: number;
end: number;
}): RumErrorsProjection {
return {
body: {
query: {
bool: {
filter: [
...rangeQuery(start, end),
{ term: { [AGENT_NAME]: 'rum-js' } },
{
term: {
[SERVICE_LANGUAGE_NAME]: 'javascript',
},
},
{
terms: {
[PROCESSOR_EVENT]: [ProcessorEvent.error],
},
},
...getEsFilter(setup.uiFilters),
...(urlQuery
? [
{
wildcard: {
'url.full': `*${urlQuery}*`,
},
},
]
: []),
],
must_not: [...getEsFilter(setup.uiFilters, true)],
},
},
},
};
}

View file

@ -1,88 +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 rumJsErrorsApiTests({ getService }: FtrProviderContext) {
const registry = getService('registry');
const supertest = getService('legacySupertestAsApmReadUser');
registry.when('CSM JS errors with data', { config: 'trial', archives: [] }, () => {
it('returns no js errors', async () => {
const response = await supertest.get('/internal/apm/ux/js-errors').query({
pageSize: 5,
pageIndex: 0,
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);
expectSnapshot(response.body).toMatchInline(`
Object {
"totalErrorGroups": 0,
"totalErrorPages": 0,
"totalErrors": 0,
}
`);
});
});
registry.when(
'CSM JS errors without data',
{ config: 'trial', archives: ['8.0.0', 'rum_test_data'] },
() => {
it('returns js errors', async () => {
const response = await supertest.get('/internal/apm/ux/js-errors').query({
start: '2021-01-18T12:20:17.202Z',
end: '2021-01-18T12:25:17.203Z',
uiFilters: '{"environment":"ENVIRONMENT_ALL","serviceName":["elastic-co-frontend"]}',
pageSize: 5,
pageIndex: 0,
});
expect(response.status).to.be(200);
expectSnapshot(response.body).toMatchInline(`
Object {
"items": Array [
Object {
"count": 5,
"errorGroupId": "de32dc81e2ee5165cbff20046c080a27",
"errorMessage": "SyntaxError: Document.querySelector: '' is not a valid selector",
},
Object {
"count": 2,
"errorGroupId": "34d83587e17711a7c257ffb080ddb1c6",
"errorMessage": "Uncaught SyntaxError: Failed to execute 'querySelector' on 'Document': The provided selector is empty.",
},
Object {
"count": 43,
"errorGroupId": "3dd5604267b928139d958706f09f7e09",
"errorMessage": "Script error.",
},
Object {
"count": 1,
"errorGroupId": "cd3a2b01017ff7bcce70479644f28318",
"errorMessage": "Unhandled promise rejection: TypeError: can't convert undefined to object",
},
Object {
"count": 3,
"errorGroupId": "23539422cf714db071aba087dd041859",
"errorMessage": "Unable to get property 'left' of undefined or null reference",
},
],
"totalErrorGroups": 6,
"totalErrorPages": 120,
"totalErrors": 2846,
}
`);
});
}
);
}