diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index ef615d2cedcf..c117a5793a6d 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -1,17 +1,153 @@ -import { Event, EventHint, Severity } from '@sentry/types'; +import { Event, EventHint, Exception, Severity, StackFrame } from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, + createStackParser, + extractExceptionKeysForMessage, isDOMError, isDOMException, isError, isErrorEvent, isEvent, isPlainObject, + normalizeToSize, resolvedSyncPromise, } from '@sentry/utils'; -import { eventFromError, eventFromPlainObject, parseStackFrames } from './parsers'; +import { + chromeStackParser, + geckoStackParser, + opera10StackParser, + opera11StackParser, + winjsStackParser, +} from './stack-parsers'; + +/** + * This function creates an exception from an TraceKitStackTrace + * @param stacktrace TraceKitStackTrace that will be converted to an exception + * @hidden + */ +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: ex && ex.name, + value: extractMessage(ex), + }; + + if (frames.length) { + exception.stacktrace = { frames }; + } + + if (exception.type === undefined && exception.value === '') { + exception.value = 'Unrecoverable error caught'; + } + + return exception; +} + +/** + * @hidden + */ +export function eventFromPlainObject( + exception: Record, + syntheticException?: Error, + isUnhandledRejection?: boolean, +): Event { + const event: Event = { + exception: { + values: [ + { + type: isEvent(exception) ? exception.constructor.name : isUnhandledRejection ? 'UnhandledRejection' : 'Error', + value: `Non-Error ${ + isUnhandledRejection ? 'promise rejection' : 'exception' + } captured with keys: ${extractExceptionKeysForMessage(exception)}`, + }, + ], + }, + extra: { + __serialized__: normalizeToSize(exception), + }, + }; + + if (syntheticException) { + const frames = parseStackFrames(syntheticException); + if (frames.length) { + event.stacktrace = { frames }; + } + } + + return event; +} + +/** + * @hidden + */ +export function eventFromError(ex: Error): Event { + return { + exception: { + values: [exceptionFromError(ex)], + }, + }; +} + +/** 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 { + return createStackParser( + opera10StackParser, + opera11StackParser, + chromeStackParser, + winjsStackParser, + geckoStackParser, + )(stacktrace, popSize); + } catch (e) { + // no-empty + } + + return []; +} + +// 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; + +function getPopSize(ex: Error & { framesToPop?: number }): number { + if (ex) { + if (typeof ex.framesToPop === 'number') { + return ex.framesToPop; + } + + if (reactMinifiedRegexp.test(ex.message)) { + return 1; + } + } + + 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 + */ +function extractMessage(ex: Error & { message: { error?: Error } }): 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; +} /** * Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`. diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index 4abcf3551922..fa197b48e681 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -2,7 +2,7 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types'; import { isInstanceOf } from '@sentry/utils'; -import { exceptionFromError } from '../parsers'; +import { exceptionFromError } from '../eventbuilder'; const DEFAULT_KEY = 'cause'; const DEFAULT_LIMIT = 5; diff --git a/packages/browser/src/parsers.ts b/packages/browser/src/parsers.ts deleted file mode 100644 index ff161c0ec3c3..000000000000 --- a/packages/browser/src/parsers.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { Event, Exception, StackFrame } from '@sentry/types'; -import { createStackParser, extractExceptionKeysForMessage, isEvent, normalizeToSize } from '@sentry/utils'; - -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 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: ex && ex.name, - value: extractMessage(ex), - }; - - if (frames && frames.length) { - exception.stacktrace = { frames }; - } - - if (exception.type === undefined && exception.value === '') { - exception.value = 'Unrecoverable error caught'; - } - - return exception; -} - -/** - * @hidden - */ -export function eventFromPlainObject( - exception: Record, - syntheticException?: Error, - isUnhandledRejection?: boolean, -): Event { - const event: Event = { - exception: { - values: [ - { - type: isEvent(exception) ? exception.constructor.name : isUnhandledRejection ? 'UnhandledRejection' : 'Error', - value: `Non-Error ${ - isUnhandledRejection ? 'promise rejection' : 'exception' - } captured with keys: ${extractExceptionKeysForMessage(exception)}`, - }, - ], - }, - extra: { - __serialized__: normalizeToSize(exception), - }, - }; - - if (syntheticException) { - const frames = parseStackFrames(syntheticException); - if (frames.length) { - event.stacktrace = { frames }; - } - } - - return event; -} - -/** - * @hidden - */ -export function eventFromError(ex: Error): Event { - return { - exception: { - values: [exceptionFromError(ex)], - }, - }; -} - -/** 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 - } - - return []; -} - -// 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; - -function getPopSize(ex: Error & { framesToPop?: number }): number { - if (ex) { - if (typeof ex.framesToPop === 'number') { - return ex.framesToPop; - } - - if (reactMinifiedRegexp.test(ex.message)) { - return 1; - } - } - - 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 index d05393101843..519c2de5df0e 100644 --- a/packages/browser/src/stack-parsers.ts +++ b/packages/browser/src/stack-parsers.ts @@ -1,9 +1,15 @@ import { StackFrame } from '@sentry/types'; -import { StackLineParser } from '@sentry/utils'; +import { StackLineParser, StackLineParserFn } from '@sentry/utils'; // global reference to slice const UNKNOWN_FUNCTION = '?'; +const OPERA10_PRIORITY = 10; +const OPERA11_PRIORITY = 20; +const CHROME_PRIORITY = 30; +const WINJS_PRIORITY = 40; +const GECKO_PRIORITY = 50; + function createFrame(filename: string, func: string, lineno?: number, colno?: number): StackFrame { const frame: StackFrame = { filename, @@ -28,7 +34,7 @@ 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 chrome: StackLineParserFn = line => { const parts = chromeRegex.exec(line); if (parts) { @@ -55,6 +61,8 @@ export const chrome: StackLineParser = line => { return; }; +export const chromeStackParser: StackLineParser = [CHROME_PRIORITY, chrome]; + // 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. @@ -62,7 +70,7 @@ 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 gecko: StackLineParserFn = line => { const parts = geckoREgex.exec(line); if (parts) { @@ -89,10 +97,12 @@ export const gecko: StackLineParser = line => { return; }; +export const geckoStackParser: StackLineParser = [GECKO_PRIORITY, gecko]; + const winjsRegex = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i; -export const winjs: StackLineParser = line => { +const winjs: StackLineParserFn = line => { const parts = winjsRegex.exec(line); return parts @@ -100,21 +110,27 @@ export const winjs: StackLineParser = line => { : undefined; }; +export const winjsStackParser: StackLineParser = [WINJS_PRIORITY, winjs]; + const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i; -export const opera10: StackLineParser = line => { +const opera10: StackLineParserFn = line => { const parts = opera10Regex.exec(line); return parts ? createFrame(parts[2], parts[3] || UNKNOWN_FUNCTION, +parts[1]) : undefined; }; +export const opera10StackParser: StackLineParser = [OPERA10_PRIORITY, opera10]; + const opera11Regex = / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^)]+))\(.*\))? in (.*):\s*$/i; -export const opera11: StackLineParser = line => { +const opera11: StackLineParserFn = line => { const parts = opera11Regex.exec(line); return parts ? createFrame(parts[5], parts[3] || parts[4] || UNKNOWN_FUNCTION, +parts[1], +parts[2]) : undefined; }; +export const opera11StackParser: StackLineParser = [OPERA11_PRIORITY, opera11]; + /** * Safari web extensions, starting version unknown, can produce "frames-only" stacktraces. * What it means, is that instead of format like: diff --git a/packages/browser/test/unit/tracekit/chromium.test.ts b/packages/browser/test/unit/tracekit/chromium.test.ts index 9d9ca9ccbff7..e40abffd8d1f 100644 --- a/packages/browser/test/unit/tracekit/chromium.test.ts +++ b/packages/browser/test/unit/tracekit/chromium.test.ts @@ -1,4 +1,4 @@ -import { exceptionFromError } from '../../../src/parsers'; +import { exceptionFromError } from '../../../src/eventbuilder'; describe('Tracekit - Chrome Tests', () => { it('should parse Chrome error with no location', () => { diff --git a/packages/browser/test/unit/tracekit/firefox.test.ts b/packages/browser/test/unit/tracekit/firefox.test.ts index 075b142272e2..a14fae1e38cc 100644 --- a/packages/browser/test/unit/tracekit/firefox.test.ts +++ b/packages/browser/test/unit/tracekit/firefox.test.ts @@ -1,4 +1,4 @@ -import { exceptionFromError } from '../../../src/parsers'; +import { exceptionFromError } from '../../../src/eventbuilder'; describe('Tracekit - Firefox Tests', () => { it('should parse Firefox 3 error', () => { diff --git a/packages/browser/test/unit/tracekit/ie.test.ts b/packages/browser/test/unit/tracekit/ie.test.ts index 4017165bc6a9..cfd60ab2e6c4 100644 --- a/packages/browser/test/unit/tracekit/ie.test.ts +++ b/packages/browser/test/unit/tracekit/ie.test.ts @@ -1,4 +1,4 @@ -import { exceptionFromError } from '../../../src/parsers'; +import { exceptionFromError } from '../../../src/eventbuilder'; describe('Tracekit - IE Tests', () => { it('should parse IE 10 error', () => { diff --git a/packages/browser/test/unit/tracekit/misc.test.ts b/packages/browser/test/unit/tracekit/misc.test.ts index 1e7a057dc8c8..3aa59754cc9a 100644 --- a/packages/browser/test/unit/tracekit/misc.test.ts +++ b/packages/browser/test/unit/tracekit/misc.test.ts @@ -1,4 +1,4 @@ -import { exceptionFromError } from '../../../src/parsers'; +import { exceptionFromError } from '../../../src/eventbuilder'; describe('Tracekit - Misc Tests', () => { it('should parse PhantomJS 1.19 error', () => { diff --git a/packages/browser/test/unit/tracekit/opera.test.ts b/packages/browser/test/unit/tracekit/opera.test.ts index 81316df250ef..472c4a55e2ca 100644 --- a/packages/browser/test/unit/tracekit/opera.test.ts +++ b/packages/browser/test/unit/tracekit/opera.test.ts @@ -1,4 +1,4 @@ -import { exceptionFromError } from '../../../src/parsers'; +import { exceptionFromError } from '../../../src/eventbuilder'; describe('Tracekit - Opera Tests', () => { it('should parse Opera 10 error', () => { diff --git a/packages/browser/test/unit/tracekit/react-native.test.ts b/packages/browser/test/unit/tracekit/react-native.test.ts index 85e9b2355fab..6935acd615fd 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 { exceptionFromError } from '../../../src/parsers'; +import { exceptionFromError } from '../../../src/eventbuilder'; describe('Tracekit - React Native Tests', () => { it('should parse exceptions for react-native-v8', () => { diff --git a/packages/browser/test/unit/tracekit/react.test.ts b/packages/browser/test/unit/tracekit/react.test.ts index 876b2ea97c9a..dba60cceab4f 100644 --- a/packages/browser/test/unit/tracekit/react.test.ts +++ b/packages/browser/test/unit/tracekit/react.test.ts @@ -1,4 +1,4 @@ -import { exceptionFromError } from '../../../src/parsers'; +import { exceptionFromError } from '../../../src/eventbuilder'; describe('Tracekit - React Tests', () => { it('should correctly parse Invariant Violation errors and use framesToPop to drop info message', () => { diff --git a/packages/browser/test/unit/tracekit/safari.test.ts b/packages/browser/test/unit/tracekit/safari.test.ts index a2fb96294166..e3cce33c7563 100644 --- a/packages/browser/test/unit/tracekit/safari.test.ts +++ b/packages/browser/test/unit/tracekit/safari.test.ts @@ -1,4 +1,4 @@ -import { exceptionFromError } from '../../../src/parsers'; +import { exceptionFromError } from '../../../src/eventbuilder'; describe('Tracekit - Safari Tests', () => { it('should parse Safari 6 error', () => { diff --git a/packages/node/src/backend.ts b/packages/node/src/backend.ts index 209450a375a3..61cbfad9c819 100644 --- a/packages/node/src/backend.ts +++ b/packages/node/src/backend.ts @@ -2,7 +2,7 @@ import { BaseBackend } from '@sentry/core'; import { Event, EventHint, Severity, Transport, TransportOptions } from '@sentry/types'; import { makeDsn, resolvedSyncPromise } from '@sentry/utils'; -import { eventFromError, eventFromMessage } from './eventbuilder'; +import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; import { HTTPSTransport, HTTPTransport } from './transports'; import { NodeOptions } from './types'; @@ -16,14 +16,14 @@ export class NodeBackend extends BaseBackend { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types public eventFromException(exception: any, hint?: EventHint): PromiseLike { - return resolvedSyncPromise(eventFromError(exception, hint)); + return resolvedSyncPromise(eventFromUnknownInput(exception, hint)); } /** * @inheritDoc */ public eventFromMessage(message: string, level: Severity = Severity.Info, hint?: EventHint): PromiseLike { - return resolvedSyncPromise(eventFromMessage(this._options, message, level, hint)); + return resolvedSyncPromise(eventFromMessage(message, level, hint, this._options.attachStacktrace)); } /** diff --git a/packages/node/src/eventbuilder.ts b/packages/node/src/eventbuilder.ts index bf5af4ab2a58..bd1a6707d58b 100644 --- a/packages/node/src/eventbuilder.ts +++ b/packages/node/src/eventbuilder.ts @@ -1,5 +1,5 @@ import { getCurrentHub } from '@sentry/hub'; -import { Event, EventHint, Exception, Mechanism, Options, Severity, StackFrame } from '@sentry/types'; +import { Event, EventHint, Exception, Mechanism, Severity, StackFrame } from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, @@ -15,8 +15,8 @@ import { nodeStackParser } from './stack-parser'; /** * Extracts stack frames from the error.stack string */ -export function extractStackFromError(error: Error): StackFrame[] { - return createStackParser(nodeStackParser)(error.stack || ''); +export function parseStackFrames(error: Error): StackFrame[] { + return createStackParser(nodeStackParser)(error.stack || '', 1); } /** @@ -28,7 +28,7 @@ export function exceptionFromError(error: Error): Exception { value: error.message, }; - const frames = extractStackFromError(error); + const frames = parseStackFrames(error); if (frames.length) { exception.stacktrace = { frames }; } @@ -40,9 +40,9 @@ export function exceptionFromError(error: Error): Exception { * Builds and Event from a Exception * @hidden */ -export function eventFromError(exception: unknown, hint?: EventHint): Event { +export function eventFromUnknownInput(exception: unknown, hint?: EventHint): Event { // eslint-disable-next-line @typescript-eslint/no-explicit-any - let ex: any = exception; + let ex: unknown = exception; const providedMechanism: Mechanism | undefined = hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism; const mechanism: Mechanism = providedMechanism || { @@ -91,10 +91,10 @@ export function eventFromError(exception: unknown, hint?: EventHint): Event { * @hidden */ export function eventFromMessage( - options: Options, message: string, level: Severity = Severity.Info, hint?: EventHint, + attachStacktrace?: boolean, ): Event { const event: Event = { event_id: hint && hint.event_id, @@ -102,8 +102,8 @@ export function eventFromMessage( message, }; - if (options.attachStacktrace && hint && hint.syntheticException) { - const frames = extractStackFromError(hint.syntheticException); + if (attachStacktrace && hint && hint.syntheticException) { + const frames = parseStackFrames(hint.syntheticException); if (frames.length) { event.stacktrace = { frames }; } diff --git a/packages/node/src/stack-parser.ts b/packages/node/src/stack-parser.ts index bd71f39a9bf5..2cbd6f116ccf 100644 --- a/packages/node/src/stack-parser.ts +++ b/packages/node/src/stack-parser.ts @@ -1,4 +1,4 @@ -import { basename, dirname, StackLineParser } from '@sentry/utils'; +import { basename, dirname, StackLineParser, StackLineParserFn } from '@sentry/utils'; /** Gets the module */ function getModule(filename: string | undefined): string | undefined { @@ -38,7 +38,7 @@ function getModule(filename: string | undefined): string | undefined { const FILENAME_MATCH = /^\s*[-]{4,}$/; const FULL_MATCH = /at (?:(.+?)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/; -export const nodeStackParser: StackLineParser = (line: string) => { +const node: StackLineParserFn = (line: string) => { if (line.match(FILENAME_MATCH)) { return { filename: line, @@ -106,3 +106,5 @@ export const nodeStackParser: StackLineParser = (line: string) => { in_app, }; }; + +export const nodeStackParser: StackLineParser = [90, node]; diff --git a/packages/node/test/parsers.test.ts b/packages/node/test/context-lines.test.ts similarity index 88% rename from packages/node/test/parsers.test.ts rename to packages/node/test/context-lines.test.ts index 456435a4cbfb..1182e63360c6 100644 --- a/packages/node/test/parsers.test.ts +++ b/packages/node/test/context-lines.test.ts @@ -1,11 +1,11 @@ import { StackFrame } from '@sentry/types'; import * as fs from 'fs'; -import { extractStackFromError } from '../src/eventbuilder'; +import { parseStackFrames } from '../src/eventbuilder'; import { ContextLines, resetFileContentCache } from '../src/integrations/contextlines'; import { getError } from './helper/error'; -describe('parsers.ts', () => { +describe('ContextLines', () => { let readFileSpy: jest.SpyInstance; let contextLines: ContextLines; @@ -27,7 +27,7 @@ describe('parsers.ts', () => { test('parseStack with same file', async () => { expect.assertions(1); - const frames = extractStackFromError(new Error('test')); + const frames = parseStackFrames(new Error('test')); await addContext(Array.from(frames)); @@ -57,12 +57,12 @@ describe('parsers.ts', () => { test('parseStack with adding different file', async () => { expect.assertions(1); - const frames = extractStackFromError(new Error('test')); + const frames = parseStackFrames(new Error('test')); await addContext(frames); const numCalls = readFileSpy.mock.calls.length; - const parsedFrames = extractStackFromError(getError()); + const parsedFrames = parseStackFrames(getError()); await addContext(parsedFrames); const newErrorCalls = readFileSpy.mock.calls.length; @@ -98,7 +98,7 @@ describe('parsers.ts', () => { test('parseStack with no context', async () => { expect.assertions(1); - const frames = extractStackFromError(new Error('test')); + const frames = parseStackFrames(new Error('test')); await addContext(frames, 0); expect(readFileSpy).toHaveBeenCalledTimes(0); diff --git a/packages/node/test/stacktrace.test.ts b/packages/node/test/stacktrace.test.ts index 78567a78b8d9..1c4cd5126c85 100644 --- a/packages/node/test/stacktrace.test.ts +++ b/packages/node/test/stacktrace.test.ts @@ -10,7 +10,7 @@ * @license MIT */ -import { extractStackFromError } from '../src/eventbuilder'; +import { parseStackFrames } from '../src/eventbuilder'; function testBasic() { return new Error('something went wrong'); @@ -26,7 +26,7 @@ function evalWrapper() { describe('Stack parsing', () => { test('test basic error', () => { - const frames = extractStackFromError(testBasic()); + const frames = parseStackFrames(testBasic()); const last = frames.length - 1; expect(frames[last].filename).toEqual(__filename); @@ -36,7 +36,7 @@ describe('Stack parsing', () => { }); test('test error with wrapper', () => { - const frames = extractStackFromError(testWrapper()); + const frames = parseStackFrames(testWrapper()); const last = frames.length - 1; expect(frames[last].function).toEqual('testBasic'); @@ -44,7 +44,7 @@ describe('Stack parsing', () => { }); test('test error with eval wrapper', () => { - const frames = extractStackFromError(evalWrapper()); + const frames = parseStackFrames(evalWrapper()); const last = frames.length - 1; expect(frames[last].function).toEqual('testBasic'); @@ -59,7 +59,7 @@ describe('Stack parsing', () => { ' at [object Object].global.every [as _onTimeout] (/Users/hoitz/develop/test.coffee:36:3)\n' + ' at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)\n'; - const frames = extractStackFromError(err as Error); + const frames = parseStackFrames(err as Error); expect(frames).toEqual([ { @@ -83,7 +83,7 @@ describe('Stack parsing', () => { test('parses undefined stack', () => { const err = { stack: undefined }; - const trace = extractStackFromError(err as Error); + const trace = parseStackFrames(err as Error); expect(trace).toEqual([]); }); @@ -97,7 +97,7 @@ describe('Stack parsing', () => { 'oh no' + ' at TestCase.run (/Users/felix/code/node-fast-or-slow/lib/test_case.js:61:8)\n'; - const frames = extractStackFromError(err as Error); + const frames = parseStackFrames(err as Error); expect(frames).toEqual([ { @@ -126,7 +126,7 @@ describe('Stack parsing', () => { ' at Test.fn (/Users/felix/code/node-fast-or-slow/test/fast/example/test-example.js:6)\n' + ' at Test.run (/Users/felix/code/node-fast-or-slow/lib/test.js:45)'; - const frames = extractStackFromError(err as Error); + const frames = parseStackFrames(err as Error); expect(frames).toEqual([ { @@ -157,7 +157,7 @@ describe('Stack parsing', () => { ' at Array.0 (native)\n' + ' at EventEmitter._tickCallback (node.js:126:26)'; - const frames = extractStackFromError(err as Error); + const frames = parseStackFrames(err as Error); expect(frames).toEqual([ { @@ -212,7 +212,7 @@ describe('Stack parsing', () => { const err: { [key: string]: any } = {}; err.stack = 'AssertionError: true == false\n' + ' at /Users/felix/code/node-fast-or-slow/lib/test_case.js:80:10'; - const frames = extractStackFromError(err as Error); + const frames = parseStackFrames(err as Error); expect(frames).toEqual([ { @@ -232,7 +232,7 @@ describe('Stack parsing', () => { 'AssertionError: true == false\nAnd some more shit\n' + ' at /Users/felix/code/node-fast-or-slow/lib/test_case.js:80:10'; - const frames = extractStackFromError(err as Error); + const frames = parseStackFrames(err as Error); expect(frames).toEqual([ { @@ -252,7 +252,7 @@ describe('Stack parsing', () => { 'AssertionError: expected [] to be arguments\n' + ' at Assertion.prop.(anonymous function) (/Users/den/Projects/should.js/lib/should.js:60:14)\n'; - const frames = extractStackFromError(err as Error); + const frames = parseStackFrames(err as Error); expect(frames).toEqual([ { @@ -273,7 +273,7 @@ describe('Stack parsing', () => { ' at Test.run (/Users/felix (something)/code/node-fast-or-slow/lib/test.js:45:10)\n' + ' at TestCase.run (/Users/felix (something)/code/node-fast-or-slow/lib/test_case.js:61:8)\n'; - const frames = extractStackFromError(err as Error); + const frames = parseStackFrames(err as Error); expect(frames).toEqual([ { diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index eefcd225a06b..483af0f94903 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -87,7 +87,7 @@ export interface Options { /** Attaches stacktraces to pure capture message / log integrations */ attachStacktrace?: boolean; - /** Maxium number of chars a single value can have before it will be truncated. */ + /** Maximum number of chars a single value can have before it will be truncated. */ maxValueLength?: number; /** diff --git a/packages/utils/src/stacktrace.ts b/packages/utils/src/stacktrace.ts index 4d615a3cd53b..7c9dddeee298 100644 --- a/packages/utils/src/stacktrace.ts +++ b/packages/utils/src/stacktrace.ts @@ -2,7 +2,9 @@ import { StackFrame } from '@sentry/types'; const STACKTRACE_LIMIT = 50; -export type StackLineParser = (line: string) => StackFrame | undefined; +export type StackParser = (stack: string, skipFirst?: number) => StackFrame[]; +export type StackLineParserFn = (line: string) => StackFrame | undefined; +export type StackLineParser = [number, StackLineParserFn]; /** * Creates a stack parser with the supplied line parsers @@ -10,13 +12,15 @@ export type StackLineParser = (line: string) => StackFrame | undefined; * 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[]) { + */ +export function createStackParser(...parsers: StackLineParser[]): StackParser { + const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map(p => p[1]); + return (stack: string, skipFirst: number = 0): StackFrame[] => { const frames: StackFrame[] = []; for (const line of stack.split('\n').slice(skipFirst)) { - for (const parser of parsers) { + for (const parser of sortedParsers) { const frame = parser(line); if (frame) {