Skip to content

Commit 9e7e8d2

Browse files
[CSM] Js errors (elastic#77919)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 7918405 commit 9e7e8d2

File tree

22 files changed

+665
-51
lines changed

22 files changed

+665
-51
lines changed

x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts

+34-10
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,40 @@ When(/^the user filters by "([^"]*)"$/, (filterName) => {
1414
cy.get('.euiStat__title-isLoading').should('not.be.visible');
1515
cy.get(`#local-filter-${filterName}`).click();
1616

17-
if (filterName === 'os') {
18-
cy.get('span.euiSelectableListItem__text', DEFAULT_TIMEOUT)
19-
.contains('Mac OS X')
20-
.click();
21-
} else {
22-
cy.get('span.euiSelectableListItem__text', DEFAULT_TIMEOUT)
23-
.contains('DE')
24-
.click();
25-
}
26-
cy.get('[data-cy=applyFilter]').click();
17+
cy.get(`#local-filter-popover-${filterName}`, DEFAULT_TIMEOUT).within(() => {
18+
if (filterName === 'os') {
19+
const osItem = cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(2);
20+
osItem.should('have.text', 'Mac OS X8 ');
21+
osItem.click();
22+
23+
// sometimes click doesn't work as expected so we need to retry here
24+
osItem.invoke('attr', 'aria-selected').then((val) => {
25+
if (val === 'false') {
26+
cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(2).click();
27+
}
28+
});
29+
} else {
30+
const deItem = cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(0);
31+
deItem.should('have.text', 'DE28 ');
32+
deItem.click();
33+
34+
// sometimes click doesn't work as expected so we need to retry here
35+
deItem.invoke('attr', 'aria-selected').then((val) => {
36+
if (val === 'false') {
37+
cy.get('li.euiSelectableListItem', DEFAULT_TIMEOUT).eq(0).click();
38+
}
39+
});
40+
}
41+
cy.get('[data-cy=applyFilter]').click();
42+
});
43+
44+
cy.get(`div#local-filter-values-${filterName}`, DEFAULT_TIMEOUT).within(
45+
() => {
46+
cy.get('span.euiBadge__content')
47+
.eq(0)
48+
.should('have.text', filterName === 'os' ? 'Mac OS X' : 'DE');
49+
}
50+
);
2751
});
2852

2953
Then(/^it filters the client metrics "([^"]*)"$/, (filterName) => {

x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@
55
*/
66

77
import { When, Then } from 'cypress-cucumber-preprocessor/steps';
8-
import { DEFAULT_TIMEOUT } from '../apm';
98
import { verifyClientMetrics } from './client_metrics_helper';
9+
import { DEFAULT_TIMEOUT } from './csm_dashboard';
1010

11-
When('the user changes the selected service name', (filterName) => {
11+
When('the user changes the selected service name', () => {
1212
// wait for all loading to finish
1313
cy.get('kbnLoadingIndicator').should('not.be.visible');
14-
cy.get(`[data-cy=serviceNameFilter]`, { timeout: DEFAULT_TIMEOUT }).select(
15-
'client'
16-
);
14+
cy.get(`[data-cy=serviceNameFilter]`, DEFAULT_TIMEOUT).select('client');
1715
});
1816

1917
Then(`it displays relevant client metrics`, () => {

x-pack/plugins/apm/e2e/ingest-data/replay.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ function setRumAgent(item) {
9696
if (item.body) {
9797
item.body = item.body.replace(
9898
'"name":"client"',
99-
'"name":"opbean-client-rum"'
99+
'"name":"elastic-frontend"'
100100
);
101101
}
102102
}

x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import * as React from 'react';
77
import numeral from '@elastic/numeral';
88
import styled from 'styled-components';
9+
import { useContext, useEffect } from 'react';
910
import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiToolTip } from '@elastic/eui';
1011
import { useFetcher } from '../../../../hooks/useFetcher';
1112
import { useUrlParams } from '../../../../hooks/useUrlParams';
1213
import { I18LABELS } from '../translations';
14+
import { CsmSharedContext } from '../CsmSharedContext';
1315

1416
const ClFlexGroup = styled(EuiFlexGroup)`
1517
flex-direction: row;
@@ -45,6 +47,12 @@ export function ClientMetrics() {
4547
[start, end, uiFilters, searchTerm]
4648
);
4749

50+
const { setSharedData } = useContext(CsmSharedContext);
51+
52+
useEffect(() => {
53+
setSharedData({ totalPageViews: data?.pageViews?.value ?? 0 });
54+
}, [data, setSharedData]);
55+
4856
const STAT_STYLE = { width: '240px' };
4957

5058
return (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { createContext, useMemo, useState } from 'react';
8+
9+
interface SharedData {
10+
totalPageViews: number;
11+
}
12+
13+
interface Index {
14+
sharedData: SharedData;
15+
setSharedData: (data: SharedData) => void;
16+
}
17+
18+
const defaultContext: Index = {
19+
sharedData: { totalPageViews: 0 },
20+
setSharedData: (d) => {
21+
throw new Error(
22+
'setSharedData was not initialized, set it when you invoke the context'
23+
);
24+
},
25+
};
26+
27+
export const CsmSharedContext = createContext(defaultContext);
28+
29+
export function CsmSharedContextProvider({
30+
children,
31+
}: {
32+
children: JSX.Element[];
33+
}) {
34+
const [newData, setNewData] = useState<SharedData>({ totalPageViews: 0 });
35+
36+
const setSharedData = React.useCallback((data: SharedData) => {
37+
setNewData(data);
38+
}, []);
39+
40+
const value = useMemo(() => {
41+
return { sharedData: newData, setSharedData };
42+
}, [newData, setSharedData]);
43+
44+
return <CsmSharedContext.Provider value={value} children={children} />;
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { useContext, useState } from 'react';
8+
import {
9+
EuiBasicTable,
10+
EuiFlexItem,
11+
EuiFlexGroup,
12+
EuiSpacer,
13+
EuiTitle,
14+
EuiStat,
15+
EuiToolTip,
16+
} from '@elastic/eui';
17+
import numeral from '@elastic/numeral';
18+
import { i18n } from '@kbn/i18n';
19+
import { useUrlParams } from '../../../../hooks/useUrlParams';
20+
import { useFetcher } from '../../../../hooks/useFetcher';
21+
import { I18LABELS } from '../translations';
22+
import { CsmSharedContext } from '../CsmSharedContext';
23+
24+
export function JSErrors() {
25+
const { urlParams, uiFilters } = useUrlParams();
26+
27+
const { start, end, serviceName } = urlParams;
28+
29+
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 5 });
30+
31+
const { data, status } = useFetcher(
32+
(callApmApi) => {
33+
if (start && end && serviceName) {
34+
return callApmApi({
35+
pathname: '/api/apm/rum-client/js-errors',
36+
params: {
37+
query: {
38+
start,
39+
end,
40+
uiFilters: JSON.stringify(uiFilters),
41+
pageSize: String(pagination.pageSize),
42+
pageIndex: String(pagination.pageIndex),
43+
},
44+
},
45+
});
46+
}
47+
return Promise.resolve(null);
48+
},
49+
[start, end, serviceName, uiFilters, pagination]
50+
);
51+
52+
const {
53+
sharedData: { totalPageViews },
54+
} = useContext(CsmSharedContext);
55+
56+
const items = (data?.items ?? []).map(({ errorMessage, count }) => ({
57+
errorMessage,
58+
percent: i18n.translate('xpack.apm.rum.jsErrors.percent', {
59+
defaultMessage: '{pageLoadPercent} %',
60+
values: { pageLoadPercent: ((count / totalPageViews) * 100).toFixed(1) },
61+
}),
62+
}));
63+
64+
const cols = [
65+
{
66+
field: 'errorMessage',
67+
name: I18LABELS.errorMessage,
68+
},
69+
{
70+
name: I18LABELS.impactedPageLoads,
71+
field: 'percent',
72+
align: 'right' as const,
73+
},
74+
];
75+
76+
const onTableChange = ({
77+
page,
78+
}: {
79+
page: { size: number; index: number };
80+
}) => {
81+
setPagination({
82+
pageIndex: page.index,
83+
pageSize: page.size,
84+
});
85+
};
86+
87+
return (
88+
<>
89+
<EuiTitle size="xs">
90+
<h3>{I18LABELS.jsErrors}</h3>
91+
</EuiTitle>
92+
<EuiSpacer size="s" />
93+
<EuiFlexGroup>
94+
<EuiFlexItem grow={false}>
95+
<EuiStat
96+
titleSize="s"
97+
title={
98+
<EuiToolTip content={data?.totalErrors ?? 0}>
99+
<>{numeral(data?.totalErrors ?? 0).format('0 a')}</>
100+
</EuiToolTip>
101+
}
102+
description={I18LABELS.totalErrors}
103+
isLoading={status !== 'success'}
104+
/>
105+
</EuiFlexItem>
106+
<EuiFlexItem grow={false}>
107+
<EuiStat
108+
titleSize="s"
109+
title={i18n.translate('xpack.apm.rum.jsErrors.errorRateValue', {
110+
defaultMessage: '{errorRate} %',
111+
values: {
112+
errorRate: (
113+
((data?.totalErrorPages ?? 0) / totalPageViews) *
114+
100
115+
).toFixed(0),
116+
},
117+
})}
118+
description={I18LABELS.errorRate}
119+
isLoading={status !== 'success'}
120+
/>
121+
</EuiFlexItem>{' '}
122+
</EuiFlexGroup>
123+
<EuiSpacer size="s" />
124+
<EuiBasicTable
125+
loading={status !== 'success'}
126+
responsive={false}
127+
compressed={true}
128+
columns={cols}
129+
items={items}
130+
onChange={onTableChange}
131+
pagination={{
132+
...pagination,
133+
totalItemCount: data?.totalErrorGroups ?? 0,
134+
}}
135+
/>
136+
</>
137+
);
138+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import { EuiFlexItem, EuiPanel, EuiFlexGroup, EuiSpacer } from '@elastic/eui';
9+
import { JSErrors } from './JSErrors';
10+
11+
export function ImpactfulMetrics() {
12+
return (
13+
<EuiPanel>
14+
<EuiSpacer size="xs" />
15+
<EuiFlexGroup wrap>
16+
<EuiFlexItem style={{ flexBasis: 650 }}>
17+
<JSErrors />
18+
</EuiFlexItem>
19+
</EuiFlexGroup>
20+
</EuiPanel>
21+
);
22+
}

x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { I18LABELS } from './translations';
1919
import { VisitorBreakdown } from './VisitorBreakdown';
2020
import { UXMetrics } from './UXMetrics';
2121
import { VisitorBreakdownMap } from './VisitorBreakdownMap';
22+
import { ImpactfulMetrics } from './ImpactfulMetrics';
2223

2324
export function RumDashboard() {
2425
return (
@@ -66,6 +67,9 @@ export function RumDashboard() {
6667
</EuiFlexItem>
6768
</EuiFlexGroup>
6869
</EuiFlexItem>
70+
<EuiFlexItem>
71+
<ImpactfulMetrics />
72+
</EuiFlexItem>
6973
</EuiFlexGroup>
7074
);
7175
}

x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx

+13-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import React from 'react';
99
import { i18n } from '@kbn/i18n';
1010
import { RumOverview } from '../RumDashboard';
1111
import { RumHeader } from './RumHeader';
12+
import { CsmSharedContextProvider } from './CsmSharedContext';
1213

1314
export const UX_LABEL = i18n.translate('xpack.apm.ux.title', {
1415
defaultMessage: 'User Experience',
@@ -17,16 +18,18 @@ export const UX_LABEL = i18n.translate('xpack.apm.ux.title', {
1718
export function RumHome() {
1819
return (
1920
<div>
20-
<RumHeader>
21-
<EuiFlexGroup alignItems="center">
22-
<EuiFlexItem grow={false}>
23-
<EuiTitle size="l">
24-
<h1>{UX_LABEL}</h1>
25-
</EuiTitle>
26-
</EuiFlexItem>
27-
</EuiFlexGroup>
28-
</RumHeader>
29-
<RumOverview />
21+
<CsmSharedContextProvider>
22+
<RumHeader>
23+
<EuiFlexGroup alignItems="center">
24+
<EuiFlexItem grow={false}>
25+
<EuiTitle size="l">
26+
<h1>{UX_LABEL}</h1>
27+
</EuiTitle>
28+
</EuiFlexItem>
29+
</EuiFlexGroup>
30+
</RumHeader>
31+
<RumOverview />
32+
</CsmSharedContextProvider>
3033
</div>
3134
);
3235
}

0 commit comments

Comments
 (0)