diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index 2228b006242b..f441d1b835a6 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -11,8 +11,7 @@ import { resolvedSyncPromise, } from '@sentry/utils'; -import { eventFromPlainObject, eventFromStacktrace, prepareFramesForEvent } from './parsers'; -import { computeStackTrace } from './tracekit'; +import { eventFromError, eventFromPlainObject, parseStackFrames } from './parsers'; /** * Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`. @@ -68,10 +67,7 @@ export function eventFromUnknownInput( if (isErrorEvent(exception as ErrorEvent) && (exception as ErrorEvent).error) { // If it is an ErrorEvent with `error` property, extract it to get actual Error const errorEvent = exception as ErrorEvent; - // eslint-disable-next-line no-param-reassign - exception = errorEvent.error; - event = eventFromStacktrace(computeStackTrace(exception as Error)); - return event; + return eventFromError(errorEvent.error as Error); } // If it is a `DOMError` (which is a legacy API, but still supported in some browsers) then we just extract the name @@ -85,7 +81,7 @@ export function eventFromUnknownInput( const domException = exception as DOMException; if ('stack' in (exception as Error)) { - event = eventFromStacktrace(computeStackTrace(exception as Error)); + event = eventFromError(exception as Error); } else { const name = domException.name || (isDOMError(domException) ? 'DOMError' : 'DOMException'); const message = domException.message ? `${name}: ${domException.message}` : name; @@ -100,8 +96,7 @@ export function eventFromUnknownInput( } if (isError(exception as Error)) { // we have a real Error object, do nothing - event = eventFromStacktrace(computeStackTrace(exception as Error)); - return event; + return eventFromError(exception as Error); } if (isPlainObject(exception) || isEvent(exception)) { // If it's a plain object or an instance of `Event` (the built-in JS kind, not this SDK's `Event` type), serialize @@ -148,10 +143,8 @@ export function eventFromString( }; if (options.attachStacktrace && syntheticException) { - const stacktrace = computeStackTrace(syntheticException); - const frames = prepareFramesForEvent(stacktrace.stack); event.stacktrace = { - frames, + frames: parseStackFrames(syntheticException), }; } diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index 8870e689f8ef..4abcf3551922 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -2,8 +2,7 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types'; import { isInstanceOf } from '@sentry/utils'; -import { exceptionFromStacktrace } from '../parsers'; -import { computeStackTrace } from '../tracekit'; +import { exceptionFromError } from '../parsers'; const DEFAULT_KEY = 'cause'; const DEFAULT_LIMIT = 5; @@ -73,7 +72,6 @@ export function _walkErrorTree(limit: number, error: ExtendedError, key: string, if (!isInstanceOf(error[key], Error) || stack.length + 1 >= limit) { return stack; } - const stacktrace = computeStackTrace(error[key]); - const exception = exceptionFromStacktrace(stacktrace); + const exception = exceptionFromError(error[key]); return _walkErrorTree(limit, error[key], key, [exception, ...stack]); } diff --git a/packages/browser/src/parsers.ts b/packages/browser/src/parsers.ts index e7dc375ac014..b66042ccb162 100644 --- a/packages/browser/src/parsers.ts +++ b/packages/browser/src/parsers.ts @@ -1,21 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { Event, Exception, StackFrame } from '@sentry/types'; -import { extractExceptionKeysForMessage, isEvent, normalizeToSize } from '@sentry/utils'; +import { createStackParser, extractExceptionKeysForMessage, isEvent, normalizeToSize } from '@sentry/utils'; -import { computeStackTrace, StackTrace as TraceKitStackTrace } from './tracekit'; - -const STACKTRACE_LIMIT = 50; +import { chrome, gecko, opera10, opera11, winjs } from './stack-parsers'; /** * This function creates an exception from an TraceKitStackTrace * @param stacktrace TraceKitStackTrace that will be converted to an exception * @hidden */ -export function exceptionFromStacktrace(stacktrace: TraceKitStackTrace): Exception { - const frames = prepareFramesForEvent(stacktrace.stack); +export function exceptionFromError(ex: Error): Exception { + // Get the frames first since Opera can lose the stack if we touch anything else first + const frames = parseStackFrames(ex); const exception: Exception = { - type: stacktrace.name, - value: stacktrace.message, + type: ex && ex.name, + value: extractMessage(ex), }; if (frames && frames.length) { @@ -54,10 +54,8 @@ export function eventFromPlainObject( }; if (syntheticException) { - const stacktrace = computeStackTrace(syntheticException); - const frames = prepareFramesForEvent(stacktrace.stack); event.stacktrace = { - frames, + frames: parseStackFrames(syntheticException), }; } @@ -67,48 +65,63 @@ export function eventFromPlainObject( /** * @hidden */ -export function eventFromStacktrace(stacktrace: TraceKitStackTrace): Event { - const exception = exceptionFromStacktrace(stacktrace); - +export function eventFromError(ex: Error): Event { return { exception: { - values: [exception], + values: [exceptionFromError(ex)], }, }; } -/** - * @hidden - */ -export function prepareFramesForEvent(stack: StackFrame[]): StackFrame[] { - if (!stack.length) { - return []; +/** Parses stack frames from an error */ +export function parseStackFrames(ex: Error & { framesToPop?: number; stacktrace?: string }): StackFrame[] { + // Access and store the stacktrace property before doing ANYTHING + // else to it because Opera is not very good at providing it + // reliably in other circumstances. + const stacktrace = ex.stacktrace || ex.stack || ''; + + const popSize = getPopSize(ex); + + try { + // The order of the parsers in important + return createStackParser(opera10, opera11, chrome, winjs, gecko)(stacktrace, popSize); + } catch (e) { + // no-empty } - let localStack = stack; + return []; +} - const firstFrameFunction = localStack[0].function || ''; - const lastFrameFunction = localStack[localStack.length - 1].function || ''; +// Based on our own mapping pattern - https://github.com/getsentry/sentry/blob/9f08305e09866c8bd6d0c24f5b0aabdd7dd6c59c/src/sentry/lang/javascript/errormapping.py#L83-L108 +const reactMinifiedRegexp = /Minified React error #\d+;/i; - // If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call) - if (firstFrameFunction.indexOf('captureMessage') !== -1 || firstFrameFunction.indexOf('captureException') !== -1) { - localStack = localStack.slice(1); - } +function getPopSize(ex: Error & { framesToPop?: number }): number { + if (ex) { + if (typeof ex.framesToPop === 'number') { + return ex.framesToPop; + } - // If stack ends with one of our internal API calls, remove it (ends, meaning it's the bottom of the stack - aka top-most call) - if (lastFrameFunction.indexOf('sentryWrapped') !== -1) { - localStack = localStack.slice(0, -1); + if (reactMinifiedRegexp.test(ex.message)) { + return 1; + } } - // The frame where the crash happened, should be the last entry in the array - return localStack - .slice(0, STACKTRACE_LIMIT) - .map(frame => ({ - filename: frame.filename || localStack[0].filename, - function: frame.function || '?', - lineno: frame.lineno, - colno: frame.colno, - in_app: true, - })) - .reverse(); + return 0; +} + +/** + * There are cases where stacktrace.message is an Event object + * https://github.com/getsentry/sentry-javascript/issues/1949 + * In this specific case we try to extract stacktrace.message.error.message + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extractMessage(ex: any): string { + const message = ex && ex.message; + if (!message) { + return 'No error message'; + } + if (message.error && typeof message.error.message === 'string') { + return message.error.message; + } + return message; } diff --git a/packages/browser/src/stack-parsers.ts b/packages/browser/src/stack-parsers.ts new file mode 100644 index 000000000000..d05393101843 --- /dev/null +++ b/packages/browser/src/stack-parsers.ts @@ -0,0 +1,148 @@ +import { StackFrame } from '@sentry/types'; +import { StackLineParser } from '@sentry/utils'; + +// global reference to slice +const UNKNOWN_FUNCTION = '?'; + +function createFrame(filename: string, func: string, lineno?: number, colno?: number): StackFrame { + const frame: StackFrame = { + filename, + function: func, + // All browser frames are considered in_app + in_app: true, + }; + + if (lineno !== undefined) { + frame.lineno = lineno; + } + + if (colno !== undefined) { + frame.colno = colno; + } + + return frame; +} + +// Chromium based browsers: Chrome, Brave, new Opera, new Edge +const chromeRegex = + /^\s*at (?:(.*?) ?\((?:address at )?)?((?:file|https?|blob|chrome-extension|address|native|eval|webpack||[-a-z]+:|.*bundle|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i; +const chromeEvalRegex = /\((\S*)(?::(\d+))(?::(\d+))\)/; + +export const chrome: StackLineParser = line => { + const parts = chromeRegex.exec(line); + + if (parts) { + const isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line + + if (isEval) { + const subMatch = chromeEvalRegex.exec(parts[2]); + + if (subMatch) { + // throw out eval line/column and use top-most line/column number + parts[2] = subMatch[1]; // url + parts[3] = subMatch[2]; // line + parts[4] = subMatch[3]; // column + } + } + + // 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) + const [func, filename] = extractSafariExtensionDetails(parts[1] || UNKNOWN_FUNCTION, parts[2]); + + return createFrame(filename, func, parts[3] ? +parts[3] : undefined, parts[4] ? +parts[4] : undefined); + } + + return; +}; + +// gecko regex: `(?:bundle|\d+\.js)`: `bundle` is for react native, `\d+\.js` also but specifically for ram bundles because it +// generates filenames without a prefix like `file://` the filenames in the stacktrace are just 42.js +// We need this specific case for now because we want no other regex to match. +const geckoREgex = + /^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:file|https?|blob|chrome|webpack|resource|moz-extension|capacitor).*?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. /=]+)(?::(\d+))?(?::(\d+))?\s*$/i; +const geckoEvalRegex = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i; + +export const gecko: StackLineParser = line => { + const parts = geckoREgex.exec(line); + + if (parts) { + const isEval = parts[3] && parts[3].indexOf(' > eval') > -1; + if (isEval) { + const subMatch = geckoEvalRegex.exec(parts[3]); + + if (subMatch) { + // throw out eval line/column and use top-most line number + parts[1] = parts[1] || `eval`; + parts[3] = subMatch[1]; + parts[4] = subMatch[2]; + parts[5] = ''; // no column when eval + } + } + + let filename = parts[3]; + let func = parts[1] || UNKNOWN_FUNCTION; + [func, filename] = extractSafariExtensionDetails(func, filename); + + return createFrame(filename, func, parts[4] ? +parts[4] : undefined, parts[5] ? +parts[5] : undefined); + } + + return; +}; + +const winjsRegex = + /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i; + +export const winjs: StackLineParser = line => { + const parts = winjsRegex.exec(line); + + return parts + ? createFrame(parts[2], parts[1] || UNKNOWN_FUNCTION, +parts[3], parts[4] ? +parts[4] : undefined) + : undefined; +}; + +const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i; + +export const opera10: StackLineParser = line => { + const parts = opera10Regex.exec(line); + return parts ? createFrame(parts[2], parts[3] || UNKNOWN_FUNCTION, +parts[1]) : undefined; +}; + +const opera11Regex = + / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^)]+))\(.*\))? in (.*):\s*$/i; + +export const opera11: StackLineParser = line => { + const parts = opera11Regex.exec(line); + return parts ? createFrame(parts[5], parts[3] || parts[4] || UNKNOWN_FUNCTION, +parts[1], +parts[2]) : undefined; +}; + +/** + * 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. + * Unfortunately "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, filename: 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:${filename}` : `safari-web-extension:${filename}`, + ] + : [func, filename]; +}; diff --git a/packages/browser/src/tracekit.ts b/packages/browser/src/tracekit.ts deleted file mode 100644 index 86dcdac850f3..000000000000 --- a/packages/browser/src/tracekit.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { StackFrame } from '@sentry/types'; - -/** - * This was originally forked from https://github.com/occ/TraceKit, but has since been - * largely modified and is now maintained as part of Sentry JS SDK. - */ - -/* eslint-disable @typescript-eslint/no-unsafe-member-access, max-lines */ - -/** - * An object representing a JavaScript stack trace. - * {Object} StackTrace - * {string} name The name of the thrown exception. - * {string} message The exception error message. - * {TraceKit.StackFrame[]} stack An array of stack frames. - */ -export interface StackTrace { - name: string; - message: string; - stack: StackFrame[]; -} - -// global reference to slice -const UNKNOWN_FUNCTION = '?'; - -// Chromium based browsers: Chrome, Brave, new Opera, new Edge -const chrome = - /^\s*at (?:(.*?) ?\((?:address at )?)?((?:file|https?|blob|chrome-extension|address|native|eval|webpack||[-a-z]+:|.*bundle|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i; -// gecko regex: `(?:bundle|\d+\.js)`: `bundle` is for react native, `\d+\.js` also but specifically for ram bundles because it -// generates filenames without a prefix like `file://` the filenames in the stacktrace are just 42.js -// We need this specific case for now because we want no other regex to match. -const gecko = - /^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:file|https?|blob|chrome|webpack|resource|moz-extension|capacitor).*?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. /=]+)(?::(\d+))?(?::(\d+))?\s*$/i; -const winjs = - /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i; -const geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i; -const chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/; -// Based on our own mapping pattern - https://github.com/getsentry/sentry/blob/9f08305e09866c8bd6d0c24f5b0aabdd7dd6c59c/src/sentry/lang/javascript/errormapping.py#L83-L108 -const reactMinifiedRegexp = /Minified React error #\d+;/i; -const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i; -const opera11Regex = - / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^)]+))\(.*\))? in (.*):\s*$/i; - -/** JSDoc */ -export function computeStackTrace(ex: Error & { framesToPop?: number; stacktrace?: string }): StackTrace { - let frames: StackFrame[] = []; - let popSize = 0; - - if (ex) { - if (typeof ex.framesToPop === 'number') { - popSize = ex.framesToPop; - } else if (reactMinifiedRegexp.test(ex.message)) { - popSize = 1; - } - } - - try { - // Access and store the stacktrace property before doing ANYTHING - // else to it because Opera is not very good at providing it - // reliably in other circumstances. - const stacktrace = ex.stacktrace || ex.stack || ''; - - frames = parseFrames(stacktrace); - } catch (e) { - // no-empty - } - - if (frames.length && popSize > 0) { - frames = frames.slice(popSize); - } - - return { - message: extractMessage(ex), - name: ex && ex.name, - stack: frames, - }; -} - -/** JSDoc */ -// eslint-disable-next-line complexity -function parseFrames(stackString: string): StackFrame[] { - const frames: StackFrame[] = []; - const lines = stackString.split('\n'); - let isEval; - let submatch; - let parts; - let element: StackFrame | undefined; - - for (const line of lines) { - if ((parts = opera10Regex.exec(line))) { - element = { - filename: parts[2], - function: parts[3] || UNKNOWN_FUNCTION, - lineno: +parts[1], - }; - } else if ((parts = opera11Regex.exec(line))) { - element = { - filename: parts[5], - function: parts[3] || parts[4] || UNKNOWN_FUNCTION, - lineno: +parts[1], - colno: +parts[2], - }; - } else if ((parts = chrome.exec(line))) { - isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line - if (isEval && (submatch = chromeEval.exec(parts[2]))) { - // throw out eval line/column and use top-most line/column number - parts[2] = submatch[1]; // url - parts[3] = submatch[2]; // line - parts[4] = submatch[3]; // column - } - - // 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) - const [func, filename] = extractSafariExtensionDetails(parts[1] || UNKNOWN_FUNCTION, parts[2]); - - element = { - filename, - function: func, - lineno: parts[3] ? +parts[3] : undefined, - colno: parts[4] ? +parts[4] : undefined, - }; - } else if ((parts = winjs.exec(line))) { - element = { - filename: parts[2], - function: parts[1] || UNKNOWN_FUNCTION, - lineno: +parts[3], - colno: parts[4] ? +parts[4] : undefined, - }; - } else if ((parts = gecko.exec(line))) { - isEval = parts[3] && parts[3].indexOf(' > eval') > -1; - if (isEval && (submatch = geckoEval.exec(parts[3]))) { - // throw out eval line/column and use top-most line number - parts[1] = parts[1] || `eval`; - parts[3] = submatch[1]; - parts[4] = submatch[2]; - parts[5] = ''; // no column when eval - } - - let filename = parts[3]; - let func = parts[1] || UNKNOWN_FUNCTION; - [func, filename] = extractSafariExtensionDetails(func, filename); - - element = { - filename, - function: func, - lineno: parts[4] ? +parts[4] : undefined, - colno: parts[5] ? +parts[5] : undefined, - }; - } else { - continue; - } - - frames.push(element); - } - - return frames; -} - -/** - * 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, filename: 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:${filename}` : `safari-web-extension:${filename}`, - ] - : [func, filename]; -}; - -/** - * There are cases where stacktrace.message is an Event object - * https://github.com/getsentry/sentry-javascript/issues/1949 - * In this specific case we try to extract stacktrace.message.error.message - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function extractMessage(ex: any): string { - const message = ex && ex.message; - if (!message) { - return 'No error message'; - } - if (message.error && typeof message.error.message === 'string') { - return message.error.message; - } - return message; -} diff --git a/packages/browser/test/unit/tracekit/chromium.test.ts b/packages/browser/test/unit/tracekit/chromium.test.ts index c35fbb6a2a96..9d9ca9ccbff7 100644 --- a/packages/browser/test/unit/tracekit/chromium.test.ts +++ b/packages/browser/test/unit/tracekit/chromium.test.ts @@ -1,14 +1,14 @@ -import { computeStackTrace } from '../../../src/tracekit'; +import { exceptionFromError } from '../../../src/parsers'; describe('Tracekit - Chrome Tests', () => { it('should parse Chrome error with no location', () => { const NO_LOCATION = { message: 'foo', name: 'bar', stack: 'error\n at Array.forEach (native)' }; - const stackFrames = computeStackTrace(NO_LOCATION); + const ex = exceptionFromError(NO_LOCATION); - expect(stackFrames).toEqual({ - message: 'foo', - name: 'bar', - stack: [{ filename: 'native', function: 'Array.forEach' }], + expect(ex).toEqual({ + value: 'foo', + type: 'bar', + stacktrace: { frames: [{ filename: 'native', function: 'Array.forEach', in_app: true }] }, }); }); @@ -25,17 +25,19 @@ describe('Tracekit - Chrome Tests', () => { ' at http://path/to/file.js:24:4', }; - const stackFrames = computeStackTrace(CHROME_15); - - expect(stackFrames).toEqual({ - message: "Object # has no method 'undef'", - name: 'foo', - stack: [ - { filename: 'http://path/to/file.js', function: 'bar', lineno: 13, colno: 17 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 16, colno: 5 }, - { filename: 'http://path/to/file.js', function: 'foo', lineno: 20, colno: 5 }, - { filename: 'http://path/to/file.js', function: '?', lineno: 24, colno: 4 }, - ], + const ex = exceptionFromError(CHROME_15); + + expect(ex).toEqual({ + value: "Object # has no method 'undef'", + type: 'foo', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: '?', lineno: 24, colno: 4, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 20, colno: 5, in_app: true }, + { filename: 'http://path/to/file.js', function: 'bar', lineno: 16, colno: 5, in_app: true }, + { filename: 'http://path/to/file.js', function: 'bar', lineno: 13, colno: 17, in_app: true }, + ], + }, }); }); @@ -50,21 +52,36 @@ describe('Tracekit - Chrome Tests', () => { ' at I.e.fn.(anonymous function) [as index] (http://localhost:8080/file.js:10:3651)', }; - const stackFrames = computeStackTrace(CHROME_36); - - expect(stackFrames).toEqual({ - message: 'Default error', - name: 'Error', - stack: [ - { filename: 'http://localhost:8080/file.js', function: 'dumpExceptionError', lineno: 41, colno: 27 }, - { filename: 'http://localhost:8080/file.js', function: 'HTMLButtonElement.onclick', lineno: 107, colno: 146 }, - { - filename: 'http://localhost:8080/file.js', - function: 'I.e.fn.(anonymous function) [as index]', - lineno: 10, - colno: 3651, - }, - ], + const ex = exceptionFromError(CHROME_36); + + expect(ex).toEqual({ + value: 'Default error', + type: 'Error', + stacktrace: { + frames: [ + { + filename: 'http://localhost:8080/file.js', + function: 'I.e.fn.(anonymous function) [as index]', + lineno: 10, + colno: 3651, + in_app: true, + }, + { + filename: 'http://localhost:8080/file.js', + function: 'HTMLButtonElement.onclick', + lineno: 107, + colno: 146, + in_app: true, + }, + { + filename: 'http://localhost:8080/file.js', + function: 'dumpExceptionError', + lineno: 41, + colno: 27, + in_app: true, + }, + ], + }, }); }); @@ -81,37 +98,43 @@ describe('Tracekit - Chrome Tests', () => { ' at TESTTESTTEST.proxiedMethod(webpack:///./~/react-proxy/modules/createPrototypeProxy.js?:44:30)', }; - const stackFrames = computeStackTrace(CHROME_XX_WEBPACK); - - expect(stackFrames).toEqual({ - message: "Cannot read property 'error' of undefined", - name: 'TypeError', - stack: [ - { - filename: 'webpack:///./src/components/test/test.jsx?', - function: 'TESTTESTTEST.eval', - lineno: 295, - colno: 108, - }, - { - filename: 'webpack:///./src/components/test/test.jsx?', - function: 'TESTTESTTEST.render', - lineno: 272, - colno: 32, - }, - { - filename: 'webpack:///./~/react-transform-catch-errors/lib/index.js?', - function: 'TESTTESTTEST.tryRender', - lineno: 34, - colno: 31, - }, - { - filename: 'webpack:///./~/react-proxy/modules/createPrototypeProxy.js?', - function: 'TESTTESTTEST.proxiedMethod', - lineno: 44, - colno: 30, - }, - ], + const ex = exceptionFromError(CHROME_XX_WEBPACK); + + expect(ex).toEqual({ + value: "Cannot read property 'error' of undefined", + type: 'TypeError', + stacktrace: { + frames: [ + { + filename: 'webpack:///./~/react-proxy/modules/createPrototypeProxy.js?', + function: 'TESTTESTTEST.proxiedMethod', + lineno: 44, + colno: 30, + in_app: true, + }, + { + filename: 'webpack:///./~/react-transform-catch-errors/lib/index.js?', + function: 'TESTTESTTEST.tryRender', + lineno: 34, + colno: 31, + in_app: true, + }, + { + filename: 'webpack:///./src/components/test/test.jsx?', + function: 'TESTTESTTEST.render', + lineno: 272, + colno: 32, + in_app: true, + }, + { + filename: 'webpack:///./src/components/test/test.jsx?', + function: 'TESTTESTTEST.eval', + lineno: 295, + colno: 108, + in_app: true, + }, + ], + }, }); }); @@ -128,18 +151,20 @@ describe('Tracekit - Chrome Tests', () => { 'at http://localhost:8080/file.js:31:13\n', }; - const stackFrames = computeStackTrace(CHROME_48_EVAL); - - expect(stackFrames).toEqual({ - message: 'message string', - name: 'Error', - stack: [ - { filename: 'http://localhost:8080/file.js', function: 'baz', lineno: 21, colno: 17 }, - { filename: 'http://localhost:8080/file.js', function: 'foo', lineno: 21, colno: 17 }, - { filename: 'http://localhost:8080/file.js', function: 'eval', lineno: 21, colno: 17 }, - { filename: 'http://localhost:8080/file.js', function: 'Object.speak', lineno: 21, colno: 17 }, - { filename: 'http://localhost:8080/file.js', function: '?', lineno: 31, colno: 13 }, - ], + const ex = exceptionFromError(CHROME_48_EVAL); + + expect(ex).toEqual({ + value: 'message string', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://localhost:8080/file.js', function: '?', lineno: 31, colno: 13, in_app: true }, + { filename: 'http://localhost:8080/file.js', function: 'Object.speak', lineno: 21, colno: 17, in_app: true }, + { filename: 'http://localhost:8080/file.js', function: 'eval', lineno: 21, colno: 17, in_app: true }, + { filename: 'http://localhost:8080/file.js', function: 'foo', lineno: 21, colno: 17, in_app: true }, + { filename: 'http://localhost:8080/file.js', function: 'baz', lineno: 21, colno: 17, in_app: true }, + ], + }, }); }); @@ -158,50 +183,58 @@ describe('Tracekit - Chrome Tests', () => { ' at n.handle (blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379:7:2863)', }; - const stackFrames = computeStackTrace(CHROME_48_BLOB); - - expect(stackFrames).toEqual({ - message: 'Error: test', - name: 'Error', - stack: [ - { filename: 'native', function: 'Error' }, - { - filename: 'blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379', - function: 's', - lineno: 31, - colno: 29146, - }, - { - filename: 'blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379', - function: 'Object.d [as add]', - lineno: 31, - colno: 30039, - }, - { - filename: 'blob:http%3A//localhost%3A8080/d4eefe0f-361a-4682-b217-76587d9f712a', - function: '?', - lineno: 15, - colno: 10978, - }, - { - filename: 'blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379', - function: '?', - lineno: 1, - colno: 6911, - }, - { - filename: 'blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379', - function: 'n.fire', - lineno: 7, - colno: 3019, - }, - { - filename: 'blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379', - function: 'n.handle', - lineno: 7, - colno: 2863, - }, - ], + const ex = exceptionFromError(CHROME_48_BLOB); + + expect(ex).toEqual({ + value: 'Error: test', + type: 'Error', + stacktrace: { + frames: [ + { + filename: 'blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379', + function: 'n.handle', + lineno: 7, + colno: 2863, + in_app: true, + }, + { + filename: 'blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379', + function: 'n.fire', + lineno: 7, + colno: 3019, + in_app: true, + }, + { + filename: 'blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379', + function: '?', + lineno: 1, + colno: 6911, + in_app: true, + }, + { + filename: 'blob:http%3A//localhost%3A8080/d4eefe0f-361a-4682-b217-76587d9f712a', + function: '?', + lineno: 15, + colno: 10978, + in_app: true, + }, + { + filename: 'blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379', + function: 'Object.d [as add]', + lineno: 31, + colno: 30039, + in_app: true, + }, + { + filename: 'blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379', + function: 's', + lineno: 31, + colno: 29146, + in_app: true, + }, + { filename: 'native', function: 'Error', in_app: true }, + ], + }, }); }); @@ -213,19 +246,22 @@ describe('Tracekit - Chrome Tests', () => { at examplescheme://examplehost/cd351f7250857e22ceaa.worker.js:70179:15`, }; - const stacktrace = computeStackTrace(CHROMIUM_EMBEDDED_FRAMEWORK_CUSTOM_SCHEME); - - expect(stacktrace).toEqual({ - message: 'message string', - name: 'Error', - stack: [ - { - filename: 'examplescheme://examplehost/cd351f7250857e22ceaa.worker.js', - function: '?', - lineno: 70179, - colno: 15, - }, - ], + const ex = exceptionFromError(CHROMIUM_EMBEDDED_FRAMEWORK_CUSTOM_SCHEME); + + expect(ex).toEqual({ + value: 'message string', + type: 'Error', + stacktrace: { + frames: [ + { + filename: 'examplescheme://examplehost/cd351f7250857e22ceaa.worker.js', + function: '?', + lineno: 70179, + colno: 15, + in_app: true, + }, + ], + }, }); }); @@ -240,17 +276,19 @@ describe('Tracekit - Chrome Tests', () => { at http://localhost:5000/test:24:7`, }; - const stacktrace = computeStackTrace(CHROME73_NATIVE_CODE_EXCEPTION); - - expect(stacktrace).toEqual({ - message: 'test', - name: 'Error', - stack: [ - { filename: 'http://localhost:5000/test', function: 'fooIterator', lineno: 20, colno: 17 }, - { filename: '', function: 'Array.map' }, - { filename: 'http://localhost:5000/test', function: 'foo', lineno: 19, colno: 19 }, - { filename: 'http://localhost:5000/test', function: '?', lineno: 24, colno: 7 }, - ], + const ex = exceptionFromError(CHROME73_NATIVE_CODE_EXCEPTION); + + expect(ex).toEqual({ + value: 'test', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://localhost:5000/test', function: '?', lineno: 24, colno: 7, in_app: true }, + { filename: 'http://localhost:5000/test', function: 'foo', lineno: 19, colno: 19, in_app: true }, + { filename: '', function: 'Array.map', in_app: true }, + { filename: 'http://localhost:5000/test', function: 'fooIterator', lineno: 20, colno: 17, in_app: true }, + ], + }, }); }); @@ -271,23 +309,25 @@ describe('Tracekit - Chrome Tests', () => { at http://localhost:5000/:50:19`, }; - const stacktrace = computeStackTrace(CHROME73_EVAL_EXCEPTION); - - expect(stacktrace).toEqual({ - message: 'bad', - name: 'Error', - stack: [ - { filename: 'http://localhost:5000/', function: 'Object.aha', lineno: 19, colno: 13 }, - { filename: 'http://localhost:5000/', function: 'callAnotherThing', lineno: 20, colno: 16 }, - { filename: 'http://localhost:5000/', function: 'Object.callback', lineno: 25, colno: 7 }, - { filename: 'http://localhost:5000/', function: '?', lineno: 34, colno: 17 }, - { filename: '', function: 'Array.map' }, - { filename: 'http://localhost:5000/', function: 'test', lineno: 33, colno: 23 }, - { filename: 'http://localhost:5000/', function: 'eval', lineno: 37, colno: 5 }, - { filename: 'http://localhost:5000/', function: 'aha', lineno: 39, colno: 5 }, - { filename: 'http://localhost:5000/', function: 'Foo.testMethod', lineno: 44, colno: 7 }, - { filename: 'http://localhost:5000/', function: '?', lineno: 50, colno: 19 }, - ], + const ex = exceptionFromError(CHROME73_EVAL_EXCEPTION); + + expect(ex).toEqual({ + value: 'bad', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://localhost:5000/', function: '?', lineno: 50, colno: 19, in_app: true }, + { filename: 'http://localhost:5000/', function: 'Foo.testMethod', lineno: 44, colno: 7, in_app: true }, + { filename: 'http://localhost:5000/', function: 'aha', lineno: 39, colno: 5, in_app: true }, + { filename: 'http://localhost:5000/', function: 'eval', lineno: 37, colno: 5, in_app: true }, + { filename: 'http://localhost:5000/', function: 'test', lineno: 33, colno: 23, in_app: true }, + { filename: '', function: 'Array.map', in_app: true }, + { filename: 'http://localhost:5000/', function: '?', lineno: 34, colno: 17, in_app: true }, + { filename: 'http://localhost:5000/', function: 'Object.callback', lineno: 25, colno: 7, in_app: true }, + { filename: 'http://localhost:5000/', function: 'callAnotherThing', lineno: 20, colno: 16, in_app: true }, + { filename: 'http://localhost:5000/', function: 'Object.aha', lineno: 19, colno: 13, in_app: true }, + ], + }, }); }); @@ -302,17 +342,19 @@ describe('Tracekit - Chrome Tests', () => { at Global code (http://localhost:5000/test:24:7)`, }; - const stacktrace = computeStackTrace(EDGE44_NATIVE_CODE_EXCEPTION); - - expect(stacktrace).toEqual({ - message: 'test', - name: 'Error', - stack: [ - { filename: 'http://localhost:5000/test', function: 'fooIterator', lineno: 20, colno: 11 }, - { filename: 'native code', function: 'Array.prototype.map' }, - { filename: 'http://localhost:5000/test', function: 'foo', lineno: 19, colno: 9 }, - { filename: 'http://localhost:5000/test', function: 'Global code', lineno: 24, colno: 7 }, - ], + const ex = exceptionFromError(EDGE44_NATIVE_CODE_EXCEPTION); + + expect(ex).toEqual({ + value: 'test', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://localhost:5000/test', function: 'Global code', lineno: 24, colno: 7, in_app: true }, + { filename: 'http://localhost:5000/test', function: 'foo', lineno: 19, colno: 9, in_app: true }, + { filename: 'native code', function: 'Array.prototype.map', in_app: true }, + { filename: 'http://localhost:5000/test', function: 'fooIterator', lineno: 20, colno: 11, in_app: true }, + ], + }, }); }); @@ -333,23 +375,31 @@ describe('Tracekit - Chrome Tests', () => { at Anonymous function (http://localhost:5000/:50:8)`, }; - const stacktrace = computeStackTrace(EDGE44_EVAL_EXCEPTION); - - expect(stacktrace).toEqual({ - message: 'aha', - name: 'Error', - stack: [ - { filename: 'http://localhost:5000/', function: 'aha', lineno: 19, colno: 7 }, - { filename: 'http://localhost:5000/', function: 'callAnotherThing', lineno: 18, colno: 6 }, - { filename: 'http://localhost:5000/', function: 'callback', lineno: 25, colno: 7 }, - { filename: 'http://localhost:5000/', function: 'Anonymous function', lineno: 34, colno: 7 }, - { filename: 'native code', function: 'Array.prototype.map' }, - { filename: 'http://localhost:5000/', function: 'test', lineno: 33, colno: 5 }, - { filename: 'eval code', function: 'eval code', lineno: 1, colno: 1 }, - { filename: 'http://localhost:5000/', function: 'aha', lineno: 39, colno: 5 }, - { filename: 'http://localhost:5000/', function: 'Foo.prototype.testMethod', lineno: 44, colno: 7 }, - { filename: 'http://localhost:5000/', function: 'Anonymous function', lineno: 50, colno: 8 }, - ], + const ex = exceptionFromError(EDGE44_EVAL_EXCEPTION); + + expect(ex).toEqual({ + value: 'aha', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://localhost:5000/', function: 'Anonymous function', lineno: 50, colno: 8, in_app: true }, + { + filename: 'http://localhost:5000/', + function: 'Foo.prototype.testMethod', + lineno: 44, + colno: 7, + in_app: true, + }, + { filename: 'http://localhost:5000/', function: 'aha', lineno: 39, colno: 5, in_app: true }, + { filename: 'eval code', function: 'eval code', lineno: 1, colno: 1, in_app: true }, + { filename: 'http://localhost:5000/', function: 'test', lineno: 33, colno: 5, in_app: true }, + { filename: 'native code', function: 'Array.prototype.map', in_app: true }, + { filename: 'http://localhost:5000/', function: 'Anonymous function', lineno: 34, colno: 7, in_app: true }, + { filename: 'http://localhost:5000/', function: 'callback', lineno: 25, colno: 7, in_app: true }, + { filename: 'http://localhost:5000/', function: 'callAnotherThing', lineno: 18, colno: 6, in_app: true }, + { filename: 'http://localhost:5000/', function: 'aha', lineno: 19, colno: 7, in_app: true }, + ], + }, }); }); @@ -361,19 +411,22 @@ describe('Tracekit - Chrome Tests', () => { at TESTTESTTEST.someMethod (C:\\Users\\user\\path\\to\\file.js:295:108)`, }; - const stacktrace = computeStackTrace(CHROME_ELECTRON_RENDERER); - - expect(stacktrace).toEqual({ - message: "Cannot read property 'error' of undefined", - name: 'TypeError', - stack: [ - { - filename: 'C:\\Users\\user\\path\\to\\file.js', - function: 'TESTTESTTEST.someMethod', - lineno: 295, - colno: 108, - }, - ], + const ex = exceptionFromError(CHROME_ELECTRON_RENDERER); + + expect(ex).toEqual({ + value: "Cannot read property 'error' of undefined", + type: 'TypeError', + stacktrace: { + frames: [ + { + filename: 'C:\\Users\\user\\path\\to\\file.js', + function: 'TESTTESTTEST.someMethod', + lineno: 295, + colno: 108, + in_app: true, + }, + ], + }, }); }); }); diff --git a/packages/browser/test/unit/tracekit/firefox.test.ts b/packages/browser/test/unit/tracekit/firefox.test.ts index 21a80d9a0d4e..075b142272e2 100644 --- a/packages/browser/test/unit/tracekit/firefox.test.ts +++ b/packages/browser/test/unit/tracekit/firefox.test.ts @@ -1,4 +1,4 @@ -import { computeStackTrace } from '../../../src/tracekit'; +import { exceptionFromError } from '../../../src/parsers'; describe('Tracekit - Firefox Tests', () => { it('should parse Firefox 3 error', () => { @@ -18,24 +18,22 @@ describe('Tracekit - Firefox Tests', () => { '', }; - const stackFrames = computeStackTrace(FIREFOX_3); + const ex = exceptionFromError(FIREFOX_3); - expect(stackFrames).toEqual({ - message: 'this.undef is not a function', - name: 'TypeError', - stack: [ - { filename: 'http://127.0.0.1:8000/js/stacktrace.js', function: '?', lineno: 44 }, - { filename: 'http://127.0.0.1:8000/js/stacktrace.js', function: '?', lineno: 31 }, - { - filename: 'http://127.0.0.1:8000/js/stacktrace.js', - function: 'printStackTrace', - lineno: 18, - }, - { filename: 'http://127.0.0.1:8000/js/file.js', function: 'bar', lineno: 13 }, - { filename: 'http://127.0.0.1:8000/js/file.js', function: 'bar', lineno: 16 }, - { filename: 'http://127.0.0.1:8000/js/file.js', function: 'foo', lineno: 20 }, - { filename: 'http://127.0.0.1:8000/js/file.js', function: '?', lineno: 24 }, - ], + expect(ex).toEqual({ + value: 'this.undef is not a function', + type: 'TypeError', + stacktrace: { + frames: [ + { filename: 'http://127.0.0.1:8000/js/file.js', function: '?', lineno: 24, in_app: true }, + { filename: 'http://127.0.0.1:8000/js/file.js', function: 'foo', lineno: 20, in_app: true }, + { filename: 'http://127.0.0.1:8000/js/file.js', function: 'bar', lineno: 16, in_app: true }, + { filename: 'http://127.0.0.1:8000/js/file.js', function: 'bar', lineno: 13, in_app: true }, + { filename: 'http://127.0.0.1:8000/js/stacktrace.js', function: 'printStackTrace', lineno: 18, in_app: true }, + { filename: 'http://127.0.0.1:8000/js/stacktrace.js', function: '?', lineno: 31, in_app: true }, + { filename: 'http://127.0.0.1:8000/js/stacktrace.js', function: '?', lineno: 44, in_app: true }, + ], + }, }); }); @@ -56,20 +54,22 @@ describe('Tracekit - Firefox Tests', () => { '', }; - const stackFrames = computeStackTrace(FIREFOX_7); + const ex = exceptionFromError(FIREFOX_7); - expect(stackFrames).toEqual({ - message: 'bar', - name: 'foo', - stack: [ - { filename: 'file:///G:/js/stacktrace.js', function: '?', lineno: 44 }, - { filename: 'file:///G:/js/stacktrace.js', function: '?', lineno: 31 }, - { filename: 'file:///G:/js/stacktrace.js', function: 'printStackTrace', lineno: 18 }, - { filename: 'file:///G:/js/file.js', function: 'bar', lineno: 13 }, - { filename: 'file:///G:/js/file.js', function: 'bar', lineno: 16 }, - { filename: 'file:///G:/js/file.js', function: 'foo', lineno: 20 }, - { filename: 'file:///G:/js/file.js', function: '?', lineno: 24 }, - ], + expect(ex).toEqual({ + value: 'bar', + type: 'foo', + stacktrace: { + frames: [ + { filename: 'file:///G:/js/file.js', function: '?', lineno: 24, in_app: true }, + { filename: 'file:///G:/js/file.js', function: 'foo', lineno: 20, in_app: true }, + { filename: 'file:///G:/js/file.js', function: 'bar', lineno: 16, in_app: true }, + { filename: 'file:///G:/js/file.js', function: 'bar', lineno: 13, in_app: true }, + { filename: 'file:///G:/js/stacktrace.js', function: 'printStackTrace', lineno: 18, in_app: true }, + { filename: 'file:///G:/js/stacktrace.js', function: '?', lineno: 31, in_app: true }, + { filename: 'file:///G:/js/stacktrace.js', function: '?', lineno: 44, in_app: true }, + ], + }, }); }); @@ -86,16 +86,18 @@ describe('Tracekit - Firefox Tests', () => { lineNumber: 48, }; - const stackFrames = computeStackTrace(FIREFOX_14); + const ex = exceptionFromError(FIREFOX_14); - expect(stackFrames).toEqual({ - message: 'x is null', - name: 'foo', - stack: [ - { filename: 'http://path/to/file.js', function: '?', lineno: 48 }, - { filename: 'http://path/to/file.js', function: 'dumpException3', lineno: 52 }, - { filename: 'http://path/to/file.js', function: 'onclick', lineno: 1 }, - ], + expect(ex).toEqual({ + value: 'x is null', + type: 'foo', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: 'onclick', lineno: 1, in_app: true }, + { filename: 'http://path/to/file.js', function: 'dumpException3', lineno: 52, in_app: true }, + { filename: 'http://path/to/file.js', function: '?', lineno: 48, in_app: true }, + ], + }, }); }); @@ -113,16 +115,18 @@ describe('Tracekit - Firefox Tests', () => { columnNumber: 12, }; - const stackFrames = computeStackTrace(FIREFOX_31); + const ex = exceptionFromError(FIREFOX_31); - expect(stackFrames).toEqual({ - message: 'Default error', - name: 'Error', - stack: [ - { filename: 'http://path/to/file.js', function: 'foo', lineno: 41, colno: 13 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 1, colno: 1 }, - { filename: 'http://path/to/file.js', function: '.plugin/e.fn[c]/<', lineno: 1, colno: 1 }, - ], + expect(ex).toEqual({ + value: 'Default error', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: '.plugin/e.fn[c]/<', lineno: 1, colno: 1, in_app: true }, + { filename: 'http://path/to/file.js', function: 'bar', lineno: 1, colno: 1, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 41, colno: 13, in_app: true }, + ], + }, }); }); @@ -146,17 +150,25 @@ describe('Tracekit - Firefox Tests', () => { result: 2147500037, }; - const stackFrames = computeStackTrace(FIREFOX_44_NS_EXCEPTION); + const ex = exceptionFromError(FIREFOX_44_NS_EXCEPTION); - expect(stackFrames).toEqual({ - message: 'No error message', - name: 'NS_ERROR_FAILURE', - stack: [ - { filename: 'http://path/to/file.js', function: '[2] { name: 'TypeError', }; - const stackFrames = computeStackTrace(FIREFOX_50_RESOURCE_URL); + const ex = exceptionFromError(FIREFOX_50_RESOURCE_URL); - expect(stackFrames).toEqual({ - message: 'this.props.raw[this.state.dataSource].rows is undefined', - name: 'TypeError', - stack: [ - { filename: 'resource://path/data/content/bundle.js', function: 'render', lineno: 5529, colno: 16 }, - { - filename: 'resource://path/data/content/vendor.bundle.js', - function: 'dispatchEvent', - lineno: 18, - colno: 23028, - }, - { filename: 'resource://path/data/content/bundle.js', function: 'wrapped', lineno: 7270, colno: 25 }, - ], + expect(ex).toEqual({ + value: 'this.props.raw[this.state.dataSource].rows is undefined', + type: 'TypeError', + stacktrace: { + frames: [ + { + filename: 'resource://path/data/content/bundle.js', + function: 'wrapped', + lineno: 7270, + colno: 25, + in_app: true, + }, + { + filename: 'resource://path/data/content/vendor.bundle.js', + function: 'dispatchEvent', + lineno: 18, + colno: 23028, + in_app: true, + }, + { + filename: 'resource://path/data/content/bundle.js', + function: 'render', + lineno: 5529, + colno: 16, + in_app: true, + }, + ], + }, }); }); @@ -206,18 +233,20 @@ describe('Tracekit - Firefox Tests', () => { '@http://localhost:8080/file.js:33:9', }; - const stackFrames = computeStackTrace(FIREFOX_43_EVAL); + const ex = exceptionFromError(FIREFOX_43_EVAL); - expect(stackFrames).toEqual({ - message: 'message string', - name: 'foo', - stack: [ - { filename: 'http://localhost:8080/file.js', function: 'baz', lineno: 26 }, - { filename: 'http://localhost:8080/file.js', function: 'foo', lineno: 26 }, - { filename: 'http://localhost:8080/file.js', function: 'eval', lineno: 26 }, - { filename: 'http://localhost:8080/file.js', function: 'speak', lineno: 26, colno: 17 }, - { filename: 'http://localhost:8080/file.js', function: '?', lineno: 33, colno: 9 }, - ], + expect(ex).toEqual({ + value: 'message string', + type: 'foo', + stacktrace: { + frames: [ + { filename: 'http://localhost:8080/file.js', function: '?', lineno: 33, colno: 9, in_app: true }, + { filename: 'http://localhost:8080/file.js', function: 'speak', lineno: 26, colno: 17, in_app: true }, + { filename: 'http://localhost:8080/file.js', function: 'eval', lineno: 26, in_app: true }, + { filename: 'http://localhost:8080/file.js', function: 'foo', lineno: 26, in_app: true }, + { filename: 'http://localhost:8080/file.js', function: 'baz', lineno: 26, in_app: true }, + ], + }, }); }); @@ -230,16 +259,18 @@ describe('Tracekit - Firefox Tests', () => { @http://localhost:5000/test:24:7`, }; - const stacktrace = computeStackTrace(FIREFOX66_NATIVE_CODE_EXCEPTION); + const stacktrace = exceptionFromError(FIREFOX66_NATIVE_CODE_EXCEPTION); expect(stacktrace).toEqual({ - message: 'test', - name: 'Error', - stack: [ - { filename: 'http://localhost:5000/test', function: 'fooIterator', lineno: 20, colno: 17 }, - { filename: 'http://localhost:5000/test', function: 'foo', lineno: 19, colno: 19 }, - { filename: 'http://localhost:5000/test', function: '?', lineno: 24, colno: 7 }, - ], + value: 'test', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://localhost:5000/test', function: '?', lineno: 24, colno: 7, in_app: true }, + { filename: 'http://localhost:5000/test', function: 'foo', lineno: 19, colno: 19, in_app: true }, + { filename: 'http://localhost:5000/test', function: 'fooIterator', lineno: 20, colno: 17, in_app: true }, + ], + }, }); }); @@ -258,22 +289,24 @@ describe('Tracekit - Firefox Tests', () => { @http://localhost:5000/:50:19`, }; - const stacktrace = computeStackTrace(FIREFOX66_EVAL_EXCEPTION); + const stacktrace = exceptionFromError(FIREFOX66_EVAL_EXCEPTION); expect(stacktrace).toEqual({ - message: 'aha', - name: 'Error', - stack: [ - { filename: 'http://localhost:5000/', function: 'aha', lineno: 19, colno: 13 }, - { filename: 'http://localhost:5000/', function: 'callAnotherThing', lineno: 20, colno: 15 }, - { filename: 'http://localhost:5000/', function: 'callback', lineno: 25, colno: 7 }, - { filename: 'http://localhost:5000/', function: 'test/<', lineno: 34, colno: 7 }, - { filename: 'http://localhost:5000/', function: 'test', lineno: 33, colno: 23 }, - { filename: 'http://localhost:5000/', function: 'eval', lineno: 39 }, - { filename: 'http://localhost:5000/', function: 'aha', lineno: 39, colno: 5 }, - { filename: 'http://localhost:5000/', function: 'testMethod', lineno: 44, colno: 7 }, - { filename: 'http://localhost:5000/', function: '?', lineno: 50, colno: 19 }, - ], + value: 'aha', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://localhost:5000/', function: '?', lineno: 50, colno: 19, in_app: true }, + { filename: 'http://localhost:5000/', function: 'testMethod', lineno: 44, colno: 7, in_app: true }, + { filename: 'http://localhost:5000/', function: 'aha', lineno: 39, colno: 5, in_app: true }, + { filename: 'http://localhost:5000/', function: 'eval', lineno: 39, in_app: true }, + { filename: 'http://localhost:5000/', function: 'test', lineno: 33, colno: 23, in_app: true }, + { filename: 'http://localhost:5000/', function: 'test/<', lineno: 34, colno: 7, in_app: true }, + { filename: 'http://localhost:5000/', function: 'callback', lineno: 25, colno: 7, in_app: true }, + { filename: 'http://localhost:5000/', function: 'callAnotherThing', lineno: 20, colno: 15, in_app: true }, + { filename: 'http://localhost:5000/', function: 'aha', lineno: 19, colno: 13, in_app: true }, + ], + }, }); }); }); diff --git a/packages/browser/test/unit/tracekit/ie.test.ts b/packages/browser/test/unit/tracekit/ie.test.ts index f746335d9cb3..4017165bc6a9 100644 --- a/packages/browser/test/unit/tracekit/ie.test.ts +++ b/packages/browser/test/unit/tracekit/ie.test.ts @@ -1,4 +1,4 @@ -import { computeStackTrace } from '../../../src/tracekit'; +import { exceptionFromError } from '../../../src/parsers'; describe('Tracekit - IE Tests', () => { it('should parse IE 10 error', () => { @@ -14,17 +14,19 @@ describe('Tracekit - IE Tests', () => { number: -2146823281, }; - const stackFrames = computeStackTrace(IE_10); + const ex = exceptionFromError(IE_10); // TODO: func should be normalized - expect(stackFrames).toEqual({ - message: "Unable to get property 'undef' of undefined or null reference", - name: 'foo', - stack: [ - { filename: 'http://path/to/file.js', function: 'Anonymous function', lineno: 48, colno: 13 }, - { filename: 'http://path/to/file.js', function: 'foo', lineno: 46, colno: 9 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 82, colno: 1 }, - ], + expect(ex).toEqual({ + value: "Unable to get property 'undef' of undefined or null reference", + type: 'foo', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: 'bar', lineno: 82, colno: 1, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 46, colno: 9, in_app: true }, + { filename: 'http://path/to/file.js', function: 'Anonymous function', lineno: 48, colno: 13, in_app: true }, + ], + }, }); }); @@ -41,17 +43,19 @@ describe('Tracekit - IE Tests', () => { number: -2146823281, }; - const stackFrames = computeStackTrace(IE_11); + const ex = exceptionFromError(IE_11); // TODO: func should be normalized - expect(stackFrames).toEqual({ - message: "Unable to get property 'undef' of undefined or null reference", - name: 'TypeError', - stack: [ - { filename: 'http://path/to/file.js', function: 'Anonymous function', lineno: 47, colno: 21 }, - { filename: 'http://path/to/file.js', function: 'foo', lineno: 45, colno: 13 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 108, colno: 1 }, - ], + expect(ex).toEqual({ + value: "Unable to get property 'undef' of undefined or null reference", + type: 'TypeError', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: 'bar', lineno: 108, colno: 1, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 45, colno: 13, in_app: true }, + { filename: 'http://path/to/file.js', function: 'Anonymous function', lineno: 47, colno: 21, in_app: true }, + ], + }, }); }); @@ -68,16 +72,18 @@ describe('Tracekit - IE Tests', () => { number: -2146823279, }; - const stackFrames = computeStackTrace(IE_11_EVAL); + const ex = exceptionFromError(IE_11_EVAL); - expect(stackFrames).toEqual({ - message: "'getExceptionProps' is undefined", - name: 'ReferenceError', - stack: [ - { filename: 'eval code', function: 'eval code', lineno: 1, colno: 1 }, - { filename: 'http://path/to/file.js', function: 'foo', lineno: 58, colno: 17 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 109, colno: 1 }, - ], + expect(ex).toEqual({ + value: "'getExceptionProps' is undefined", + type: 'ReferenceError', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: 'bar', lineno: 109, colno: 1, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 58, colno: 17, in_app: true }, + { filename: 'eval code', function: 'eval code', lineno: 1, colno: 1, in_app: true }, + ], + }, }); }); }); diff --git a/packages/browser/test/unit/tracekit/misc.test.ts b/packages/browser/test/unit/tracekit/misc.test.ts index 91038d014e64..1e7a057dc8c8 100644 --- a/packages/browser/test/unit/tracekit/misc.test.ts +++ b/packages/browser/test/unit/tracekit/misc.test.ts @@ -1,4 +1,4 @@ -import { computeStackTrace } from '../../../src/tracekit'; +import { exceptionFromError } from '../../../src/parsers'; describe('Tracekit - Misc Tests', () => { it('should parse PhantomJS 1.19 error', () => { @@ -11,16 +11,18 @@ describe('Tracekit - Misc Tests', () => { ' at foo (http://path/to/file.js:4283)\n' + ' at http://path/to/file.js:4287', }; - const stackFrames = computeStackTrace(PHANTOMJS_1_19); + const ex = exceptionFromError(PHANTOMJS_1_19); - expect(stackFrames).toEqual({ - message: 'bar', - name: 'foo', - stack: [ - { filename: 'file:///path/to/file.js', function: '?', lineno: 878 }, - { filename: 'http://path/to/file.js', function: 'foo', lineno: 4283 }, - { filename: 'http://path/to/file.js', function: '?', lineno: 4287 }, - ], + expect(ex).toEqual({ + value: 'bar', + type: 'foo', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: '?', lineno: 4287, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 4283, in_app: true }, + { filename: 'file:///path/to/file.js', function: '?', lineno: 878, in_app: true }, + ], + }, }); }); }); diff --git a/packages/browser/test/unit/tracekit/opera.test.ts b/packages/browser/test/unit/tracekit/opera.test.ts index 7d86bb68909c..81316df250ef 100644 --- a/packages/browser/test/unit/tracekit/opera.test.ts +++ b/packages/browser/test/unit/tracekit/opera.test.ts @@ -1,4 +1,4 @@ -import { computeStackTrace } from '../../../src/tracekit'; +import { exceptionFromError } from '../../../src/parsers'; describe('Tracekit - Opera Tests', () => { it('should parse Opera 10 error', () => { @@ -24,20 +24,22 @@ describe('Tracekit - Opera Tests', () => { '', }; - const stackFrames = computeStackTrace(OPERA_10); + const ex = exceptionFromError(OPERA_10); - expect(stackFrames).toEqual({ - message: 'Statement on line 42: Type mismatch (usually non-object value supplied where object required)', - name: 'foo', - stack: [ - { filename: 'http://path/to/file.js', function: '?', lineno: 42 }, - { filename: 'http://path/to/file.js', function: '?', lineno: 27 }, - { filename: 'http://path/to/file.js', function: 'printStackTrace', lineno: 18 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 4 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 7 }, - { filename: 'http://path/to/file.js', function: 'foo', lineno: 11 }, - { filename: 'http://path/to/file.js', function: '?', lineno: 15 }, - ], + expect(ex).toEqual({ + value: 'Statement on line 42: Type mismatch (usually non-object value supplied where object required)', + type: 'foo', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: '?', lineno: 15, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 11, in_app: true }, + { filename: 'http://path/to/file.js', function: 'bar', lineno: 7, in_app: true }, + { filename: 'http://path/to/file.js', function: 'bar', lineno: 4, in_app: true }, + { filename: 'http://path/to/file.js', function: 'printStackTrace', lineno: 18, in_app: true }, + { filename: 'http://path/to/file.js', function: '?', lineno: 27, in_app: true }, + { filename: 'http://path/to/file.js', function: '?', lineno: 42, in_app: true }, + ], + }, }); }); @@ -68,20 +70,22 @@ describe('Tracekit - Opera Tests', () => { ' foo();', }; - const stackFrames = computeStackTrace(OPERA_11); + const ex = exceptionFromError(OPERA_11); - expect(stackFrames).toEqual({ - message: "'this.undef' is not a function", - name: 'foo', - stack: [ - { filename: 'http://path/to/file.js', function: 'createException', lineno: 42, colno: 12 }, - { filename: 'http://path/to/file.js', function: 'run', lineno: 27, colno: 8 }, - { filename: 'http://path/to/file.js', function: 'printStackTrace', lineno: 18, colno: 4 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 4, colno: 5 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 7, colno: 4 }, - { filename: 'http://path/to/file.js', function: 'foo', lineno: 11, colno: 4 }, - { filename: 'http://path/to/file.js', function: '?', lineno: 15, colno: 3 }, - ], + expect(ex).toEqual({ + value: "'this.undef' is not a function", + type: 'foo', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: '?', lineno: 15, colno: 3, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 11, colno: 4, in_app: true }, + { filename: 'http://path/to/file.js', function: 'bar', lineno: 7, colno: 4, in_app: true }, + { filename: 'http://path/to/file.js', function: 'bar', lineno: 4, colno: 5, in_app: true }, + { filename: 'http://path/to/file.js', function: 'printStackTrace', lineno: 18, colno: 4, in_app: true }, + { filename: 'http://path/to/file.js', function: 'run', lineno: 27, colno: 8, in_app: true }, + { filename: 'http://path/to/file.js', function: 'createException', lineno: 42, colno: 12, in_app: true }, + ], + }, }); }); @@ -103,21 +107,36 @@ describe('Tracekit - Opera Tests', () => { ' dumpException3();', }; - const stackFrames = computeStackTrace(OPERA_12); + const ex = exceptionFromError(OPERA_12); - expect(stackFrames).toEqual({ - message: "Cannot convert 'x' to object", - name: 'foo', - stack: [ - { - filename: 'http://localhost:8000/ExceptionLab.html', - function: '', - lineno: 48, - colno: 12, - }, - { filename: 'http://localhost:8000/ExceptionLab.html', function: 'dumpException3', lineno: 46, colno: 8 }, - { filename: 'http://localhost:8000/ExceptionLab.html', function: '', lineno: 1, colno: 0 }, - ], + expect(ex).toEqual({ + value: "Cannot convert 'x' to object", + type: 'foo', + stacktrace: { + frames: [ + { + filename: 'http://localhost:8000/ExceptionLab.html', + function: '', + lineno: 1, + colno: 0, + in_app: true, + }, + { + filename: 'http://localhost:8000/ExceptionLab.html', + function: 'dumpException3', + lineno: 46, + colno: 8, + in_app: true, + }, + { + filename: 'http://localhost:8000/ExceptionLab.html', + function: '', + lineno: 48, + colno: 12, + in_app: true, + }, + ], + }, }); }); @@ -132,16 +151,18 @@ describe('Tracekit - Opera Tests', () => { ' at bar (http://path/to/file.js:108:168)', }; - const stackFrames = computeStackTrace(OPERA_25); + const ex = exceptionFromError(OPERA_25); - expect(stackFrames).toEqual({ - message: "Cannot read property 'undef' of null", - name: 'TypeError', - stack: [ - { filename: 'http://path/to/file.js', function: '?', lineno: 47, colno: 22 }, - { filename: 'http://path/to/file.js', function: 'foo', lineno: 52, colno: 15 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 108, colno: 168 }, - ], + expect(ex).toEqual({ + value: "Cannot read property 'undef' of null", + type: 'TypeError', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: 'bar', lineno: 108, colno: 168, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 52, colno: 15, in_app: true }, + { filename: 'http://path/to/file.js', function: '?', lineno: 47, colno: 22, in_app: true }, + ], + }, }); }); }); diff --git a/packages/browser/test/unit/tracekit/react-native.test.ts b/packages/browser/test/unit/tracekit/react-native.test.ts index 89ae3c430ca1..85e9b2355fab 100644 --- a/packages/browser/test/unit/tracekit/react-native.test.ts +++ b/packages/browser/test/unit/tracekit/react-native.test.ts @@ -1,4 +1,4 @@ -import { computeStackTrace } from '../../../src/tracekit'; +import { exceptionFromError } from '../../../src/parsers'; describe('Tracekit - React Native Tests', () => { it('should parse exceptions for react-native-v8', () => { @@ -14,20 +14,40 @@ describe('Tracekit - React Native Tests', () => { at Object.y(index.android.bundle:93:571) at P(index.android.bundle:93:714)`, }; - const stacktrace = computeStackTrace(REACT_NATIVE_V8_EXCEPTION); + const stacktrace = exceptionFromError(REACT_NATIVE_V8_EXCEPTION); expect(stacktrace).toEqual({ - message: 'Manually triggered crash to test Sentry reporting', - name: 'Error', - stack: [ - { filename: 'index.android.bundle', function: 'Object.onPress', lineno: 2342, colno: 3773 }, - { filename: 'index.android.bundle', function: 's.touchableHandlePress', lineno: 214, colno: 2048 }, - { filename: 'index.android.bundle', function: 's._performSideEffectsForTransition', lineno: 198, colno: 9608 }, - { filename: 'index.android.bundle', function: 's._receiveSignal', lineno: 198, colno: 8309 }, - { filename: 'index.android.bundle', function: 's.touchableHandleResponderRelease', lineno: 198, colno: 5615 }, - { filename: 'index.android.bundle', function: 'Object.y', lineno: 93, colno: 571 }, - { filename: 'index.android.bundle', function: 'P', lineno: 93, colno: 714 }, - ], + value: 'Manually triggered crash to test Sentry reporting', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'index.android.bundle', function: 'P', lineno: 93, colno: 714, in_app: true }, + { filename: 'index.android.bundle', function: 'Object.y', lineno: 93, colno: 571, in_app: true }, + { + filename: 'index.android.bundle', + function: 's.touchableHandleResponderRelease', + lineno: 198, + colno: 5615, + in_app: true, + }, + { filename: 'index.android.bundle', function: 's._receiveSignal', lineno: 198, colno: 8309, in_app: true }, + { + filename: 'index.android.bundle', + function: 's._performSideEffectsForTransition', + lineno: 198, + colno: 9608, + in_app: true, + }, + { + filename: 'index.android.bundle', + function: 's.touchableHandlePress', + lineno: 214, + colno: 2048, + in_app: true, + }, + { filename: 'index.android.bundle', function: 'Object.onPress', lineno: 2342, colno: 3773, in_app: true }, + ], + }, }); }); @@ -41,42 +61,48 @@ describe('Tracekit - React Native Tests', () => { p@/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3:96:385 forEach@[native code]`, }; - const stacktrace = computeStackTrace(REACT_NATIVE_EXPO_EXCEPTION); + const stacktrace = exceptionFromError(REACT_NATIVE_EXPO_EXCEPTION); expect(stacktrace).toEqual({ - message: 'Test Error Expo', - name: 'Error', - stack: [ - { - filename: - '/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', - function: 'onPress', - lineno: 595, - colno: 658, - }, - { - filename: - '/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', - function: 'value', - lineno: 221, - colno: 7656, - }, - { - filename: - '/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', - function: 'onResponderRelease', - lineno: 221, - colno: 5666, - }, - { - filename: - '/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', - function: 'p', - lineno: 96, - colno: 385, - }, - { filename: '[native code]', function: 'forEach' }, - ], + value: 'Test Error Expo', + type: 'Error', + stacktrace: { + frames: [ + { filename: '[native code]', function: 'forEach', in_app: true }, + { + filename: + '/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', + function: 'p', + lineno: 96, + colno: 385, + in_app: true, + }, + { + filename: + '/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', + function: 'onResponderRelease', + lineno: 221, + colno: 5666, + in_app: true, + }, + { + filename: + '/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', + function: 'value', + lineno: 221, + colno: 7656, + in_app: true, + }, + { + filename: + '/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', + function: 'onPress', + lineno: 595, + colno: 658, + in_app: true, + }, + ], + }, }); }); @@ -96,68 +122,78 @@ describe('Tracekit - React Native Tests', () => { 'at this(/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/native/ReactNativeBaseComponent.js:74:41)\n', }; - const stackFrames = computeStackTrace(ANDROID_REACT_NATIVE); + const ex = exceptionFromError(ANDROID_REACT_NATIVE); - expect(stackFrames).toEqual({ - message: 'Error: test', - name: 'Error', - stack: [ - { - filename: '/home/username/sample-workspace/sampleapp.collect.react/src/components/GpsMonitorScene.js', - function: 'render', - lineno: 78, - colno: 24, - }, - { - filename: - '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js', - function: '_renderValidatedComponentWithoutOwnerOrContext', - lineno: 1050, - colno: 29, - }, - { - filename: - '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js', - function: '_renderValidatedComponent', - lineno: 1075, - colno: 15, - }, - { - filename: - '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js', - function: 'renderedElement', - lineno: 484, - colno: 29, - }, - { - filename: - '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js', - function: '_currentElement', - lineno: 346, - colno: 40, - }, - { - filename: - '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactReconciler.js', - function: 'child', - lineno: 68, - colno: 25, - }, - { - filename: - '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactMultiChild.js', - function: 'children', - lineno: 264, - colno: 10, - }, - { - filename: - '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/native/ReactNativeBaseComponent.js', - function: 'this', - lineno: 74, - colno: 41, - }, - ], + expect(ex).toEqual({ + value: 'Error: test', + type: 'Error', + stacktrace: { + frames: [ + { + filename: + '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/native/ReactNativeBaseComponent.js', + function: 'this', + lineno: 74, + colno: 41, + in_app: true, + }, + { + filename: + '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactMultiChild.js', + function: 'children', + lineno: 264, + colno: 10, + in_app: true, + }, + { + filename: + '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactReconciler.js', + function: 'child', + lineno: 68, + colno: 25, + in_app: true, + }, + { + filename: + '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js', + function: '_currentElement', + lineno: 346, + colno: 40, + in_app: true, + }, + { + filename: + '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js', + function: 'renderedElement', + lineno: 484, + colno: 29, + in_app: true, + }, + { + filename: + '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js', + function: '_renderValidatedComponent', + lineno: 1075, + colno: 15, + in_app: true, + }, + { + filename: + '/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js', + function: '_renderValidatedComponentWithoutOwnerOrContext', + lineno: 1050, + colno: 29, + in_app: true, + }, + { + filename: '/home/username/sample-workspace/sampleapp.collect.react/src/components/GpsMonitorScene.js', + function: 'render', + lineno: 78, + colno: 24, + in_app: true, + }, + ], + }, }); }); @@ -205,55 +241,82 @@ describe('Tracekit - React Native Tests', () => { '[native code]', }; - const stackFrames = computeStackTrace(ANDROID_REACT_NATIVE_PROD); + const ex = exceptionFromError(ANDROID_REACT_NATIVE_PROD); - expect(stackFrames).toEqual({ - message: 'Error: test', - name: 'Error', - stack: [ - { filename: 'index.android.bundle', function: 'value', lineno: 12, colno: 1917 }, - { filename: 'index.android.bundle', function: 'onPress', lineno: 12, colno: 2336 }, - { filename: 'index.android.bundle', function: 'touchableHandlePress', lineno: 258, colno: 1497 }, - { filename: '[native code]', function: '?' }, - { filename: 'index.android.bundle', function: '_performSideEffectsForTransition', lineno: 252, colno: 8508 }, - { filename: '[native code]', function: '?' }, - { filename: 'index.android.bundle', function: '_receiveSignal', lineno: 252, colno: 7291 }, - { filename: '[native code]', function: '?' }, - { filename: 'index.android.bundle', function: 'touchableHandleResponderRelease', lineno: 252, colno: 4735 }, - { filename: '[native code]', function: '?' }, - { filename: 'index.android.bundle', function: 'u', lineno: 79, colno: 142 }, - { filename: 'index.android.bundle', function: 'invokeGuardedCallback', lineno: 79, colno: 459 }, - { - filename: 'index.android.bundle', - function: 'invokeGuardedCallbackAndCatchFirstError', - lineno: 79, - colno: 580, - }, - { filename: 'index.android.bundle', function: 'c', lineno: 95, colno: 365 }, - { filename: 'index.android.bundle', function: 'a', lineno: 95, colno: 567 }, - { filename: 'index.android.bundle', function: 'v', lineno: 146, colno: 501 }, - { filename: 'index.android.bundle', function: 'g', lineno: 146, colno: 604 }, - { filename: '[native code]', function: 'forEach' }, - { filename: 'index.android.bundle', function: 'i', lineno: 149, colno: 80 }, - { filename: 'index.android.bundle', function: 'processEventQueue', lineno: 146, colno: 1432 }, - { filename: 'index.android.bundle', function: 's', lineno: 157, colno: 88 }, - { filename: 'index.android.bundle', function: 'handleTopLevel', lineno: 157, colno: 174 }, - { filename: 'index.android.bundle', function: '?', lineno: 156, colno: 572 }, - { filename: 'index.android.bundle', function: 'a', lineno: 93, colno: 276 }, - { filename: 'index.android.bundle', function: 'c', lineno: 93, colno: 60 }, - { filename: 'index.android.bundle', function: 'perform', lineno: 177, colno: 596 }, - { filename: 'index.android.bundle', function: 'batchedUpdates', lineno: 188, colno: 464 }, - { filename: 'index.android.bundle', function: 'i', lineno: 176, colno: 358 }, - { filename: 'index.android.bundle', function: 'i', lineno: 93, colno: 90 }, - { filename: 'index.android.bundle', function: 'u', lineno: 93, colno: 150 }, - { filename: 'index.android.bundle', function: '_receiveRootNodeIDEvent', lineno: 156, colno: 544 }, - { filename: 'index.android.bundle', function: 'receiveTouches', lineno: 156, colno: 918 }, - { filename: 'index.android.bundle', function: 'value', lineno: 29, colno: 3016 }, - { filename: 'index.android.bundle', function: '?', lineno: 29, colno: 955 }, - { filename: 'index.android.bundle', function: 'value', lineno: 29, colno: 2417 }, - { filename: 'index.android.bundle', function: 'value', lineno: 29, colno: 927 }, - { filename: '[native code]', function: '?' }, - ], + expect(ex).toEqual({ + value: 'Error: test', + type: 'Error', + stacktrace: { + frames: [ + { filename: '[native code]', function: '?', in_app: true }, + { filename: 'index.android.bundle', function: 'value', lineno: 29, colno: 927, in_app: true }, + { filename: 'index.android.bundle', function: 'value', lineno: 29, colno: 2417, in_app: true }, + { filename: 'index.android.bundle', function: '?', lineno: 29, colno: 955, in_app: true }, + { filename: 'index.android.bundle', function: 'value', lineno: 29, colno: 3016, in_app: true }, + { filename: 'index.android.bundle', function: 'receiveTouches', lineno: 156, colno: 918, in_app: true }, + { + filename: 'index.android.bundle', + function: '_receiveRootNodeIDEvent', + lineno: 156, + colno: 544, + in_app: true, + }, + { filename: 'index.android.bundle', function: 'u', lineno: 93, colno: 150, in_app: true }, + { filename: 'index.android.bundle', function: 'i', lineno: 93, colno: 90, in_app: true }, + { filename: 'index.android.bundle', function: 'i', lineno: 176, colno: 358, in_app: true }, + { filename: 'index.android.bundle', function: 'batchedUpdates', lineno: 188, colno: 464, in_app: true }, + { filename: 'index.android.bundle', function: 'perform', lineno: 177, colno: 596, in_app: true }, + { filename: 'index.android.bundle', function: 'c', lineno: 93, colno: 60, in_app: true }, + { filename: 'index.android.bundle', function: 'a', lineno: 93, colno: 276, in_app: true }, + { filename: 'index.android.bundle', function: '?', lineno: 156, colno: 572, in_app: true }, + { filename: 'index.android.bundle', function: 'handleTopLevel', lineno: 157, colno: 174, in_app: true }, + { filename: 'index.android.bundle', function: 's', lineno: 157, colno: 88, in_app: true }, + { filename: 'index.android.bundle', function: 'processEventQueue', lineno: 146, colno: 1432, in_app: true }, + { filename: 'index.android.bundle', function: 'i', lineno: 149, colno: 80, in_app: true }, + { filename: '[native code]', function: 'forEach', in_app: true }, + { filename: 'index.android.bundle', function: 'g', lineno: 146, colno: 604, in_app: true }, + { filename: 'index.android.bundle', function: 'v', lineno: 146, colno: 501, in_app: true }, + { filename: 'index.android.bundle', function: 'a', lineno: 95, colno: 567, in_app: true }, + { filename: 'index.android.bundle', function: 'c', lineno: 95, colno: 365, in_app: true }, + { + filename: 'index.android.bundle', + function: 'invokeGuardedCallbackAndCatchFirstError', + lineno: 79, + colno: 580, + in_app: true, + }, + { filename: 'index.android.bundle', function: 'invokeGuardedCallback', lineno: 79, colno: 459, in_app: true }, + { filename: 'index.android.bundle', function: 'u', lineno: 79, colno: 142, in_app: true }, + { filename: '[native code]', function: '?', in_app: true }, + { + filename: 'index.android.bundle', + function: 'touchableHandleResponderRelease', + lineno: 252, + colno: 4735, + in_app: true, + }, + { filename: '[native code]', function: '?', in_app: true }, + { filename: 'index.android.bundle', function: '_receiveSignal', lineno: 252, colno: 7291, in_app: true }, + { filename: '[native code]', function: '?', in_app: true }, + { + filename: 'index.android.bundle', + function: '_performSideEffectsForTransition', + lineno: 252, + colno: 8508, + in_app: true, + }, + { filename: '[native code]', function: '?', in_app: true }, + { + filename: 'index.android.bundle', + function: 'touchableHandlePress', + lineno: 258, + colno: 1497, + in_app: true, + }, + { filename: 'index.android.bundle', function: 'onPress', lineno: 12, colno: 2336, in_app: true }, + { filename: 'index.android.bundle', function: 'value', lineno: 12, colno: 1917, in_app: true }, + ], + }, }); }); @@ -289,39 +352,47 @@ describe('Tracekit - React Native Tests', () => { 'at value (address at index.android.bundle:1:32776)\n' + 'at value (address at index.android.bundle:1:31561)', }; - const stackFrames = computeStackTrace(ANDROID_REACT_NATIVE_HERMES); + const ex = exceptionFromError(ANDROID_REACT_NATIVE_HERMES); - expect(stackFrames).toEqual({ - message: 'Error: lets throw!', - name: 'Error', - stack: [ - { filename: 'index.android.bundle', function: 'onPress', lineno: 1, colno: 452701 }, - { filename: 'index.android.bundle', function: 'anonymous', lineno: 1, colno: 224280 }, - { filename: 'index.android.bundle', function: '_performSideEffectsForTransition', lineno: 1, colno: 230843 }, - { filename: 'native', function: '_receiveSignal' }, - { filename: 'native', function: 'touchableHandleResponderRelease' }, - { filename: 'native', function: 'onResponderRelease' }, - { filename: 'native', function: 'apply' }, - { filename: 'index.android.bundle', function: 'b', lineno: 1, colno: 74037 }, - { filename: 'native', function: 'apply' }, - { filename: 'index.android.bundle', function: 'k', lineno: 1, colno: 74094 }, - { filename: 'native', function: 'apply' }, - { filename: 'index.android.bundle', function: 'C', lineno: 1, colno: 74126 }, - { filename: 'index.android.bundle', function: 'N', lineno: 1, colno: 74267 }, - { filename: 'index.android.bundle', function: 'A', lineno: 1, colno: 74709 }, - { filename: 'native', function: 'forEach' }, - { filename: 'index.android.bundle', function: 'z', lineno: 1, colno: 74642 }, - { filename: 'index.android.bundle', function: 'anonymous', lineno: 1, colno: 77747 }, - { filename: 'index.android.bundle', function: '_e', lineno: 1, colno: 127755 }, - { filename: 'index.android.bundle', function: 'Ne', lineno: 1, colno: 77238 }, - { filename: 'index.android.bundle', function: 'Ue', lineno: 1, colno: 77571 }, - { filename: 'index.android.bundle', function: 'receiveTouches', lineno: 1, colno: 122512 }, - { filename: 'native', function: 'apply' }, - { filename: 'index.android.bundle', function: 'value', lineno: 1, colno: 33176 }, - { filename: 'index.android.bundle', function: 'anonymous', lineno: 1, colno: 31603 }, - { filename: 'index.android.bundle', function: 'value', lineno: 1, colno: 32776 }, - { filename: 'index.android.bundle', function: 'value', lineno: 1, colno: 31561 }, - ], + expect(ex).toEqual({ + value: 'Error: lets throw!', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'index.android.bundle', function: 'value', lineno: 1, colno: 31561, in_app: true }, + { filename: 'index.android.bundle', function: 'value', lineno: 1, colno: 32776, in_app: true }, + { filename: 'index.android.bundle', function: 'anonymous', lineno: 1, colno: 31603, in_app: true }, + { filename: 'index.android.bundle', function: 'value', lineno: 1, colno: 33176, in_app: true }, + { filename: 'native', function: 'apply', in_app: true }, + { filename: 'index.android.bundle', function: 'receiveTouches', lineno: 1, colno: 122512, in_app: true }, + { filename: 'index.android.bundle', function: 'Ue', lineno: 1, colno: 77571, in_app: true }, + { filename: 'index.android.bundle', function: 'Ne', lineno: 1, colno: 77238, in_app: true }, + { filename: 'index.android.bundle', function: '_e', lineno: 1, colno: 127755, in_app: true }, + { filename: 'index.android.bundle', function: 'anonymous', lineno: 1, colno: 77747, in_app: true }, + { filename: 'index.android.bundle', function: 'z', lineno: 1, colno: 74642, in_app: true }, + { filename: 'native', function: 'forEach', in_app: true }, + { filename: 'index.android.bundle', function: 'A', lineno: 1, colno: 74709, in_app: true }, + { filename: 'index.android.bundle', function: 'N', lineno: 1, colno: 74267, in_app: true }, + { filename: 'index.android.bundle', function: 'C', lineno: 1, colno: 74126, in_app: true }, + { filename: 'native', function: 'apply', in_app: true }, + { filename: 'index.android.bundle', function: 'k', lineno: 1, colno: 74094, in_app: true }, + { filename: 'native', function: 'apply', in_app: true }, + { filename: 'index.android.bundle', function: 'b', lineno: 1, colno: 74037, in_app: true }, + { filename: 'native', function: 'apply', in_app: true }, + { filename: 'native', function: 'onResponderRelease', in_app: true }, + { filename: 'native', function: 'touchableHandleResponderRelease', in_app: true }, + { filename: 'native', function: '_receiveSignal', in_app: true }, + { + filename: 'index.android.bundle', + function: '_performSideEffectsForTransition', + lineno: 1, + colno: 230843, + in_app: true, + }, + { filename: 'index.android.bundle', function: 'anonymous', lineno: 1, colno: 224280, in_app: true }, + { filename: 'index.android.bundle', function: 'onPress', lineno: 1, colno: 452701, in_app: true }, + ], + }, }); }); }); diff --git a/packages/browser/test/unit/tracekit/react.test.ts b/packages/browser/test/unit/tracekit/react.test.ts index caa5e832d2ec..876b2ea97c9a 100644 --- a/packages/browser/test/unit/tracekit/react.test.ts +++ b/packages/browser/test/unit/tracekit/react.test.ts @@ -1,4 +1,4 @@ -import { computeStackTrace } from '../../../src/tracekit'; +import { exceptionFromError } from '../../../src/parsers'; describe('Tracekit - React Tests', () => { it('should correctly parse Invariant Violation errors and use framesToPop to drop info message', () => { @@ -14,18 +14,38 @@ describe('Tracekit - React Tests', () => { at f (http://localhost:5000/:1:980)`, }; - const stacktrace = computeStackTrace(REACT_INVARIANT_VIOLATION_EXCEPTION); + const ex = exceptionFromError(REACT_INVARIANT_VIOLATION_EXCEPTION); - expect(stacktrace).toEqual({ - message: + expect(ex).toEqual({ + value: 'Minified React error #31; visit https://reactjs.org/docs/error-decoder.html?invariant=31&args[]=object%20with%20keys%20%7B%7D&args[]= for the full message or use the non-minified dev environment for full errors and additional helpful warnings. ', - name: 'Invariant Violation', - stack: [ - { filename: 'http://localhost:5000/static/js/foo.chunk.js', function: '?', lineno: 1, colno: 21738 }, - { filename: 'http://localhost:5000/static/js/foo.chunk.js', function: 'a', lineno: 1, colno: 21841 }, - { filename: 'http://localhost:5000/static/js/foo.chunk.js', function: 'ho', lineno: 1, colno: 68735 }, - { filename: 'http://localhost:5000/', function: 'f', lineno: 1, colno: 980 }, - ], + type: 'Invariant Violation', + stacktrace: { + frames: [ + { filename: 'http://localhost:5000/', function: 'f', lineno: 1, colno: 980, in_app: true }, + { + filename: 'http://localhost:5000/static/js/foo.chunk.js', + function: 'ho', + lineno: 1, + colno: 68735, + in_app: true, + }, + { + filename: 'http://localhost:5000/static/js/foo.chunk.js', + function: 'a', + lineno: 1, + colno: 21841, + in_app: true, + }, + { + filename: 'http://localhost:5000/static/js/foo.chunk.js', + function: '?', + lineno: 1, + colno: 21738, + in_app: true, + }, + ], + }, }); }); @@ -41,18 +61,38 @@ describe('Tracekit - React Tests', () => { at f (http://localhost:5000/:1:980)`, }; - const stacktrace = computeStackTrace(REACT_PRODUCTION_ERROR); + const ex = exceptionFromError(REACT_PRODUCTION_ERROR); - expect(stacktrace).toEqual({ - message: + expect(ex).toEqual({ + value: 'Minified React error #200; visit https://reactjs.org/docs/error-decoder.html?invariant=200 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.', - name: 'Error', - stack: [ - { filename: 'http://localhost:5000/static/js/foo.chunk.js', function: '?', lineno: 1, colno: 21738 }, - { filename: 'http://localhost:5000/static/js/foo.chunk.js', function: 'a', lineno: 1, colno: 21841 }, - { filename: 'http://localhost:5000/static/js/foo.chunk.js', function: 'ho', lineno: 1, colno: 68735 }, - { filename: 'http://localhost:5000/', function: 'f', lineno: 1, colno: 980 }, - ], + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://localhost:5000/', function: 'f', lineno: 1, colno: 980, in_app: true }, + { + filename: 'http://localhost:5000/static/js/foo.chunk.js', + function: 'ho', + lineno: 1, + colno: 68735, + in_app: true, + }, + { + filename: 'http://localhost:5000/static/js/foo.chunk.js', + function: 'a', + lineno: 1, + colno: 21841, + in_app: true, + }, + { + filename: 'http://localhost:5000/static/js/foo.chunk.js', + function: '?', + lineno: 1, + colno: 21738, + in_app: true, + }, + ], + }, }); }); @@ -69,18 +109,38 @@ describe('Tracekit - React Tests', () => { at f (http://localhost:5000/:1:980)`, }; - const stacktrace = computeStackTrace(REACT_PRODUCTION_ERROR); + const ex = exceptionFromError(REACT_PRODUCTION_ERROR); - expect(stacktrace).toEqual({ - message: + expect(ex).toEqual({ + value: 'Minified React error #200; visit https://reactjs.org/docs/error-decoder.html?invariant=200 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.', - name: 'Error', - stack: [ - { filename: 'http://localhost:5000/static/js/foo.chunk.js', function: '?', lineno: 1, colno: 21738 }, - { filename: 'http://localhost:5000/static/js/foo.chunk.js', function: 'a', lineno: 1, colno: 21841 }, - { filename: 'http://localhost:5000/static/js/foo.chunk.js', function: 'ho', lineno: 1, colno: 68735 }, - { filename: 'http://localhost:5000/', function: 'f', lineno: 1, colno: 980 }, - ], + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://localhost:5000/', function: 'f', lineno: 1, colno: 980, in_app: true }, + { + filename: 'http://localhost:5000/static/js/foo.chunk.js', + function: 'ho', + lineno: 1, + colno: 68735, + in_app: true, + }, + { + filename: 'http://localhost:5000/static/js/foo.chunk.js', + function: 'a', + lineno: 1, + colno: 21841, + in_app: true, + }, + { + filename: 'http://localhost:5000/static/js/foo.chunk.js', + function: '?', + lineno: 1, + colno: 21738, + in_app: true, + }, + ], + }, }); }); }); diff --git a/packages/browser/test/unit/tracekit/safari.test.ts b/packages/browser/test/unit/tracekit/safari.test.ts index 5f43e042a279..a2fb96294166 100644 --- a/packages/browser/test/unit/tracekit/safari.test.ts +++ b/packages/browser/test/unit/tracekit/safari.test.ts @@ -1,4 +1,4 @@ -import { computeStackTrace } from '../../../src/tracekit'; +import { exceptionFromError } from '../../../src/parsers'; describe('Tracekit - Safari Tests', () => { it('should parse Safari 6 error', () => { @@ -14,17 +14,19 @@ describe('Tracekit - Safari Tests', () => { sourceURL: 'http://path/to/file.js', }; - const stackFrames = computeStackTrace(SAFARI_6); + const stackFrames = exceptionFromError(SAFARI_6); expect(stackFrames).toEqual({ - message: "'null' is not an object (evaluating 'x.undef')", - name: 'foo', - stack: [ - { filename: 'http://path/to/file.js', function: '?', lineno: 48 }, - { filename: 'http://path/to/file.js', function: 'dumpException3', lineno: 52 }, - { filename: 'http://path/to/file.js', function: 'onclick', lineno: 82 }, - { filename: '[native code]', function: '?' }, - ], + value: "'null' is not an object (evaluating 'x.undef')", + type: 'foo', + stacktrace: { + frames: [ + { filename: '[native code]', function: '?', in_app: true }, + { filename: 'http://path/to/file.js', function: 'onclick', lineno: 82, in_app: true }, + { filename: 'http://path/to/file.js', function: 'dumpException3', lineno: 52, in_app: true }, + { filename: 'http://path/to/file.js', function: '?', lineno: 48, in_app: true }, + ], + }, }); }); @@ -38,16 +40,18 @@ describe('Tracekit - Safari Tests', () => { sourceURL: 'http://path/to/file.js', }; - const stackFrames = computeStackTrace(SAFARI_7); + const stackFrames = exceptionFromError(SAFARI_7); expect(stackFrames).toEqual({ - message: "'null' is not an object (evaluating 'x.undef')", - name: 'TypeError', - stack: [ - { filename: 'http://path/to/file.js', function: '?', lineno: 48, colno: 22 }, - { filename: 'http://path/to/file.js', function: 'foo', lineno: 52, colno: 15 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 108, colno: 107 }, - ], + value: "'null' is not an object (evaluating 'x.undef')", + type: 'TypeError', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: 'bar', lineno: 108, colno: 107, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 52, colno: 15, in_app: true }, + { filename: 'http://path/to/file.js', function: '?', lineno: 48, colno: 22, in_app: true }, + ], + }, }); }); @@ -62,16 +66,18 @@ describe('Tracekit - Safari Tests', () => { sourceURL: 'http://path/to/file.js', }; - const stackFrames = computeStackTrace(SAFARI_8); + const stackFrames = exceptionFromError(SAFARI_8); expect(stackFrames).toEqual({ - message: "null is not an object (evaluating 'x.undef')", - name: 'TypeError', - stack: [ - { filename: 'http://path/to/file.js', function: '?', lineno: 47, colno: 22 }, - { filename: 'http://path/to/file.js', function: 'foo', lineno: 52, colno: 15 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 108, colno: 23 }, - ], + value: "null is not an object (evaluating 'x.undef')", + type: 'TypeError', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: 'bar', lineno: 108, colno: 23, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 52, colno: 15, in_app: true }, + { filename: 'http://path/to/file.js', function: '?', lineno: 47, colno: 22, in_app: true }, + ], + }, }); }); @@ -90,16 +96,18 @@ describe('Tracekit - Safari Tests', () => { column: 18, }; - const stackFrames = computeStackTrace(SAFARI_8_EVAL); + const stackFrames = exceptionFromError(SAFARI_8_EVAL); expect(stackFrames).toEqual({ - message: "Can't find variable: getExceptionProps", - name: 'ReferenceError', - stack: [ - { filename: '[native code]', function: 'eval' }, - { filename: 'http://path/to/file.js', function: 'foo', lineno: 58, colno: 21 }, - { filename: 'http://path/to/file.js', function: 'bar', lineno: 109, colno: 91 }, - ], + value: "Can't find variable: getExceptionProps", + type: 'ReferenceError', + stacktrace: { + frames: [ + { filename: 'http://path/to/file.js', function: 'bar', lineno: 109, colno: 91, in_app: true }, + { filename: 'http://path/to/file.js', function: 'foo', lineno: 58, colno: 21, in_app: true }, + { filename: '[native code]', function: 'eval', in_app: true }, + ], + }, }); }); @@ -113,25 +121,29 @@ describe('Tracekit - Safari Tests', () => { at safari-extension:(//3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js:3313:26)`, }; - const stacktrace = computeStackTrace(SAFARI_EXTENSION_EXCEPTION); + const ex = exceptionFromError(SAFARI_EXTENSION_EXCEPTION); - expect(stacktrace).toEqual({ - message: 'wat', - name: 'Error', - stack: [ - { - filename: 'safari-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/commons.js', - function: 'ClipperError', - lineno: 223036, - colno: 10, - }, - { - filename: 'safari-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js', - function: '?', - lineno: 3313, - colno: 26, - }, - ], + expect(ex).toEqual({ + value: 'wat', + type: 'Error', + stacktrace: { + frames: [ + { + filename: 'safari-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js', + function: '?', + lineno: 3313, + colno: 26, + in_app: true, + }, + { + filename: 'safari-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/commons.js', + function: 'ClipperError', + lineno: 223036, + colno: 10, + in_app: true, + }, + ], + }, }); }); @@ -143,26 +155,30 @@ describe('Tracekit - Safari Tests', () => { safari-extension://com.grammarly.safari.extension.ext2-W8F64X92K3/ee7759dd/Grammarly.js:2:1588410 promiseReactionJob@[native code]`, }; - const stacktrace = computeStackTrace(SAFARI_EXTENSION_EXCEPTION); + const ex = exceptionFromError(SAFARI_EXTENSION_EXCEPTION); - expect(stacktrace).toEqual({ - message: "undefined is not an object (evaluating 'e.groups.includes')", - name: 'TypeError', - stack: [ - { - filename: 'safari-extension://com.grammarly.safari.extension.ext2-W8F64X92K3/ee7759dd/Grammarly.js', - function: 'isClaimed', - lineno: 2, - colno: 929865, - }, - { - filename: 'safari-extension://com.grammarly.safari.extension.ext2-W8F64X92K3/ee7759dd/Grammarly.js', - function: '?', - lineno: 2, - colno: 1588410, - }, - { filename: '[native code]', function: 'promiseReactionJob' }, - ], + expect(ex).toEqual({ + value: "undefined is not an object (evaluating 'e.groups.includes')", + type: 'TypeError', + stacktrace: { + frames: [ + { filename: '[native code]', function: 'promiseReactionJob', in_app: true }, + { + filename: 'safari-extension://com.grammarly.safari.extension.ext2-W8F64X92K3/ee7759dd/Grammarly.js', + function: '?', + lineno: 2, + colno: 1588410, + in_app: true, + }, + { + filename: 'safari-extension://com.grammarly.safari.extension.ext2-W8F64X92K3/ee7759dd/Grammarly.js', + function: 'isClaimed', + lineno: 2, + colno: 929865, + in_app: true, + }, + ], + }, }); }); @@ -175,25 +191,29 @@ describe('Tracekit - Safari Tests', () => { at safari-web-extension:(//3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js:3313:26)`, }; - const stacktrace = computeStackTrace(SAFARI_WEB_EXTENSION_EXCEPTION); + const ex = exceptionFromError(SAFARI_WEB_EXTENSION_EXCEPTION); - expect(stacktrace).toEqual({ - message: 'wat', - name: 'Error', - stack: [ - { - filename: 'safari-web-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/commons.js', - function: 'ClipperError', - lineno: 223036, - colno: 10, - }, - { - filename: 'safari-web-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js', - function: '?', - lineno: 3313, - colno: 26, - }, - ], + expect(ex).toEqual({ + value: 'wat', + type: 'Error', + stacktrace: { + frames: [ + { + filename: 'safari-web-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js', + function: '?', + lineno: 3313, + colno: 26, + in_app: true, + }, + { + filename: 'safari-web-extension://3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/commons.js', + function: 'ClipperError', + lineno: 223036, + colno: 10, + in_app: true, + }, + ], + }, }); }); @@ -205,26 +225,30 @@ describe('Tracekit - Safari Tests', () => { safari-web-extension://46434E60-F5BD-48A4-80C8-A422C5D16897/scripts/content-script.js:29:56027 promiseReactionJob@[native code]`, }; - const stacktrace = computeStackTrace(SAFARI_EXTENSION_EXCEPTION); + const ex = exceptionFromError(SAFARI_EXTENSION_EXCEPTION); - expect(stacktrace).toEqual({ - message: "undefined is not an object (evaluating 'e.groups.includes')", - name: 'TypeError', - stack: [ - { - filename: 'safari-web-extension://46434E60-F5BD-48A4-80C8-A422C5D16897/scripts/content-script.js', - function: 'p_', - lineno: 29, - colno: 33314, - }, - { - filename: 'safari-web-extension://46434E60-F5BD-48A4-80C8-A422C5D16897/scripts/content-script.js', - function: '?', - lineno: 29, - colno: 56027, - }, - { filename: '[native code]', function: 'promiseReactionJob' }, - ], + expect(ex).toEqual({ + value: "undefined is not an object (evaluating 'e.groups.includes')", + type: 'TypeError', + stacktrace: { + frames: [ + { filename: '[native code]', function: 'promiseReactionJob', in_app: true }, + { + filename: 'safari-web-extension://46434E60-F5BD-48A4-80C8-A422C5D16897/scripts/content-script.js', + function: '?', + lineno: 29, + colno: 56027, + in_app: true, + }, + { + filename: 'safari-web-extension://46434E60-F5BD-48A4-80C8-A422C5D16897/scripts/content-script.js', + function: 'p_', + lineno: 29, + colno: 33314, + in_app: true, + }, + ], + }, }); }); }); @@ -239,17 +263,19 @@ describe('Tracekit - Safari Tests', () => { global code@http://localhost:5000/test:24:10`, }; - const stacktrace = computeStackTrace(SAFARI12_NATIVE_CODE_EXCEPTION); + const ex = exceptionFromError(SAFARI12_NATIVE_CODE_EXCEPTION); - expect(stacktrace).toEqual({ - message: 'test', - name: 'Error', - stack: [ - { filename: 'http://localhost:5000/test', function: 'fooIterator', lineno: 20, colno: 26 }, - { filename: '[native code]', function: 'map' }, - { filename: 'http://localhost:5000/test', function: 'foo', lineno: 19, colno: 22 }, - { filename: 'http://localhost:5000/test', function: 'global code', lineno: 24, colno: 10 }, - ], + expect(ex).toEqual({ + value: 'test', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://localhost:5000/test', function: 'global code', lineno: 24, colno: 10, in_app: true }, + { filename: 'http://localhost:5000/test', function: 'foo', lineno: 19, colno: 22, in_app: true }, + { filename: '[native code]', function: 'map', in_app: true }, + { filename: 'http://localhost:5000/test', function: 'fooIterator', lineno: 20, colno: 26, in_app: true }, + ], + }, }); }); @@ -271,24 +297,26 @@ describe('Tracekit - Safari Tests', () => { http://localhost:5000/:50:29`, }; - const stacktrace = computeStackTrace(SAFARI12_EVAL_EXCEPTION); + const ex = exceptionFromError(SAFARI12_EVAL_EXCEPTION); - expect(stacktrace).toEqual({ - message: 'aha', - name: 'Error', - stack: [ - { filename: 'http://localhost:5000/', function: 'aha', lineno: 19, colno: 22 }, - { filename: '[native code]', function: 'aha' }, - { filename: 'http://localhost:5000/', function: 'callAnotherThing', lineno: 20, colno: 16 }, - { filename: 'http://localhost:5000/', function: 'callback', lineno: 25, colno: 23 }, - { filename: 'http://localhost:5000/', function: '?', lineno: 34, colno: 25 }, - { filename: '[native code]', function: 'map' }, - { filename: 'http://localhost:5000/', function: 'test', lineno: 33, colno: 26 }, - { filename: '[native code]', function: 'eval' }, - { filename: 'http://localhost:5000/', function: 'aha', lineno: 39, colno: 9 }, - { filename: 'http://localhost:5000/', function: 'testMethod', lineno: 44, colno: 10 }, - { filename: 'http://localhost:5000/', function: '?', lineno: 50, colno: 29 }, - ], + expect(ex).toEqual({ + value: 'aha', + type: 'Error', + stacktrace: { + frames: [ + { filename: 'http://localhost:5000/', function: '?', lineno: 50, colno: 29, in_app: true }, + { filename: 'http://localhost:5000/', function: 'testMethod', lineno: 44, colno: 10, in_app: true }, + { filename: 'http://localhost:5000/', function: 'aha', lineno: 39, colno: 9, in_app: true }, + { filename: '[native code]', function: 'eval', in_app: true }, + { filename: 'http://localhost:5000/', function: 'test', lineno: 33, colno: 26, in_app: true }, + { filename: '[native code]', function: 'map', in_app: true }, + { filename: 'http://localhost:5000/', function: '?', lineno: 34, colno: 25, in_app: true }, + { filename: 'http://localhost:5000/', function: 'callback', lineno: 25, colno: 23, in_app: true }, + { filename: 'http://localhost:5000/', function: 'callAnotherThing', lineno: 20, colno: 16, in_app: true }, + { filename: '[native code]', function: 'aha', in_app: true }, + { filename: 'http://localhost:5000/', function: 'aha', lineno: 19, colno: 22, in_app: true }, + ], + }, }); }); }); diff --git a/packages/utils/src/stacktrace.ts b/packages/utils/src/stacktrace.ts index 3ee2344d2dd4..4d615a3cd53b 100644 --- a/packages/utils/src/stacktrace.ts +++ b/packages/utils/src/stacktrace.ts @@ -1,3 +1,69 @@ +import { StackFrame } from '@sentry/types'; + +const STACKTRACE_LIMIT = 50; + +export type StackLineParser = (line: string) => StackFrame | undefined; + +/** + * Creates a stack parser with the supplied line parsers + * + * StackFrames are returned in the correct order for Sentry Exception + * frames and with Sentry SDK internal frames removed from the top and bottom + * + * */ +export function createStackParser(...parsers: StackLineParser[]) { + return (stack: string, skipFirst: number = 0): StackFrame[] => { + const frames: StackFrame[] = []; + + for (const line of stack.split('\n').slice(skipFirst)) { + for (const parser of parsers) { + const frame = parser(line); + + if (frame) { + frames.push(frame); + break; + } + } + } + + return stripSentryFramesAndReverse(frames); + }; +} + +/** + * @hidden + */ +export function stripSentryFramesAndReverse(stack: StackFrame[]): StackFrame[] { + if (!stack.length) { + return []; + } + + let localStack = stack; + + const firstFrameFunction = localStack[0].function || ''; + const lastFrameFunction = localStack[localStack.length - 1].function || ''; + + // If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call) + if (firstFrameFunction.indexOf('captureMessage') !== -1 || firstFrameFunction.indexOf('captureException') !== -1) { + localStack = localStack.slice(1); + } + + // If stack ends with one of our internal API calls, remove it (ends, meaning it's the bottom of the stack - aka top-most call) + if (lastFrameFunction.indexOf('sentryWrapped') !== -1) { + localStack = localStack.slice(0, -1); + } + + // The frame where the crash happened, should be the last entry in the array + return localStack + .slice(0, STACKTRACE_LIMIT) + .map(frame => ({ + ...frame, + filename: frame.filename || localStack[0].filename, + function: frame.function || '?', + })) + .reverse(); +} + const defaultFunctionName = ''; /** diff --git a/packages/browser/test/unit/parsers.test.ts b/packages/utils/test/stacktrace.test.ts similarity index 86% rename from packages/browser/test/unit/parsers.test.ts rename to packages/utils/test/stacktrace.test.ts index 475d720fd3d1..61b44de34366 100644 --- a/packages/browser/test/unit/parsers.test.ts +++ b/packages/utils/test/stacktrace.test.ts @@ -1,7 +1,7 @@ -import { prepareFramesForEvent } from '../../src/parsers'; +import { stripSentryFramesAndReverse } from '../src/stacktrace'; -describe('Parsers', () => { - describe('prepareFramesForEvent()', () => { +describe('Stacktrace', () => { + describe('stripSentryFramesAndReverse()', () => { describe('removed top frame if its internally reserved word (public API)', () => { it('reserved captureException', () => { const stack = [ @@ -11,7 +11,7 @@ describe('Parsers', () => { ]; // Should remove `captureException` as its a name considered "internal" - const frames = prepareFramesForEvent(stack); + const frames = stripSentryFramesAndReverse(stack); expect(frames.length).toBe(2); expect(frames[0].function).toBe('bar'); @@ -26,7 +26,7 @@ describe('Parsers', () => { ]; // Should remove `captureMessage` as its a name considered "internal" - const frames = prepareFramesForEvent(stack); + const frames = stripSentryFramesAndReverse(stack); expect(frames.length).toBe(2); expect(frames[0].function).toBe('bar'); @@ -43,7 +43,7 @@ describe('Parsers', () => { ]; // Should remove `sentryWrapped` as its a name considered "internal" - const frames = prepareFramesForEvent(stack); + const frames = stripSentryFramesAndReverse(stack); expect(frames.length).toBe(2); expect(frames[0].function).toBe('bar'); @@ -60,7 +60,7 @@ describe('Parsers', () => { ]; // Should remove `captureMessage` and `sentryWrapped` as its a name considered "internal" - const frames = prepareFramesForEvent(stack); + const frames = stripSentryFramesAndReverse(stack); expect(frames.length).toBe(2); expect(frames[0].function).toBe('bar');