diff --git a/packages/angular/src/errorhandler.ts b/packages/angular/src/errorhandler.ts index ea6dea85d04c..2cc6550c63cb 100644 --- a/packages/angular/src/errorhandler.ts +++ b/packages/angular/src/errorhandler.ts @@ -2,8 +2,8 @@ import { HttpErrorResponse } from '@angular/common/http'; import type { ErrorHandler as AngularErrorHandler } from '@angular/core'; import { Inject, Injectable } from '@angular/core'; import * as Sentry from '@sentry/browser'; -import type { Event, Scope } from '@sentry/types'; -import { addExceptionMechanism, isString } from '@sentry/utils'; +import type { Event } from '@sentry/types'; +import { isString } from '@sentry/utils'; import { runOutsideAngular } from './zone'; @@ -102,17 +102,8 @@ class SentryErrorHandler implements AngularErrorHandler { // Capture handled exception and send it to Sentry. const eventId = runOutsideAngular(() => - Sentry.captureException(extractedError, (scope: Scope) => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'angular', - handled: false, - }); - - return event; - }); - - return scope; + Sentry.captureException(extractedError, { + mechanism: { type: 'angular', handled: false }, }), ); diff --git a/packages/angular/test/errorhandler.test.ts b/packages/angular/test/errorhandler.test.ts index 3a78d715de27..d298fd72cdee 100644 --- a/packages/angular/test/errorhandler.test.ts +++ b/packages/angular/test/errorhandler.test.ts @@ -1,28 +1,17 @@ import { HttpErrorResponse } from '@angular/common/http'; import * as SentryBrowser from '@sentry/browser'; -import { Scope } from '@sentry/browser'; import type { Event } from '@sentry/types'; -import * as SentryUtils from '@sentry/utils'; import { createErrorHandler, SentryErrorHandler } from '../src/errorhandler'; -const FakeScope = new Scope(); - -jest.mock('@sentry/browser', () => { - const original = jest.requireActual('@sentry/browser'); - return { - ...original, - captureException: (err: unknown, cb: (arg0?: unknown) => unknown) => { - cb(FakeScope); - return original.captureException(err, cb); - }, - }; -}); - const captureExceptionSpy = jest.spyOn(SentryBrowser, 'captureException'); jest.spyOn(console, 'error').mockImplementation(); +const captureExceptionEventHint = { + mechanism: { handled: false, type: 'angular' }, +}; + class CustomError extends Error { public name: string; @@ -55,34 +44,18 @@ describe('SentryErrorHandler', () => { }); describe('handleError method', () => { - it('handleError method assigns the correct mechanism', () => { - const addEventProcessorSpy = jest.spyOn(FakeScope, 'addEventProcessor').mockImplementationOnce(callback => { - void (callback as (event: any, hint: any) => void)({}, { event_id: 'fake-event-id' }); - return FakeScope; - }); - - const addExceptionMechanismSpy = jest.spyOn(SentryUtils, 'addExceptionMechanism'); - - const errorHandler = createErrorHandler(); - errorHandler.handleError(new Error('test')); - - expect(addEventProcessorSpy).toBeCalledTimes(1); - expect(addExceptionMechanismSpy).toBeCalledTimes(1); - expect(addExceptionMechanismSpy).toBeCalledWith({}, { handled: false, type: 'angular' }); - }); - it('extracts `null` error', () => { createErrorHandler().handleError(null); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', captureExceptionEventHint); }); it('extracts `undefined` error', () => { createErrorHandler().handleError(undefined); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', captureExceptionEventHint); }); it('extracts a string', () => { @@ -90,7 +63,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(str); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(str, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(str, { mechanism: { handled: false, type: 'angular' } }); }); it('extracts an empty Error', () => { @@ -98,7 +71,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(err, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(err, { mechanism: { handled: false, type: 'angular' } }); }); it('extracts a non-empty Error', () => { @@ -107,7 +80,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(err, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(err, { mechanism: { handled: false, type: 'angular' } }); }); it('extracts an error-like object without stack', () => { @@ -119,7 +92,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(errorLikeWithoutStack); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithoutStack, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithoutStack, captureExceptionEventHint); }); it('extracts an error-like object with a stack', () => { @@ -132,7 +105,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(errorLikeWithStack); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithStack, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithStack, captureExceptionEventHint); }); it('extracts an object that could look like an error but is not (does not have a message)', () => { @@ -144,7 +117,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(notErr); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', captureExceptionEventHint); }); it('extracts an object that could look like an error but is not (does not have an explicit name)', () => { @@ -155,7 +128,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(notErr); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', captureExceptionEventHint); }); it('extracts an object that could look like an error but is not: the name is of the wrong type', () => { @@ -167,7 +140,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(notErr); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', captureExceptionEventHint); }); it('extracts an object that could look like an error but is not: the message is of the wrong type', () => { @@ -179,7 +152,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(notErr); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', captureExceptionEventHint); }); it('extracts an instance of a class extending Error', () => { @@ -188,7 +161,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(err, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(err, { mechanism: { handled: false, type: 'angular' } }); }); it('extracts an instance of class not extending Error but that has an error-like shape', () => { @@ -197,7 +170,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(err, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(err, { mechanism: { handled: false, type: 'angular' } }); }); it('extracts an instance of a class that does not extend Error and does not have an error-like shape', () => { @@ -206,7 +179,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(notErr); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', captureExceptionEventHint); }); it('extracts ErrorEvent which has a string as an error', () => { @@ -215,7 +188,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', captureExceptionEventHint); }); it('extracts ErrorEvent which has an error as an error', () => { @@ -224,7 +197,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', captureExceptionEventHint); }); it('extracts ErrorEvent which has an error-like object as an error', () => { @@ -237,7 +210,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', captureExceptionEventHint); }); it('extracts ErrorEvent which has a non-error-like object as an error', () => { @@ -246,7 +219,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', captureExceptionEventHint); }); it('extracts an Error with `ngOriginalError`', () => { @@ -258,7 +231,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(ngErr, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(ngErr, { mechanism: { handled: false, type: 'angular' } }); }); it('extracts an `HttpErrorResponse` with `Error`', () => { @@ -268,7 +241,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(httpErr, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(httpErr, { mechanism: { handled: false, type: 'angular' } }); }); it('extracts an `HttpErrorResponse` with `ErrorEvent`', () => { @@ -278,7 +251,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('sentry-http-test', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('sentry-http-test', captureExceptionEventHint); }); it('extracts an `HttpErrorResponse` with string', () => { @@ -288,7 +261,7 @@ describe('SentryErrorHandler', () => { expect(captureExceptionSpy).toHaveBeenCalledTimes(1); expect(captureExceptionSpy).toHaveBeenCalledWith( 'Server returned code 0 with body "sentry-http-test"', - expect.any(Function), + captureExceptionEventHint, ); }); @@ -302,7 +275,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithoutStack, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithoutStack, captureExceptionEventHint); }); it('extracts an `HttpErrorResponse` with error-like object with a stack', () => { @@ -316,7 +289,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithStack, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithStack, captureExceptionEventHint); }); it('extracts an `HttpErrorResponse` with an object that could look like an error but is not (does not have a message)', () => { @@ -331,7 +304,7 @@ describe('SentryErrorHandler', () => { expect(captureExceptionSpy).toHaveBeenCalledTimes(1); expect(captureExceptionSpy).toHaveBeenCalledWith( 'Http failure response for (unknown url): undefined undefined', - expect.any(Function), + captureExceptionEventHint, ); }); @@ -346,7 +319,7 @@ describe('SentryErrorHandler', () => { expect(captureExceptionSpy).toHaveBeenCalledTimes(1); expect(captureExceptionSpy).toHaveBeenCalledWith( 'Http failure response for (unknown url): undefined undefined', - expect.any(Function), + captureExceptionEventHint, ); }); @@ -362,7 +335,7 @@ describe('SentryErrorHandler', () => { expect(captureExceptionSpy).toHaveBeenCalledTimes(1); expect(captureExceptionSpy).toHaveBeenCalledWith( 'Http failure response for (unknown url): undefined undefined', - expect.any(Function), + captureExceptionEventHint, ); }); @@ -378,7 +351,7 @@ describe('SentryErrorHandler', () => { expect(captureExceptionSpy).toHaveBeenCalledTimes(1); expect(captureExceptionSpy).toHaveBeenCalledWith( 'Http failure response for (unknown url): undefined undefined', - expect.any(Function), + captureExceptionEventHint, ); }); @@ -395,7 +368,7 @@ describe('SentryErrorHandler', () => { expect(captureExceptionSpy).toHaveBeenCalledTimes(1); expect(captureExceptionSpy).toHaveBeenCalledWith( 'Http failure response for (unknown url): undefined undefined', - expect.any(Function), + captureExceptionEventHint, ); }); @@ -412,7 +385,7 @@ describe('SentryErrorHandler', () => { expect(captureExceptionSpy).toHaveBeenCalledTimes(1); expect(captureExceptionSpy).toHaveBeenCalledWith( 'Http failure response for (unknown url): undefined undefined', - expect.any(Function), + captureExceptionEventHint, ); }); @@ -428,7 +401,7 @@ describe('SentryErrorHandler', () => { expect(captureExceptionSpy).toHaveBeenCalledTimes(1); expect(captureExceptionSpy).toHaveBeenCalledWith( 'Http failure response for (unknown url): undefined undefined', - expect.any(Function), + captureExceptionEventHint, ); }); @@ -438,7 +411,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(err, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(err, { mechanism: { handled: false, type: 'angular' } }); }); it('extracts an `HttpErrorResponse` with an instance of class not extending Error but that has an error-like shape', () => { @@ -448,7 +421,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(innerErr, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(innerErr, { mechanism: { handled: false, type: 'angular' } }); }); it('extracts an `HttpErrorResponse` with an instance of a class that does not extend Error and does not have an error-like shape', () => { @@ -460,7 +433,7 @@ describe('SentryErrorHandler', () => { expect(captureExceptionSpy).toHaveBeenCalledTimes(1); expect(captureExceptionSpy).toHaveBeenCalledWith( 'Http failure response for (unknown url): undefined undefined', - expect.any(Function), + captureExceptionEventHint, ); }); @@ -471,7 +444,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('something happened', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('something happened', captureExceptionEventHint); }); it('extracts an `HttpErrorResponse` with an ErrorEvent which has an error as an error', () => { @@ -481,7 +454,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('something happened', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('something happened', captureExceptionEventHint); }); it('extracts an `HttpErrorResponse` with an ErrorEvent which has an error-like object as an error', () => { @@ -495,7 +468,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('something happened', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('something happened', captureExceptionEventHint); }); it('extracts an `HttpErrorResponse` with an ErrorEvent which has a non-error-like object as an error', () => { @@ -505,7 +478,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('something happened', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith('something happened', captureExceptionEventHint); }); it('extracts error with a custom extractor', () => { @@ -520,7 +493,7 @@ describe('SentryErrorHandler', () => { errorHandler.handleError('error'); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith(new Error('custom error'), expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(new Error('custom error'), captureExceptionEventHint); }); describe('opens the report dialog if `showDialog` is true', () => { diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index b5b7aa7d8c71..22823f1e6130 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,6 +1,6 @@ import { captureException, configureScope, getCurrentHub, startSpan } from '@sentry/node'; import type { Hub, Span } from '@sentry/types'; -import { addExceptionMechanism, objectify, stripUrlQueryAndFragment, tracingContextFromHeaders } from '@sentry/utils'; +import { objectify, stripUrlQueryAndFragment, tracingContextFromHeaders } from '@sentry/utils'; import type { APIContext, MiddlewareResponseHandler } from 'astro'; import { getTracingMetaTags } from './meta'; @@ -34,19 +34,14 @@ function sendErrorToSentry(e: unknown): unknown { // store a seen flag on it. const objectifiedErr = objectify(e); - captureException(objectifiedErr, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'astro', - handled: false, - data: { - function: 'astroMiddleware', - }, - }); - return event; - }); - - return scope; + captureException(objectifiedErr, { + mechanism: { + type: 'astro', + handled: false, + data: { + function: 'astroMiddleware', + }, + }, }); return objectifiedErr; diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index 59ab8c18a3c4..39146cdaa1d7 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,5 +1,4 @@ import * as SentryNode from '@sentry/node'; -import * as SentryUtils from '@sentry/utils'; import { vi } from 'vitest'; import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware'; @@ -59,12 +58,7 @@ describe('sentryMiddleware', () => { }); it('throws and sends an error to sentry if `next()` throws', async () => { - const scope = { - addEventProcessor: vi.fn().mockImplementation(cb => cb({})), - }; - // @ts-expect-error, just testing the callback, this is okay for this test - const captureExceptionSpy = vi.spyOn(SentryNode, 'captureException').mockImplementation((ex, cb) => cb(scope)); - const addExMechanismSpy = vi.spyOn(SentryUtils, 'addExceptionMechanism'); + const captureExceptionSpy = vi.spyOn(SentryNode, 'captureException'); const middleware = handleRequest(); const ctx = { @@ -86,16 +80,9 @@ describe('sentryMiddleware', () => { // @ts-expect-error, a partial ctx object is fine here await expect(async () => middleware(ctx, next)).rejects.toThrowError(); - expect(captureExceptionSpy).toHaveBeenCalledWith(error, expect.any(Function)); - expect(scope.addEventProcessor).toHaveBeenCalledTimes(1); - expect(addExMechanismSpy).toHaveBeenCalledWith( - {}, // the mocked event - { - handled: false, - type: 'astro', - data: { function: 'astroMiddleware' }, - }, - ); + expect(captureExceptionSpy).toHaveBeenCalledWith(error, { + mechanism: { handled: false, type: 'astro', data: { function: 'astroMiddleware' } }, + }); }); it('attaches tracing headers', async () => { diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts index 5f7b6cee7df3..faa0762c8163 100644 --- a/packages/browser/src/helpers.ts +++ b/packages/browser/src/helpers.ts @@ -1,5 +1,5 @@ import { captureException, withScope } from '@sentry/core'; -import type { DsnLike, Event as SentryEvent, Mechanism, Scope, WrappedFunction } from '@sentry/types'; +import type { DsnLike, Mechanism, WrappedFunction } from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, @@ -99,8 +99,8 @@ export function wrap( } catch (ex) { ignoreNextOnError(); - withScope((scope: Scope) => { - scope.addEventProcessor((event: SentryEvent) => { + withScope(scope => { + scope.addEventProcessor(event => { if (options.mechanism) { addExceptionTypeValue(event, undefined, undefined); addExceptionMechanism(event, options.mechanism); diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index 3a6ca996ef01..af2f917daf96 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -1,15 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { getCurrentHub } from '@sentry/core'; -import type { Event, EventHint, Hub, Integration, Primitive, StackParser } from '@sentry/types'; -import { - addExceptionMechanism, - addInstrumentationHandler, - getLocationHref, - isErrorEvent, - isPrimitive, - isString, - logger, -} from '@sentry/utils'; +import type { Event, Hub, Integration, Primitive, StackParser } from '@sentry/types'; +import { addInstrumentationHandler, getLocationHref, isErrorEvent, isPrimitive, isString, logger } from '@sentry/utils'; import type { BrowserClient } from '../client'; import { eventFromUnknownInput } from '../eventbuilder'; @@ -103,7 +95,13 @@ function _installGlobalOnErrorHandler(): void { event.level = 'error'; - addMechanismAndCapture(hub, error, event, 'onerror'); + hub.captureEvent(event, { + originalException: error, + mechanism: { + handled: false, + type: 'onerror', + }, + }); }, ); } @@ -149,7 +147,14 @@ function _installGlobalOnUnhandledRejectionHandler(): void { event.level = 'error'; - addMechanismAndCapture(hub, error, event, 'onunhandledrejection'); + hub.captureEvent(event, { + originalException: error, + mechanism: { + handled: false, + type: 'onunhandledrejection', + }, + }); + return; }, ); @@ -243,16 +248,6 @@ function globalHandlerLog(type: string): void { __DEBUG_BUILD__ && logger.log(`Global Handler attached: ${type}`); } -function addMechanismAndCapture(hub: Hub, error: EventHint['originalException'], event: Event, type: string): void { - addExceptionMechanism(event, { - handled: false, - type, - }); - hub.captureEvent(event, { - originalException: error, - }); -} - function getHubAndOptions(): [Hub, StackParser, boolean | undefined] { const hub = getCurrentHub(); const client = hub.getClient(); diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index d20953628544..f7cd8cd8cbb9 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -1,25 +1,6 @@ import { captureException, continueTrace, runWithAsyncContext, startSpan, Transaction } from '@sentry/core'; import type { Integration } from '@sentry/types'; -import { addExceptionMechanism, getSanitizedUrlString, parseUrl } from '@sentry/utils'; - -function sendErrorToSentry(e: unknown): unknown { - captureException(e, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'bun', - handled: false, - data: { - function: 'serve', - }, - }); - return event; - }); - - return scope; - }); - - return e; -} +import { getSanitizedUrlString, parseUrl } from '@sentry/utils'; /** * Instruments `Bun.serve` to automatically create transactions and capture errors. @@ -115,7 +96,15 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] } return response; } catch (e) { - sendErrorToSentry(e); + captureException(e, { + mechanism: { + type: 'bun', + handled: false, + data: { + function: 'serve', + }, + }, + }); throw e; } }, diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 6569bc4e4c25..2be61f2fa940 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -20,6 +20,8 @@ import { isThenable, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; import type { Hub } from './hub'; import { getCurrentHub } from './hub'; import type { Scope } from './scope'; +import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; +import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; // Note: All functions in this file are typed with a return value of `ReturnType`, // where HUB_FUNCTION is some method on the Hub class. @@ -30,14 +32,15 @@ import type { Scope } from './scope'; /** * Captures an exception event and sends it to Sentry. - * - * @param exception An exception-like object. - * @param captureContext Additional scope data to apply to exception event. - * @returns The generated eventId. + * This accepts an event hint as optional second parameter. + * Alternatively, you can also pass a CaptureContext directly as second parameter. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types -export function captureException(exception: any, captureContext?: CaptureContext): ReturnType { - return getCurrentHub().captureException(exception, { captureContext }); +export function captureException( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + exception: any, + hint?: ExclusiveEventHintOrCaptureContext, +): ReturnType { + return getCurrentHub().captureException(exception, parseEventHintOrCaptureContext(hint)); } /** diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 6e473a9b1cb1..33e9eca33ef1 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -1,10 +1,37 @@ -import type { Client, ClientOptions, Event, EventHint, StackFrame, StackParser } from '@sentry/types'; -import { dateTimestampInSeconds, GLOBAL_OBJ, normalize, resolvedSyncPromise, truncate, uuid4 } from '@sentry/utils'; +import type { + CaptureContext, + Client, + ClientOptions, + Event, + EventHint, + Scope as ScopeInterface, + ScopeContext, + StackFrame, + StackParser, +} from '@sentry/types'; +import { + addExceptionMechanism, + dateTimestampInSeconds, + GLOBAL_OBJ, + normalize, + resolvedSyncPromise, + truncate, + uuid4, +} from '@sentry/utils'; import { DEFAULT_ENVIRONMENT } from '../constants'; import { getGlobalEventProcessors, notifyEventProcessors } from '../eventProcessors'; import { Scope } from '../scope'; +/** + * This type makes sure that we get either a CaptureContext, OR an EventHint. + * It does not allow mixing them, which could lead to unexpected outcomes, e.g. this is disallowed: + * { user: { id: '123' }, mechanism: { handled: false } } + */ +export type ExclusiveEventHintOrCaptureContext = + | (CaptureContext & Partial<{ [key in keyof EventHint]: never }>) + | (EventHint & Partial<{ [key in keyof ScopeContext]: never }>); + /** * Adds common information to events. * @@ -52,6 +79,10 @@ export function prepareEvent( finalScope = Scope.clone(finalScope).update(hint.captureContext); } + if (hint.mechanism) { + addExceptionMechanism(prepared, hint.mechanism); + } + // We prepare the result here with a resolved Event. let result = resolvedSyncPromise(prepared); @@ -309,3 +340,50 @@ function normalizeEvent(event: Event | null, depth: number, maxBreadth: number): return normalized; } + +/** + * Parse either an `EventHint` directly, or convert a `CaptureContext` to an `EventHint`. + * This is used to allow to update method signatures that used to accept a `CaptureContext` but should now accept an `EventHint`. + */ +export function parseEventHintOrCaptureContext( + hint: ExclusiveEventHintOrCaptureContext | undefined, +): EventHint | undefined { + if (!hint) { + return undefined; + } + + // If you pass a Scope or `() => Scope` as CaptureContext, we just return this as captureContext + if (hintIsScopeOrFunction(hint)) { + return { captureContext: hint }; + } + + if (hintIsScopeContext(hint)) { + return { + captureContext: hint, + }; + } + + return hint; +} + +function hintIsScopeOrFunction( + hint: CaptureContext | EventHint, +): hint is ScopeInterface | ((scope: ScopeInterface) => ScopeInterface) { + return hint instanceof Scope || typeof hint === 'function'; +} + +type ScopeContextProperty = keyof ScopeContext; +const captureContextKeys: readonly ScopeContextProperty[] = [ + 'user', + 'level', + 'extra', + 'contexts', + 'tags', + 'fingerprint', + 'requestSession', + 'propagationContext', +] as const; + +function hintIsScopeContext(hint: Partial | EventHint): hint is Partial { + return Object.keys(hint).some(key => captureContextKeys.includes(key as ScopeContextProperty)); +} diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index 1e8b53e60f8c..9d00df469112 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -1,7 +1,8 @@ -import type { Event } from '@sentry/types'; +import type { Event, EventHint, ScopeContext } from '@sentry/types'; import { createStackParser, GLOBAL_OBJ } from '@sentry/utils'; -import { applyDebugIds, applyDebugMeta } from '../../src/utils/prepareEvent'; +import { Scope } from '../../src/scope'; +import { applyDebugIds, applyDebugMeta, parseEventHintOrCaptureContext } from '../../src/utils/prepareEvent'; describe('applyDebugIds', () => { afterEach(() => { @@ -105,3 +106,70 @@ describe('applyDebugMeta', () => { }); }); }); + +describe('parseEventHintOrCaptureContext', () => { + it('works with undefined', () => { + const actual = parseEventHintOrCaptureContext(undefined); + expect(actual).toEqual(undefined); + }); + + it('works with an empty object', () => { + const actual = parseEventHintOrCaptureContext({}); + expect(actual).toEqual({}); + }); + + it('works with a Scope', () => { + const scope = new Scope(); + const actual = parseEventHintOrCaptureContext(scope); + expect(actual).toEqual({ captureContext: scope }); + }); + + it('works with a function', () => { + const scope = () => new Scope(); + const actual = parseEventHintOrCaptureContext(scope); + expect(actual).toEqual({ captureContext: scope }); + }); + + it('works with an EventHint', () => { + const hint: EventHint = { + mechanism: { handled: false }, + }; + const actual = parseEventHintOrCaptureContext(hint); + expect(actual).toEqual(hint); + }); + + it('works with a ScopeContext', () => { + const scopeContext: ScopeContext = { + user: { id: 'xxx' }, + level: 'debug', + extra: { foo: 'bar' }, + contexts: { os: { name: 'linux' } }, + tags: { foo: 'bar' }, + fingerprint: ['xx', 'yy'], + requestSession: { status: 'ok' }, + propagationContext: { + traceId: 'xxx', + spanId: 'yyy', + }, + }; + + const actual = parseEventHintOrCaptureContext(scopeContext); + expect(actual).toEqual({ captureContext: scopeContext }); + }); + + it('triggers a TS error if trying to mix ScopeContext & EventHint', () => { + const actual = parseEventHintOrCaptureContext({ + // @ts-expect-error We are specifically testing that this errors! + user: { id: 'xxx' }, + mechanism: { handled: false }, + }); + + // ScopeContext takes presedence in this case, but this is actually not supported + expect(actual).toEqual({ + captureContext: { + user: { id: 'xxx' }, + mechanism: { handled: false }, + }, + }); + }); +}); diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts index 7e4d2e003673..d173780cfa50 100644 --- a/packages/deno/src/integrations/globalhandlers.ts +++ b/packages/deno/src/integrations/globalhandlers.ts @@ -1,7 +1,7 @@ import type { ServerRuntimeClient } from '@sentry/core'; import { flush, getCurrentHub } from '@sentry/core'; -import type { Event, EventHint, Hub, Integration, Primitive, StackParser } from '@sentry/types'; -import { addExceptionMechanism, eventFromUnknownInput, isPrimitive } from '@sentry/utils'; +import type { Event, Hub, Integration, Primitive, StackParser } from '@sentry/types'; +import { eventFromUnknownInput, isPrimitive } from '@sentry/utils'; type GlobalHandlersIntegrationsOptionKeys = 'error' | 'unhandledrejection'; @@ -74,7 +74,13 @@ function installGlobalErrorHandler(): void { event.level = 'fatal'; - addMechanismAndCapture(hub, error, event, 'error'); + hub.captureEvent(event, { + originalException: error, + mechanism: { + handled: false, + type: 'error', + }, + }); // Stop the app from exiting for now data.preventDefault(); @@ -111,7 +117,13 @@ function installGlobalUnhandledRejectionHandler(): void { event.level = 'fatal'; - addMechanismAndCapture(hub, error as unknown as Error, event, 'unhandledrejection'); + hub.captureEvent(event, { + originalException: error, + mechanism: { + handled: false, + type: 'unhandledrejection', + }, + }); // Stop the app from exiting for now e.preventDefault(); @@ -144,16 +156,6 @@ function eventFromRejectionWithPrimitive(reason: Primitive): Event { }; } -function addMechanismAndCapture(hub: Hub, error: EventHint['originalException'], event: Event, type: string): void { - addExceptionMechanism(event, { - handled: false, - type, - }); - hub.captureEvent(event, { - originalException: error, - }); -} - function getHubAndOptions(): [Hub, StackParser] { const hub = getCurrentHub(); const client = hub.getClient(); diff --git a/packages/hub/test/exports.test.ts b/packages/hub/test/exports.test.ts index 967448a05d4a..c2b71f84ca47 100644 --- a/packages/hub/test/exports.test.ts +++ b/packages/hub/test/exports.test.ts @@ -80,7 +80,7 @@ describe('Top Level API', () => { const captureContext = { extra: { foo: 'wat' } }; captureException(e, captureContext); expect(client.captureException.mock.calls[0][0]).toBe(e); - expect(client.captureException.mock.calls[0][1].captureContext).toBe(captureContext); + expect(client.captureException.mock.calls[0][1].captureContext).toEqual(captureContext); }); }); diff --git a/packages/nextjs/src/common/_error.ts b/packages/nextjs/src/common/_error.ts index 1ad27cc0b67f..c69c725c3137 100644 --- a/packages/nextjs/src/common/_error.ts +++ b/packages/nextjs/src/common/_error.ts @@ -1,5 +1,4 @@ import { captureException, getCurrentHub, withScope } from '@sentry/core'; -import { addExceptionMechanism } from '@sentry/utils'; import type { NextPageContext } from 'next'; type ContextOrProps = { @@ -42,24 +41,21 @@ export async function captureUnderscoreErrorException(contextOrProps: ContextOrP } withScope(scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'instrument', - handled: false, - data: { - function: '_error.getInitialProps', - }, - }); - return event; - }); - if (req) { scope.setSDKProcessingMetadata({ request: req }); } // If third-party libraries (or users themselves) throw something falsy, we want to capture it as a message (which // is what passing a string to `captureException` will wind up doing) - captureException(err || `_error.js called with falsy error (${err})`); + captureException(err || `_error.js called with falsy error (${err})`, { + mechanism: { + type: 'instrument', + handled: false, + data: { + function: '_error.getInitialProps', + }, + }, + }); }); // In case this is being run as part of a serverless function (as is the case with the server half of nextjs apps diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index 9ae2cde39f83..44487608e251 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -6,7 +6,7 @@ import { startTransaction, } from '@sentry/core'; import type { Span, Transaction } from '@sentry/types'; -import { addExceptionMechanism, isString, tracingContextFromHeaders } from '@sentry/utils'; +import { isString, tracingContextFromHeaders } from '@sentry/utils'; import type { IncomingMessage, ServerResponse } from 'http'; import { platformSupportsStreaming } from './platformSupportsStreaming'; @@ -47,16 +47,7 @@ export function withErrorInstrumentation any>( return await origFunction.apply(this, origFunctionArguments); } catch (e) { // TODO: Extract error logic from `withSentry` in here or create a new wrapper with said logic or something like that. - captureException(e, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, - }); - return event; - }); - - return scope; - }); + captureException(e, { mechanism: { handled: false } }); throw e; } @@ -232,16 +223,7 @@ export async function callDataFetcherTraced Promis span.finish(); // TODO Copy more robust error handling over from `withSentry` - captureException(err, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, - }); - return event; - }); - - return scope; - }); + captureException(err, { mechanism: { handled: false } }); throw err; } diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 1ec28f372cab..8f458fc728a6 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -1,5 +1,5 @@ import { addTracingExtensions, captureException, flush, getCurrentHub, runWithAsyncContext, trace } from '@sentry/core'; -import { addExceptionMechanism, logger, tracingContextFromHeaders } from '@sentry/utils'; +import { logger, tracingContextFromHeaders } from '@sentry/utils'; import { platformSupportsStreaming } from './utils/platformSupportsStreaming'; @@ -111,16 +111,7 @@ async function withServerActionInstrumentationImplementation { - captureException(error, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, - }); - return event; - }); - - return scope; - }); + captureException(error, { mechanism: { handled: false } }); }, ); } finally { diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index 5a93a257a209..0c94a3ce4aa1 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -6,14 +6,7 @@ import { startTransaction, } from '@sentry/core'; import type { Transaction } from '@sentry/types'; -import { - addExceptionMechanism, - isString, - logger, - objectify, - stripUrlQueryAndFragment, - tracingContextFromHeaders, -} from '@sentry/utils'; +import { isString, logger, objectify, stripUrlQueryAndFragment, tracingContextFromHeaders } from '@sentry/utils'; import type { AugmentedNextApiRequest, AugmentedNextApiResponse, NextApiHandler } from './types'; import { platformSupportsStreaming } from './utils/platformSupportsStreaming'; @@ -187,20 +180,17 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri // way to prevent it from actually being reported twice.) const objectifiedErr = objectify(e); - currentScope.addEventProcessor(event => { - addExceptionMechanism(event, { + captureException(objectifiedErr, { + mechanism: { type: 'instrument', handled: false, data: { wrapped_handler: wrappingTarget.name, function: 'withSentry', }, - }); - return event; + }, }); - captureException(objectifiedErr); - // Because we're going to finish and send the transaction before passing the error onto nextjs, it won't yet // have had a chance to set the status to 500, so unless we do it ourselves now, we'll incorrectly report that // the transaction was error-free diff --git a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts index 8d7dc3a2e8d1..ece566bc2e5a 100644 --- a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts @@ -1,5 +1,5 @@ import { addTracingExtensions, captureException, configureScope, runWithAsyncContext } from '@sentry/core'; -import { addExceptionMechanism, extractTraceparentData } from '@sentry/utils'; +import { extractTraceparentData } from '@sentry/utils'; interface FunctionComponent { (...args: unknown[]): unknown; @@ -48,15 +48,10 @@ export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | C try { return super.render(...args); } catch (e) { - captureException(e, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, - }); - return event; - }); - - return scope; + captureException(e, { + mechanism: { + handled: false, + }, }); throw e; } @@ -82,15 +77,10 @@ export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | C try { return target.apply(thisArg, argArray); } catch (e) { - captureException(e, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, - }); - return event; - }); - - return scope; + captureException(e, { + mechanism: { + handled: false, + }, }); throw e; } diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index a111bbe0666d..d407a2578b5b 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -1,5 +1,5 @@ import { addTracingExtensions, captureException, flush, getCurrentHub, runWithAsyncContext, trace } from '@sentry/core'; -import { addExceptionMechanism, tracingContextFromHeaders } from '@sentry/utils'; +import { tracingContextFromHeaders } from '@sentry/utils'; import { isRedirectNavigationError } from './nextNavigationErrorUtils'; import type { RouteHandlerContext } from './types'; @@ -54,15 +54,10 @@ export function wrapRouteHandlerWithSentry any>( error => { // Next.js throws errors when calling `redirect()`. We don't wanna report these. if (!isRedirectNavigationError(error)) { - captureException(error, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, - }); - return event; - }); - - return scope; + captureException(error, { + mechanism: { + handled: false, + }, }); } }, diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index e909bd114c7c..fc215e495b58 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -6,7 +6,7 @@ import { runWithAsyncContext, startTransaction, } from '@sentry/core'; -import { addExceptionMechanism, tracingContextFromHeaders } from '@sentry/utils'; +import { tracingContextFromHeaders } from '@sentry/utils'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; @@ -62,15 +62,10 @@ export function wrapServerComponentWithSentry any> } else { transaction.setStatus('internal_error'); - captureException(e, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, - }); - return event; - }); - - return scope; + captureException(e, { + mechanism: { + handled: false, + }, }); } diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 9ba531dff428..df438276231e 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -12,7 +12,6 @@ import { import type { Span } from '@sentry/types'; import type { AddRequestDataToEventOptions } from '@sentry/utils'; import { - addExceptionMechanism, addRequestDataToTransaction, dropUndefinedKeys, extractPathForTransaction, @@ -298,12 +297,7 @@ export function errorHandler(options?: { } } - _scope.addEventProcessor(event => { - addExceptionMechanism(event, { type: 'middleware', handled: false }); - return event; - }); - - const eventId = captureException(error); + const eventId = captureException(error, { mechanism: { type: 'middleware', handled: false } }); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (res as any).sentry = eventId; next(error); @@ -367,16 +361,7 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { function handleErrorCase(e: unknown): void { if (shouldCaptureError(e)) { - captureException(e, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, - }); - return event; - }); - - return scope; - }); + captureException(e, { mechanism: { handled: false } }); } } diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index 66006b06d7b6..075b5f8b00bf 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -1,6 +1,6 @@ import type { ReportDialogOptions, Scope } from '@sentry/browser'; import { captureException, getCurrentHub, showReportDialog, withScope } from '@sentry/browser'; -import { addExceptionMechanism, isError, logger } from '@sentry/utils'; +import { isError, logger } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; @@ -139,12 +139,12 @@ class ErrorBoundary extends React.Component { - addExceptionMechanism(event, { handled: false }) - return event; - }) - - const eventId = captureException(error, { contexts: { react: { componentStack } } }); + const eventId = captureException(error, { + captureContext: { + contexts: { react: { componentStack } }, + }, + mechanism: { handled: false }, + }); if (onError) { onError(error, componentStack, eventId); diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index 33486043f675..52f71552f703 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -238,7 +238,10 @@ describe('ErrorBoundary', () => { expect(mockCaptureException).toHaveBeenCalledTimes(1); expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), { - contexts: { react: { componentStack: expect.any(String) } }, + captureContext: { + contexts: { react: { componentStack: expect.any(String) } }, + }, + mechanism: { handled: false }, }); expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]); @@ -246,7 +249,7 @@ describe('ErrorBoundary', () => { // Check if error.cause -> react component stack const error = mockCaptureException.mock.calls[0][0]; const cause = error.cause; - expect(cause.stack).toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack); + expect(cause.stack).toEqual(mockCaptureException.mock.calls[0][1].captureContext.contexts.react.componentStack); expect(cause.name).toContain('React ErrorBoundary'); expect(cause.message).toEqual(error.message); }); @@ -293,7 +296,10 @@ describe('ErrorBoundary', () => { expect(mockCaptureException).toHaveBeenCalledTimes(1); expect(mockCaptureException).toHaveBeenLastCalledWith('bam', { - contexts: { react: { componentStack: expect.any(String) } }, + captureContext: { + contexts: { react: { componentStack: expect.any(String) } }, + }, + mechanism: { handled: false }, }); // Check if error.cause -> react component stack @@ -329,7 +335,10 @@ describe('ErrorBoundary', () => { expect(mockCaptureException).toHaveBeenCalledTimes(1); expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), { - contexts: { react: { componentStack: expect.any(String) } }, + captureContext: { + contexts: { react: { componentStack: expect.any(String) } }, + }, + mechanism: { handled: false }, }); expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]); @@ -338,7 +347,7 @@ describe('ErrorBoundary', () => { const secondError = thirdError.cause; const firstError = secondError.cause; const cause = firstError.cause; - expect(cause.stack).toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack); + expect(cause.stack).toEqual(mockCaptureException.mock.calls[0][1].captureContext.contexts.react.componentStack); expect(cause.name).toContain('React ErrorBoundary'); expect(cause.message).toEqual(thirdError.message); }); @@ -370,7 +379,10 @@ describe('ErrorBoundary', () => { expect(mockCaptureException).toHaveBeenCalledTimes(1); expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), { - contexts: { react: { componentStack: expect.any(String) } }, + captureContext: { + contexts: { react: { componentStack: expect.any(String) } }, + }, + mechanism: { handled: false }, }); expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]); @@ -378,7 +390,9 @@ describe('ErrorBoundary', () => { const error = mockCaptureException.mock.calls[0][0]; const cause = error.cause; // We need to make sure that recursive error.cause does not cause infinite loop - expect(cause.stack).not.toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack); + expect(cause.stack).not.toEqual( + mockCaptureException.mock.calls[0][1].captureContext.contexts.react.componentStack, + ); expect(cause.name).not.toContain('React ErrorBoundary'); }); diff --git a/packages/remix/src/client/errors.tsx b/packages/remix/src/client/errors.tsx index 6732ed5cdc91..a6afefbc0ef7 100644 --- a/packages/remix/src/client/errors.tsx +++ b/packages/remix/src/client/errors.tsx @@ -1,7 +1,8 @@ -import { captureException, withScope } from '@sentry/core'; -import { addExceptionMechanism, isNodeEnv, isString } from '@sentry/utils'; +import { captureException } from '@sentry/core'; +import { isNodeEnv, isString } from '@sentry/utils'; import { isRouteErrorResponse } from '../utils/vendor/response'; +import type { ErrorResponse } from '../utils/vendor/types'; /** * Captures an error that is thrown inside a Remix ErrorBoundary. @@ -28,29 +29,26 @@ export function captureRemixErrorBoundaryError(error: unknown): string | undefin function: 'ReactError', }; - withScope(scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'instrument', - handled: false, - data: eventData, - }); - return event; - }); - - if (isRemixErrorResponse) { - if (isString(error.data)) { - eventId = captureException(error.data); - } else if (error.statusText) { - eventId = captureException(error.statusText); - } else { - eventId = captureException(error); - } - } else { - eventId = captureException(error); - } + const actualError = isRemixErrorResponse ? getExceptionToCapture(error) : error; + eventId = captureException(actualError, { + mechanism: { + type: 'instrument', + handled: false, + data: eventData, + }, }); } return eventId; } + +function getExceptionToCapture(error: ErrorResponse): string | ErrorResponse { + if (isString(error.data)) { + return error.data; + } + if (error.statusText) { + return error.statusText; + } + + return error; +} diff --git a/packages/sveltekit/src/client/handleError.ts b/packages/sveltekit/src/client/handleError.ts index 5b24c2ef9357..6c8a30fb1e03 100644 --- a/packages/sveltekit/src/client/handleError.ts +++ b/packages/sveltekit/src/client/handleError.ts @@ -1,5 +1,4 @@ import { captureException } from '@sentry/svelte'; -import { addExceptionMechanism } from '@sentry/utils'; // For now disable the import/no-unresolved rule, because we don't have a way to // tell eslint that we are only importing types from the @sveltejs/kit package without // adding a custom resolver, which will take too much time. @@ -20,15 +19,11 @@ function defaultErrorHandler({ error }: Parameters[0]): Retur */ export function handleErrorWithSentry(handleError: HandleClientError = defaultErrorHandler): HandleClientError { return (input: { error: unknown; event: NavigationEvent }): ReturnType => { - captureException(input.error, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'sveltekit', - handled: false, - }); - return event; - }); - return scope; + captureException(input.error, { + mechanism: { + type: 'sveltekit', + handled: false, + }, }); return handleError(input); diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts index 93d835dd72d1..ebc21c35eaf0 100644 --- a/packages/sveltekit/src/client/load.ts +++ b/packages/sveltekit/src/client/load.ts @@ -1,6 +1,6 @@ import { trace } from '@sentry/core'; import { captureException } from '@sentry/svelte'; -import { addExceptionMechanism, addNonEnumerableProperty, objectify } from '@sentry/utils'; +import { addNonEnumerableProperty, objectify } from '@sentry/utils'; import type { LoadEvent } from '@sveltejs/kit'; import type { SentryWrappedFlag } from '../common/utils'; @@ -18,19 +18,14 @@ function sendErrorToSentry(e: unknown): unknown { return objectifiedErr; } - captureException(objectifiedErr, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'sveltekit', - handled: false, - data: { - function: 'load', - }, - }); - return event; - }); - - return scope; + captureException(objectifiedErr, { + mechanism: { + type: 'sveltekit', + handled: false, + data: { + function: 'load', + }, + }, }); return objectifiedErr; diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 5076710970a8..3b16f659f6e0 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -2,7 +2,7 @@ import type { Span } from '@sentry/core'; import { getActiveTransaction, getCurrentHub, runWithAsyncContext, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; -import { addExceptionMechanism, dynamicSamplingContextToSentryBaggageHeader, objectify } from '@sentry/utils'; +import { dynamicSamplingContextToSentryBaggageHeader, objectify } from '@sentry/utils'; import type { Handle, ResolveOptions } from '@sveltejs/kit'; import { isHttpError, isRedirect } from '../common/utils'; @@ -39,19 +39,14 @@ function sendErrorToSentry(e: unknown): unknown { return objectifiedErr; } - captureException(objectifiedErr, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'sveltekit', - handled: false, - data: { - function: 'handle', - }, - }); - return event; - }); - - return scope; + captureException(objectifiedErr, { + mechanism: { + type: 'sveltekit', + handled: false, + data: { + function: 'handle', + }, + }, }); return objectifiedErr; diff --git a/packages/sveltekit/src/server/handleError.ts b/packages/sveltekit/src/server/handleError.ts index 938cbf612e2f..c0f27d181928 100644 --- a/packages/sveltekit/src/server/handleError.ts +++ b/packages/sveltekit/src/server/handleError.ts @@ -1,5 +1,4 @@ import { captureException } from '@sentry/node'; -import { addExceptionMechanism } from '@sentry/utils'; // For now disable the import/no-unresolved rule, because we don't have a way to // tell eslint that we are only importing types from the @sveltejs/kit package without // adding a custom resolver, which will take too much time. @@ -27,15 +26,11 @@ export function handleErrorWithSentry(handleError: HandleServerError = defaultEr return handleError(input); } - captureException(input.error, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'sveltekit', - handled: false, - }); - return event; - }); - return scope; + captureException(input.error, { + mechanism: { + type: 'sveltekit', + handled: false, + }, }); await flushIfServerless(); diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts index e819c434e81b..c902fe4376d6 100644 --- a/packages/sveltekit/src/server/load.ts +++ b/packages/sveltekit/src/server/load.ts @@ -2,7 +2,7 @@ import { getCurrentHub, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; import type { TransactionContext } from '@sentry/types'; -import { addExceptionMechanism, addNonEnumerableProperty, objectify } from '@sentry/utils'; +import { addNonEnumerableProperty, objectify } from '@sentry/utils'; import type { LoadEvent, ServerLoadEvent } from '@sveltejs/kit'; import type { SentryWrappedFlag } from '../common/utils'; @@ -29,19 +29,14 @@ function sendErrorToSentry(e: unknown): unknown { return objectifiedErr; } - captureException(objectifiedErr, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'sveltekit', - handled: false, - data: { - function: 'load', - }, - }); - return event; - }); - - return scope; + captureException(objectifiedErr, { + mechanism: { + type: 'sveltekit', + handled: false, + data: { + function: 'load', + }, + }, }); return objectifiedErr; diff --git a/packages/sveltekit/test/client/handleError.test.ts b/packages/sveltekit/test/client/handleError.test.ts index 4dc2e6658af5..0262f0b1b1cc 100644 --- a/packages/sveltekit/test/client/handleError.test.ts +++ b/packages/sveltekit/test/client/handleError.test.ts @@ -1,33 +1,10 @@ -import { Scope } from '@sentry/svelte'; +import * as SentrySvelte from '@sentry/svelte'; import type { HandleClientError, NavigationEvent } from '@sveltejs/kit'; import { vi } from 'vitest'; import { handleErrorWithSentry } from '../../src/client/handleError'; -const mockCaptureException = vi.fn(); -let mockScope = new Scope(); - -vi.mock('@sentry/svelte', async () => { - const original = (await vi.importActual('@sentry/core')) as any; - return { - ...original, - captureException: (err: unknown, cb: (arg0: unknown) => unknown) => { - cb(mockScope); - mockCaptureException(err, cb); - return original.captureException(err, cb); - }, - }; -}); - -const mockAddExceptionMechanism = vi.fn(); - -vi.mock('@sentry/utils', async () => { - const original = (await vi.importActual('@sentry/utils')) as any; - return { - ...original, - addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args), - }; -}); +const mockCaptureException = vi.spyOn(SentrySvelte, 'captureException').mockImplementation(() => 'xx'); function handleError(_input: { error: unknown; event: NavigationEvent }): ReturnType { return { @@ -45,14 +22,16 @@ const navigationEvent: NavigationEvent = { url: new URL('http://example.org/users/123'), }; +const captureExceptionEventHint = { + mechanism: { handled: false, type: 'sveltekit' }, +}; + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(_ => {}); describe('handleError', () => { beforeEach(() => { mockCaptureException.mockClear(); - mockAddExceptionMechanism.mockClear(); consoleErrorSpy.mockClear(); - mockScope = new Scope(); }); describe('calls captureException', () => { @@ -63,7 +42,7 @@ describe('handleError', () => { expect(returnVal).not.toBeDefined(); expect(mockCaptureException).toHaveBeenCalledTimes(1); - expect(mockCaptureException).toHaveBeenCalledWith(mockError, expect.any(Function)); + expect(mockCaptureException).toHaveBeenCalledWith(mockError, captureExceptionEventHint); // The default handler logs the error to the console expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); @@ -75,24 +54,9 @@ describe('handleError', () => { expect(returnVal.message).toEqual('Whoops!'); expect(mockCaptureException).toHaveBeenCalledTimes(1); - expect(mockCaptureException).toHaveBeenCalledWith(mockError, expect.any(Function)); + expect(mockCaptureException).toHaveBeenCalledWith(mockError, captureExceptionEventHint); // Check that the default handler wasn't invoked expect(consoleErrorSpy).toHaveBeenCalledTimes(0); }); }); - - it('adds an exception mechanism', async () => { - const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => { - void callback({}, { event_id: 'fake-event-id' }); - return mockScope; - }); - - const wrappedHandleError = handleErrorWithSentry(handleError); - const mockError = new Error('test'); - await wrappedHandleError({ error: mockError, event: navigationEvent }); - - expect(addEventProcessorSpy).toBeCalledTimes(1); - expect(mockAddExceptionMechanism).toBeCalledTimes(1); - expect(mockAddExceptionMechanism).toBeCalledWith({}, { handled: false, type: 'sveltekit' }); - }); }); diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts index 64b5ef70fe3b..bd6e38fa7a2f 100644 --- a/packages/sveltekit/test/client/load.test.ts +++ b/packages/sveltekit/test/client/load.test.ts @@ -1,24 +1,11 @@ -import { addTracingExtensions, Scope } from '@sentry/svelte'; +import * as SentrySvelte from '@sentry/svelte'; import type { Load } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'; import { vi } from 'vitest'; import { wrapLoadWithSentry } from '../../src/client/load'; -const mockCaptureException = vi.fn(); -let mockScope = new Scope(); - -vi.mock('@sentry/svelte', async () => { - const original = (await vi.importActual('@sentry/svelte')) as any; - return { - ...original, - captureException: (err: unknown, cb: (arg0: unknown) => unknown) => { - cb(mockScope); - mockCaptureException(err, cb); - return original.captureException(err, cb); - }, - }; -}); +const mockCaptureException = vi.spyOn(SentrySvelte, 'captureException').mockImplementation(() => 'xx'); const mockTrace = vi.fn(); @@ -33,16 +20,6 @@ vi.mock('@sentry/core', async () => { }; }); -const mockAddExceptionMechanism = vi.fn(); - -vi.mock('@sentry/utils', async () => { - const original = (await vi.importActual('@sentry/utils')) as any; - return { - ...original, - addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args), - }; -}); - function getById(_id?: string) { throw new Error('error'); } @@ -56,15 +33,13 @@ const MOCK_LOAD_ARGS: any = { }; beforeAll(() => { - addTracingExtensions(); + SentrySvelte.addTracingExtensions(); }); describe('wrapLoadWithSentry', () => { beforeEach(() => { mockCaptureException.mockClear(); - mockAddExceptionMechanism.mockClear(); mockTrace.mockClear(); - mockScope = new Scope(); }); it('calls captureException', async () => { @@ -151,11 +126,6 @@ describe('wrapLoadWithSentry', () => { }); it('adds an exception mechanism', async () => { - const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => { - void callback({}, { event_id: 'fake-event-id' }); - return mockScope; - }); - async function load({ params }: Parameters[0]): Promise> { return { post: getById(params.id), @@ -166,12 +136,10 @@ describe('wrapLoadWithSentry', () => { const res = wrappedLoad(MOCK_LOAD_ARGS); await expect(res).rejects.toThrow(); - expect(addEventProcessorSpy).toBeCalledTimes(1); - expect(mockAddExceptionMechanism).toBeCalledTimes(1); - expect(mockAddExceptionMechanism).toBeCalledWith( - {}, - { handled: false, type: 'sveltekit', data: { function: 'load' } }, - ); + expect(mockCaptureException).toHaveBeenCalledTimes(1); + expect(mockCaptureException).toHaveBeenCalledWith(expect.any(Error), { + mechanism: { handled: false, type: 'sveltekit', data: { function: 'load' } }, + }); }); it("doesn't wrap load more than once if the wrapper was applied multiple times", async () => { diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index 23528dcf6870..2c726127a43b 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -1,5 +1,6 @@ -import { addTracingExtensions, Hub, makeMain, Scope } from '@sentry/core'; +import { addTracingExtensions, Hub, makeMain } from '@sentry/core'; import { NodeClient } from '@sentry/node'; +import * as SentryNode from '@sentry/node'; import type { Transaction } from '@sentry/types'; import type { Handle } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'; @@ -8,30 +9,7 @@ import { vi } from 'vitest'; import { sentryHandle, transformPageChunk } from '../../src/server/handle'; import { getDefaultNodeClientOptions } from '../utils'; -const mockCaptureException = vi.fn(); -let mockScope = new Scope(); - -vi.mock('@sentry/node', async () => { - const original = (await vi.importActual('@sentry/node')) as any; - return { - ...original, - captureException: (err: unknown, cb: (arg0: unknown) => unknown) => { - cb(mockScope); - mockCaptureException(err, cb); - return original.captureException(err, cb); - }, - }; -}); - -const mockAddExceptionMechanism = vi.fn(); - -vi.mock('@sentry/utils', async () => { - const original = (await vi.importActual('@sentry/utils')) as any; - return { - ...original, - addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args), - }; -}); +const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => 'xx'); function mockEvent(override: Record = {}): Parameters[0]['event'] { const event: Parameters[0]['event'] = { @@ -111,14 +89,12 @@ beforeAll(() => { }); beforeEach(() => { - mockScope = new Scope(); const options = getDefaultNodeClientOptions({ tracesSampleRate: 1.0 }); client = new NodeClient(options); hub = new Hub(client); makeMain(hub); mockCaptureException.mockClear(); - mockAddExceptionMechanism.mockClear(); }); describe('handleSentry', () => { @@ -286,21 +262,13 @@ describe('handleSentry', () => { }); it('send errors to Sentry', async () => { - const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => { - void callback({}, { event_id: 'fake-event-id' }); - return mockScope; - }); - try { await sentryHandle()({ event: mockEvent(), resolve: resolve(type, isError) }); } catch (e) { expect(mockCaptureException).toBeCalledTimes(1); - expect(addEventProcessorSpy).toBeCalledTimes(1); - expect(mockAddExceptionMechanism).toBeCalledTimes(2); - expect(mockAddExceptionMechanism).toBeCalledWith( - {}, - { handled: false, type: 'sveltekit', data: { function: 'handle' } }, - ); + expect(mockCaptureException).toBeCalledWith(expect.any(Error), { + mechanism: { handled: false, type: 'sveltekit', data: { function: 'handle' } }, + }); } }); diff --git a/packages/sveltekit/test/server/handleError.test.ts b/packages/sveltekit/test/server/handleError.test.ts index 12ecb83b44e6..157108a8b68a 100644 --- a/packages/sveltekit/test/server/handleError.test.ts +++ b/packages/sveltekit/test/server/handleError.test.ts @@ -1,33 +1,14 @@ -import { Scope } from '@sentry/node'; +import * as SentryNode from '@sentry/node'; import type { HandleServerError, RequestEvent } from '@sveltejs/kit'; import { vi } from 'vitest'; import { handleErrorWithSentry } from '../../src/server/handleError'; -const mockCaptureException = vi.fn(); -let mockScope = new Scope(); +const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => 'xx'); -vi.mock('@sentry/node', async () => { - const original = (await vi.importActual('@sentry/node')) as any; - return { - ...original, - captureException: (err: unknown, cb: (arg0: unknown) => unknown) => { - cb(mockScope); - mockCaptureException(err, cb); - return original.captureException(err, cb); - }, - }; -}); - -const mockAddExceptionMechanism = vi.fn(); - -vi.mock('@sentry/utils', async () => { - const original = (await vi.importActual('@sentry/utils')) as any; - return { - ...original, - addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args), - }; -}); +const captureExceptionEventHint = { + mechanism: { handled: false, type: 'sveltekit' }, +}; function handleError(_input: { error: unknown; event: RequestEvent }): ReturnType { return { @@ -42,9 +23,7 @@ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(_ => {}); describe('handleError', () => { beforeEach(() => { mockCaptureException.mockClear(); - mockAddExceptionMechanism.mockClear(); consoleErrorSpy.mockClear(); - mockScope = new Scope(); }); it('doesn\'t capture "Not found" errors for incorrect navigations', async () => { @@ -60,7 +39,6 @@ describe('handleError', () => { expect(returnVal).not.toBeDefined(); expect(mockCaptureException).toHaveBeenCalledTimes(0); - expect(mockAddExceptionMechanism).toHaveBeenCalledTimes(0); expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); @@ -72,7 +50,7 @@ describe('handleError', () => { expect(returnVal).not.toBeDefined(); expect(mockCaptureException).toHaveBeenCalledTimes(1); - expect(mockCaptureException).toHaveBeenCalledWith(mockError, expect.any(Function)); + expect(mockCaptureException).toHaveBeenCalledWith(mockError, captureExceptionEventHint); // The default handler logs the error to the console expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); @@ -84,24 +62,9 @@ describe('handleError', () => { expect(returnVal.message).toEqual('Whoops!'); expect(mockCaptureException).toHaveBeenCalledTimes(1); - expect(mockCaptureException).toHaveBeenCalledWith(mockError, expect.any(Function)); + expect(mockCaptureException).toHaveBeenCalledWith(mockError, captureExceptionEventHint); // Check that the default handler wasn't invoked expect(consoleErrorSpy).toHaveBeenCalledTimes(0); }); }); - - it('adds an exception mechanism', async () => { - const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => { - void callback({}, { event_id: 'fake-event-id' }); - return mockScope; - }); - - const wrappedHandleError = handleErrorWithSentry(handleError); - const mockError = new Error('test'); - await wrappedHandleError({ error: mockError, event: requestEvent }); - - expect(addEventProcessorSpy).toBeCalledTimes(1); - expect(mockAddExceptionMechanism).toBeCalledTimes(1); - expect(mockAddExceptionMechanism).toBeCalledWith({}, { handled: false, type: 'sveltekit' }); - }); }); diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts index e68e075c7ebd..6b86ca6b32f6 100644 --- a/packages/sveltekit/test/server/load.test.ts +++ b/packages/sveltekit/test/server/load.test.ts @@ -1,25 +1,12 @@ import { addTracingExtensions } from '@sentry/core'; -import { Scope } from '@sentry/node'; +import * as SentryNode from '@sentry/node'; import type { Load, ServerLoad } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit'; import { vi } from 'vitest'; import { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../../src/server/load'; -const mockCaptureException = vi.fn(); -let mockScope = new Scope(); - -vi.mock('@sentry/node', async () => { - const original = (await vi.importActual('@sentry/node')) as any; - return { - ...original, - captureException: (err: unknown, cb: (arg0: unknown) => unknown) => { - cb(mockScope); - mockCaptureException(err, cb); - return original.captureException(err, cb); - }, - }; -}); +const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => 'xx'); const mockStartSpan = vi.fn(); @@ -34,18 +21,6 @@ vi.mock('@sentry/core', async () => { }; }); -const mockAddExceptionMechanism = vi.fn((_e, _m) => {}); - -vi.mock('@sentry/utils', async () => { - const original = (await vi.importActual('@sentry/utils')) as any; - return { - ...original, - addExceptionMechanism: (...args: unknown[]) => { - return mockAddExceptionMechanism(args[0], args[1]); - }, - }; -}); - function getById(_id?: string) { throw new Error('error'); } @@ -131,9 +106,7 @@ beforeAll(() => { afterEach(() => { mockCaptureException.mockClear(); - mockAddExceptionMechanism.mockClear(); mockStartSpan.mockClear(); - mockScope = new Scope(); }); describe.each([ @@ -194,11 +167,6 @@ describe.each([ }); it('adds an exception mechanism', async () => { - const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => { - void callback({}, { event_id: 'fake-event-id' }); - return mockScope; - }); - async function load({ params }) { return { post: getById(params.id), @@ -209,12 +177,10 @@ describe.each([ const res = wrappedLoad(getServerOnlyArgs()); await expect(res).rejects.toThrow(); - expect(addEventProcessorSpy).toHaveBeenCalledTimes(1); - expect(mockAddExceptionMechanism).toBeCalledTimes(1); - expect(mockAddExceptionMechanism).toBeCalledWith( - {}, - { handled: false, type: 'sveltekit', data: { function: 'load' } }, - ); + expect(mockCaptureException).toHaveBeenCalledTimes(1); + expect(mockCaptureException).toHaveBeenCalledWith(expect.any(Error), { + mechanism: { handled: false, type: 'sveltekit', data: { function: 'load' } }, + }); }); }); describe('wrapLoadWithSentry calls trace', () => { diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index 55207f89337d..f04386968280 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -5,6 +5,7 @@ import type { DebugMeta } from './debugMeta'; import type { Exception } from './exception'; import type { Extras } from './extra'; import type { Measurements } from './measurement'; +import type { Mechanism } from './mechanism'; import type { Primitive } from './misc'; import type { Request } from './request'; import type { CaptureContext } from './scope'; @@ -74,6 +75,7 @@ export interface TransactionEvent extends Event { export interface EventHint { event_id?: string; captureContext?: CaptureContext; + mechanism?: Partial; syntheticException?: Error | null; originalException?: unknown; attachments?: Attachment[]; diff --git a/packages/vue/src/errorhandler.ts b/packages/vue/src/errorhandler.ts index 900bed5a5074..40a950ae9374 100644 --- a/packages/vue/src/errorhandler.ts +++ b/packages/vue/src/errorhandler.ts @@ -1,5 +1,4 @@ import { getCurrentHub } from '@sentry/browser'; -import { addExceptionMechanism } from '@sentry/utils'; import type { ViewModel, Vue, VueOptions } from './types'; import { formatComponentName, generateComponentTrace } from './vendor/components'; @@ -30,17 +29,9 @@ export const attachErrorHandler = (app: Vue, options: VueOptions): void => { // Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time. setTimeout(() => { - getCurrentHub().withScope(scope => { - scope.setContext('vue', metadata); - - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, - }); - return event; - }); - - getCurrentHub().captureException(error); + getCurrentHub().captureException(error, { + captureContext: { contexts: { vue: metadata } }, + mechanism: { handled: false }, }); }); diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index 2e3fb3476eb1..75f2c573cdda 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -1,6 +1,5 @@ import { captureException, WINDOW } from '@sentry/browser'; import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; -import { addExceptionMechanism } from '@sentry/utils'; import { getActiveTransaction } from './tracing'; @@ -79,16 +78,7 @@ export function vueRouterInstrumentation( }); } - router.onError(error => - captureException(error, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { handled: false }); - return event; - }); - - return scope; - }), - ); + router.onError(error => captureException(error, { mechanism: { handled: false } })); router.beforeEach((to, from, next) => { // According to docs we could use `from === VueRouter.START_LOCATION` but I couldnt get it working for Vue 2 diff --git a/packages/vue/test/errorHandler.test.ts b/packages/vue/test/errorHandler.test.ts index 9098e7307f28..05727f561297 100644 --- a/packages/vue/test/errorHandler.test.ts +++ b/packages/vue/test/errorHandler.test.ts @@ -390,7 +390,7 @@ const testHarness = ({ const captureExceptionSpy = client.captureException; expect(captureExceptionSpy).toHaveBeenCalledTimes(1); const error = captureExceptionSpy.mock.calls[0][0]; - const contexts = captureExceptionSpy.mock.calls[0][2]._contexts; + const contexts = captureExceptionSpy.mock.calls[0][1].captureContext.contexts; expect(error).toBeInstanceOf(DummyError); diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index 6e74c7c51251..da1e962c9645 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -72,8 +72,7 @@ describe('vueRouterInstrumentation()', () => { onErrorCallback(testError); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - // second function is the scope callback - expect(captureExceptionSpy).toHaveBeenCalledWith(testError, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(testError, { mechanism: { handled: false } }); }); it.each([