Skip to content

Commit 943e3f8

Browse files
AbhiPrasadHazAT
andauthored
feat(react): Allow for scope to be accessed before error (#2753)
Co-authored-by: Daniel Griesser <[email protected]>
1 parent cd7d887 commit 943e3f8

File tree

3 files changed

+61
-22
lines changed

3 files changed

+61
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [tracing] feat: `Add @sentry/tracing` (#2719)
88
- [gatsby] fix: Make APM optional in gatsby package (#2752)
99
- [tracing] ref: Use idleTimout if no activities occur in idle transaction (#2752)
10+
- [react] feat: Add `beforeCapture` option to ErrorBoundary (#2753)
1011

1112
## 5.19.2
1213

packages/react/src/errorboundary.tsx

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as Sentry from '@sentry/browser';
1+
import { captureException, ReportDialogOptions, Scope, showReportDialog, withScope } from '@sentry/browser';
22
import * as hoistNonReactStatic from 'hoist-non-react-statics';
33
import * as React from 'react';
44

@@ -18,7 +18,7 @@ export type ErrorBoundaryProps = {
1818
* Options to be passed into the Sentry report dialog.
1919
* No-op if {@link showDialog} is false.
2020
*/
21-
dialogOptions?: Sentry.ReportDialogOptions;
21+
dialogOptions?: ReportDialogOptions;
2222
// tslint:disable no-null-undefined-union
2323
/**
2424
* A fallback component that gets rendered when the error boundary encounters an error.
@@ -38,6 +38,8 @@ export type ErrorBoundaryProps = {
3838
onReset?(error: Error | null, componentStack: string | null, eventId: string | null): void;
3939
/** Called on componentWillUnmount() */
4040
onUnmount?(error: Error | null, componentStack: string | null, eventId: string | null): void;
41+
/** Called before the error is captured by Sentry, allows for you to add tags or context using the scope */
42+
beforeCapture?(scope: Scope, error: Error | null, componentStack: string | null): void;
4143
};
4244

4345
type ErrorBoundaryState = {
@@ -60,18 +62,24 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
6062
public state: ErrorBoundaryState = INITIAL_STATE;
6163

6264
public componentDidCatch(error: Error, { componentStack }: React.ErrorInfo): void {
63-
const eventId = Sentry.captureException(error, { contexts: { react: { componentStack } } });
64-
const { onError, showDialog, dialogOptions } = this.props;
65-
if (onError) {
66-
onError(error, componentStack, eventId);
67-
}
68-
if (showDialog) {
69-
Sentry.showReportDialog({ ...dialogOptions, eventId });
70-
}
65+
const { beforeCapture, onError, showDialog, dialogOptions } = this.props;
66+
67+
withScope(scope => {
68+
if (beforeCapture) {
69+
beforeCapture(scope, error, componentStack);
70+
}
71+
const eventId = captureException(error, { contexts: { react: { componentStack } } });
72+
if (onError) {
73+
onError(error, componentStack, eventId);
74+
}
75+
if (showDialog) {
76+
showReportDialog({ ...dialogOptions, eventId });
77+
}
7178

72-
// componentDidCatch is used over getDerivedStateFromError
73-
// so that componentStack is accessible through state.
74-
this.setState({ error, componentStack, eventId });
79+
// componentDidCatch is used over getDerivedStateFromError
80+
// so that componentStack is accessible through state.
81+
this.setState({ error, componentStack, eventId });
82+
});
7583
}
7684

7785
public componentDidMount(): void {

packages/react/test/errorboundary.test.tsx

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Scope } from '@sentry/browser';
12
import { fireEvent, render, screen } from '@testing-library/react';
23
import * as React from 'react';
34

@@ -7,15 +8,19 @@ const mockCaptureException = jest.fn();
78
const mockShowReportDialog = jest.fn();
89
const EVENT_ID = 'test-id-123';
910

10-
jest.mock('@sentry/browser', () => ({
11-
captureException: (err: any, ctx: any) => {
12-
mockCaptureException(err, ctx);
13-
return EVENT_ID;
14-
},
15-
showReportDialog: (options: any) => {
16-
mockShowReportDialog(options);
17-
},
18-
}));
11+
jest.mock('@sentry/browser', () => {
12+
const actual = jest.requireActual('@sentry/browser');
13+
return {
14+
...actual,
15+
captureException: (err: any, ctx: any) => {
16+
mockCaptureException(err, ctx);
17+
return EVENT_ID;
18+
},
19+
showReportDialog: (options: any) => {
20+
mockShowReportDialog(options);
21+
},
22+
};
23+
});
1924

2025
const TestApp: React.FC<ErrorBoundaryProps> = ({ children, ...props }) => {
2126
const [isError, setError] = React.useState(false);
@@ -197,6 +202,31 @@ describe('ErrorBoundary', () => {
197202
});
198203
});
199204

205+
it('calls `beforeCapture()` when an error occurs', () => {
206+
const mockBeforeCapture = jest.fn();
207+
208+
const testBeforeCapture = (...args: any[]) => {
209+
expect(mockCaptureException).toHaveBeenCalledTimes(0);
210+
mockBeforeCapture(...args);
211+
};
212+
213+
render(
214+
<TestApp fallback={<p>You have hit an error</p>} beforeCapture={testBeforeCapture}>
215+
<h1>children</h1>
216+
</TestApp>,
217+
);
218+
219+
expect(mockBeforeCapture).toHaveBeenCalledTimes(0);
220+
expect(mockCaptureException).toHaveBeenCalledTimes(0);
221+
222+
const btn = screen.getByTestId('errorBtn');
223+
fireEvent.click(btn);
224+
225+
expect(mockBeforeCapture).toHaveBeenCalledTimes(1);
226+
expect(mockBeforeCapture).toHaveBeenLastCalledWith(expect.any(Scope), expect.any(Error), expect.any(String));
227+
expect(mockCaptureException).toHaveBeenCalledTimes(1);
228+
});
229+
200230
it('shows a Sentry Report Dialog with correct options', () => {
201231
const options = { title: 'custom title' };
202232
render(

0 commit comments

Comments
 (0)