diff --git a/packages/browser/src/tracekit.ts b/packages/browser/src/tracekit.ts index a58fecd66eec..7d2203a0007e 100644 --- a/packages/browser/src/tracekit.ts +++ b/packages/browser/src/tracekit.ts @@ -3,7 +3,7 @@ * largely modified and is now maintained as part of Sentry JS SDK. */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access, max-lines */ /** * An object representing a single stack frame. @@ -124,16 +124,10 @@ function computeStackTraceFromStackProp(ex: any): StackTrace | null { // Arpad: Working with the regexp above is super painful. it is quite a hack, but just stripping the `address at ` // prefix here seems like the quickest solution for now. let url = parts[2] && parts[2].indexOf('address at ') === 0 ? parts[2].substr('address at '.length) : parts[2]; - // Kamil: One more hack won't hurt us right? Understanding and adding more rules on top of these regexps right now // would be way too time consuming. (TODO: Rewrite whole RegExp to be more readable) let func = parts[1] || UNKNOWN_FUNCTION; - const isSafariExtension = func.indexOf('safari-extension') !== -1; - const isSafariWebExtension = func.indexOf('safari-web-extension') !== -1; - if (isSafariExtension || isSafariWebExtension) { - func = func.indexOf('@') !== -1 ? func.split('@')[0] : UNKNOWN_FUNCTION; - url = isSafariExtension ? `safari-extension:${url}` : `safari-web-extension:${url}`; - } + [func, url] = extractSafariExtensionDetails(func, url); element = { url, @@ -165,9 +159,14 @@ function computeStackTraceFromStackProp(ex: any): StackTrace | null { // NOTE: this hack doesn't work if top-most frame is eval stack[0].column = (ex.columnNumber as number) + 1; } + + let url = parts[3]; + let func = parts[1] || UNKNOWN_FUNCTION; + [func, url] = extractSafariExtensionDetails(func, url); + element = { - url: parts[3], - func: parts[1] || UNKNOWN_FUNCTION, + url, + func, args: parts[2] ? parts[2].split(',') : [], line: parts[4] ? +parts[4] : null, column: parts[5] ? +parts[5] : null, @@ -249,6 +248,38 @@ function computeStackTraceFromStacktraceProp(ex: any): StackTrace | null { }; } +/** + * Safari web extensions, starting version unknown, can produce "frames-only" stacktraces. + * What it means, is that instead of format like: + * + * Error: wat + * at function@url:row:col + * at function@url:row:col + * at function@url:row:col + * + * it produces something like: + * + * function@url:row:col + * function@url:row:col + * function@url:row:col + * + * Because of that, it won't be captured by `chrome` RegExp and will fall into `Gecko` branch. + * This function is extracted so that we can use it in both places without duplicating the logic. + * Unfortunatelly "just" changing RegExp is too complicated now and making it pass all tests + * and fix this case seems like an impossible, or at least way too time-consuming task. + */ +const extractSafariExtensionDetails = (func: string, url: string): [string, string] => { + const isSafariExtension = func.indexOf('safari-extension') !== -1; + const isSafariWebExtension = func.indexOf('safari-web-extension') !== -1; + + return isSafariExtension || isSafariWebExtension + ? [ + func.indexOf('@') !== -1 ? func.split('@')[0] : UNKNOWN_FUNCTION, + isSafariExtension ? `safari-extension:${url}` : `safari-web-extension:${url}`, + ] + : [func, url]; +}; + /** Remove N number of frames from the stack */ function popFrames(stacktrace: StackTrace, popSize: number): StackTrace { try { diff --git a/packages/browser/test/unit/tracekit/custom.test.ts b/packages/browser/test/unit/tracekit/custom.test.ts index 835990837bc2..2fbaabf9edfb 100644 --- a/packages/browser/test/unit/tracekit/custom.test.ts +++ b/packages/browser/test/unit/tracekit/custom.test.ts @@ -24,58 +24,130 @@ describe('Tracekit - Custom Tests', () => { ]); }); - it('should parse exceptions for safari-extension', () => { - const SAFARI_EXTENSION_EXCEPTION = { - message: 'wat', - name: 'Error', - stack: `Error: wat + describe('Safari extensions', () => { + it('should parse exceptions for safari-extension', () => { + const SAFARI_EXTENSION_EXCEPTION = { + message: 'wat', + name: 'Error', + stack: `Error: wat at ClipperError@safari-extension:(//3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/commons.js:223036:10) at safari-extension:(//3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js:3313:26)`, - }; - const stacktrace = computeStackTrace(SAFARI_EXTENSION_EXCEPTION); - expect(stacktrace.stack).deep.equal([ - { - url: 'safari-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/commons.js', - func: 'ClipperError', - args: [], - line: 223036, - column: 10, - }, - { - url: 'safari-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js', - func: '?', - args: [], - line: 3313, - column: 26, - }, - ]); - }); + }; + const stacktrace = computeStackTrace(SAFARI_EXTENSION_EXCEPTION); + expect(stacktrace.stack).deep.equal([ + { + url: 'safari-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/commons.js', + func: 'ClipperError', + args: [], + line: 223036, + column: 10, + }, + { + url: 'safari-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js', + func: '?', + args: [], + line: 3313, + column: 26, + }, + ]); + }); - it('should parse exceptions for safari-web-extension', () => { - const SAFARI_WEB_EXTENSION_EXCEPTION = { - message: 'wat', - name: 'Error', - stack: `Error: wat + it('should parse exceptions for safari-extension with frames-only stack', () => { + const SAFARI_EXTENSION_EXCEPTION = { + message: `undefined is not an object (evaluating 'e.groups.includes')`, + name: `TypeError`, + stack: `isClaimed@safari-extension://com.grammarly.safari.extension.ext2-W8F64X92K3/ee7759dd/Grammarly.js:2:929865 + safari-extension://com.grammarly.safari.extension.ext2-W8F64X92K3/ee7759dd/Grammarly.js:2:1588410 + promiseReactionJob@[native code]`, + }; + const stacktrace = computeStackTrace(SAFARI_EXTENSION_EXCEPTION); + + expect(stacktrace.stack).deep.equal([ + { + url: 'safari-extension://com.grammarly.safari.extension.ext2-W8F64X92K3/ee7759dd/Grammarly.js', + func: 'isClaimed', + args: [], + line: 2, + column: 929865, + }, + { + url: 'safari-extension://com.grammarly.safari.extension.ext2-W8F64X92K3/ee7759dd/Grammarly.js', + func: '?', + args: [], + line: 2, + column: 1588410, + }, + { + url: '[native code]', + func: 'promiseReactionJob', + args: [], + line: null, + column: null, + }, + ]); + }); + + it('should parse exceptions for safari-web-extension', () => { + const SAFARI_WEB_EXTENSION_EXCEPTION = { + message: 'wat', + name: 'Error', + stack: `Error: wat at ClipperError@safari-web-extension:(//3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/commons.js:223036:10) at safari-web-extension:(//3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js:3313:26)`, - }; - const stacktrace = computeStackTrace(SAFARI_WEB_EXTENSION_EXCEPTION); - expect(stacktrace.stack).deep.equal([ - { - url: 'safari-web-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/commons.js', - func: 'ClipperError', - args: [], - line: 223036, - column: 10, - }, - { - url: 'safari-web-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js', - func: '?', - args: [], - line: 3313, - column: 26, - }, - ]); + }; + const stacktrace = computeStackTrace(SAFARI_WEB_EXTENSION_EXCEPTION); + expect(stacktrace.stack).deep.equal([ + { + url: 'safari-web-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/commons.js', + func: 'ClipperError', + args: [], + line: 223036, + column: 10, + }, + { + url: 'safari-web-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js', + func: '?', + args: [], + line: 3313, + column: 26, + }, + ]); + }); + + it('should parse exceptions for safari-web-extension with frames-only stack', () => { + const SAFARI_EXTENSION_EXCEPTION = { + message: `undefined is not an object (evaluating 'e.groups.includes')`, + name: `TypeError`, + stack: `p_@safari-web-extension://46434E60-F5BD-48A4-80C8-A422C5D16897/scripts/content-script.js:29:33314 + safari-web-extension://46434E60-F5BD-48A4-80C8-A422C5D16897/scripts/content-script.js:29:56027 + promiseReactionJob@[native code]`, + }; + const stacktrace = computeStackTrace(SAFARI_EXTENSION_EXCEPTION); + + expect(stacktrace.stack).deep.equal([ + { + url: 'safari-web-extension://46434E60-F5BD-48A4-80C8-A422C5D16897/scripts/content-script.js', + func: 'p_', + args: [], + line: 29, + column: 33314, + }, + { + url: 'safari-web-extension://46434E60-F5BD-48A4-80C8-A422C5D16897/scripts/content-script.js', + func: '?', + args: [], + line: 29, + column: 56027, + }, + { + url: '[native code]', + func: 'promiseReactionJob', + args: [], + line: null, + column: null, + }, + ]); + }); }); it('should parse exceptions for react-native-v8', () => {