Skip to content

Commit f892179

Browse files
committed
feat(core): Allow to pass mechanism as event hint
Also, allow to pass `hint` as an alternative to `CaptureContext` to `captureException` as second argument.
1 parent ad4f2d1 commit f892179

38 files changed

+424
-631
lines changed

packages/angular/src/errorhandler.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { HttpErrorResponse } from '@angular/common/http';
22
import type { ErrorHandler as AngularErrorHandler } from '@angular/core';
33
import { Inject, Injectable } from '@angular/core';
44
import * as Sentry from '@sentry/browser';
5-
import type { Event, Scope } from '@sentry/types';
6-
import { addExceptionMechanism, isString } from '@sentry/utils';
5+
import type { Event } from '@sentry/types';
6+
import { isString } from '@sentry/utils';
77

88
import { runOutsideAngular } from './zone';
99

@@ -102,17 +102,8 @@ class SentryErrorHandler implements AngularErrorHandler {
102102

103103
// Capture handled exception and send it to Sentry.
104104
const eventId = runOutsideAngular(() =>
105-
Sentry.captureException(extractedError, (scope: Scope) => {
106-
scope.addEventProcessor(event => {
107-
addExceptionMechanism(event, {
108-
type: 'angular',
109-
handled: false,
110-
});
111-
112-
return event;
113-
});
114-
115-
return scope;
105+
Sentry.captureException(extractedError, {
106+
mechanism: { type: 'angular', handled: false },
116107
}),
117108
);
118109

packages/angular/test/errorhandler.test.ts

Lines changed: 43 additions & 70 deletions
Large diffs are not rendered by default.

packages/astro/src/server/middleware.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { captureException, configureScope, getCurrentHub, startSpan } from '@sentry/node';
22
import type { Hub, Span } from '@sentry/types';
3-
import { addExceptionMechanism, objectify, stripUrlQueryAndFragment, tracingContextFromHeaders } from '@sentry/utils';
3+
import { objectify, stripUrlQueryAndFragment, tracingContextFromHeaders } from '@sentry/utils';
44
import type { APIContext, MiddlewareResponseHandler } from 'astro';
55

66
import { getTracingMetaTags } from './meta';
@@ -34,19 +34,14 @@ function sendErrorToSentry(e: unknown): unknown {
3434
// store a seen flag on it.
3535
const objectifiedErr = objectify(e);
3636

37-
captureException(objectifiedErr, scope => {
38-
scope.addEventProcessor(event => {
39-
addExceptionMechanism(event, {
40-
type: 'astro',
41-
handled: false,
42-
data: {
43-
function: 'astroMiddleware',
44-
},
45-
});
46-
return event;
47-
});
48-
49-
return scope;
37+
captureException(objectifiedErr, {
38+
mechanism: {
39+
type: 'astro',
40+
handled: false,
41+
data: {
42+
function: 'astroMiddleware',
43+
},
44+
},
5045
});
5146

5247
return objectifiedErr;

packages/astro/test/server/middleware.test.ts

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as SentryNode from '@sentry/node';
2-
import * as SentryUtils from '@sentry/utils';
32
import { vi } from 'vitest';
43

54
import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware';
@@ -59,12 +58,7 @@ describe('sentryMiddleware', () => {
5958
});
6059

6160
it('throws and sends an error to sentry if `next()` throws', async () => {
62-
const scope = {
63-
addEventProcessor: vi.fn().mockImplementation(cb => cb({})),
64-
};
65-
// @ts-expect-error, just testing the callback, this is okay for this test
66-
const captureExceptionSpy = vi.spyOn(SentryNode, 'captureException').mockImplementation((ex, cb) => cb(scope));
67-
const addExMechanismSpy = vi.spyOn(SentryUtils, 'addExceptionMechanism');
61+
const captureExceptionSpy = vi.spyOn(SentryNode, 'captureException');
6862

6963
const middleware = handleRequest();
7064
const ctx = {
@@ -86,16 +80,9 @@ describe('sentryMiddleware', () => {
8680
// @ts-expect-error, a partial ctx object is fine here
8781
await expect(async () => middleware(ctx, next)).rejects.toThrowError();
8882

89-
expect(captureExceptionSpy).toHaveBeenCalledWith(error, expect.any(Function));
90-
expect(scope.addEventProcessor).toHaveBeenCalledTimes(1);
91-
expect(addExMechanismSpy).toHaveBeenCalledWith(
92-
{}, // the mocked event
93-
{
94-
handled: false,
95-
type: 'astro',
96-
data: { function: 'astroMiddleware' },
97-
},
98-
);
83+
expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
84+
mechanism: { handled: false, type: 'astro', data: { function: 'astroMiddleware' } },
85+
});
9986
});
10087

10188
it('attaches tracing headers', async () => {

packages/browser/src/helpers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { captureException, withScope } from '@sentry/core';
2-
import type { DsnLike, Event as SentryEvent, Mechanism, Scope, WrappedFunction } from '@sentry/types';
2+
import type { DsnLike, Mechanism, WrappedFunction } from '@sentry/types';
33
import {
44
addExceptionMechanism,
55
addExceptionTypeValue,
@@ -99,8 +99,8 @@ export function wrap(
9999
} catch (ex) {
100100
ignoreNextOnError();
101101

102-
withScope((scope: Scope) => {
103-
scope.addEventProcessor((event: SentryEvent) => {
102+
withScope(scope => {
103+
scope.addEventProcessor(event => {
104104
if (options.mechanism) {
105105
addExceptionTypeValue(event, undefined, undefined);
106106
addExceptionMechanism(event, options.mechanism);

packages/browser/src/integrations/globalhandlers.ts

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
22
import { getCurrentHub } from '@sentry/core';
3-
import type { Event, EventHint, Hub, Integration, Primitive, StackParser } from '@sentry/types';
4-
import {
5-
addExceptionMechanism,
6-
addInstrumentationHandler,
7-
getLocationHref,
8-
isErrorEvent,
9-
isPrimitive,
10-
isString,
11-
logger,
12-
} from '@sentry/utils';
3+
import type { Event, Hub, Integration, Primitive, StackParser } from '@sentry/types';
4+
import { addInstrumentationHandler, getLocationHref, isErrorEvent, isPrimitive, isString, logger } from '@sentry/utils';
135

146
import type { BrowserClient } from '../client';
157
import { eventFromUnknownInput } from '../eventbuilder';
@@ -103,7 +95,13 @@ function _installGlobalOnErrorHandler(): void {
10395

10496
event.level = 'error';
10597

106-
addMechanismAndCapture(hub, error, event, 'onerror');
98+
hub.captureEvent(event, {
99+
originalException: error,
100+
mechanism: {
101+
handled: false,
102+
type: 'onerror',
103+
},
104+
});
107105
},
108106
);
109107
}
@@ -149,7 +147,14 @@ function _installGlobalOnUnhandledRejectionHandler(): void {
149147

150148
event.level = 'error';
151149

152-
addMechanismAndCapture(hub, error, event, 'onunhandledrejection');
150+
hub.captureEvent(event, {
151+
originalException: error,
152+
mechanism: {
153+
handled: false,
154+
type: 'onunhandledrejection',
155+
},
156+
});
157+
153158
return;
154159
},
155160
);
@@ -243,16 +248,6 @@ function globalHandlerLog(type: string): void {
243248
__DEBUG_BUILD__ && logger.log(`Global Handler attached: ${type}`);
244249
}
245250

246-
function addMechanismAndCapture(hub: Hub, error: EventHint['originalException'], event: Event, type: string): void {
247-
addExceptionMechanism(event, {
248-
handled: false,
249-
type,
250-
});
251-
hub.captureEvent(event, {
252-
originalException: error,
253-
});
254-
}
255-
256251
function getHubAndOptions(): [Hub, StackParser, boolean | undefined] {
257252
const hub = getCurrentHub();
258253
const client = hub.getClient<BrowserClient>();

packages/bun/src/integrations/bunserver.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,6 @@
11
import { captureException, getCurrentHub, runWithAsyncContext, startSpan, Transaction } from '@sentry/core';
22
import type { Integration } from '@sentry/types';
3-
import { addExceptionMechanism, getSanitizedUrlString, parseUrl, tracingContextFromHeaders } from '@sentry/utils';
4-
5-
function sendErrorToSentry(e: unknown): unknown {
6-
captureException(e, scope => {
7-
scope.addEventProcessor(event => {
8-
addExceptionMechanism(event, {
9-
type: 'bun',
10-
handled: false,
11-
data: {
12-
function: 'serve',
13-
},
14-
});
15-
return event;
16-
});
17-
18-
return scope;
19-
});
20-
21-
return e;
22-
}
3+
import { getSanitizedUrlString, parseUrl, tracingContextFromHeaders } from '@sentry/utils';
234

245
/**
256
* Instruments `Bun.serve` to automatically create transactions and capture errors.
@@ -121,7 +102,15 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
121102
}
122103
return response;
123104
} catch (e) {
124-
sendErrorToSentry(e);
105+
captureException(e, {
106+
mechanism: {
107+
type: 'bun',
108+
handled: false,
109+
data: {
110+
function: 'serve',
111+
},
112+
},
113+
});
125114
throw e;
126115
}
127116
},

packages/core/src/exports.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { isThenable, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
2020
import type { Hub } from './hub';
2121
import { getCurrentHub } from './hub';
2222
import type { Scope } from './scope';
23+
import { parseEventHintOrCaptureContext } from './utils/prepareEvent';
2324

2425
// Note: All functions in this file are typed with a return value of `ReturnType<Hub[HUB_FUNCTION]>`,
2526
// where HUB_FUNCTION is some method on the Hub class.
@@ -30,14 +31,15 @@ import type { Scope } from './scope';
3031

3132
/**
3233
* Captures an exception event and sends it to Sentry.
33-
*
34-
* @param exception An exception-like object.
35-
* @param captureContext Additional scope data to apply to exception event.
36-
* @returns The generated eventId.
34+
* This accepts an event hint as optional second parameter.
35+
* Alternatively, you can also pass a CaptureContext directly as second parameter.
3736
*/
38-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
39-
export function captureException(exception: any, captureContext?: CaptureContext): ReturnType<Hub['captureException']> {
40-
return getCurrentHub().captureException(exception, { captureContext });
37+
export function captureException(
38+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
39+
exception: any,
40+
hint?: CaptureContext | EventHint,
41+
): ReturnType<Hub['captureException']> {
42+
return getCurrentHub().captureException(exception, parseEventHintOrCaptureContext(hint));
4143
}
4244

4345
/**

packages/core/src/utils/prepareEvent.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
1-
import type { Client, ClientOptions, Event, EventHint, StackFrame, StackParser } from '@sentry/types';
2-
import { dateTimestampInSeconds, GLOBAL_OBJ, normalize, resolvedSyncPromise, truncate, uuid4 } from '@sentry/utils';
1+
import type {
2+
CaptureContext,
3+
Client,
4+
ClientOptions,
5+
Event,
6+
EventHint,
7+
ScopeContext,
8+
StackFrame,
9+
StackParser,
10+
} from '@sentry/types';
11+
import {
12+
addExceptionMechanism,
13+
dateTimestampInSeconds,
14+
dropUndefinedKeys,
15+
GLOBAL_OBJ,
16+
normalize,
17+
resolvedSyncPromise,
18+
truncate,
19+
uuid4,
20+
} from '@sentry/utils';
321

422
import { DEFAULT_ENVIRONMENT } from '../constants';
523
import { getGlobalEventProcessors, notifyEventProcessors } from '../eventProcessors';
@@ -52,6 +70,10 @@ export function prepareEvent(
5270
finalScope = Scope.clone(finalScope).update(hint.captureContext);
5371
}
5472

73+
if (hint.mechanism) {
74+
addExceptionMechanism(prepared, hint.mechanism);
75+
}
76+
5577
// We prepare the result here with a resolved Event.
5678
let result = resolvedSyncPromise<Event | null>(prepared);
5779

@@ -309,3 +331,39 @@ function normalizeEvent(event: Event | null, depth: number, maxBreadth: number):
309331

310332
return normalized;
311333
}
334+
335+
/**
336+
* Parse either an `EventHint` directly, or convert a `CaptureContext` to an `EventHint`.
337+
* This is used to allow to update method signatures that used to accept a `CaptureContext` but should now accept an `EventHint`.
338+
*/
339+
export function parseEventHintOrCaptureContext(hint: CaptureContext | EventHint | undefined): EventHint | undefined {
340+
if (!hint) {
341+
return undefined;
342+
}
343+
344+
// If you pass a Scope or `() => Scope` as CaptureContext, we just return this as captureContext
345+
if (hintIsScopeOrFunction(hint)) {
346+
return { captureContext: hint };
347+
}
348+
349+
const hintOrScopeContext = hint as Partial<ScopeContext> & Partial<EventHint>;
350+
351+
// Else, we need to make sure to pick the legacy CaptureContext fields off & merge them into the hint
352+
const { user, level, extra, contexts, tags, fingerprint, requestSession, propagationContext, ...eventHint } =
353+
hintOrScopeContext;
354+
355+
const captureContext = {
356+
...dropUndefinedKeys({ user, level, extra, contexts, tags, fingerprint, requestSession, propagationContext }),
357+
...hintOrScopeContext.captureContext,
358+
};
359+
360+
if (Object.keys(captureContext).length) {
361+
eventHint.captureContext = captureContext;
362+
}
363+
364+
return eventHint;
365+
}
366+
367+
function hintIsScopeOrFunction(hint: CaptureContext | EventHint): hint is Scope | (() => Scope) {
368+
return hint instanceof Scope || typeof hint === 'function';
369+
}

0 commit comments

Comments
 (0)