-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(browser): Better event name handling for non-Error objects #8374
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
class MyTestClass { | ||
prop1 = 'value1'; | ||
prop2 = 2; | ||
} | ||
|
||
Sentry.captureException(new MyTestClass()); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { expect } from '@playwright/test'; | ||
import type { Event } from '@sentry/types'; | ||
|
||
import { sentryTest } from '../../../../utils/fixtures'; | ||
import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; | ||
|
||
sentryTest('should capture an POJO', async ({ getLocalTestPath, page }) => { | ||
const url = await getLocalTestPath({ testDir: __dirname }); | ||
|
||
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url); | ||
|
||
expect(eventData.exception?.values).toHaveLength(1); | ||
expect(eventData.exception?.values?.[0]).toMatchObject({ | ||
type: 'Error', | ||
value: 'Object captured as exception with keys: prop1, prop2', | ||
mechanism: { | ||
type: 'generic', | ||
handled: true, | ||
}, | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import * as Sentry from '@sentry/browser'; | ||
|
||
window.Sentry = Sentry; | ||
|
||
Sentry.init({ | ||
dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
defaultIntegrations: false, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
window.addEventListener('error', function (event) { | ||
Sentry.captureException(event); | ||
}); | ||
|
||
window.thisDoesNotExist(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { expect } from '@playwright/test'; | ||
import type { Event } from '@sentry/types'; | ||
|
||
import { sentryTest } from '../../../../utils/fixtures'; | ||
import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; | ||
|
||
sentryTest('should capture an ErrorEvent', async ({ getLocalTestPath, page, browserName }) => { | ||
// On Firefox, the ErrorEvent has the `error` property and thus is handled separately | ||
if (browserName === 'firefox') { | ||
sentryTest.skip(); | ||
} | ||
Comment on lines
+9
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we just want to add the FF behaviour to this test? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FF has a completely different behavior, the error does not go into the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I leave it up to you. If you think it's valuable we can add a test - otherwise it's fine too. |
||
const url = await getLocalTestPath({ testDir: __dirname }); | ||
|
||
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url); | ||
|
||
expect(eventData.exception?.values).toHaveLength(1); | ||
expect(eventData.exception?.values?.[0]).toMatchObject({ | ||
type: 'ErrorEvent', | ||
value: 'Event `ErrorEvent` captured as exception with message `Script error.`', | ||
mechanism: { | ||
type: 'generic', | ||
handled: true, | ||
}, | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Sentry.captureException(new Event('custom')); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { expect } from '@playwright/test'; | ||
import type { Event } from '@sentry/types'; | ||
|
||
import { sentryTest } from '../../../../utils/fixtures'; | ||
import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; | ||
|
||
sentryTest('should capture an Event', async ({ getLocalTestPath, page }) => { | ||
const url = await getLocalTestPath({ testDir: __dirname }); | ||
|
||
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url); | ||
|
||
expect(eventData.exception?.values).toHaveLength(1); | ||
expect(eventData.exception?.values?.[0]).toMatchObject({ | ||
type: 'Event', | ||
value: 'Event `Event` (type=custom) captured as exception', | ||
mechanism: { | ||
type: 'generic', | ||
handled: true, | ||
}, | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Sentry.captureException({ | ||
prop1: 'value1', | ||
prop2: 2, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { expect } from '@playwright/test'; | ||
import type { Event } from '@sentry/types'; | ||
|
||
import { sentryTest } from '../../../../utils/fixtures'; | ||
import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; | ||
|
||
sentryTest('should capture an class instance', async ({ getLocalTestPath, page }) => { | ||
const url = await getLocalTestPath({ testDir: __dirname }); | ||
|
||
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url); | ||
|
||
expect(eventData.exception?.values).toHaveLength(1); | ||
expect(eventData.exception?.values?.[0]).toMatchObject({ | ||
type: 'Error', | ||
value: 'Object captured as exception with keys: prop1, prop2', | ||
mechanism: { | ||
type: 'generic', | ||
handled: true, | ||
}, | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,8 @@ import { | |
resolvedSyncPromise, | ||
} from '@sentry/utils'; | ||
|
||
type Prototype = { constructor: (...args: unknown[]) => unknown }; | ||
|
||
/** | ||
* This function creates an exception from a JavaScript Error | ||
*/ | ||
|
@@ -55,9 +57,7 @@ export function eventFromPlainObject( | |
values: [ | ||
{ | ||
type: isEvent(exception) ? exception.constructor.name : isUnhandledRejection ? 'UnhandledRejection' : 'Error', | ||
value: `Non-Error ${ | ||
isUnhandledRejection ? 'promise rejection' : 'exception' | ||
} captured with keys: ${extractExceptionKeysForMessage(exception)}`, | ||
value: getNonErrorObjectExceptionValue(exception, { isUnhandledRejection }), | ||
}, | ||
], | ||
}, | ||
|
@@ -283,3 +283,33 @@ export function eventFromString( | |
|
||
return event; | ||
} | ||
|
||
function getNonErrorObjectExceptionValue( | ||
exception: Record<string, unknown>, | ||
{ isUnhandledRejection }: { isUnhandledRejection?: boolean }, | ||
): string { | ||
const keys = extractExceptionKeysForMessage(exception); | ||
const captureType = isUnhandledRejection ? 'promise rejection' : 'exception'; | ||
|
||
// Some ErrorEvent instances do not have an `error` property, which is why they are not handled before | ||
// We still want to try to get a decent message for these cases | ||
if (isErrorEvent(exception)) { | ||
return `Event \`ErrorEvent\` captured as ${captureType} with message \`${exception.message}\``; | ||
} | ||
|
||
if (isEvent(exception)) { | ||
const className = getObjectClassName(exception); | ||
return `Event \`${className}\` (type=${exception.type}) captured as ${captureType}`; | ||
} | ||
|
||
return `Object captured as ${captureType} with keys: ${keys}`; | ||
} | ||
|
||
function getObjectClassName(obj: unknown): string | undefined | void { | ||
try { | ||
const prototype: Prototype | null = Object.getPrototypeOf(obj); | ||
return prototype ? prototype.constructor.name : undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am worried about this for similar reasons outlined in: #8161 (comment) If people throw an object of a custom class and their code is minified, I don't know yet how I would proceeed here though 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, this is true (tried it in a test app). What about the following - slightly hacky but maybe OK - solution: if (className.length === 1) {
// this is most likely minified/mangled, so just use "Object"
return "Object"
} ? This will not capture everything (you may have mangled class names with more than one char), but usually mangling will reduce to one character as it is the smallest, and you rarely have more than 26 entities in the same scope 🤔 Or possibly we could even do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't do that. That seems a bit silly. The more I think about this, the more I wouldn't change anything. We're just introducing new complexity that doesn't solve any particular issue. The one thing I would consider is either updating the Error message to be a bit more clear what is happening or removing the "with keys ..." part and using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would shy away from using the fingerprint because it make it harder for users to understand how to filter the events out and understand how they are being grouped. How about let's just not differentiate between class instances and plain js object? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, for simplicity I will just go with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yup sounds good to me - I think we can keep iterating |
||
} catch (e) { | ||
// ignore errors here | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"extends": "./tsconfig.json", | ||
|
||
"include": ["test/integration/**/*"], | ||
|
||
"compilerOptions": { | ||
// should include all types from `./tsconfig.json` plus types for all test frameworks used | ||
"types": ["node", "mocha", "chai", "sinon"] | ||
|
||
// other package-specific, test-specific options | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if this should just read
'Object captured as exception'
. Probably fine to save bundle.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally wanted to do that as well, but the
[object has no keys]
part comes from the existingextractExceptionKeysForMessage
which is used in multiple places, so I figured we can just keep it?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea let's keep it. I trust people to understand what's going on here.