From 943e3f8c6ebd5c9d22e7be2d63dbdfcba2b7a0eb Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Mon, 20 Jul 2020 12:04:03 -0400
Subject: [PATCH] feat(react): Allow for scope to be accessed before error
(#2753)
Co-authored-by: Daniel Griesser
---
CHANGELOG.md | 1 +
packages/react/src/errorboundary.tsx | 34 +++++++++------
packages/react/test/errorboundary.test.tsx | 48 ++++++++++++++++++----
3 files changed, 61 insertions(+), 22 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 90cca849d346..a9e91e017575 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
- [tracing] feat: `Add @sentry/tracing` (#2719)
- [gatsby] fix: Make APM optional in gatsby package (#2752)
- [tracing] ref: Use idleTimout if no activities occur in idle transaction (#2752)
+- [react] feat: Add `beforeCapture` option to ErrorBoundary (#2753)
## 5.19.2
diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index ec05ad73dd43..6f78c41f56e7 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import { captureException, ReportDialogOptions, Scope, showReportDialog, withScope } from '@sentry/browser';
import * as hoistNonReactStatic from 'hoist-non-react-statics';
import * as React from 'react';
@@ -18,7 +18,7 @@ export type ErrorBoundaryProps = {
* Options to be passed into the Sentry report dialog.
* No-op if {@link showDialog} is false.
*/
- dialogOptions?: Sentry.ReportDialogOptions;
+ dialogOptions?: ReportDialogOptions;
// tslint:disable no-null-undefined-union
/**
* A fallback component that gets rendered when the error boundary encounters an error.
@@ -38,6 +38,8 @@ export type ErrorBoundaryProps = {
onReset?(error: Error | null, componentStack: string | null, eventId: string | null): void;
/** Called on componentWillUnmount() */
onUnmount?(error: Error | null, componentStack: string | null, eventId: string | null): void;
+ /** Called before the error is captured by Sentry, allows for you to add tags or context using the scope */
+ beforeCapture?(scope: Scope, error: Error | null, componentStack: string | null): void;
};
type ErrorBoundaryState = {
@@ -60,18 +62,24 @@ class ErrorBoundary extends React.Component {
+ if (beforeCapture) {
+ beforeCapture(scope, error, componentStack);
+ }
+ const eventId = captureException(error, { contexts: { react: { componentStack } } });
+ if (onError) {
+ onError(error, componentStack, eventId);
+ }
+ if (showDialog) {
+ showReportDialog({ ...dialogOptions, eventId });
+ }
- // componentDidCatch is used over getDerivedStateFromError
- // so that componentStack is accessible through state.
- this.setState({ error, componentStack, eventId });
+ // componentDidCatch is used over getDerivedStateFromError
+ // so that componentStack is accessible through state.
+ this.setState({ error, componentStack, eventId });
+ });
}
public componentDidMount(): void {
diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx
index e522841636e8..fe427d0e53ac 100644
--- a/packages/react/test/errorboundary.test.tsx
+++ b/packages/react/test/errorboundary.test.tsx
@@ -1,3 +1,4 @@
+import { Scope } from '@sentry/browser';
import { fireEvent, render, screen } from '@testing-library/react';
import * as React from 'react';
@@ -7,15 +8,19 @@ const mockCaptureException = jest.fn();
const mockShowReportDialog = jest.fn();
const EVENT_ID = 'test-id-123';
-jest.mock('@sentry/browser', () => ({
- captureException: (err: any, ctx: any) => {
- mockCaptureException(err, ctx);
- return EVENT_ID;
- },
- showReportDialog: (options: any) => {
- mockShowReportDialog(options);
- },
-}));
+jest.mock('@sentry/browser', () => {
+ const actual = jest.requireActual('@sentry/browser');
+ return {
+ ...actual,
+ captureException: (err: any, ctx: any) => {
+ mockCaptureException(err, ctx);
+ return EVENT_ID;
+ },
+ showReportDialog: (options: any) => {
+ mockShowReportDialog(options);
+ },
+ };
+});
const TestApp: React.FC = ({ children, ...props }) => {
const [isError, setError] = React.useState(false);
@@ -197,6 +202,31 @@ describe('ErrorBoundary', () => {
});
});
+ it('calls `beforeCapture()` when an error occurs', () => {
+ const mockBeforeCapture = jest.fn();
+
+ const testBeforeCapture = (...args: any[]) => {
+ expect(mockCaptureException).toHaveBeenCalledTimes(0);
+ mockBeforeCapture(...args);
+ };
+
+ render(
+ You have hit an error
} beforeCapture={testBeforeCapture}>
+ children
+ ,
+ );
+
+ expect(mockBeforeCapture).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(mockCaptureException).toHaveBeenCalledTimes(1);
+ });
+
it('shows a Sentry Report Dialog with correct options', () => {
const options = { title: 'custom title' };
render(