Skip to content

Commit e9248e0

Browse files
authored
feat(react): Export captureReactException method (#15746)
This PR exports the `captureReactException` method from the React package, making it available as a public API. Additionally, it refactors the implementation to use `withScope` instead of directly passing context to `captureException`. This was done so that the arguments to `captureReactException` better align to `captureException`. Users can now directly access and use this method in their applications: ```javascript import * as Sentry from '@sentry/react'; class ErrorBoundary extends React.Component { componentDidCatch(error, info) { Sentry.captureReactException(error, info); } render() { return this.props.children; } } ``` The reason why `captureReactException` is named as such and not `captureReactErrorBoundaryException` is because it can be used in other scenarios as well, like with the new React 19 error APIs. Therefore it was generalized to just be `captureReactException`.
1 parent 46eff72 commit e9248e0

File tree

3 files changed

+31
-33
lines changed

3 files changed

+31
-33
lines changed

packages/react/src/error.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { captureException } from '@sentry/browser';
1+
import { captureException, withScope } from '@sentry/browser';
22
import { isError } from '@sentry/core';
3-
import type { EventHint } from '@sentry/core';
43
import { version } from 'react';
54
import type { ErrorInfo } from 'react';
65

@@ -46,7 +45,7 @@ export function captureReactException(
4645
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4746
error: any,
4847
{ componentStack }: ErrorInfo,
49-
hint?: EventHint,
48+
hint?: Parameters<typeof captureException>[1],
5049
): string {
5150
// If on React version >= 17, create stack trace from componentStack param and links
5251
// to to the original error using `error.cause` otherwise relies on error param for stacktrace.
@@ -65,11 +64,9 @@ export function captureReactException(
6564
setCause(error, errorBoundaryError);
6665
}
6766

68-
return captureException(error, {
69-
...hint,
70-
captureContext: {
71-
contexts: { react: { componentStack } },
72-
},
67+
return withScope(scope => {
68+
scope.setContext('react', { componentStack });
69+
return captureException(error, hint);
7370
});
7471
}
7572

packages/react/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export * from '@sentry/browser';
22

33
export { init } from './sdk';
4-
export { reactErrorHandler } from './error';
4+
export { captureReactException, reactErrorHandler } from './error';
55
export { Profiler, withProfiler, useProfiler } from './profiler';
66
export type { ErrorBoundaryProps, FallbackRender } from './errorboundary';
77
export { ErrorBoundary, withErrorBoundary } from './errorboundary';

packages/react/test/errorboundary.test.tsx

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { useState } from 'react';
1212
import type { ErrorBoundaryProps, FallbackRender } from '../src/errorboundary';
1313
import { ErrorBoundary, UNKNOWN_COMPONENT, withErrorBoundary } from '../src/errorboundary';
1414

15+
const mockScope = new Scope();
16+
const scopeSetContextSpy = vi.spyOn(mockScope, 'setContext');
1517
const mockCaptureException = vi.fn();
1618
const mockShowReportDialog = vi.fn();
1719
const mockClientOn = vi.fn();
@@ -27,6 +29,9 @@ vi.mock('@sentry/browser', async requireActual => {
2729
showReportDialog: (options: any) => {
2830
mockShowReportDialog(options);
2931
},
32+
withScope: (callback: (scope: any) => any) => {
33+
return callback(mockScope);
34+
},
3035
};
3136
});
3237

@@ -102,6 +107,7 @@ describe('ErrorBoundary', () => {
102107
mockCaptureException.mockClear();
103108
mockShowReportDialog.mockClear();
104109
mockClientOn.mockClear();
110+
(mockScope.setContext as any).mockClear();
105111
});
106112

107113
it('renders null if not given a valid `fallback` prop', () => {
@@ -265,20 +271,19 @@ describe('ErrorBoundary', () => {
265271

266272
expect(mockCaptureException).toHaveBeenCalledTimes(1);
267273
expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), {
268-
captureContext: {
269-
contexts: { react: { componentStack: expect.any(String) } },
270-
},
271274
mechanism: { handled: true },
272275
});
273276

277+
expect(scopeSetContextSpy).toHaveBeenCalledTimes(1);
278+
expect(scopeSetContextSpy).toHaveBeenCalledWith('react', { componentStack: expect.any(String) });
279+
274280
expect(mockOnError.mock.calls[0]?.[0]).toEqual(mockCaptureException.mock.calls[0]?.[0]);
275281

276282
// Check if error.cause -> react component stack
277283
const error = mockCaptureException.mock.calls[0]?.[0];
278284
const cause = error.cause;
279-
expect(cause.stack).toEqual(
280-
mockCaptureException.mock.calls[0]?.[1]?.captureContext.contexts.react.componentStack,
281-
);
285+
286+
expect(cause.stack).toEqual(scopeSetContextSpy.mock.calls[0]?.[1]?.componentStack);
282287
expect(cause.name).toContain('React ErrorBoundary');
283288
expect(cause.message).toEqual(error.message);
284289
});
@@ -325,12 +330,12 @@ describe('ErrorBoundary', () => {
325330

326331
expect(mockCaptureException).toHaveBeenCalledTimes(1);
327332
expect(mockCaptureException).toHaveBeenLastCalledWith('bam', {
328-
captureContext: {
329-
contexts: { react: { componentStack: expect.any(String) } },
330-
},
331333
mechanism: { handled: true },
332334
});
333335

336+
expect(scopeSetContextSpy).toHaveBeenCalledTimes(1);
337+
expect(scopeSetContextSpy).toHaveBeenCalledWith('react', { componentStack: expect.any(String) });
338+
334339
// Check if error.cause -> react component stack
335340
const error = mockCaptureException.mock.calls[0]?.[0];
336341
expect(error.cause).not.toBeDefined();
@@ -364,21 +369,19 @@ describe('ErrorBoundary', () => {
364369

365370
expect(mockCaptureException).toHaveBeenCalledTimes(1);
366371
expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), {
367-
captureContext: {
368-
contexts: { react: { componentStack: expect.any(String) } },
369-
},
370372
mechanism: { handled: true },
371373
});
372374

375+
expect(scopeSetContextSpy).toHaveBeenCalledTimes(1);
376+
expect(scopeSetContextSpy).toHaveBeenCalledWith('react', { componentStack: expect.any(String) });
377+
373378
expect(mockOnError.mock.calls[0]?.[0]).toEqual(mockCaptureException.mock.calls[0]?.[0]);
374379

375380
const thirdError = mockCaptureException.mock.calls[0]?.[0];
376381
const secondError = thirdError.cause;
377382
const firstError = secondError.cause;
378383
const cause = firstError.cause;
379-
expect(cause.stack).toEqual(
380-
mockCaptureException.mock.calls[0]?.[1]?.captureContext.contexts.react.componentStack,
381-
);
384+
expect(cause.stack).toEqual(scopeSetContextSpy.mock.calls[0]?.[1]?.componentStack);
382385
expect(cause.name).toContain('React ErrorBoundary');
383386
expect(cause.message).toEqual(thirdError.message);
384387
});
@@ -410,20 +413,18 @@ describe('ErrorBoundary', () => {
410413

411414
expect(mockCaptureException).toHaveBeenCalledTimes(1);
412415
expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), {
413-
captureContext: {
414-
contexts: { react: { componentStack: expect.any(String) } },
415-
},
416416
mechanism: { handled: true },
417417
});
418418

419+
expect(scopeSetContextSpy).toHaveBeenCalledTimes(1);
420+
expect(scopeSetContextSpy).toHaveBeenCalledWith('react', { componentStack: expect.any(String) });
421+
419422
expect(mockOnError.mock.calls[0]?.[0]).toEqual(mockCaptureException.mock.calls[0]?.[0]);
420423

421424
const error = mockCaptureException.mock.calls[0]?.[0];
422425
const cause = error.cause;
423426
// We need to make sure that recursive error.cause does not cause infinite loop
424-
expect(cause.stack).not.toEqual(
425-
mockCaptureException.mock.calls[0]?.[1]?.captureContext.contexts.react.componentStack,
426-
);
427+
expect(cause.stack).not.toEqual(scopeSetContextSpy.mock.calls[0]?.[1]?.componentStack);
427428
expect(cause.name).not.toContain('React ErrorBoundary');
428429
});
429430

@@ -580,11 +581,11 @@ describe('ErrorBoundary', () => {
580581

581582
expect(mockCaptureException).toHaveBeenCalledTimes(1);
582583
expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Object), {
583-
captureContext: {
584-
contexts: { react: { componentStack: expect.any(String) } },
585-
},
586584
mechanism: { handled: expected },
587585
});
586+
587+
expect(scopeSetContextSpy).toHaveBeenCalledTimes(1);
588+
expect(scopeSetContextSpy).toHaveBeenCalledWith('react', { componentStack: expect.any(String) });
588589
},
589590
);
590591
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy