From 62598b32cff3a000bac171dcd621191f9a2f0c1c Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Wed, 22 Sep 2021 14:44:19 -0400
Subject: [PATCH 1/4] ref(react): Rely on error.cause to link ErrorBoundary
errors
---
packages/react/src/errorboundary.tsx | 44 +++-----------
.../test/errorboundary-integration.test.tsx | 59 +++++++++++++++++++
2 files changed, 68 insertions(+), 35 deletions(-)
create mode 100644 packages/react/test/errorboundary-integration.test.tsx
diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index 162bb543b99b..b843b5f5d589 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -1,13 +1,4 @@
-import {
- captureEvent,
- captureException,
- eventFromException,
- ReportDialogOptions,
- Scope,
- showReportDialog,
- withScope,
-} from '@sentry/browser';
-import { Event } from '@sentry/types';
+import { captureException, ReportDialogOptions, Scope, showReportDialog, withScope } from '@sentry/browser';
import { logger, parseSemver } from '@sentry/utils';
import hoistNonReactStatics from 'hoist-non-react-statics';
import * as React from 'react';
@@ -71,31 +62,14 @@ const INITIAL_STATE = {
* @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.message);
- errorBoundaryError.name = `React ErrorBoundary ${errorBoundaryError.name}`;
- errorBoundaryError.stack = componentStack;
-
- 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;
- });
- if (originalEvent.exception && Array.isArray(originalEvent.exception.values)) {
- originalEvent.exception.values = [...errorBoundaryEvent.exception.values, ...originalEvent.exception.values];
- }
-
- return captureEvent(originalEvent);
+function captureReactErrorBoundaryError(error: Error & { cause?: Error }, componentStack: string): string {
+ if (reactVersion.major && reactVersion.major >= 17) {
+ const errorBoundaryError = new Error(error.message);
+ errorBoundaryError.name = `React ErrorBoundary ${errorBoundaryError.name}`;
+ errorBoundaryError.stack = componentStack;
+
+ error.cause = errorBoundaryError;
+ return captureException(error);
}
return captureException(error, { contexts: { react: { componentStack } } });
diff --git a/packages/react/test/errorboundary-integration.test.tsx b/packages/react/test/errorboundary-integration.test.tsx
new file mode 100644
index 000000000000..4dc3b117f9bc
--- /dev/null
+++ b/packages/react/test/errorboundary-integration.test.tsx
@@ -0,0 +1,59 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import * as React from 'react';
+import { useState } from 'react';
+
+import { init } from '../src';
+import { ErrorBoundary, ErrorBoundaryProps } from '../src/errorboundary';
+
+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 (
+ {
+ setError(false);
+ if (props.onReset) {
+ props.onReset(...args);
+ }
+ }}
+ >
+ {isError ? : children}
+
+ );
+};
+
+describe.only('Integration Test', () => {
+ it('captures an error and sends it to Sentry', () => {
+ init({
+ dsn: '',
+ beforeSend: event => {
+ console.log(event);
+ return event;
+ },
+ });
+
+ render(
+ You have hit an error
}>
+ children
+ ,
+ );
+
+ const btn = screen.getByTestId('errorBtn');
+ fireEvent.click(btn);
+ });
+});
From 8d602823141d3c07cd6efd9017992e53a6e03267 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Mon, 27 Sep 2021 09:51:52 -0400
Subject: [PATCH 2/4] add integration test
---
...egration.test.tsx => integration.test.tsx} | 52 +++++++++++++++----
1 file changed, 41 insertions(+), 11 deletions(-)
rename packages/react/test/{errorboundary-integration.test.tsx => integration.test.tsx} (51%)
diff --git a/packages/react/test/errorboundary-integration.test.tsx b/packages/react/test/integration.test.tsx
similarity index 51%
rename from packages/react/test/errorboundary-integration.test.tsx
rename to packages/react/test/integration.test.tsx
index 4dc3b117f9bc..b9e28d4bec35 100644
--- a/packages/react/test/errorboundary-integration.test.tsx
+++ b/packages/react/test/integration.test.tsx
@@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import * as React from 'react';
import { useState } from 'react';
-import { init } from '../src';
+import { init, getCurrentHub, BrowserClient } from '../src';
import { ErrorBoundary, ErrorBoundaryProps } from '../src/errorboundary';
function Boo({ title }: { title: string }): JSX.Element {
@@ -37,18 +37,48 @@ const TestApp: React.FC = ({ children, ...props }) => {
);
};
-describe.only('Integration Test', () => {
- it('captures an error and sends it to Sentry', () => {
- init({
- dsn: '',
- beforeSend: event => {
- console.log(event);
- return event;
- },
- });
+const dsn = 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012';
+
+describe('React Integration Test', () => {
+ beforeAll(() => {
+ init({ dsn });
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ getCurrentHub().pushScope();
+ });
+
+ afterEach(() => {
+ getCurrentHub().popScope();
+ });
+
+ it('captures an error and sends it to Sentry', done => {
+ let capturedHint;
+ let error: Error;
+ let eventId: string;
+
+ expect.assertions(6);
+ getCurrentHub().bindClient(
+ new BrowserClient({
+ beforeSend: (event: Event) => {
+ expect(event.tags).toEqual({ test: '1' });
+ expect(event.exception).not.toBeUndefined();
+e done();
+ return null;
+ },
+ dsn,
+ }),
+ );
render(
- You have hit an error}>
+ You have hit an error}
+ onError={(e, _, id) => {
+ error = e;
+ eventId = id;
+ }}
+ >
children
,
);
From 33482e8c79fc76f8865c615970a8932b7c84bcbc Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Tue, 5 Oct 2021 11:53:05 -0400
Subject: [PATCH 3/4] always add react context in capture
---
packages/react/src/errorboundary.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index b843b5f5d589..03b0798cfac7 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -69,7 +69,6 @@ function captureReactErrorBoundaryError(error: Error & { cause?: Error }, compon
errorBoundaryError.stack = componentStack;
error.cause = errorBoundaryError;
- return captureException(error);
}
return captureException(error, { contexts: { react: { componentStack } } });
From feb8434d35c1d463bb91651cac13735b21525725 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Tue, 5 Oct 2021 12:49:10 -0400
Subject: [PATCH 4/4] ref: re-arrange and add tests
---
packages/react/src/errorboundary.tsx | 37 ++++-----
packages/react/test/errorboundary.test.tsx | 68 +++++------------
packages/react/test/integration.test.tsx | 89 ----------------------
3 files changed, 36 insertions(+), 158 deletions(-)
delete mode 100644 packages/react/test/integration.test.tsx
diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index 03b0798cfac7..4b12fd7c6b0f 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -44,7 +44,7 @@ export type ErrorBoundaryProps = {
};
type ErrorBoundaryState = {
- componentStack: string | null;
+ componentStack: React.ErrorInfo['componentStack'] | null;
error: Error | null;
eventId: string | null;
};
@@ -55,25 +55,6 @@ 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 & { cause?: Error }, componentStack: string): string {
- if (reactVersion.major && reactVersion.major >= 17) {
- const errorBoundaryError = new Error(error.message);
- errorBoundaryError.name = `React ErrorBoundary ${errorBoundaryError.name}`;
- errorBoundaryError.stack = componentStack;
-
- error.cause = errorBoundaryError;
- }
-
- return captureException(error, { contexts: { react: { componentStack } } });
-}
-
/**
* 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
@@ -83,14 +64,26 @@ function captureReactErrorBoundaryError(error: Error & { cause?: Error }, compon
class ErrorBoundary extends React.Component {
public state: ErrorBoundaryState = INITIAL_STATE;
- public componentDidCatch(error: Error, { componentStack }: React.ErrorInfo): void {
+ public componentDidCatch(error: Error & { cause?: Error }, { componentStack }: React.ErrorInfo): void {
const { beforeCapture, onError, showDialog, dialogOptions } = this.props;
withScope(scope => {
+ // If on React version >= 17, create stack trace from componentStack param and links
+ // to to the original error using `error.cause` otherwise relies on error param for stacktrace.
+ // Linking errors requires the `LinkedErrors` integration be enabled.
+ if (reactVersion.major && reactVersion.major >= 17) {
+ const errorBoundaryError = new Error(error.message);
+ errorBoundaryError.name = `React ErrorBoundary ${errorBoundaryError.name}`;
+ errorBoundaryError.stack = componentStack;
+
+ // Using the `LinkedErrors` integration to link the errors together.
+ error.cause = errorBoundaryError;
+ }
+
if (beforeCapture) {
beforeCapture(scope, error, componentStack);
}
- const eventId = captureReactErrorBoundaryError(error, componentStack);
+ const eventId = captureException(error, { contexts: { react: { componentStack } } });
if (onError) {
onError(error, componentStack, eventId);
}
diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx
index b0b8dd4c0b3f..ac6318be053c 100644
--- a/packages/react/test/errorboundary.test.tsx
+++ b/packages/react/test/errorboundary.test.tsx
@@ -6,7 +6,7 @@ import { useState } from 'react';
import { ErrorBoundary, ErrorBoundaryProps, UNKNOWN_COMPONENT, withErrorBoundary } from '../src/errorboundary';
-const mockCaptureEvent = jest.fn();
+const mockCaptureException = jest.fn();
const mockShowReportDialog = jest.fn();
const EVENT_ID = 'test-id-123';
@@ -14,8 +14,8 @@ jest.mock('@sentry/browser', () => {
const actual = jest.requireActual('@sentry/browser');
return {
...actual,
- captureEvent: (event: Event) => {
- mockCaptureEvent(event);
+ captureException: (...args: unknown[]) => {
+ mockCaptureException(...args);
return EVENT_ID;
},
showReportDialog: (options: any) => {
@@ -74,7 +74,7 @@ describe('ErrorBoundary', () => {
jest.spyOn(console, 'error').mockImplementation();
afterEach(() => {
- mockCaptureEvent.mockClear();
+ mockCaptureException.mockClear();
mockShowReportDialog.mockClear();
});
@@ -220,7 +220,7 @@ describe('ErrorBoundary', () => {
);
expect(mockOnError).toHaveBeenCalledTimes(0);
- expect(mockCaptureEvent).toHaveBeenCalledTimes(0);
+ expect(mockCaptureException).toHaveBeenCalledTimes(0);
const btn = screen.getByTestId('errorBtn');
fireEvent.click(btn);
@@ -228,52 +228,26 @@ describe('ErrorBoundary', () => {
expect(mockOnError).toHaveBeenCalledTimes(1);
expect(mockOnError).toHaveBeenCalledWith(expect.any(Error), expect.any(String), expect.any(String));
- expect(mockCaptureEvent).toHaveBeenCalledTimes(1);
-
- // 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[0].type).toEqual('React ErrorBoundary Error');
- expect(event.exception.values[0].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),
- },
- ]);
+ expect(mockCaptureException).toHaveBeenCalledTimes(1);
+ expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), {
+ contexts: { react: { componentStack: expect.any(String) } },
+ });
+
+ expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]);
+
+ // Check if error.cause -> react component stack
+ const error = mockCaptureException.mock.calls[0][0];
+ const cause = error.cause;
+ expect(cause.stack).toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack);
+ expect(cause.name).toContain('React ErrorBoundary');
+ expect(cause.message).toEqual(error.message);
});
it('calls `beforeCapture()` when an error occurs', () => {
const mockBeforeCapture = jest.fn();
const testBeforeCapture = (...args: any[]) => {
- expect(mockCaptureEvent).toHaveBeenCalledTimes(0);
+ expect(mockCaptureException).toHaveBeenCalledTimes(0);
mockBeforeCapture(...args);
};
@@ -284,14 +258,14 @@ describe('ErrorBoundary', () => {
);
expect(mockBeforeCapture).toHaveBeenCalledTimes(0);
- expect(mockCaptureEvent).toHaveBeenCalledTimes(0);
+ expect(mockCaptureException).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(mockCaptureEvent).toHaveBeenCalledTimes(1);
+ expect(mockCaptureException).toHaveBeenCalledTimes(1);
});
it('shows a Sentry Report Dialog with correct options', () => {
diff --git a/packages/react/test/integration.test.tsx b/packages/react/test/integration.test.tsx
deleted file mode 100644
index b9e28d4bec35..000000000000
--- a/packages/react/test/integration.test.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { fireEvent, render, screen } from '@testing-library/react';
-import * as React from 'react';
-import { useState } from 'react';
-
-import { init, getCurrentHub, BrowserClient } from '../src';
-import { ErrorBoundary, ErrorBoundaryProps } from '../src/errorboundary';
-
-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 (
- {
- setError(false);
- if (props.onReset) {
- props.onReset(...args);
- }
- }}
- >
- {isError ? : children}
-
- );
-};
-
-const dsn = 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012';
-
-describe('React Integration Test', () => {
- beforeAll(() => {
- init({ dsn });
- });
-
- beforeEach(() => {
- jest.clearAllMocks();
- getCurrentHub().pushScope();
- });
-
- afterEach(() => {
- getCurrentHub().popScope();
- });
-
- it('captures an error and sends it to Sentry', done => {
- let capturedHint;
- let error: Error;
- let eventId: string;
-
- expect.assertions(6);
- getCurrentHub().bindClient(
- new BrowserClient({
- beforeSend: (event: Event) => {
- expect(event.tags).toEqual({ test: '1' });
- expect(event.exception).not.toBeUndefined();
-e done();
- return null;
- },
- dsn,
- }),
- );
-
- render(
- You have hit an error}
- onError={(e, _, id) => {
- error = e;
- eventId = id;
- }}
- >
- children
- ,
- );
-
- const btn = screen.getByTestId('errorBtn');
- fireEvent.click(btn);
- });
-});