From e37e6e7c2ac7dc33f9eb33a8db3163e974a72bf8 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Tue, 21 Mar 2023 18:43:35 +0100
Subject: [PATCH 1/3] feat(react): Handle case where error.cause already
defined
---
packages/react/src/errorboundary.tsx | 9 +++-
packages/react/test/errorboundary.test.tsx | 62 ++++++++++++++++++----
2 files changed, 60 insertions(+), 11 deletions(-)
diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index 1553028195a2..d7312fdeffbb 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -66,6 +66,13 @@ const INITIAL_STATE = {
eventId: null,
};
+function setCause(error: Error & { cause?: Error }, cause: Error): void {
+ if (error.cause) {
+ return setCause(error.cause, cause);
+ }
+ error.cause = cause;
+}
+
/**
* 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
@@ -93,7 +100,7 @@ class ErrorBoundary extends React.Component;
}
-const TestApp: React.FC = ({ children, ...props }) => {
+interface TestAppProps extends ErrorBoundaryProps {
+ errorComp?: JSX.Element;
+}
+
+const TestApp: React.FC = ({ children, errorComp, ...props }) => {
+ // eslint-disable-next-line no-param-reassign
+ const customErrorComp = errorComp || ;
const [isError, setError] = React.useState(false);
return (
= ({ children, ...props }) => {
}
}}
>
- {isError ? : children}
+ {isError ? customErrorComp : children}
} onError={mockOnError} errorComp={}>
+ children
+ ,
+ );
+
+ expect(mockOnError).toHaveBeenCalledTimes(0);
+ expect(mockCaptureException).toHaveBeenCalledTimes(0);
+
+ const btn = screen.getByTestId('errorBtn');
+ fireEvent.click(btn);
+
+ 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]);
+
+ const thirdError = mockCaptureException.mock.calls[0][0];
+ const secondError = thirdError.cause;
+ const firstError = secondError.cause;
+ const cause = firstError.cause;
+ expect(cause.stack).toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack);
+ expect(cause.name).toContain('React ErrorBoundary');
+ expect(cause.message).toEqual(thirdError.message);
+ });
+
it('calls `beforeCapture()` when an error occurs', () => {
const mockBeforeCapture = jest.fn();
From d0c99b279d5bb38fb2f6d6fe998489149f8ca004 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Wed, 22 Mar 2023 12:10:02 +0100
Subject: [PATCH 2/3] Be more defensive when recursing through error cause
chain
---
packages/react/src/errorboundary.tsx | 18 ++++++++--
packages/react/test/errorboundary.test.tsx | 39 ++++++++++++++++++++++
2 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index d7312fdeffbb..377eedc7fd47 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -67,10 +67,22 @@ const INITIAL_STATE = {
};
function setCause(error: Error & { cause?: Error }, cause: Error): void {
- if (error.cause) {
- return setCause(error.cause, cause);
+ const seenErrors = new Map();
+
+ function recurse(error: Error & { cause?: Error }, cause: Error): void {
+ // If we've already seen the error, there is a recursive loop somewhere in the error's
+ // cause chain. Let's just bail out then to prevent a stack overflow.
+ if (seenErrors.has(error)) {
+ return;
+ }
+ if (error.cause) {
+ seenErrors.set(error, true);
+ return recurse(error.cause, cause);
+ }
+ error.cause = cause;
}
- error.cause = cause;
+
+ recurse(error, cause);
}
/**
diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx
index 3f9284d03efa..251112242313 100644
--- a/packages/react/test/errorboundary.test.tsx
+++ b/packages/react/test/errorboundary.test.tsx
@@ -341,6 +341,45 @@ describe('ErrorBoundary', () => {
expect(cause.message).toEqual(thirdError.message);
});
+ it('handles when `error.cause` is recursive', () => {
+ const mockOnError = jest.fn();
+
+ function CustomBam(): JSX.Element {
+ const firstError = new Error('bam');
+ const secondError = new Error('bam2');
+ // @ts-ignore Need to set cause on error
+ firstError.cause = secondError;
+ // @ts-ignore Need to set cause on error
+ secondError.cause = firstError;
+ throw firstError;
+ }
+
+ render(
+ You have hit an error} onError={mockOnError} errorComp={}>
+ children
+ ,
+ );
+
+ expect(mockOnError).toHaveBeenCalledTimes(0);
+ expect(mockCaptureException).toHaveBeenCalledTimes(0);
+
+ const btn = screen.getByTestId('errorBtn');
+ fireEvent.click(btn);
+
+ 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]);
+
+ const error = mockCaptureException.mock.calls[0][0];
+ const cause = error.cause;
+ // We need to make sure that recursive error.cause does not cause infinite loop
+ expect(cause.stack).not.toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack);
+ expect(cause.name).not.toContain('React ErrorBoundary');
+ });
+
it('calls `beforeCapture()` when an error occurs', () => {
const mockBeforeCapture = jest.fn();
From 54aaa3ef8ffa2003df56c6a40da8d25a0bba7ba2 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Wed, 22 Mar 2023 16:16:14 +0100
Subject: [PATCH 3/3] use weakmap
---
packages/react/src/errorboundary.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index 377eedc7fd47..96a88cf31e73 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -67,7 +67,7 @@ const INITIAL_STATE = {
};
function setCause(error: Error & { cause?: Error }, cause: Error): void {
- const seenErrors = new Map();
+ const seenErrors = new WeakMap();
function recurse(error: Error & { cause?: Error }, cause: Error): void {
// If we've already seen the error, there is a recursive loop somewhere in the error's