From 8e78e6e5ec3b5894176555ee4e19b3368d64c969 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Wed, 22 Mar 2023 16:38:09 +0100
Subject: [PATCH] fix(react): Handle case where error.cause already defined
(#7557)
If `error.cause` is already defined, attempt to walk down the error chain to set the `ReactErrorBoundary` error.
---
packages/react/src/errorboundary.tsx | 21 ++++-
packages/react/test/errorboundary.test.tsx | 101 +++++++++++++++++++--
2 files changed, 111 insertions(+), 11 deletions(-)
diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index 1553028195a2..96a88cf31e73 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -66,6 +66,25 @@ const INITIAL_STATE = {
eventId: null,
};
+function setCause(error: Error & { cause?: Error }, cause: Error): void {
+ 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
+ // 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;
+ }
+
+ recurse(error, 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 +112,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('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();