Skip to content

Commit c93c584

Browse files
committed
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. (cherry picked from commit cea253d)
1 parent 3e0f5cb commit c93c584

File tree

9 files changed

+37
-57
lines changed

9 files changed

+37
-57
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
@@ -38,7 +38,7 @@ describe('<KibanaErrorBoundaryProvider>', () => {
3838
expect(reportEventSpy).toBeCalledWith('fatal-error-react', {
3939
component_name: 'BadComponent',
4040
component_stack: expect.any(String),
41-
error_message: 'FatalReactError: This is an error to show the test user!',
41+
error_message: 'Error: This is an error to show the test user!',
4242
error_stack: expect.any(String),
4343
});
4444
});
@@ -67,7 +67,7 @@ describe('<KibanaErrorBoundaryProvider>', () => {
6767
expect(reportEventSpy1).toBeCalledWith('fatal-error-react', {
6868
component_name: 'BadComponent',
6969
component_stack: expect.any(String),
70-
error_message: 'FatalReactError: This is an error to show the test user!',
70+
error_message: 'Error: This is an error to show the test user!',
7171
error_stack: expect.any(String),
7272
});
7373
});

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: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('<KibanaErrorBoundary>', () => {
9797
expect(mockDeps.analytics.reportEvent.mock.calls[0][0]).toBe('fatal-error-react');
9898
expect(mockDeps.analytics.reportEvent.mock.calls[0][1]).toMatchObject({
9999
component_name: 'BadComponent',
100-
error_message: 'FatalReactError: This is an error to show the test user!',
100+
error_message: 'Error: This is an error to show the test user!',
101101
});
102102
});
103103

@@ -134,15 +134,8 @@ describe('<KibanaErrorBoundary>', () => {
134134

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

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: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe('<KibanaSectionErrorBoundary>', () => {
9393
expect(mockDeps.analytics.reportEvent.mock.calls[0][0]).toBe('fatal-error-react');
9494
expect(mockDeps.analytics.reportEvent.mock.calls[0][1]).toMatchObject({
9595
component_name: 'BadComponent',
96-
error_message: 'FatalReactError: This is an error to show the test user!',
96+
error_message: 'Error: This is an error to show the test user!',
9797
});
9898
});
9999

@@ -130,15 +130,8 @@ describe('<KibanaSectionErrorBoundary>', () => {
130130

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

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)