Skip to content

Commit d51855e

Browse files
authored
fix(core): Be stricter about mechanism values (#4068)
Though we have an existing `Mechanism` type, we haven't actually been using it in `addExceptionMechanism`. As a result, I didn't catch the fact that the `mechanism` data I added in #4046 was malformed, and therefore being ignored by Sentry. This fixes both of those problems (the not using of the type and the malformed data being sent). Using the correct type also allowed `addExceptionMechanism` to be streamlined quite a bit. Finally, given that the question of what the different attributes and their values actually mean has come up more than once, I conferred with the team, and added a docstring to the type. Hopefully this will help anyone who needs to add `mechanism` data in the future.
1 parent 753b719 commit d51855e

File tree

7 files changed

+90
-31
lines changed

7 files changed

+90
-31
lines changed

packages/browser/src/eventbuilder.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ export function eventFromException(options: Options, exception: unknown, hint?:
2323
const event = eventFromUnknownInput(exception, syntheticException, {
2424
attachStacktrace: options.attachStacktrace,
2525
});
26-
addExceptionMechanism(event, {
27-
handled: true,
28-
type: 'generic',
29-
});
26+
addExceptionMechanism(event); // defaults to { type: 'generic', handled: true }
3027
event.level = Severity.Error;
3128
if (hint && hint.event_id) {
3229
event.event_id = hint.event_id;

packages/nextjs/src/utils/withSentry.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type WrappedNextApiHandler = NextApiHandler;
1313
type AugmentedResponse = NextApiResponse & { __sentryTransaction?: Transaction };
1414

1515
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
16-
export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
16+
export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler => {
1717
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
1818
return async (req, res) => {
1919
// first order of business: monkeypatch `res.end()` so that it will wait for us to send events to sentry before it
@@ -74,13 +74,17 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
7474
}
7575

7676
try {
77-
return await handler(req, res); // Call original handler
77+
return await origHandler(req, res);
7878
} catch (e) {
7979
if (currentScope) {
8080
currentScope.addEventProcessor(event => {
8181
addExceptionMechanism(event, {
82-
mechanism: 'withSentry',
83-
handled: false,
82+
type: 'instrument',
83+
handled: true,
84+
data: {
85+
wrapped_handler: origHandler.name,
86+
function: 'withSentry',
87+
},
8488
});
8589
return event;
8690
});

packages/serverless/test/awslambda.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ describe('AWSLambda', () => {
390390
{
391391
mechanism: {
392392
handled: false,
393+
type: 'generic',
393394
},
394395
},
395396
],

packages/serverless/test/gcpfunction.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ describe('GCPFunction', () => {
458458
{
459459
mechanism: {
460460
handled: false,
461+
type: 'generic',
461462
},
462463
},
463464
],

packages/types/src/mechanism.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
1-
/** JSDoc */
1+
/**
2+
* Metadata about a captured exception, intended to provide a hint as to the means by which it was captured.
3+
*/
24
export interface Mechanism {
5+
/**
6+
* For now, restricted to `onerror`, `onunhandledrejection` (both obvious), `instrument` (the result of
7+
* auto-instrumentation), and `generic` (everything else). Converted to a tag on ingest.
8+
*/
39
type: string;
10+
11+
/**
12+
* In theory, whether or not the exception has been handled by the user. In practice, whether or not we see it before
13+
* it hits the global error/rejection handlers, whether through explicit handling by the user or auto instrumentation.
14+
* Converted to a tag on ingest and used in various ways in the UI.
15+
*/
416
handled: boolean;
17+
18+
/**
19+
* Arbitrary data to be associated with the mechanism (for example, errors coming from event handlers include the
20+
* handler name and the event target. Will show up in the UI directly above the stacktrace.
21+
*/
522
data?: {
623
[key: string]: string | boolean;
724
};
25+
26+
/**
27+
* True when `captureException` is called with anything other than an instance of `Error` (or, in the case of browser,
28+
* an instance of `ErrorEvent`, `DOMError`, or `DOMException`). causing us to create a synthetic error in an attempt
29+
* to recreate the stacktrace.
30+
*/
831
synthetic?: boolean;
932
}

packages/utils/src/misc.ts

+17-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { Event, StackFrame } from '@sentry/types';
2+
import { Event, Mechanism, StackFrame } from '@sentry/types';
33

44
import { getGlobalObject } from './global';
55
import { snipLine } from './string';
@@ -125,29 +125,25 @@ export function addExceptionTypeValue(event: Event, value?: string, type?: strin
125125
}
126126

127127
/**
128-
* Adds exception mechanism to a given event.
128+
* Adds exception mechanism data to a given event. Uses defaults if the second parameter is not passed.
129+
*
129130
* @param event The event to modify.
130-
* @param mechanism Mechanism of the mechanism.
131+
* @param newMechanism Mechanism data to add to the event.
131132
* @hidden
132133
*/
133-
export function addExceptionMechanism(
134-
event: Event,
135-
mechanism: {
136-
[key: string]: any;
137-
} = {},
138-
): void {
139-
// TODO: Use real type with `keyof Mechanism` thingy and maybe make it better?
140-
try {
141-
// @ts-ignore Type 'Mechanism | {}' is not assignable to type 'Mechanism | undefined'
142-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
143-
event.exception!.values![0].mechanism = event.exception!.values![0].mechanism || {};
144-
Object.keys(mechanism).forEach(key => {
145-
// @ts-ignore Mechanism has no index signature
146-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
147-
event.exception!.values![0].mechanism[key] = mechanism[key];
148-
});
149-
} catch (_oO) {
150-
// no-empty
134+
export function addExceptionMechanism(event: Event, newMechanism?: Partial<Mechanism>): void {
135+
if (!event.exception || !event.exception.values) {
136+
return;
137+
}
138+
const exceptionValue0 = event.exception.values[0];
139+
140+
const defaultMechanism = { type: 'generic', handled: true };
141+
const currentMechanism = exceptionValue0.mechanism;
142+
exceptionValue0.mechanism = { ...defaultMechanism, ...currentMechanism, ...newMechanism };
143+
144+
if (newMechanism && 'data' in newMechanism) {
145+
const mergedData = { ...currentMechanism?.data, ...newMechanism.data };
146+
exceptionValue0.mechanism.data = mergedData;
151147
}
152148
}
153149

packages/utils/test/misc.test.ts

+38-1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ describe('stripQueryStringAndFragment', () => {
228228
});
229229

230230
describe('addExceptionMechanism', () => {
231+
const defaultMechanism = { type: 'generic', handled: true };
232+
231233
type EventWithException = Event & {
232234
exception: {
233235
values: [{ type?: string; value?: string; mechanism?: Mechanism }];
@@ -238,7 +240,26 @@ describe('addExceptionMechanism', () => {
238240
exception: { values: [{ type: 'Error', value: 'Oh, no! Charlie ate the flip-flops! :-(' }] },
239241
};
240242

241-
it('adds data to event, preferring incoming values to current values', () => {
243+
it('uses default values', () => {
244+
const event = { ...baseEvent };
245+
246+
addExceptionMechanism(event);
247+
248+
expect(event.exception.values[0].mechanism).toEqual(defaultMechanism);
249+
});
250+
251+
it('prefers current values to defaults', () => {
252+
const event = { ...baseEvent };
253+
254+
const nonDefaultMechanism = { type: 'instrument', handled: false };
255+
event.exception.values[0].mechanism = nonDefaultMechanism;
256+
257+
addExceptionMechanism(event);
258+
259+
expect(event.exception.values[0].mechanism).toEqual(nonDefaultMechanism);
260+
});
261+
262+
it('prefers incoming values to current values', () => {
242263
const event = { ...baseEvent };
243264

244265
const currentMechanism = { type: 'instrument', handled: false };
@@ -250,4 +271,20 @@ describe('addExceptionMechanism', () => {
250271
// the new `handled` value took precedence
251272
expect(event.exception.values[0].mechanism).toEqual({ type: 'instrument', handled: true, synthetic: true });
252273
});
274+
275+
it('merges data values', () => {
276+
const event = { ...baseEvent };
277+
278+
const currentMechanism = { ...defaultMechanism, data: { function: 'addEventListener' } };
279+
const newMechanism = { data: { handler: 'organizeShoes', target: 'closet' } };
280+
event.exception.values[0].mechanism = currentMechanism;
281+
282+
addExceptionMechanism(event, newMechanism);
283+
284+
expect(event.exception.values[0].mechanism.data).toEqual({
285+
function: 'addEventListener',
286+
handler: 'organizeShoes',
287+
target: 'closet',
288+
});
289+
});
253290
});

0 commit comments

Comments
 (0)