From 6c67461844ae9565a3d6c318dad51e72d47adb73 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 11 May 2021 15:18:22 -0400 Subject: [PATCH 1/7] feat(react): Add support for React 17 Error Boundaries --- packages/react/package.json | 4 +- packages/react/src/errorboundary.tsx | 47 +++++++++++++++++++++- packages/react/test/errorboundary.test.tsx | 31 +++++++------- yarn.lock | 29 +++++++++---- 4 files changed, 85 insertions(+), 26 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index b497dcdb6822..41ce4697cef1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -46,8 +46,8 @@ "jsdom": "^16.2.2", "npm-run-all": "^4.1.2", "prettier": "1.19.0", - "react": "^16.0.0", - "react-dom": "^16.0.0", + "react": "^17.0.0", + "react-dom": "^17.0.0", "react-router-3": "npm:react-router@3.2.0", "react-router-4": "npm:react-router@4.1.0", "react-router-5": "npm:react-router@5.0.0", diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index eedb34490227..eaa52bb2b7d0 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -1,7 +1,19 @@ -import { captureException, ReportDialogOptions, Scope, showReportDialog, withScope } from '@sentry/browser'; +import { + captureEvent, + captureException, + eventFromException, + ReportDialogOptions, + Scope, + showReportDialog, + withScope, +} from '@sentry/browser'; +import { Event, Severity } from '@sentry/types'; +import { parseSemver } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; +const reactVersion = parseSemver(React.version); + export const UNKNOWN_COMPONENT = 'unknown'; export type FallbackRender = (errorData: { @@ -52,6 +64,37 @@ const INITIAL_STATE = { eventId: null, }; +function captureReactErrorBoundaryError(error: Error, componentStack: string): string { + const errorBoundaryError = new Error(error.name); + errorBoundaryError.stack = componentStack; + errorBoundaryError.message = error.message; + + let errorBoundaryEvent: Event = {}; + void eventFromException({}, errorBoundaryError).then(e => { + errorBoundaryEvent = e; + }); + + if ( + errorBoundaryEvent.exception && + Array.isArray(errorBoundaryEvent.exception.values) && + reactVersion.major && + reactVersion.major >= 17 + ) { + let originalEvent: Event = {}; + void eventFromException({}, error).then(e => { + originalEvent = e; + }); + originalEvent.level = Severity.Error; + if (originalEvent.exception && Array.isArray(originalEvent.exception.values)) { + originalEvent.exception.values.push(...errorBoundaryEvent.exception.values); + } + + return captureEvent(originalEvent); + } + + return captureException(error, { contexts: { react: { componentStack } } }); +} + /** * A ErrorBoundary component that logs errors to Sentry. * Requires React >= 16 @@ -66,7 +109,7 @@ class ErrorBoundary extends React.Component { const actual = jest.requireActual('@sentry/browser'); return { ...actual, - captureException: (err: any, ctx: any) => { - mockCaptureException(err, ctx); + captureEvent: (event: Event) => { + mockCaptureEvent(event); return EVENT_ID; }, showReportDialog: (options: any) => { @@ -67,7 +68,7 @@ describe('ErrorBoundary', () => { jest.spyOn(console, 'error').mockImplementation(); afterEach(() => { - mockCaptureException.mockClear(); + mockCaptureEvent.mockClear(); mockShowReportDialog.mockClear(); }); @@ -170,10 +171,7 @@ describe('ErrorBoundary', () => { expect(container.innerHTML).toBe('
Fallback here
'); expect(errorString).toBe('Error: boom'); - expect(compStack).toBe(` - in Bam (created by TestApp) - in ErrorBoundary (created by TestApp) - in TestApp`); + expect(compStack).toMatch(/\s+(at Bam) \(.*?\)\s+(at ErrorBoundary) \(.*?\)\s+(at TestApp) \(.*?\)/g); expect(eventIdString).toBe(EVENT_ID); }); }); @@ -188,7 +186,7 @@ describe('ErrorBoundary', () => { ); expect(mockOnError).toHaveBeenCalledTimes(0); - expect(mockCaptureException).toHaveBeenCalledTimes(0); + expect(mockCaptureEvent).toHaveBeenCalledTimes(0); const btn = screen.getByTestId('errorBtn'); fireEvent.click(btn); @@ -196,9 +194,12 @@ describe('ErrorBoundary', () => { expect(mockOnError).toHaveBeenCalledTimes(1); expect(mockOnError).toHaveBeenCalledWith(expect.any(Error), expect.any(String), expect.any(String)); - expect(mockCaptureException).toHaveBeenCalledTimes(1); - expect(mockCaptureException).toHaveBeenCalledWith(expect.any(Error), { - contexts: { react: { componentStack: expect.any(String) } }, + expect(mockCaptureEvent).toHaveBeenCalledTimes(1); + expect(mockCaptureEvent).toHaveBeenCalledWith({ + exception: { + values: expect.any(Array), + }, + level: Severity.Error, }); }); @@ -206,7 +207,7 @@ describe('ErrorBoundary', () => { const mockBeforeCapture = jest.fn(); const testBeforeCapture = (...args: any[]) => { - expect(mockCaptureException).toHaveBeenCalledTimes(0); + expect(mockCaptureEvent).toHaveBeenCalledTimes(0); mockBeforeCapture(...args); }; @@ -217,14 +218,14 @@ describe('ErrorBoundary', () => { ); expect(mockBeforeCapture).toHaveBeenCalledTimes(0); - expect(mockCaptureException).toHaveBeenCalledTimes(0); + expect(mockCaptureEvent).toHaveBeenCalledTimes(0); const btn = screen.getByTestId('errorBtn'); fireEvent.click(btn); expect(mockBeforeCapture).toHaveBeenCalledTimes(1); expect(mockBeforeCapture).toHaveBeenLastCalledWith(expect.any(Scope), expect.any(Error), expect.any(String)); - expect(mockCaptureException).toHaveBeenCalledTimes(1); + expect(mockCaptureEvent).toHaveBeenCalledTimes(1); }); it('shows a Sentry Report Dialog with correct options', () => { diff --git a/yarn.lock b/yarn.lock index 888aca22443c..a3750c692dbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16583,15 +16583,14 @@ rc@^1.0.1, rc@^1.1.6: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^16.0.0: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" - integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== +react-dom@^17.0.0: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" + scheduler "^0.20.2" react-error-boundary@^3.1.0: version "3.1.1" @@ -16667,7 +16666,7 @@ react-test-renderer@^16.13.1: react-is "^16.8.6" scheduler "^0.19.1" -react@^16.0.0, react@^16.13.1: +react@^16.13.1: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== @@ -16676,6 +16675,14 @@ react@^16.0.0, react@^16.13.1: object-assign "^4.1.1" prop-types "^15.6.2" +react@^17.0.0: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + read-cmd-shim@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16" @@ -17583,6 +17590,14 @@ scheduler@^0.19.1: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" From 7ced5c1ccd8fed6e1894490faec93b0a615abb39 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 12 May 2021 08:51:09 -0400 Subject: [PATCH 2/7] test: Update react + gatsby tests --- packages/gatsby/package.json | 2 +- packages/gatsby/test/integration.test.tsx | 3 +- packages/react/src/errorboundary.tsx | 1 - packages/react/test/errorboundary.test.tsx | 66 ++++++++++++++++++---- yarn.lock | 9 --- 5 files changed, 59 insertions(+), 22 deletions(-) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index f01b62e3e96e..67bba3ef94ad 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -39,7 +39,7 @@ "jest": "^24.7.1", "npm-run-all": "^4.1.2", "prettier": "1.19.0", - "react": "^16.13.1", + "react": "^17.0.0", "rimraf": "^2.6.3", "typescript": "3.7.5" }, diff --git a/packages/gatsby/test/integration.test.tsx b/packages/gatsby/test/integration.test.tsx index ae5b34d25fa9..8d420e37d0f0 100644 --- a/packages/gatsby/test/integration.test.tsx +++ b/packages/gatsby/test/integration.test.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { render } from '@testing-library/react'; import * as React from 'react'; +import { useEffect } from 'react'; import { onClientEntry } from '../gatsby-browser'; @@ -24,7 +25,7 @@ describe('useEffect', () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars function TestComponent() { - React.useEffect(() => { + useEffect(() => { const error = new Error('testing 123'); (window as any).Sentry.captureException(error); }); diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index eaa52bb2b7d0..06d4c1aea992 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -84,7 +84,6 @@ function captureReactErrorBoundaryError(error: Error, componentStack: string): s void eventFromException({}, error).then(e => { originalEvent = e; }); - originalEvent.level = Severity.Error; if (originalEvent.exception && Array.isArray(originalEvent.exception.values)) { originalEvent.exception.values.push(...errorBoundaryEvent.exception.values); } diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index 640361005052..193cdf5b931b 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -2,6 +2,7 @@ import { Scope } from '@sentry/browser'; import { Event, Severity } from '@sentry/types'; import { fireEvent, render, screen } from '@testing-library/react'; import * as React from 'react'; +import { useState } from 'react'; import { ErrorBoundary, ErrorBoundaryProps, UNKNOWN_COMPONENT, withErrorBoundary } from '../src/errorboundary'; @@ -23,6 +24,15 @@ jest.mock('@sentry/browser', () => { }; }); +function Boo({ title }: { title: string }): JSX.Element { + throw new Error(title); +} + +function Bam(): JSX.Element { + const [title] = useState('boom'); + return ; +} + const TestApp: React.FC = ({ children, ...props }) => { const [isError, setError] = React.useState(false); return ( @@ -46,10 +56,6 @@ const TestApp: React.FC = ({ children, ...props }) => { ); }; -function Bam(): JSX.Element { - throw new Error('boom'); -} - describe('withErrorBoundary', () => { it('sets displayName properly', () => { const TestComponent = () =>

Hello World

; @@ -171,7 +177,15 @@ describe('ErrorBoundary', () => { expect(container.innerHTML).toBe('
Fallback here
'); expect(errorString).toBe('Error: boom'); - expect(compStack).toMatch(/\s+(at Bam) \(.*?\)\s+(at ErrorBoundary) \(.*?\)\s+(at TestApp) \(.*?\)/g); + /* + at Boo (/path/to/sentry-javascript/packages/react/test/errorboundary.test.tsx:23:20) + at Bam (/path/to/sentry-javascript/packages/react/test/errorboundary.test.tsx:40:11) + at ErrorBoundary (/path/to/sentry-javascript/packages/react/src/errorboundary.tsx:2026:39) + at TestApp (/path/to/sentry-javascript/packages/react/test/errorboundary.test.tsx:22:23) + */ + expect(compStack).toMatch( + /\s+(at Boo) \(.*?\)\s+(at Bam) \(.*?\)\s+(at ErrorBoundary) \(.*?\)\s+(at TestApp) \(.*?\)/g, + ); expect(eventIdString).toBe(EVENT_ID); }); }); @@ -195,11 +209,43 @@ describe('ErrorBoundary', () => { expect(mockOnError).toHaveBeenCalledWith(expect.any(Error), expect.any(String), expect.any(String)); expect(mockCaptureEvent).toHaveBeenCalledTimes(1); - expect(mockCaptureEvent).toHaveBeenCalledWith({ - exception: { - values: expect.any(Array), - }, - level: Severity.Error, + + // We do a detailed assert on the stacktrace as a regression test against future + // react changes (that way we can update the docs if frames change in a major way). + const event = mockCaptureEvent.mock.calls[0][0]; + expect(event.exception.values).toHaveLength(2); + expect(event.level).toBe(Severity.Error); + expect(event.exception.values[1].stacktrace).toEqual({ + frames: [ + { + colno: 23, + filename: expect.stringContaining('errorboundary.test.tsx'), + function: 'TestApp', + in_app: true, + lineno: 31, + }, + { + colno: 39, + filename: expect.stringContaining('errorboundary.tsx'), + function: 'ErrorBoundary', + in_app: true, + lineno: 2026, + }, + { + colno: 37, + filename: expect.stringContaining('errorboundary.test.tsx'), + function: 'Bam', + in_app: true, + lineno: 27, + }, + { + colno: 20, + filename: expect.stringContaining('errorboundary.test.tsx'), + function: 'Boo', + in_app: true, + lineno: 23, + }, + ], }); }); diff --git a/yarn.lock b/yarn.lock index a3750c692dbf..c540a546ba46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16666,15 +16666,6 @@ react-test-renderer@^16.13.1: react-is "^16.8.6" scheduler "^0.19.1" -react@^16.13.1: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" - integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - react@^17.0.0: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" From f5dec7752813ee6a9928c910e858669897ee81b8 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 12 May 2021 08:52:27 -0400 Subject: [PATCH 3/7] yarn fix --- packages/gatsby/test/integration.test.tsx | 3 +-- packages/react/src/errorboundary.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/gatsby/test/integration.test.tsx b/packages/gatsby/test/integration.test.tsx index 8d420e37d0f0..08a85d7e101e 100644 --- a/packages/gatsby/test/integration.test.tsx +++ b/packages/gatsby/test/integration.test.tsx @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { render } from '@testing-library/react'; -import * as React from 'react'; import { useEffect } from 'react'; import { onClientEntry } from '../gatsby-browser'; @@ -15,7 +14,7 @@ describe('useEffect', () => { let calls = 0; onClientEntry(undefined, { - beforeSend: event => { + beforeSend: (event: any) => { expect(event).not.toBeUndefined(); calls += 1; diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index 06d4c1aea992..dd8d8809d0d8 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -7,7 +7,7 @@ import { showReportDialog, withScope, } from '@sentry/browser'; -import { Event, Severity } from '@sentry/types'; +import { Event } from '@sentry/types'; import { parseSemver } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; @@ -64,6 +64,13 @@ const INITIAL_STATE = { eventId: null, }; +/** + * Logs react error boundary errors to Sentry. If on React version >= 17, creates stack trace + * from componentStack param, otherwise relies on error param for stacktrace. + * + * @param error An error captured by React Error Boundary + * @param componentStack The component stacktrace + */ function captureReactErrorBoundaryError(error: Error, componentStack: string): string { const errorBoundaryError = new Error(error.name); errorBoundaryError.stack = componentStack; From 08c4d55c47e843140596bd8432a83217ee90ac0c Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 12 May 2021 09:13:16 -0400 Subject: [PATCH 4/7] test: Cleanup frames assertion --- packages/react/test/errorboundary.test.tsx | 63 +++++++++++----------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index 193cdf5b931b..74e227178ee3 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -215,38 +215,37 @@ describe('ErrorBoundary', () => { const event = mockCaptureEvent.mock.calls[0][0]; expect(event.exception.values).toHaveLength(2); expect(event.level).toBe(Severity.Error); - expect(event.exception.values[1].stacktrace).toEqual({ - frames: [ - { - colno: 23, - filename: expect.stringContaining('errorboundary.test.tsx'), - function: 'TestApp', - in_app: true, - lineno: 31, - }, - { - colno: 39, - filename: expect.stringContaining('errorboundary.tsx'), - function: 'ErrorBoundary', - in_app: true, - lineno: 2026, - }, - { - colno: 37, - filename: expect.stringContaining('errorboundary.test.tsx'), - function: 'Bam', - in_app: true, - lineno: 27, - }, - { - colno: 20, - filename: expect.stringContaining('errorboundary.test.tsx'), - function: 'Boo', - in_app: true, - lineno: 23, - }, - ], - }); + + expect(event.exception.values[1].stacktrace.frames).toEqual([ + { + colno: expect.any(Number), + filename: expect.stringContaining('errorboundary.test.tsx'), + function: 'TestApp', + in_app: true, + lineno: expect.any(Number), + }, + { + colno: expect.any(Number), + filename: expect.stringContaining('errorboundary.tsx'), + function: 'ErrorBoundary', + in_app: true, + lineno: expect.any(Number), + }, + { + colno: expect.any(Number), + filename: expect.stringContaining('errorboundary.test.tsx'), + function: 'Bam', + in_app: true, + lineno: expect.any(Number), + }, + { + colno: expect.any(Number), + filename: expect.stringContaining('errorboundary.test.tsx'), + function: 'Boo', + in_app: true, + lineno: expect.any(Number), + }, + ]); }); it('calls `beforeCapture()` when an error occurs', () => { From 4d61b41a78d75524d28973aaaff621d44f0e6e88 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 12 May 2021 10:40:58 -0400 Subject: [PATCH 5/7] fix: Add back react import --- packages/gatsby/test/integration.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/gatsby/test/integration.test.tsx b/packages/gatsby/test/integration.test.tsx index 08a85d7e101e..adde157acc6e 100644 --- a/packages/gatsby/test/integration.test.tsx +++ b/packages/gatsby/test/integration.test.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { render } from '@testing-library/react'; import { useEffect } from 'react'; +import * as React from 'react'; import { onClientEntry } from '../gatsby-browser'; From 1c3c8a95d3b2f9184ce0f0aa3208d6f58acee7af Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 13 May 2021 13:44:09 -0400 Subject: [PATCH 6/7] fix: Generate error correctly --- packages/react/src/errorboundary.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index dd8d8809d0d8..1cd300257604 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -72,9 +72,8 @@ const INITIAL_STATE = { * @param componentStack The component stacktrace */ function captureReactErrorBoundaryError(error: Error, componentStack: string): string { - const errorBoundaryError = new Error(error.name); + const errorBoundaryError = new Error(error.message); errorBoundaryError.stack = componentStack; - errorBoundaryError.message = error.message; let errorBoundaryEvent: Event = {}; void eventFromException({}, errorBoundaryError).then(e => { From 5fc12f80f36363c2af678a7c7b1628202fae0bd6 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 14 May 2021 07:59:51 -0400 Subject: [PATCH 7/7] chore: Add comment for error boundary frame --- packages/react/src/errorboundary.tsx | 9 ++++++--- packages/react/test/errorboundary.test.tsx | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index 1cd300257604..b4a752c87930 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -73,6 +73,7 @@ const INITIAL_STATE = { */ function captureReactErrorBoundaryError(error: Error, componentStack: string): string { const errorBoundaryError = new Error(error.message); + errorBoundaryError.name = `React ErrorBoundary ${errorBoundaryError.name}`; errorBoundaryError.stack = componentStack; let errorBoundaryEvent: Event = {}; @@ -91,7 +92,7 @@ function captureReactErrorBoundaryError(error: Error, componentStack: string): s originalEvent = e; }); if (originalEvent.exception && Array.isArray(originalEvent.exception.values)) { - originalEvent.exception.values.push(...errorBoundaryEvent.exception.values); + originalEvent.exception.values = [...errorBoundaryEvent.exception.values, ...originalEvent.exception.values]; } return captureEvent(originalEvent); @@ -101,8 +102,10 @@ function captureReactErrorBoundaryError(error: Error, componentStack: string): s } /** - * A ErrorBoundary component that logs errors to Sentry. - * Requires React >= 16 + * A ErrorBoundary component that logs errors to Sentry. Requires React >= 16. + * NOTE: If you are a Sentry user, and you are seeing this stack frame, it means the + * Sentry React SDK ErrorBoundary caught an error invoking your application code. This + * is expected behavior and NOT indicative of a bug with the Sentry React SDK. */ class ErrorBoundary extends React.Component { public state: ErrorBoundaryState = INITIAL_STATE; diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index 74e227178ee3..27be3f82706d 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -216,7 +216,8 @@ describe('ErrorBoundary', () => { expect(event.exception.values).toHaveLength(2); expect(event.level).toBe(Severity.Error); - expect(event.exception.values[1].stacktrace.frames).toEqual([ + expect(event.exception.values[0].type).toEqual('React ErrorBoundary Error'); + expect(event.exception.values[0].stacktrace.frames).toEqual([ { colno: expect.any(Number), filename: expect.stringContaining('errorboundary.test.tsx'),