mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[CSM] Js errors (#77919)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
aa9774faa0
commit
d7a8641f3a
22 changed files with 665 additions and 51 deletions
|
@ -14,16 +14,40 @@ When(/^the user filters by "([^"]*)"$/, (filterName) => {
|
|||
cy.get('.euiStat__title-isLoading').should('not.be.visible');
|
||||
cy.get(`#local-filter-${filterName}`).click();
|
||||
|
||||
if (filterName === 'os') {
|
||||
cy.get('span.euiSelectableListItem__text', DEFAULT_TIMEOUT)
|
||||
.contains('Mac OS X')
|
||||
.click();
|
||||
} else {
|
||||
cy.get('span.euiSelectableListItem__text', DEFAULT_TIMEOUT)
|
||||
.contains('DE')
|
||||
.click();
|
||||
}
|
||||
cy.get('[data-cy=applyFilter]').click();
|
||||
cy.get(`#local-filter-popover-${filterName}`, DEFAULT_TIMEOUT).within(() => {
|
||||
if (filterName === 'os') {
|
||||
const osItem = cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(2);
|
||||
osItem.should('have.text', 'Mac OS X8 ');
|
||||
osItem.click();
|
||||
|
||||
// sometimes click doesn't work as expected so we need to retry here
|
||||
osItem.invoke('attr', 'aria-selected').then((val) => {
|
||||
if (val === 'false') {
|
||||
cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(2).click();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const deItem = cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(0);
|
||||
deItem.should('have.text', 'DE28 ');
|
||||
deItem.click();
|
||||
|
||||
// sometimes click doesn't work as expected so we need to retry here
|
||||
deItem.invoke('attr', 'aria-selected').then((val) => {
|
||||
if (val === 'false') {
|
||||
cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(0).click();
|
||||
}
|
||||
});
|
||||
}
|
||||
cy.get('[data-cy=applyFilter]').click();
|
||||
});
|
||||
|
||||
cy.get(`div#local-filter-values-${filterName}`, DEFAULT_TIMEOUT).within(
|
||||
() => {
|
||||
cy.get('span.euiBadge__content')
|
||||
.eq(0)
|
||||
.should('have.text', filterName === 'os' ? 'Mac OS X' : 'DE');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Then(/^it filters the client metrics "([^"]*)"$/, (filterName) => {
|
||||
|
|
|
@ -5,15 +5,13 @@
|
|||
*/
|
||||
|
||||
import { When, Then } from 'cypress-cucumber-preprocessor/steps';
|
||||
import { DEFAULT_TIMEOUT } from '../apm';
|
||||
import { verifyClientMetrics } from './client_metrics_helper';
|
||||
import { DEFAULT_TIMEOUT } from './csm_dashboard';
|
||||
|
||||
When('the user changes the selected service name', (filterName) => {
|
||||
When('the user changes the selected service name', () => {
|
||||
// wait for all loading to finish
|
||||
cy.get('kbnLoadingIndicator').should('not.be.visible');
|
||||
cy.get(`[data-cy=serviceNameFilter]`, { timeout: DEFAULT_TIMEOUT }).select(
|
||||
'client'
|
||||
);
|
||||
cy.get(`[data-cy=serviceNameFilter]`, DEFAULT_TIMEOUT).select('client');
|
||||
});
|
||||
|
||||
Then(`it displays relevant client metrics`, () => {
|
||||
|
|
|
@ -96,7 +96,7 @@ function setRumAgent(item) {
|
|||
if (item.body) {
|
||||
item.body = item.body.replace(
|
||||
'"name":"client"',
|
||||
'"name":"opbean-client-rum"'
|
||||
'"name":"elastic-frontend"'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
import * as React from 'react';
|
||||
import numeral from '@elastic/numeral';
|
||||
import styled from 'styled-components';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiToolTip } from '@elastic/eui';
|
||||
import { useFetcher } from '../../../../hooks/useFetcher';
|
||||
import { useUrlParams } from '../../../../hooks/useUrlParams';
|
||||
import { I18LABELS } from '../translations';
|
||||
import { CsmSharedContext } from '../CsmSharedContext';
|
||||
|
||||
const ClFlexGroup = styled(EuiFlexGroup)`
|
||||
flex-direction: row;
|
||||
|
@ -45,6 +47,12 @@ export function ClientMetrics() {
|
|||
[start, end, uiFilters, searchTerm]
|
||||
);
|
||||
|
||||
const { setSharedData } = useContext(CsmSharedContext);
|
||||
|
||||
useEffect(() => {
|
||||
setSharedData({ totalPageViews: data?.pageViews?.value ?? 0 });
|
||||
}, [data, setSharedData]);
|
||||
|
||||
const STAT_STYLE = { width: '240px' };
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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, { createContext, useMemo, useState } from 'react';
|
||||
|
||||
interface SharedData {
|
||||
totalPageViews: number;
|
||||
}
|
||||
|
||||
interface Index {
|
||||
sharedData: SharedData;
|
||||
setSharedData: (data: SharedData) => void;
|
||||
}
|
||||
|
||||
const defaultContext: Index = {
|
||||
sharedData: { totalPageViews: 0 },
|
||||
setSharedData: (d) => {
|
||||
throw new Error(
|
||||
'setSharedData was not initialized, set it when you invoke the context'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CsmSharedContext = createContext(defaultContext);
|
||||
|
||||
export function CsmSharedContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element[];
|
||||
}) {
|
||||
const [newData, setNewData] = useState<SharedData>({ totalPageViews: 0 });
|
||||
|
||||
const setSharedData = React.useCallback((data: SharedData) => {
|
||||
setNewData(data);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(() => {
|
||||
return { sharedData: newData, setSharedData };
|
||||
}, [newData, setSharedData]);
|
||||
|
||||
return <CsmSharedContext.Provider value={value} children={children} />;
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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, { useContext, useState } from 'react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiStat,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useUrlParams } from '../../../../hooks/useUrlParams';
|
||||
import { useFetcher } from '../../../../hooks/useFetcher';
|
||||
import { I18LABELS } from '../translations';
|
||||
import { CsmSharedContext } from '../CsmSharedContext';
|
||||
|
||||
export function JSErrors() {
|
||||
const { urlParams, uiFilters } = useUrlParams();
|
||||
|
||||
const { start, end, serviceName } = urlParams;
|
||||
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 5 });
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end && serviceName) {
|
||||
return callApmApi({
|
||||
pathname: '/api/apm/rum-client/js-errors',
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
uiFilters: JSON.stringify(uiFilters),
|
||||
pageSize: String(pagination.pageSize),
|
||||
pageIndex: String(pagination.pageIndex),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
[start, end, serviceName, uiFilters, pagination]
|
||||
);
|
||||
|
||||
const {
|
||||
sharedData: { totalPageViews },
|
||||
} = useContext(CsmSharedContext);
|
||||
|
||||
const items = (data?.items ?? []).map(({ errorMessage, count }) => ({
|
||||
errorMessage,
|
||||
percent: i18n.translate('xpack.apm.rum.jsErrors.percent', {
|
||||
defaultMessage: '{pageLoadPercent} %',
|
||||
values: { pageLoadPercent: ((count / totalPageViews) * 100).toFixed(1) },
|
||||
}),
|
||||
}));
|
||||
|
||||
const cols = [
|
||||
{
|
||||
field: 'errorMessage',
|
||||
name: I18LABELS.errorMessage,
|
||||
},
|
||||
{
|
||||
name: I18LABELS.impactedPageLoads,
|
||||
field: 'percent',
|
||||
align: 'right' as const,
|
||||
},
|
||||
];
|
||||
|
||||
const onTableChange = ({
|
||||
page,
|
||||
}: {
|
||||
page: { size: number; index: number };
|
||||
}) => {
|
||||
setPagination({
|
||||
pageIndex: page.index,
|
||||
pageSize: page.size,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{I18LABELS.jsErrors}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
titleSize="s"
|
||||
title={
|
||||
<EuiToolTip content={data?.totalErrors ?? 0}>
|
||||
<>{numeral(data?.totalErrors ?? 0).format('0 a')}</>
|
||||
</EuiToolTip>
|
||||
}
|
||||
description={I18LABELS.totalErrors}
|
||||
isLoading={status !== 'success'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
titleSize="s"
|
||||
title={i18n.translate('xpack.apm.rum.jsErrors.errorRateValue', {
|
||||
defaultMessage: '{errorRate} %',
|
||||
values: {
|
||||
errorRate: (
|
||||
((data?.totalErrorPages ?? 0) / totalPageViews) *
|
||||
100
|
||||
).toFixed(0),
|
||||
},
|
||||
})}
|
||||
description={I18LABELS.errorRate}
|
||||
isLoading={status !== 'success'}
|
||||
/>
|
||||
</EuiFlexItem>{' '}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiBasicTable
|
||||
loading={status !== 'success'}
|
||||
responsive={false}
|
||||
compressed={true}
|
||||
columns={cols}
|
||||
items={items}
|
||||
onChange={onTableChange}
|
||||
pagination={{
|
||||
...pagination,
|
||||
totalItemCount: data?.totalErrorGroups ?? 0,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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, EuiPanel, EuiFlexGroup, EuiSpacer } from '@elastic/eui';
|
||||
import { JSErrors } from './JSErrors';
|
||||
|
||||
export function ImpactfulMetrics() {
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFlexGroup wrap>
|
||||
<EuiFlexItem style={{ flexBasis: 650 }}>
|
||||
<JSErrors />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -19,6 +19,7 @@ import { I18LABELS } from './translations';
|
|||
import { VisitorBreakdown } from './VisitorBreakdown';
|
||||
import { UXMetrics } from './UXMetrics';
|
||||
import { VisitorBreakdownMap } from './VisitorBreakdownMap';
|
||||
import { ImpactfulMetrics } from './ImpactfulMetrics';
|
||||
|
||||
export function RumDashboard() {
|
||||
return (
|
||||
|
@ -66,6 +67,9 @@ export function RumDashboard() {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ImpactfulMetrics />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { RumOverview } from '../RumDashboard';
|
||||
import { RumHeader } from './RumHeader';
|
||||
import { CsmSharedContextProvider } from './CsmSharedContext';
|
||||
|
||||
export const UX_LABEL = i18n.translate('xpack.apm.ux.title', {
|
||||
defaultMessage: 'User Experience',
|
||||
|
@ -17,16 +18,18 @@ export const UX_LABEL = i18n.translate('xpack.apm.ux.title', {
|
|||
export function RumHome() {
|
||||
return (
|
||||
<div>
|
||||
<RumHeader>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="l">
|
||||
<h1>{UX_LABEL}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</RumHeader>
|
||||
<RumOverview />
|
||||
<CsmSharedContextProvider>
|
||||
<RumHeader>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="l">
|
||||
<h1>{UX_LABEL}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</RumHeader>
|
||||
<RumOverview />
|
||||
</CsmSharedContextProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -37,6 +37,18 @@ export const I18LABELS = {
|
|||
defaultMessage: 'Page load distribution',
|
||||
}
|
||||
),
|
||||
jsErrors: i18n.translate(
|
||||
'xpack.apm.rum.dashboard.impactfulMetrics.jsErrors',
|
||||
{
|
||||
defaultMessage: 'JavaScript errors',
|
||||
}
|
||||
),
|
||||
highTrafficPages: i18n.translate(
|
||||
'xpack.apm.rum.dashboard.impactfulMetrics.highTrafficPages',
|
||||
{
|
||||
defaultMessage: 'High traffic pages',
|
||||
}
|
||||
),
|
||||
resetZoom: i18n.translate('xpack.apm.rum.dashboard.resetZoom.label', {
|
||||
defaultMessage: 'Reset zoom',
|
||||
}),
|
||||
|
@ -105,6 +117,21 @@ export const I18LABELS = {
|
|||
noResults: i18n.translate('xpack.apm.rum.filters.url.noResults', {
|
||||
defaultMessage: 'No results available',
|
||||
}),
|
||||
totalErrors: i18n.translate('xpack.apm.rum.jsErrors.totalErrors', {
|
||||
defaultMessage: 'Total errors',
|
||||
}),
|
||||
errorRate: i18n.translate('xpack.apm.rum.jsErrors.errorRate', {
|
||||
defaultMessage: 'Error rate',
|
||||
}),
|
||||
errorMessage: i18n.translate('xpack.apm.rum.jsErrors.errorMessage', {
|
||||
defaultMessage: 'Error message',
|
||||
}),
|
||||
impactedPageLoads: i18n.translate(
|
||||
'xpack.apm.rum.jsErrors.impactedPageLoads',
|
||||
{
|
||||
defaultMessage: 'Impacted page loads',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
export const VisitorBreakdownLabel = i18n.translate(
|
||||
|
|
|
@ -19,6 +19,7 @@ const BadgeText = styled.div`
|
|||
interface Props {
|
||||
value: string[];
|
||||
onRemove: (val: string) => void;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const removeFilterLabel = i18n.translate(
|
||||
|
@ -26,9 +27,9 @@ const removeFilterLabel = i18n.translate(
|
|||
{ defaultMessage: 'Remove filter' }
|
||||
);
|
||||
|
||||
function FilterBadgeList({ onRemove, value }: Props) {
|
||||
function FilterBadgeList({ onRemove, value, name }: Props) {
|
||||
return (
|
||||
<EuiFlexGrid gutterSize="s">
|
||||
<EuiFlexGrid gutterSize="s" id={`local-filter-values-${name}`}>
|
||||
{value.map((val) => (
|
||||
<EuiFlexItem key={val} grow={false}>
|
||||
<EuiBadge
|
||||
|
|
|
@ -113,7 +113,7 @@ function Filter({ name, title, options, onChange, value, showCount }: Props) {
|
|||
searchable={true}
|
||||
>
|
||||
{(list, search) => (
|
||||
<SelectContainer>
|
||||
<SelectContainer id={`local-filter-popover-${name}`}>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<FlexItem grow={true}>
|
||||
<EuiTitle size="xxxs" textTransform="uppercase">
|
||||
|
@ -159,6 +159,7 @@ function Filter({ name, title, options, onChange, value, showCount }: Props) {
|
|||
{value.length ? (
|
||||
<>
|
||||
<FilterBadgeList
|
||||
name={name}
|
||||
onRemove={(val) => {
|
||||
onChange(value.filter((v) => val !== v));
|
||||
}}
|
||||
|
|
|
@ -72,6 +72,97 @@ 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,
|
||||
},
|
||||
},
|
||||
"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": 1528113600000,
|
||||
"lte": 1528977600000,
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"agent.name": "rum-js",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"transaction.type": "page-load",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.language.name": "javascript",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"my.custom.ui.filter": "foo-bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
"track_total_hits": true,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`rum client dashboard queries fetches long task metrics 1`] = `
|
||||
Object {
|
||||
"apm": Object {
|
||||
|
|
99
x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts
Normal file
99
x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { mergeProjection } from '../../projections/util/merge_projection';
|
||||
import {
|
||||
Setup,
|
||||
SetupTimeRange,
|
||||
SetupUIFilters,
|
||||
} from '../helpers/setup_request';
|
||||
import { getRumErrorsProjection } from '../../projections/rum_page_load_transactions';
|
||||
import {
|
||||
ERROR_EXC_MESSAGE,
|
||||
ERROR_EXC_TYPE,
|
||||
ERROR_GROUP_ID,
|
||||
TRANSACTION_ID,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
|
||||
export async function getJSErrors({
|
||||
setup,
|
||||
pageSize,
|
||||
pageIndex,
|
||||
}: {
|
||||
setup: Setup & SetupTimeRange & SetupUIFilters;
|
||||
pageSize: number;
|
||||
pageIndex: number;
|
||||
}) {
|
||||
const projection = getRumErrorsProjection({
|
||||
setup,
|
||||
});
|
||||
|
||||
const params = mergeProjection(projection, {
|
||||
body: {
|
||||
size: 0,
|
||||
track_total_hits: true,
|
||||
aggs: {
|
||||
totalErrorGroups: {
|
||||
cardinality: {
|
||||
field: ERROR_GROUP_ID,
|
||||
},
|
||||
},
|
||||
totalErrorPages: {
|
||||
cardinality: {
|
||||
field: TRANSACTION_ID,
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
terms: {
|
||||
field: ERROR_GROUP_ID,
|
||||
size: 500,
|
||||
},
|
||||
aggs: {
|
||||
bucket_truncate: {
|
||||
bucket_sort: {
|
||||
size: pageSize,
|
||||
from: pageIndex * pageSize,
|
||||
},
|
||||
},
|
||||
sample: {
|
||||
top_hits: {
|
||||
_source: [
|
||||
ERROR_EXC_MESSAGE,
|
||||
ERROR_EXC_TYPE,
|
||||
ERROR_GROUP_ID,
|
||||
'@timestamp',
|
||||
],
|
||||
sort: [{ '@timestamp': 'desc' as const }],
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const response = await apmEventClient.search(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, doc_count: count }) => {
|
||||
return {
|
||||
count,
|
||||
errorMessage: (sample.hits.hits[0]._source as {
|
||||
error: { exception: Array<{ message: string }> };
|
||||
}).error.exception?.[0].message,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -14,6 +14,7 @@ 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';
|
||||
import { getJSErrors } from './get_js_errors';
|
||||
|
||||
describe('rum client dashboard queries', () => {
|
||||
let mock: SearchParamsMock;
|
||||
|
@ -79,4 +80,15 @@ describe('rum client dashboard queries', () => {
|
|||
);
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('fetches js errors', async () => {
|
||||
mock = await inspectSearchParams((setup) =>
|
||||
getJSErrors({
|
||||
setup,
|
||||
pageSize: 5,
|
||||
pageIndex: 0,
|
||||
})
|
||||
);
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,9 @@ import {
|
|||
} from '../../server/lib/helpers/setup_request';
|
||||
import {
|
||||
SPAN_TYPE,
|
||||
AGENT_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
SERVICE_LANGUAGE_NAME,
|
||||
} from '../../common/elasticsearch_fieldnames';
|
||||
import { rangeFilter } from '../../common/utils/range_filter';
|
||||
import { ProcessorEvent } from '../../common/processor_event';
|
||||
|
@ -90,3 +92,36 @@ export function getRumLongTasksProjection({
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getRumErrorsProjection({
|
||||
setup,
|
||||
}: {
|
||||
setup: Setup & SetupTimeRange & SetupUIFilters;
|
||||
}) {
|
||||
const { start, end, uiFiltersES } = setup;
|
||||
|
||||
const bool = {
|
||||
filter: [
|
||||
{ range: rangeFilter(start, end) },
|
||||
{ term: { [AGENT_NAME]: 'rum-js' } },
|
||||
{ term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } },
|
||||
{
|
||||
term: {
|
||||
[SERVICE_LANGUAGE_NAME]: 'javascript',
|
||||
},
|
||||
},
|
||||
...uiFiltersES,
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
apm: {
|
||||
events: [ProcessorEvent.error],
|
||||
},
|
||||
body: {
|
||||
query: {
|
||||
bool,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -69,17 +69,6 @@ import {
|
|||
listCustomLinksRoute,
|
||||
customLinkTransactionRoute,
|
||||
} from './settings/custom_link';
|
||||
import {
|
||||
rumClientMetricsRoute,
|
||||
rumPageViewsTrendRoute,
|
||||
rumPageLoadDistributionRoute,
|
||||
rumPageLoadDistBreakdownRoute,
|
||||
rumServicesRoute,
|
||||
rumVisitorsBreakdownRoute,
|
||||
rumWebCoreVitals,
|
||||
rumUrlSearch,
|
||||
rumLongTaskMetrics,
|
||||
} from './rum_client';
|
||||
import {
|
||||
observabilityOverviewHasDataRoute,
|
||||
observabilityOverviewRoute,
|
||||
|
@ -89,6 +78,18 @@ import {
|
|||
createAnomalyDetectionJobsRoute,
|
||||
anomalyDetectionEnvironmentsRoute,
|
||||
} from './settings/anomaly_detection';
|
||||
import {
|
||||
rumClientMetricsRoute,
|
||||
rumJSErrors,
|
||||
rumLongTaskMetrics,
|
||||
rumPageLoadDistBreakdownRoute,
|
||||
rumPageLoadDistributionRoute,
|
||||
rumPageViewsTrendRoute,
|
||||
rumServicesRoute,
|
||||
rumUrlSearch,
|
||||
rumVisitorsBreakdownRoute,
|
||||
rumWebCoreVitals,
|
||||
} from './rum_client';
|
||||
|
||||
const createApmApi = () => {
|
||||
const api = createApi()
|
||||
|
@ -165,7 +166,16 @@ const createApmApi = () => {
|
|||
.add(listCustomLinksRoute)
|
||||
.add(customLinkTransactionRoute)
|
||||
|
||||
// Rum Overview
|
||||
// Observability dashboard
|
||||
.add(observabilityOverviewHasDataRoute)
|
||||
.add(observabilityOverviewRoute)
|
||||
|
||||
// Anomaly detection
|
||||
.add(anomalyDetectionJobsRoute)
|
||||
.add(createAnomalyDetectionJobsRoute)
|
||||
.add(anomalyDetectionEnvironmentsRoute)
|
||||
|
||||
// User Experience app api routes
|
||||
.add(rumOverviewLocalFiltersRoute)
|
||||
.add(rumPageViewsTrendRoute)
|
||||
.add(rumPageLoadDistributionRoute)
|
||||
|
@ -174,17 +184,9 @@ const createApmApi = () => {
|
|||
.add(rumServicesRoute)
|
||||
.add(rumVisitorsBreakdownRoute)
|
||||
.add(rumWebCoreVitals)
|
||||
.add(rumJSErrors)
|
||||
.add(rumUrlSearch)
|
||||
.add(rumLongTaskMetrics)
|
||||
|
||||
// Observability dashboard
|
||||
.add(observabilityOverviewHasDataRoute)
|
||||
.add(observabilityOverviewRoute)
|
||||
|
||||
// Anomaly detection
|
||||
.add(anomalyDetectionJobsRoute)
|
||||
.add(createAnomalyDetectionJobsRoute)
|
||||
.add(anomalyDetectionEnvironmentsRoute);
|
||||
.add(rumLongTaskMetrics);
|
||||
|
||||
return api;
|
||||
};
|
||||
|
|
|
@ -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 { getJSErrors } from '../lib/rum_client/get_js_errors';
|
||||
import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics';
|
||||
import { getUrlSearch } from '../lib/rum_client/get_url_search';
|
||||
|
||||
|
@ -191,3 +192,27 @@ export const rumUrlSearch = createRoute(() => ({
|
|||
return getUrlSearch({ setup, urlQuery });
|
||||
},
|
||||
}));
|
||||
|
||||
export const rumJSErrors = createRoute(() => ({
|
||||
path: '/api/apm/rum-client/js-errors',
|
||||
params: {
|
||||
query: t.intersection([
|
||||
uiFiltersRt,
|
||||
rangeRt,
|
||||
t.type({ pageSize: t.string, pageIndex: t.string }),
|
||||
]),
|
||||
},
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
|
||||
const {
|
||||
query: { pageSize, pageIndex },
|
||||
} = context.params;
|
||||
|
||||
return getJSErrors({
|
||||
setup,
|
||||
pageSize: Number(pageSize),
|
||||
pageIndex: Number(pageIndex),
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
|
|
@ -153,6 +153,11 @@ export interface AggregationOptionsByType {
|
|||
keyed?: boolean;
|
||||
hdr?: { number_of_significant_value_digits: number };
|
||||
} & AggregationSourceOptions;
|
||||
bucket_sort: {
|
||||
sort?: SortOptions;
|
||||
from?: number;
|
||||
size?: number;
|
||||
};
|
||||
}
|
||||
|
||||
type AggregationType = keyof AggregationOptionsByType;
|
||||
|
@ -329,6 +334,7 @@ interface AggregationResponsePart<
|
|||
? Array<{ key: number; value: number }>
|
||||
: Record<string, number>;
|
||||
};
|
||||
bucket_sort: undefined;
|
||||
}
|
||||
|
||||
// Type for debugging purposes. If you see an error in AggregationResponseMap
|
||||
|
|
|
@ -7,11 +7,22 @@
|
|||
import { SearchParams, SearchResponse } from 'elasticsearch';
|
||||
import { AggregationResponseMap, AggregationInputMap } from './aggregations';
|
||||
|
||||
interface CollapseQuery {
|
||||
field: string;
|
||||
inner_hits: {
|
||||
name: string;
|
||||
size?: number;
|
||||
sort?: [{ date: 'asc' | 'desc' }];
|
||||
};
|
||||
max_concurrent_group_searches?: number;
|
||||
}
|
||||
|
||||
export interface ESSearchBody {
|
||||
query?: any;
|
||||
size?: number;
|
||||
aggs?: AggregationInputMap;
|
||||
track_total_hits?: boolean | number;
|
||||
collapse?: CollapseQuery;
|
||||
}
|
||||
|
||||
export type ESSearchRequest = Omit<SearchParams, 'body'> & {
|
||||
|
|
61
x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts
Normal file
61
x-pack/test/apm_api_integration/trial/tests/csm/js_errors.ts
Normal 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 expect from '@kbn/expect';
|
||||
import { expectSnapshot } from '../../../common/match_snapshot';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
export default function rumJsErrorsApiTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('CSM js errors', () => {
|
||||
describe('when there is no data', () => {
|
||||
it('returns no js errors', async () => {
|
||||
const response = await supertest.get(
|
||||
'/api/apm/rum-client/js-errors?pageSize=5&pageIndex=0&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);
|
||||
expectSnapshot(response.body).toMatchInline(`
|
||||
Object {
|
||||
"totalErrorGroups": 0,
|
||||
"totalErrorPages": 0,
|
||||
"totalErrors": 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 js errors', async () => {
|
||||
const response = await supertest.get(
|
||||
'/api/apm/rum-client/js-errors?pageSize=5&pageIndex=0&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 {
|
||||
"items": Array [],
|
||||
"totalErrorGroups": 0,
|
||||
"totalErrorPages": 0,
|
||||
"totalErrors": 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -37,6 +37,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr
|
|||
loadTestFile(require.resolve('./csm/long_task_metrics.ts'));
|
||||
loadTestFile(require.resolve('./csm/url_search.ts'));
|
||||
loadTestFile(require.resolve('./csm/page_views.ts'));
|
||||
loadTestFile(require.resolve('./csm/js_errors.ts'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue