Skip to content

Commit

Permalink
feat(react): Allow for scope to be accessed before error
Browse files Browse the repository at this point in the history
  • Loading branch information
AbhiPrasad committed Jul 17, 2020
1 parent 913b0ca commit 1a095bf
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
- [react] feat: Export `createReduxEnhancer` to log redux actions as breadcrumbs, and attach state as an extra. (#2717)
- [tracing] feat: `Add @sentry/tracing` (#2719)
- [react] feat: Add `beforeSend` option to ErrorBoundary

## 5.19.2

Expand Down
34 changes: 21 additions & 13 deletions 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';

Expand All @@ -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.
Expand All @@ -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 error is sent to Sentry, allows for you to add tags or context using the scope */
beforeSend?(scope: Scope): void;
};

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

public componentDidCatch(error: Error, { componentStack }: React.ErrorInfo): void {
const eventId = Sentry.captureException(error, { contexts: { react: { componentStack } } });
const { onError, showDialog, dialogOptions } = this.props;
if (onError) {
onError(error, componentStack, eventId);
}
if (showDialog) {
Sentry.showReportDialog({ ...dialogOptions, eventId });
}
const { beforeSend, onError, showDialog, dialogOptions } = this.props;

withScope(scope => {
if (beforeSend) {
beforeSend(scope);
}
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 {
Expand Down
51 changes: 42 additions & 9 deletions packages/react/test/errorboundary.test.tsx
Expand Up @@ -2,20 +2,25 @@ import { fireEvent, render, screen } from '@testing-library/react';
import * as React from 'react';

import { ErrorBoundary, ErrorBoundaryProps, UNKNOWN_COMPONENT, withErrorBoundary } from '../src/errorboundary';
import { Scope } from '@sentry/browser';

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<ErrorBoundaryProps> = ({ children, ...props }) => {
const [isError, setError] = React.useState(false);
Expand Down Expand Up @@ -197,6 +202,34 @@ describe('ErrorBoundary', () => {
});
});

it('calls `beforeSend()` when an error occurs', () => {
const mockBeforeSend = jest.fn();
let mockScope;

const testBeforeSend = (scope: Scope) => {
expect(mockCaptureException).toHaveBeenCalledTimes(0);
// tslint:disable-next-line: no-unsafe-any
mockScope = scope;
mockBeforeSend(scope);
};

render(
<TestApp fallback={<p>You have hit an error</p>} beforeSend={testBeforeSend}>
<h1>children</h1>
</TestApp>,
);

expect(mockBeforeSend).toHaveBeenCalledTimes(0);
expect(mockCaptureException).toHaveBeenCalledTimes(0);

const btn = screen.getByTestId('errorBtn');
fireEvent.click(btn);

expect(mockBeforeSend).toHaveBeenCalledTimes(1);
expect(mockCaptureException).toHaveBeenCalledTimes(1);
expect(mockScope).toBeInstanceOf(Scope);
});

it('shows a Sentry Report Dialog with correct options', () => {
const options = { title: 'custom title' };
render(
Expand Down

0 comments on commit 1a095bf

Please sign in to comment.