Skip to content

Commit 35e464d

Browse files
authored
feat(core): Extract errors from props in unkown inputs (#11526)
This changes the handling of unknown inputs for e.g. `captureException()`, to check if there is a property that holds an `Error` and use this. So e.g. this: ```js const error = new Error('Some error'); Sentry.captureException({ message: 'something happened!', err: error }); ``` Will use `Some error` as the error message, and also take the stacktrace from the error instead of the syntethic one. There is of course the chance that this leads to false positives, if something is captured and contains a reference to an unrelated error instance. But I'd say this is not so likely, and we still keep the serialised object as `extra` on the event. Should fix e.g. https://peated.sentry.io/issues/5166150355/?project=4505138086019073
1 parent f6a3e02 commit 35e464d

File tree

4 files changed

+341
-72
lines changed

4 files changed

+341
-72
lines changed

packages/browser/src/eventbuilder.ts

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@ export function exceptionFromError(stackParser: StackParser, ex: Error): Excepti
4848
return exception;
4949
}
5050

51-
/**
52-
* @hidden
53-
*/
54-
export function eventFromPlainObject(
51+
function eventFromPlainObject(
5552
stackParser: StackParser,
5653
exception: Record<string, unknown>,
5754
syntheticException?: Error,
@@ -60,35 +57,46 @@ export function eventFromPlainObject(
6057
const client = getClient();
6158
const normalizeDepth = client && client.getOptions().normalizeDepth;
6259

63-
const event: Event = {
60+
// If we can, we extract an exception from the object properties
61+
const errorFromProp = getErrorPropertyFromObject(exception);
62+
63+
const extra = {
64+
__serialized__: normalizeToSize(exception, normalizeDepth),
65+
};
66+
67+
if (errorFromProp) {
68+
return {
69+
exception: {
70+
values: [exceptionFromError(stackParser, errorFromProp)],
71+
},
72+
extra,
73+
};
74+
}
75+
76+
const event = {
6477
exception: {
6578
values: [
6679
{
6780
type: isEvent(exception) ? exception.constructor.name : isUnhandledRejection ? 'UnhandledRejection' : 'Error',
6881
value: getNonErrorObjectExceptionValue(exception, { isUnhandledRejection }),
69-
},
82+
} as Exception,
7083
],
7184
},
72-
extra: {
73-
__serialized__: normalizeToSize(exception, normalizeDepth),
74-
},
75-
};
85+
extra,
86+
} satisfies Event;
7687

7788
if (syntheticException) {
7889
const frames = parseStackFrames(stackParser, syntheticException);
7990
if (frames.length) {
8091
// event.exception.values[0] has been set above
81-
(event.exception as { values: Exception[] }).values[0].stacktrace = { frames };
92+
event.exception.values[0].stacktrace = { frames };
8293
}
8394
}
8495

8596
return event;
8697
}
8798

88-
/**
89-
* @hidden
90-
*/
91-
export function eventFromError(stackParser: StackParser, ex: Error): Event {
99+
function eventFromError(stackParser: StackParser, ex: Error): Event {
92100
return {
93101
exception: {
94102
values: [exceptionFromError(stackParser, ex)],
@@ -97,7 +105,7 @@ export function eventFromError(stackParser: StackParser, ex: Error): Event {
97105
}
98106

99107
/** Parses stack frames from an error */
100-
export function parseStackFrames(
108+
function parseStackFrames(
101109
stackParser: StackParser,
102110
ex: Error & { framesToPop?: number; stacktrace?: string },
103111
): StackFrame[] {
@@ -283,10 +291,7 @@ export function eventFromUnknownInput(
283291
return event;
284292
}
285293

286-
/**
287-
* @hidden
288-
*/
289-
export function eventFromString(
294+
function eventFromString(
290295
stackParser: StackParser,
291296
message: ParameterizedString,
292297
syntheticException?: Error,
@@ -346,3 +351,17 @@ function getObjectClassName(obj: unknown): string | undefined | void {
346351
// ignore errors here
347352
}
348353
}
354+
355+
/** If a plain object has a property that is an `Error`, return this error. */
356+
function getErrorPropertyFromObject(obj: Record<string, unknown>): Error | undefined {
357+
for (const prop in obj) {
358+
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
359+
const value = obj[prop];
360+
if (value instanceof Error) {
361+
return value;
362+
}
363+
}
364+
}
365+
366+
return undefined;
367+
}

packages/browser/test/unit/eventbuilder.test.ts

Lines changed: 102 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defaultStackParser } from '../../src';
2-
import { eventFromPlainObject } from '../../src/eventbuilder';
2+
import { eventFromUnknownInput } from '../../src/eventbuilder';
33

44
jest.mock('@sentry/core', () => {
55
const original = jest.requireActual('@sentry/core');
@@ -12,17 +12,6 @@ jest.mock('@sentry/core', () => {
1212
},
1313
};
1414
},
15-
getCurrentHub() {
16-
return {
17-
getClient(): any {
18-
return {
19-
getOptions(): any {
20-
return { normalizeDepth: 6 };
21-
},
22-
};
23-
},
24-
};
25-
},
2615
};
2716
});
2817

@@ -35,7 +24,7 @@ afterEach(() => {
3524
jest.resetAllMocks();
3625
});
3726

38-
describe('eventFromPlainObject', () => {
27+
describe('eventFromUnknownInput', () => {
3928
it('should use normalizeDepth from init options', () => {
4029
const deepObject = {
4130
a: {
@@ -53,7 +42,7 @@ describe('eventFromPlainObject', () => {
5342
},
5443
};
5544

56-
const event = eventFromPlainObject(defaultStackParser, deepObject);
45+
const event = eventFromUnknownInput(defaultStackParser, deepObject);
5746

5847
expect(event?.extra?.__serialized__).toEqual({
5948
a: {
@@ -71,16 +60,107 @@ describe('eventFromPlainObject', () => {
7160
});
7261

7362
it.each([
74-
['empty object', {}, 'Object captured as exception with keys: [object has no keys]'],
75-
['pojo', { prop1: 'hello', prop2: 2 }, 'Object captured as exception with keys: prop1, prop2'],
76-
['Custom Class', new MyTestClass(), 'Object captured as exception with keys: prop1, prop2'],
77-
['Event', new Event('custom'), 'Event `Event` (type=custom) captured as exception'],
78-
['MouseEvent', new MouseEvent('click'), 'Event `MouseEvent` (type=click) captured as exception'],
79-
] as [string, Record<string, unknown>, string][])(
63+
['empty object', {}, {}, 'Object captured as exception with keys: [object has no keys]'],
64+
[
65+
'pojo',
66+
{ prop1: 'hello', prop2: 2 },
67+
{ prop1: 'hello', prop2: 2 },
68+
'Object captured as exception with keys: prop1, prop2',
69+
],
70+
[
71+
'Custom Class',
72+
new MyTestClass(),
73+
{ prop1: 'hello', prop2: 2 },
74+
'Object captured as exception with keys: prop1, prop2',
75+
],
76+
[
77+
'Event',
78+
new Event('custom'),
79+
{
80+
currentTarget: '[object Null]',
81+
isTrusted: false,
82+
target: '[object Null]',
83+
type: 'custom',
84+
},
85+
'Event `Event` (type=custom) captured as exception',
86+
],
87+
[
88+
'MouseEvent',
89+
new MouseEvent('click'),
90+
{
91+
currentTarget: '[object Null]',
92+
isTrusted: false,
93+
target: '[object Null]',
94+
type: 'click',
95+
},
96+
'Event `MouseEvent` (type=click) captured as exception',
97+
],
98+
] as [string, Record<string, unknown>, Record<string, unknown>, string][])(
8099
'has correct exception value for %s',
81-
(_name, exception, expected) => {
82-
const actual = eventFromPlainObject(defaultStackParser, exception);
100+
(_name, exception, serializedException, expected) => {
101+
const actual = eventFromUnknownInput(defaultStackParser, exception);
83102
expect(actual.exception?.values?.[0]?.value).toEqual(expected);
103+
104+
expect(actual.extra).toEqual({
105+
__serialized__: serializedException,
106+
});
84107
},
85108
);
109+
110+
it('handles object with error prop', () => {
111+
const error = new Error('Some error');
112+
const event = eventFromUnknownInput(defaultStackParser, {
113+
foo: { bar: 'baz' },
114+
name: 'BadType',
115+
err: error,
116+
});
117+
118+
expect(event.exception?.values?.[0]).toEqual(
119+
expect.objectContaining({
120+
mechanism: { handled: true, synthetic: true, type: 'generic' },
121+
type: 'Error',
122+
value: 'Some error',
123+
}),
124+
);
125+
expect(event.extra).toEqual({
126+
__serialized__: {
127+
foo: { bar: 'baz' },
128+
name: 'BadType',
129+
err: {
130+
message: 'Some error',
131+
name: 'Error',
132+
stack: expect.stringContaining('Error: Some error'),
133+
},
134+
},
135+
});
136+
});
137+
138+
it('handles class with error prop', () => {
139+
const error = new Error('Some error');
140+
141+
class MyTestClass {
142+
prop1 = 'hello';
143+
prop2 = error;
144+
}
145+
146+
const event = eventFromUnknownInput(defaultStackParser, new MyTestClass());
147+
148+
expect(event.exception?.values?.[0]).toEqual(
149+
expect.objectContaining({
150+
mechanism: { handled: true, synthetic: true, type: 'generic' },
151+
type: 'Error',
152+
value: 'Some error',
153+
}),
154+
);
155+
expect(event.extra).toEqual({
156+
__serialized__: {
157+
prop1: 'hello',
158+
prop2: {
159+
message: 'Some error',
160+
name: 'Error',
161+
stack: expect.stringContaining('Error: Some error'),
162+
},
163+
},
164+
});
165+
});
86166
});

0 commit comments

Comments
 (0)