Skip to content

Commit b80a55c

Browse files
[8.19] Use RUM label for fatal react errors (#218846) (#222156)
# Backport This will backport the following commits from `main` to `8.19`: - [Use RUM label for fatal react errors (#218846)](#218846) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Maryam Saeidi","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-04-29T08:14:48Z","message":"Use RUM label for fatal react errors (#218846)\n\nCloses https://github.com/elastic/observability-dev/issues/4463\n\n## Summary\n\nSince we now have support for error labels in RUM\n([PR](elastic/apm-agent-rum-js#1594)), this PR\nchanges the way that we report rum errors to use labels similar to what\nwe've done for toast errors\n([PR](https://github.com/elastic/kibana/pull/217948)).\n\n\n\nhttps://github.com/user-attachments/assets/87a06ceb-705c-4c6e-ab26-d3e5874fe5ad\n\n\n### ⚠️ Note\n\nIn local development, the error is captured twice because react bubbles\nup the error, but it does not happen in production:\n([doc](https://react.dev/reference/react/Component#componentdidcatch-caveats))\n\n> Production and development builds of React slightly differ in the way\ncomponentDidCatch handles errors. In development, the errors will bubble\nup to window, which means that any window.onerror or\nwindow.addEventListener('error', callback) will intercept the errors\nthat have been caught by componentDidCatch. In production, instead, the\nerrors will not bubble up, which means any ancestor error handler will\nonly receive errors not explicitly caught by componentDidCatch.\n\n### 🧪 How to test\n\nAdd the following to your kibana.yml file:\n\n```\nelastic.apm.active: true\nelastic.apm.transactionSampleRate: 1.0\nelastic.apm.environment: yourName <-- Change to your name\n```\n<details>\n<summary>Throw a fatal react error</summary>\n\nThrow an error in the [alerts\npage](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/observability/public/pages/alerts/alerts.tsx)\nand visit http://localhost:5601/kibana/app/observability/alerts\n\n</details>\n\nThen visit\n[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)\nfiltered for `yourName` in the environment.","sha":"cea253dcc67ed2cfb460a0707ac1e66cdb9180fa","branchLabelMapping":{"^v9.1.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:version","v9.1.0","v8.19.0"],"title":"Use RUM label for fatal react errors","number":218846,"url":"https://github.com/elastic/kibana/pull/218846","mergeCommit":{"message":"Use RUM label for fatal react errors (#218846)\n\nCloses https://github.com/elastic/observability-dev/issues/4463\n\n## Summary\n\nSince we now have support for error labels in RUM\n([PR](elastic/apm-agent-rum-js#1594)), this PR\nchanges the way that we report rum errors to use labels similar to what\nwe've done for toast errors\n([PR](https://github.com/elastic/kibana/pull/217948)).\n\n\n\nhttps://github.com/user-attachments/assets/87a06ceb-705c-4c6e-ab26-d3e5874fe5ad\n\n\n### ⚠️ Note\n\nIn local development, the error is captured twice because react bubbles\nup the error, but it does not happen in production:\n([doc](https://react.dev/reference/react/Component#componentdidcatch-caveats))\n\n> Production and development builds of React slightly differ in the way\ncomponentDidCatch handles errors. In development, the errors will bubble\nup to window, which means that any window.onerror or\nwindow.addEventListener('error', callback) will intercept the errors\nthat have been caught by componentDidCatch. In production, instead, the\nerrors will not bubble up, which means any ancestor error handler will\nonly receive errors not explicitly caught by componentDidCatch.\n\n### 🧪 How to test\n\nAdd the following to your kibana.yml file:\n\n```\nelastic.apm.active: true\nelastic.apm.transactionSampleRate: 1.0\nelastic.apm.environment: yourName <-- Change to your name\n```\n<details>\n<summary>Throw a fatal react error</summary>\n\nThrow an error in the [alerts\npage](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/observability/public/pages/alerts/alerts.tsx)\nand visit http://localhost:5601/kibana/app/observability/alerts\n\n</details>\n\nThen visit\n[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)\nfiltered for `yourName` in the environment.","sha":"cea253dcc67ed2cfb460a0707ac1e66cdb9180fa"}},"sourceBranch":"main","suggestedTargetBranches":["8.19"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/218846","number":218846,"mergeCommit":{"message":"Use RUM label for fatal react errors (#218846)\n\nCloses https://github.com/elastic/observability-dev/issues/4463\n\n## Summary\n\nSince we now have support for error labels in RUM\n([PR](elastic/apm-agent-rum-js#1594)), this PR\nchanges the way that we report rum errors to use labels similar to what\nwe've done for toast errors\n([PR](https://github.com/elastic/kibana/pull/217948)).\n\n\n\nhttps://github.com/user-attachments/assets/87a06ceb-705c-4c6e-ab26-d3e5874fe5ad\n\n\n### ⚠️ Note\n\nIn local development, the error is captured twice because react bubbles\nup the error, but it does not happen in production:\n([doc](https://react.dev/reference/react/Component#componentdidcatch-caveats))\n\n> Production and development builds of React slightly differ in the way\ncomponentDidCatch handles errors. In development, the errors will bubble\nup to window, which means that any window.onerror or\nwindow.addEventListener('error', callback) will intercept the errors\nthat have been caught by componentDidCatch. In production, instead, the\nerrors will not bubble up, which means any ancestor error handler will\nonly receive errors not explicitly caught by componentDidCatch.\n\n### 🧪 How to test\n\nAdd the following to your kibana.yml file:\n\n```\nelastic.apm.active: true\nelastic.apm.transactionSampleRate: 1.0\nelastic.apm.environment: yourName <-- Change to your name\n```\n<details>\n<summary>Throw a fatal react error</summary>\n\nThrow an error in the [alerts\npage](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/observability/public/pages/alerts/alerts.tsx)\nand visit http://localhost:5601/kibana/app/observability/alerts\n\n</details>\n\nThen visit\n[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)\nfiltered for `yourName` in the environment.","sha":"cea253dcc67ed2cfb460a0707ac1e66cdb9180fa"}},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Maryam Saeidi <[email protected]>
1 parent dcf064a commit b80a55c

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)