Skip to content

Commit 4b5edf8

Browse files
maryam-saeidiakowalska622
authored andcommitted
Use RUM label for fatal react errors (elastic#218846)
Closes elastic/observability-dev#4463 ## Summary Since we now have support for error labels in RUM ([PR](elastic/apm-agent-rum-js#1594)), this PR changes the way that we report rum errors to use labels similar to what we've done for toast errors ([PR](elastic#217948)). https://github.com/user-attachments/assets/87a06ceb-705c-4c6e-ab26-d3e5874fe5ad ### ⚠️ Note In local development, the error is captured twice because react bubbles up the error, but it does not happen in production: ([doc](https://react.dev/reference/react/Component#componentdidcatch-caveats)) > Production and development builds of React slightly differ in the way componentDidCatch handles errors. In development, the errors will bubble up to window, which means that any window.onerror or window.addEventListener('error', callback) will intercept the errors that have been caught by componentDidCatch. In production, instead, the errors will not bubble up, which means any ancestor error handler will only receive errors not explicitly caught by componentDidCatch. ### 🧪 How to test Add the following to your kibana.yml file: ``` elastic.apm.active: true elastic.apm.transactionSampleRate: 1.0 elastic.apm.environment: yourName <-- Change to your name ``` <details> <summary>Throw a fatal react error</summary> Throw an error in the [alerts page](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/observability/public/pages/alerts/alerts.tsx) and visit http://localhost:5601/kibana/app/observability/alerts </details> Then visit [kibana-cloud-apm.elastic.dev](https://kibana-cloud-apm.elastic.dev/app/apm/services/kibana-frontend/errors?comparisonEnabled=true&environment=ENVIRONMENT_ALL&kuery=&latencyAggregationType=avg&offset=1d&rangeFrom=now-1h&rangeTo=now&serviceGroup=&transactionType=page-load) filtered for `yourName` in the environment.
1 parent 9cbedab commit 4b5edf8

File tree

9 files changed

+39
-59
lines changed

9 files changed

+39
-59
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
export const getErrorBoundaryLabels = (
11+
errorType: 'PageFatalReactError' | 'SectionFatalReactError'
12+
) => {
13+
return {
14+
errorType,
15+
};
16+
};

src/platform/packages/shared/shared-ux/error_boundary/lib/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
export { mutateError } from './mutate_error';
10+
export { getErrorBoundaryLabels } from './error_boundary_labels';

src/platform/packages/shared/shared-ux/error_boundary/lib/mutate_error.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/platform/packages/shared/shared-ux/error_boundary/src/services/error_boundary_services.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('<KibanaErrorBoundaryProvider>', () => {
3737
expect(reportEventSpy).toBeCalledWith('fatal-error-react', {
3838
component_name: 'BadComponent',
3939
component_stack: expect.any(String),
40-
error_message: 'FatalReactError: This is an error to show the test user!',
40+
error_message: 'Error: This is an error to show the test user!',
4141
error_stack: expect.any(String),
4242
});
4343
});
@@ -66,7 +66,7 @@ describe('<KibanaErrorBoundaryProvider>', () => {
6666
expect(reportEventSpy1).toBeCalledWith('fatal-error-react', {
6767
component_name: 'BadComponent',
6868
component_stack: expect.any(String),
69-
error_message: 'FatalReactError: This is an error to show the test user!',
69+
error_message: 'Error: This is an error to show the test user!',
7070
error_stack: expect.any(String),
7171
});
7272
});

src/platform/packages/shared/shared-ux/error_boundary/src/services/error_service.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ export class KibanaErrorService {
4242
* or treated with "danger" coloring and include a detailed error message.
4343
*/
4444
private getIsFatal(error: Error) {
45-
const customError: Error & { react_error_type?: string; original_name?: string } = error;
46-
const errorName = customError.original_name ?? customError.name;
47-
const isChunkLoadError = MATCH_CHUNK_LOADERROR.test(errorName);
45+
const isChunkLoadError = MATCH_CHUNK_LOADERROR.test(error.name);
4846
return !isChunkLoadError; // "ChunkLoadError" is recoverable by refreshing the page
4947
}
5048

src/platform/packages/shared/shared-ux/error_boundary/src/ui/error_boundary.test.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe('<KibanaErrorBoundary>', () => {
9696
expect(mockDeps.analytics.reportEvent.mock.calls[0][0]).toBe('fatal-error-react');
9797
expect(mockDeps.analytics.reportEvent.mock.calls[0][1]).toMatchObject({
9898
component_name: 'BadComponent',
99-
error_message: 'FatalReactError: This is an error to show the test user!',
99+
error_message: 'Error: This is an error to show the test user!',
100100
});
101101
});
102102

@@ -118,7 +118,7 @@ describe('<KibanaErrorBoundary>', () => {
118118
).toBe(true);
119119
expect(
120120
mockDeps.analytics.reportEvent.mock.calls[0][1].error_stack.startsWith(
121-
'FatalReactError: This is an error to show the test user!'
121+
'Error: This is an error to show the test user!'
122122
)
123123
).toBe(true);
124124
});
@@ -133,15 +133,8 @@ describe('<KibanaErrorBoundary>', () => {
133133

134134
expect(apm.captureError).toHaveBeenCalledTimes(1);
135135
expect(apm.captureError).toHaveBeenCalledWith(
136-
new Error('This is an error to show the test user!')
137-
);
138-
expect(Object.keys((apm.captureError as jest.Mock).mock.calls[0][0])).toEqual([
139-
'react_error_type',
140-
'original_name',
141-
'name',
142-
]);
143-
expect((apm.captureError as jest.Mock).mock.calls[0][0].react_error_type).toEqual(
144-
'fatal-error-react'
136+
new Error('This is an error to show the test user!'),
137+
{ labels: { errorType: 'PageFatalReactError' } }
145138
);
146139
});
147140
});

src/platform/packages/shared/shared-ux/error_boundary/src/ui/error_boundary.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import { apm } from '@elastic/apm-rum';
1111
import React from 'react';
1212

13-
import { mutateError } from '../../lib';
13+
import { getErrorBoundaryLabels } from '../../lib';
1414
import type { KibanaErrorBoundaryServices } from '../../types';
1515
import { useErrorBoundary } from '../services';
1616
import { FatalPrompt, RecoverablePrompt } from './message_components';
@@ -41,10 +41,11 @@ class ErrorBoundaryInternal extends React.Component<
4141
}
4242

4343
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
44-
const customError = mutateError(error);
45-
apm.captureError(customError);
44+
apm.captureError(error, {
45+
labels: getErrorBoundaryLabels('PageFatalReactError'),
46+
});
4647
console.error('Error caught by Kibana React Error Boundary'); // eslint-disable-line no-console
47-
console.error(customError); // eslint-disable-line no-console
48+
console.error(error); // eslint-disable-line no-console
4849

4950
const { name, isFatal } = this.props.services.errorService.registerError(error, errorInfo);
5051
this.setState(() => {

src/platform/packages/shared/shared-ux/error_boundary/src/ui/section_error_boundary.test.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ describe('<KibanaSectionErrorBoundary>', () => {
9292
expect(mockDeps.analytics.reportEvent.mock.calls[0][0]).toBe('fatal-error-react');
9393
expect(mockDeps.analytics.reportEvent.mock.calls[0][1]).toMatchObject({
9494
component_name: 'BadComponent',
95-
error_message: 'FatalReactError: This is an error to show the test user!',
95+
error_message: 'Error: This is an error to show the test user!',
9696
});
9797
});
9898

@@ -114,7 +114,7 @@ describe('<KibanaSectionErrorBoundary>', () => {
114114
).toBe(true);
115115
expect(
116116
mockDeps.analytics.reportEvent.mock.calls[0][1].error_stack.startsWith(
117-
'FatalReactError: This is an error to show the test user!'
117+
'Error: This is an error to show the test user!'
118118
)
119119
).toBe(true);
120120
});
@@ -129,15 +129,8 @@ describe('<KibanaSectionErrorBoundary>', () => {
129129

130130
expect(apm.captureError).toHaveBeenCalledTimes(1);
131131
expect(apm.captureError).toHaveBeenCalledWith(
132-
new Error('This is an error to show the test user!')
133-
);
134-
expect(Object.keys((apm.captureError as jest.Mock).mock.calls[0][0])).toEqual([
135-
'react_error_type',
136-
'original_name',
137-
'name',
138-
]);
139-
expect((apm.captureError as jest.Mock).mock.calls[0][0].react_error_type).toEqual(
140-
'fatal-error-react'
132+
new Error('This is an error to show the test user!'),
133+
{ labels: { errorType: 'SectionFatalReactError' } }
141134
);
142135
});
143136
});

src/platform/packages/shared/shared-ux/error_boundary/src/ui/section_error_boundary.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import { apm } from '@elastic/apm-rum';
1110
import React from 'react';
11+
import { apm } from '@elastic/apm-rum';
1212

13-
import { mutateError } from '../../lib';
13+
import { getErrorBoundaryLabels } from '../../lib';
1414
import type { KibanaErrorBoundaryServices } from '../../types';
1515
import { useErrorBoundary } from '../services';
1616
import { SectionFatalPrompt, SectionRecoverablePrompt } from './message_components';
@@ -65,10 +65,11 @@ class SectionErrorBoundaryInternal extends React.Component<
6565
}
6666

6767
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
68-
const customError = mutateError(error);
69-
apm.captureError(customError);
68+
apm.captureError(error, {
69+
labels: getErrorBoundaryLabels('SectionFatalReactError'),
70+
});
7071
console.error('Error caught by Kibana React Error Boundary'); // eslint-disable-line no-console
71-
console.error(customError); // eslint-disable-line no-console
72+
console.error(error); // eslint-disable-line no-console
7273

7374
const { name, isFatal } = this.props.services.errorService.registerError(error, errorInfo);
7475
this.setState({ error, errorInfo, componentName: name, isFatal });

0 commit comments

Comments
 (0)