diff --git a/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts index 46e19b11a2ac..496c47e417d2 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts @@ -19,6 +19,20 @@ test('Catches errors caught by error boundary', async ({ page }) => { expect(errorEvent.exception?.values).toHaveLength(2); expect(errorEvent.exception?.values?.[0]?.value).toBe('caught error'); + expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({ + type: 'auto.function.react.error_handler', + handled: true, // true because a callback was provided + exception_id: 1, + parent_id: 0, + source: 'cause', + }); + + expect(errorEvent.exception?.values?.[1]?.value).toBe('caught error'); + expect(errorEvent.exception?.values?.[1]?.mechanism).toEqual({ + type: 'generic', + handled: true, // true because a callback was provided + exception_id: 0, + }); }); test('Catches errors uncaught by error boundary', async ({ page }) => { @@ -39,4 +53,18 @@ test('Catches errors uncaught by error boundary', async ({ page }) => { expect(errorEvent.exception?.values).toHaveLength(2); expect(errorEvent.exception?.values?.[0]?.value).toBe('uncaught error'); + expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({ + type: 'auto.function.react.error_handler', + handled: true, // true because a callback was provided + exception_id: 1, + parent_id: 0, + source: 'cause', + }); + + expect(errorEvent.exception?.values?.[1]?.value).toBe('uncaught error'); + expect(errorEvent.exception?.values?.[1]?.mechanism).toEqual({ + type: 'generic', + handled: true, // true because a callback was provided + exception_id: 0, + }); }); diff --git a/packages/react/src/error.ts b/packages/react/src/error.ts index ca5ccd8b2698..c10cebe4ecf4 100644 --- a/packages/react/src/error.ts +++ b/packages/react/src/error.ts @@ -95,8 +95,11 @@ export function reactErrorHandler( ): (error: any, errorInfo: ErrorInfo) => void { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (error: any, errorInfo: ErrorInfo) => { - const eventId = captureReactException(error, errorInfo); - if (callback) { + const hasCallback = !!callback; + const eventId = captureReactException(error, errorInfo, { + mechanism: { handled: hasCallback, type: 'auto.function.react.error_handler' }, + }); + if (hasCallback) { callback(error, errorInfo, eventId); } }; diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index f37afe961042..6377ae9c8399 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -123,7 +123,9 @@ class ErrorBoundary extends React.Component { test.each([ @@ -13,3 +14,45 @@ describe('isAtLeastReact17', () => { expect(isAtLeastReact17(input)).toBe(output); }); }); + +describe('reactErrorHandler', () => { + const captureException = vi.spyOn(SentryBrowser, 'captureException'); + + beforeEach(() => { + captureException.mockClear(); + }); + + it('captures errors as unhandled when no callback is provided', () => { + const error = new Error('test error'); + const errorInfo = { componentStack: 'component stack' }; + + const handler = reactErrorHandler(); + + handler(error, errorInfo); + + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith(error, { + mechanism: { handled: false, type: 'auto.function.react.error_handler' }, + }); + }); + + it('captures errors as handled when a callback is provided', () => { + captureException.mockReturnValueOnce('custom-event-id'); + + const error = new Error('test error'); + const errorInfo = { componentStack: 'component stack' }; + + const callback = vi.fn(); + const handler = reactErrorHandler(callback); + + handler(error, errorInfo); + + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith(error, { + mechanism: { handled: true, type: 'auto.function.react.error_handler' }, + }); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(error, errorInfo, 'custom-event-id'); + }); +}); diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index 5e731cc86b49..3d2198e81dc9 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -385,7 +385,7 @@ describe('ErrorBoundary', () => { expect(mockCaptureException).toHaveBeenCalledTimes(1); expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), { - mechanism: { handled: true }, + mechanism: { handled: true, type: 'auto.function.react.error_boundary' }, }); expect(scopeSetContextSpy).toHaveBeenCalledTimes(1); @@ -444,7 +444,7 @@ describe('ErrorBoundary', () => { expect(mockCaptureException).toHaveBeenCalledTimes(1); expect(mockCaptureException).toHaveBeenLastCalledWith('bam', { - mechanism: { handled: true }, + mechanism: { handled: true, type: 'auto.function.react.error_boundary' }, }); expect(scopeSetContextSpy).toHaveBeenCalledTimes(1); @@ -483,7 +483,7 @@ describe('ErrorBoundary', () => { expect(mockCaptureException).toHaveBeenCalledTimes(1); expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), { - mechanism: { handled: true }, + mechanism: { handled: true, type: 'auto.function.react.error_boundary' }, }); expect(scopeSetContextSpy).toHaveBeenCalledTimes(1); @@ -527,7 +527,7 @@ describe('ErrorBoundary', () => { expect(mockCaptureException).toHaveBeenCalledTimes(1); expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), { - mechanism: { handled: true }, + mechanism: { handled: true, type: 'auto.function.react.error_boundary' }, }); expect(scopeSetContextSpy).toHaveBeenCalledTimes(1); @@ -695,7 +695,7 @@ describe('ErrorBoundary', () => { expect(mockCaptureException).toHaveBeenCalledTimes(1); expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Object), { - mechanism: { handled: expected }, + mechanism: { handled: expected, type: 'auto.function.react.error_boundary' }, }); expect(scopeSetContextSpy).toHaveBeenCalledTimes(1);