Skip to content

Commit

Permalink
fix(react): Correctly give eventID to dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
AbhiPrasad committed Jun 10, 2020
1 parent 598c5bb commit 448829f
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 14 deletions.
28 changes: 25 additions & 3 deletions packages/react/src/errorboundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,31 @@ export type FallbackRender = (fallback: {
}) => React.ReactNode;

export type ErrorBoundaryProps = {
/** If a Sentry report dialog should be rendered on error */
showDialog?: boolean;
/**
* Options to be passed into the Sentry report dialog.
* No-op if {@link showDialog} is false.
*/
dialogOptions?: Sentry.ReportDialogOptions;
// tslint:disable-next-line: no-null-undefined-union
// tslint:disable no-null-undefined-union
/**
* A fallback component that gets rendered when the error boundary encounters an error.
*
* Can either provide a React Component, or a function that returns React Component as
* a valid fallback prop. If a function is provided, the function will be called with
* the error, the component stack, and an function that resets the error boundary on error.
*
*/
fallback?: React.ReactNode | FallbackRender;
// tslint:enable no-null-undefined-union
/** Called with the error boundary encounters an error */
onError?(error: Error, componentStack: string): void;
/** Called on componentDidMount() */
onMount?(): void;
/** Called if resetError() is called from the fallback render props function */
onReset?(error: Error | null, componentStack: string | null): void;
/** Called on componentWillUnmount() */
onUnmount?(error: Error | null, componentStack: string | null): void;
};

Expand All @@ -31,17 +49,21 @@ const INITIAL_STATE = {
error: null,
};

/**
* A ErrorBoundary component that logs errors to Sentry.
* Requires React >= 16
*/
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
public state: ErrorBoundaryState = INITIAL_STATE;

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

// componentDidCatch is used over getDerivedStateFromError
Expand Down
59 changes: 48 additions & 11 deletions packages/react/test/errorboundary.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as React from 'react';

import { ErrorBoundary, ErrorBoundaryProps } from '../src/errorboundary';
import { ErrorBoundary, ErrorBoundaryProps, UNKNOWN_COMPONENT, withErrorBoundary } from '../src/errorboundary';

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);
Expand All @@ -18,7 +20,15 @@ jest.mock('@sentry/browser', () => ({
const TestApp: React.FC<ErrorBoundaryProps> = ({ children, ...props }) => {
const [isError, setError] = React.useState(false);
return (
<ErrorBoundary {...props}>
<ErrorBoundary
{...props}
onReset={(err: Error, stack: string) => {
setError(false);
if (props.onReset) {
props.onReset(err, stack);
}
}}
>
{isError ? <Bam /> : children}
<button
data-testid="errorBtn"
Expand All @@ -34,6 +44,20 @@ function Bam(): JSX.Element {
throw new Error('boom');
}

describe('withErrorBoundary', () => {
it('sets displayName properly', () => {
const TestComponent = () => <h1>Hello World</h1>;

const Component = withErrorBoundary(TestComponent, { fallback: <h1>fallback</h1> });
expect(Component.displayName).toBe('errorBoundary(TestComponent)');
});

it('defaults to an unknown displayName', () => {
const Component = withErrorBoundary(() => <h1>Hello World</h1>, { fallback: <h1>fallback</h1> });
expect(Component.displayName).toBe(`errorBoundary(${UNKNOWN_COMPONENT})`);
});
});

describe('ErrorBoundary', () => {
jest.spyOn(console, 'error').mockImplementation();

Expand All @@ -44,7 +68,6 @@ describe('ErrorBoundary', () => {

it('renders null if not given a valid `fallback` prop', () => {
const { container } = render(
// @ts-ignore
<ErrorBoundary fallback={new Error('true')}>
<Bam />
</ErrorBoundary>,
Expand All @@ -55,7 +78,6 @@ describe('ErrorBoundary', () => {

it('renders a fallback on error', () => {
const { container } = render(
// @ts-ignore
<ErrorBoundary fallback={<h1>Error Component</h1>}>
<Bam />
</ErrorBoundary>,
Expand Down Expand Up @@ -186,12 +208,30 @@ describe('ErrorBoundary', () => {
fireEvent.click(btn);

expect(mockShowReportDialog).toHaveBeenCalledTimes(1);
expect(mockShowReportDialog).toHaveBeenCalledWith(options);
expect(mockShowReportDialog).toHaveBeenCalledWith({ ...options, eventId: EVENT_ID });
});

it('resets to initial state when reset', () => {
const mockOnReset = jest.fn();
it('resets to initial state when reset', async () => {
const { container } = render(
<TestApp fallback={({ resetError }) => <button data-testid="reset" onClick={resetError} />}>
<h1>children</h1>
</TestApp>,
);

expect(container.innerHTML).toContain('<h1>children</h1>');
const btn = screen.getByTestId('errorBtn');
fireEvent.click(btn);
expect(container.innerHTML).toContain('<button data-testid="reset">');

const reset = screen.getByTestId('reset');
fireEvent.click(reset);

expect(container.innerHTML).toContain('<h1>children</h1>');
});

it('calls `onReset()` when reset', () => {
const mockOnReset = jest.fn();
render(
<TestApp
onReset={mockOnReset}
fallback={({ resetError }) => <button data-testid="reset" onClick={resetError} />}
Expand All @@ -200,17 +240,14 @@ describe('ErrorBoundary', () => {
</TestApp>,
);

expect(container.innerHTML).toContain('<h1>children</h1>');
expect(mockOnReset).toHaveBeenCalledTimes(0);

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

expect(container.innerHTML).toContain('<button data-testid="reset">');
expect(mockOnReset).toHaveBeenCalledTimes(0);

const reset = screen.getByTestId('reset');
fireEvent.click(reset);

expect(mockOnReset).toHaveBeenCalledTimes(1);
expect(mockOnReset).toHaveBeenCalledWith(expect.any(Error), expect.any(String));
});
Expand Down

0 comments on commit 448829f

Please sign in to comment.