Skip to content

Commit feb8434

Browse files
committed
ref: re-arrange and add tests
1 parent 33482e8 commit feb8434

File tree

3 files changed

+36
-158
lines changed

3 files changed

+36
-158
lines changed

packages/react/src/errorboundary.tsx

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export type ErrorBoundaryProps = {
4444
};
4545

4646
type ErrorBoundaryState = {
47-
componentStack: string | null;
47+
componentStack: React.ErrorInfo['componentStack'] | null;
4848
error: Error | null;
4949
eventId: string | null;
5050
};
@@ -55,25 +55,6 @@ const INITIAL_STATE = {
5555
eventId: null,
5656
};
5757

58-
/**
59-
* Logs react error boundary errors to Sentry. If on React version >= 17, creates stack trace
60-
* from componentStack param, otherwise relies on error param for stacktrace.
61-
*
62-
* @param error An error captured by React Error Boundary
63-
* @param componentStack The component stacktrace
64-
*/
65-
function captureReactErrorBoundaryError(error: Error & { cause?: Error }, componentStack: string): string {
66-
if (reactVersion.major && reactVersion.major >= 17) {
67-
const errorBoundaryError = new Error(error.message);
68-
errorBoundaryError.name = `React ErrorBoundary ${errorBoundaryError.name}`;
69-
errorBoundaryError.stack = componentStack;
70-
71-
error.cause = errorBoundaryError;
72-
}
73-
74-
return captureException(error, { contexts: { react: { componentStack } } });
75-
}
76-
7758
/**
7859
* A ErrorBoundary component that logs errors to Sentry. Requires React >= 16.
7960
* NOTE: If you are a Sentry user, and you are seeing this stack frame, it means the
@@ -83,14 +64,26 @@ function captureReactErrorBoundaryError(error: Error & { cause?: Error }, compon
8364
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
8465
public state: ErrorBoundaryState = INITIAL_STATE;
8566

86-
public componentDidCatch(error: Error, { componentStack }: React.ErrorInfo): void {
67+
public componentDidCatch(error: Error & { cause?: Error }, { componentStack }: React.ErrorInfo): void {
8768
const { beforeCapture, onError, showDialog, dialogOptions } = this.props;
8869

8970
withScope(scope => {
71+
// If on React version >= 17, create stack trace from componentStack param and links
72+
// to to the original error using `error.cause` otherwise relies on error param for stacktrace.
73+
// Linking errors requires the `LinkedErrors` integration be enabled.
74+
if (reactVersion.major && reactVersion.major >= 17) {
75+
const errorBoundaryError = new Error(error.message);
76+
errorBoundaryError.name = `React ErrorBoundary ${errorBoundaryError.name}`;
77+
errorBoundaryError.stack = componentStack;
78+
79+
// Using the `LinkedErrors` integration to link the errors together.
80+
error.cause = errorBoundaryError;
81+
}
82+
9083
if (beforeCapture) {
9184
beforeCapture(scope, error, componentStack);
9285
}
93-
const eventId = captureReactErrorBoundaryError(error, componentStack);
86+
const eventId = captureException(error, { contexts: { react: { componentStack } } });
9487
if (onError) {
9588
onError(error, componentStack, eventId);
9689
}

packages/react/test/errorboundary.test.tsx

Lines changed: 21 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import { useState } from 'react';
66

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

9-
const mockCaptureEvent = jest.fn();
9+
const mockCaptureException = jest.fn();
1010
const mockShowReportDialog = jest.fn();
1111
const EVENT_ID = 'test-id-123';
1212

1313
jest.mock('@sentry/browser', () => {
1414
const actual = jest.requireActual('@sentry/browser');
1515
return {
1616
...actual,
17-
captureEvent: (event: Event) => {
18-
mockCaptureEvent(event);
17+
captureException: (...args: unknown[]) => {
18+
mockCaptureException(...args);
1919
return EVENT_ID;
2020
},
2121
showReportDialog: (options: any) => {
@@ -74,7 +74,7 @@ describe('ErrorBoundary', () => {
7474
jest.spyOn(console, 'error').mockImplementation();
7575

7676
afterEach(() => {
77-
mockCaptureEvent.mockClear();
77+
mockCaptureException.mockClear();
7878
mockShowReportDialog.mockClear();
7979
});
8080

@@ -220,60 +220,34 @@ describe('ErrorBoundary', () => {
220220
);
221221

222222
expect(mockOnError).toHaveBeenCalledTimes(0);
223-
expect(mockCaptureEvent).toHaveBeenCalledTimes(0);
223+
expect(mockCaptureException).toHaveBeenCalledTimes(0);
224224

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

228228
expect(mockOnError).toHaveBeenCalledTimes(1);
229229
expect(mockOnError).toHaveBeenCalledWith(expect.any(Error), expect.any(String), expect.any(String));
230230

231-
expect(mockCaptureEvent).toHaveBeenCalledTimes(1);
232-
233-
// We do a detailed assert on the stacktrace as a regression test against future
234-
// react changes (that way we can update the docs if frames change in a major way).
235-
const event = mockCaptureEvent.mock.calls[0][0];
236-
expect(event.exception.values).toHaveLength(2);
237-
expect(event.level).toBe(Severity.Error);
238-
239-
expect(event.exception.values[0].type).toEqual('React ErrorBoundary Error');
240-
expect(event.exception.values[0].stacktrace.frames).toEqual([
241-
{
242-
colno: expect.any(Number),
243-
filename: expect.stringContaining('errorboundary.test.tsx'),
244-
function: 'TestApp',
245-
in_app: true,
246-
lineno: expect.any(Number),
247-
},
248-
{
249-
colno: expect.any(Number),
250-
filename: expect.stringContaining('errorboundary.tsx'),
251-
function: 'ErrorBoundary',
252-
in_app: true,
253-
lineno: expect.any(Number),
254-
},
255-
{
256-
colno: expect.any(Number),
257-
filename: expect.stringContaining('errorboundary.test.tsx'),
258-
function: 'Bam',
259-
in_app: true,
260-
lineno: expect.any(Number),
261-
},
262-
{
263-
colno: expect.any(Number),
264-
filename: expect.stringContaining('errorboundary.test.tsx'),
265-
function: 'Boo',
266-
in_app: true,
267-
lineno: expect.any(Number),
268-
},
269-
]);
231+
expect(mockCaptureException).toHaveBeenCalledTimes(1);
232+
expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), {
233+
contexts: { react: { componentStack: expect.any(String) } },
234+
});
235+
236+
expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]);
237+
238+
// Check if error.cause -> react component stack
239+
const error = mockCaptureException.mock.calls[0][0];
240+
const cause = error.cause;
241+
expect(cause.stack).toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack);
242+
expect(cause.name).toContain('React ErrorBoundary');
243+
expect(cause.message).toEqual(error.message);
270244
});
271245

272246
it('calls `beforeCapture()` when an error occurs', () => {
273247
const mockBeforeCapture = jest.fn();
274248

275249
const testBeforeCapture = (...args: any[]) => {
276-
expect(mockCaptureEvent).toHaveBeenCalledTimes(0);
250+
expect(mockCaptureException).toHaveBeenCalledTimes(0);
277251
mockBeforeCapture(...args);
278252
};
279253

@@ -284,14 +258,14 @@ describe('ErrorBoundary', () => {
284258
);
285259

286260
expect(mockBeforeCapture).toHaveBeenCalledTimes(0);
287-
expect(mockCaptureEvent).toHaveBeenCalledTimes(0);
261+
expect(mockCaptureException).toHaveBeenCalledTimes(0);
288262

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

292266
expect(mockBeforeCapture).toHaveBeenCalledTimes(1);
293267
expect(mockBeforeCapture).toHaveBeenLastCalledWith(expect.any(Scope), expect.any(Error), expect.any(String));
294-
expect(mockCaptureEvent).toHaveBeenCalledTimes(1);
268+
expect(mockCaptureException).toHaveBeenCalledTimes(1);
295269
});
296270

297271
it('shows a Sentry Report Dialog with correct options', () => {

packages/react/test/integration.test.tsx

Lines changed: 0 additions & 89 deletions
This file was deleted.

0 commit comments

Comments
 (0)