fix(sentry): handle SyntaxError type/value split and Chrome short-function format

Greptile review findings:

P1: Sentry splits SyntaxError into type='SyntaxError' + value='Unexpected
token <'. The value never contains the 'SyntaxError:' prefix, so the
regex never matched. Added excType === 'SyntaxError' check (same pattern
as the existing TypeError dual-check).

P2: The short-function pattern only caught Safari's '(In ...' suffix.
Chrome/V8 emits bare 'xy is not a function'. Simplified to /^\w{1,2}
is not a function/ to cover both engines.
This commit is contained in:
Elie Habib
2026-04-07 08:57:13 +04:00
parent a0c50d4e0e
commit 60df15ecaa
2 changed files with 38 additions and 3 deletions

View File

@@ -317,7 +317,7 @@ Sentry.init({
/\.(?:toLowerCase|trim|indexOf|findIndex) is not a function/.test(msg)
|| /Maximum call stack size exceeded/.test(msg)
|| /out of memory/i.test(msg)
|| /^\w{1,2} is not a function\. \(In '\w{1,2}\(/.test(msg)
|| /^\w{1,2} is not a function/.test(msg)
|| /Cannot add property \w+, object is not extensible/.test(msg)
|| /^TypeError: Internal error$/.test(msg)
|| /NotSupportedError/.test(msg)
@@ -327,6 +327,7 @@ Sentry.init({
|| /^TypeError: NetworkError/.test(msg)
|| /Could not connect to the server/.test(msg)
|| /(?:Failed to fetch|Importing a module script failed|error loading) dynamically imported module/i.test(msg)
|| (excType === 'SyntaxError' && /^Unexpected (?:token|keyword)/.test(msg))
|| /^SyntaxError: Unexpected (?:token|keyword)/.test(msg)
|| /Invalid or unexpected token/.test(msg)
|| /^Operation timed out/.test(msg)

View File

@@ -136,11 +136,15 @@ describe('empty-stack network/timeout errors are NOT suppressed', () => {
'Importing a module script failed.',
'Operation timed out',
'signal timed out',
'SyntaxError: Unexpected token <',
'SyntaxError: Unexpected keyword \'const\'',
'Invalid or unexpected token',
];
// SyntaxErrors split by Sentry: type='SyntaxError', value='Unexpected token <'
const syntaxErrors = [
['Unexpected token <', 'SyntaxError'],
['Unexpected keyword \'const\'', 'SyntaxError'],
];
for (const msg of networkErrors) {
it(`lets through "${msg.slice(0, 60)}..." with empty stack`, () => {
const event = makeEvent(msg, msg.startsWith('SyntaxError') ? 'SyntaxError' : 'TypeError', []);
@@ -167,6 +171,25 @@ describe('empty-stack network/timeout errors are NOT suppressed', () => {
assert.ok(result !== null, `"${msg}" with first-party stack should NOT be suppressed`);
});
}
// Sentry splits SyntaxError into type='SyntaxError' + value='Unexpected token <'
// The value field never contains the 'SyntaxError:' prefix.
for (const [value, type] of syntaxErrors) {
it(`suppresses SyntaxError (split: value="${value}") with third-party stack`, () => {
const event = makeEvent(value, type, [extensionFrame()]);
assert.equal(beforeSend(event), null);
});
it(`lets through SyntaxError (split: value="${value}") with empty stack`, () => {
const event = makeEvent(value, type, []);
assert.ok(beforeSend(event) !== null);
});
it(`lets through SyntaxError (split: value="${value}") with first-party stack`, () => {
const event = makeEvent(value, type, [firstPartyFrame()]);
assert.ok(beforeSend(event) !== null);
});
}
});
// ─── All ambiguous errors require confirmed third-party stack ────────────
@@ -184,6 +207,17 @@ describe('ambiguous runtime errors', () => {
'Element not found',
];
// Chrome V8 emits "xy is not a function" without Safari's "(In 'xy(...')" suffix
it('suppresses Chrome-style "t is not a function" with third-party stack', () => {
const event = makeEvent('t is not a function', 'TypeError', [extensionFrame()]);
assert.equal(beforeSend(event), null);
});
it('suppresses Safari-style "t is not a function. (In \'t(..." with third-party stack', () => {
const event = makeEvent("t is not a function. (In 't(1,2)')", 'TypeError', [extensionFrame()]);
assert.equal(beforeSend(event), null);
});
for (const msg of ambiguousErrors) {
it(`lets through "${msg}" with empty stack (origin unknown)`, () => {
const event = makeEvent(msg, 'TypeError', []);