From f3e9384795840f121653cbf1354f75766a8d5891 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 12 Apr 2022 16:23:17 +0100 Subject: [PATCH] ref: Expose configurable stack parser (#4902) Adds a `stackParser` option to `Options`: ```ts /** * A stack parser implementation or an array of stack line parsers * By default, a stack parser is supplied for all supported browsers */ stackParser?: StackParser | StackLineParser[]; ``` Whenever we want access to a `StackParser` to use with the functions in `eventbuilder` we call `stackParserFromOptions(options)`. This converts `StackLineParser[]` to `StackParser` and saves it back in the options so this conversion only occurs once. ### Added Exports `@sentry/node` - `nodeStackParser` `@sentry/browser` - `chromeStackParser` - `geckoStackParser` - `opera10StackParser` - `opera11StackParser` - `winjsStackParser` - `defaultStackParsers` --- packages/browser/src/client.ts | 12 +++- packages/browser/src/eventbuilder.ts | 65 +++++++++---------- packages/browser/src/exports.ts | 9 +++ .../src/integrations/globalhandlers.ts | 22 ++++--- .../browser/src/integrations/linkederrors.ts | 32 ++++++--- packages/browser/src/sdk.ts | 4 ++ packages/browser/src/stack-parsers.ts | 11 +++- .../unit/integrations/linkederrors.test.ts | 20 +++--- .../test/unit/tracekit/chromium.test.ts | 29 +++++---- .../test/unit/tracekit/firefox.test.ts | 23 ++++--- .../browser/test/unit/tracekit/ie.test.ts | 11 +++- .../browser/test/unit/tracekit/misc.test.ts | 7 +- .../browser/test/unit/tracekit/opera.test.ts | 13 ++-- .../test/unit/tracekit/react-native.test.ts | 15 +++-- .../browser/test/unit/tracekit/react.test.ts | 11 +++- .../browser/test/unit/tracekit/safari.test.ts | 25 ++++--- packages/node/src/client.ts | 8 ++- packages/node/src/eventbuilder.ts | 20 +++--- packages/node/src/index.ts | 1 + .../node/src/integrations/linkederrors.ts | 31 +++++---- packages/node/src/sdk.ts | 5 ++ packages/node/src/stack-parser.ts | 3 +- packages/node/test/context-lines.test.ts | 12 ++-- packages/node/test/index.test.ts | 13 ++++ .../test/integrations/linkederrors.test.ts | 26 ++++---- packages/node/test/stacktrace.test.ts | 33 ++++++---- packages/types/src/index.ts | 2 +- packages/types/src/options.ts | 7 ++ packages/types/src/stacktrace.ts | 4 ++ packages/utils/src/stacktrace.ts | 29 +++++++-- 30 files changed, 330 insertions(+), 173 deletions(-) diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 8265889e4967..4ee24f0ceadf 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -1,6 +1,6 @@ import { BaseClient, getEnvelopeEndpointWithUrlEncodedAuth, initAPIDetails, Scope, SDK_VERSION } from '@sentry/core'; import { Event, EventHint, Options, Severity, Transport, TransportOptions } from '@sentry/types'; -import { getGlobalObject, logger, supportsFetch } from '@sentry/utils'; +import { getGlobalObject, logger, stackParserFromOptions, supportsFetch } from '@sentry/utils'; import { eventFromException, eventFromMessage } from './eventbuilder'; import { IS_DEBUG_BUILD } from './flags'; @@ -83,14 +83,20 @@ export class BrowserClient extends BaseClient { * @inheritDoc */ public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { - return eventFromException(exception, hint, this._options.attachStacktrace); + return eventFromException(stackParserFromOptions(this._options), exception, hint, this._options.attachStacktrace); } /** * @inheritDoc */ public eventFromMessage(message: string, level: Severity = Severity.Info, hint?: EventHint): PromiseLike { - return eventFromMessage(message, level, hint, this._options.attachStacktrace); + return eventFromMessage( + stackParserFromOptions(this._options), + message, + level, + hint, + this._options.attachStacktrace, + ); } /** diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index d8acd41c12c0..6ed5181eefa1 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -1,8 +1,7 @@ -import { Event, EventHint, Exception, Severity, StackFrame } from '@sentry/types'; +import { Event, EventHint, Exception, Severity, StackFrame, StackParser } from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, - createStackParser, extractExceptionKeysForMessage, isDOMError, isDOMException, @@ -14,22 +13,12 @@ import { resolvedSyncPromise, } from '@sentry/utils'; -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 { +export function exceptionFromError(stackParser: StackParser, ex: Error): Exception { // Get the frames first since Opera can lose the stack if we touch anything else first - const frames = parseStackFrames(ex); + const frames = parseStackFrames(stackParser, ex); const exception: Exception = { type: ex && ex.name, @@ -51,6 +40,7 @@ export function exceptionFromError(ex: Error): Exception { * @hidden */ export function eventFromPlainObject( + stackParser: StackParser, exception: Record, syntheticException?: Error, isUnhandledRejection?: boolean, @@ -72,7 +62,7 @@ export function eventFromPlainObject( }; if (syntheticException) { - const frames = parseStackFrames(syntheticException); + const frames = parseStackFrames(stackParser, syntheticException); if (frames.length) { // event.exception.values[0] has been set above (event.exception as { values: Exception[] }).values[0].stacktrace = { frames }; @@ -85,16 +75,19 @@ export function eventFromPlainObject( /** * @hidden */ -export function eventFromError(ex: Error): Event { +export function eventFromError(stackParser: StackParser, ex: Error): Event { return { exception: { - values: [exceptionFromError(ex)], + values: [exceptionFromError(stackParser, ex)], }, }; } /** Parses stack frames from an error */ -export function parseStackFrames(ex: Error & { framesToPop?: number; stacktrace?: string }): StackFrame[] { +export function parseStackFrames( + stackParser: StackParser, + 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. @@ -103,13 +96,7 @@ export function parseStackFrames(ex: Error & { framesToPop?: number; stacktrace? const popSize = getPopSize(ex); try { - return createStackParser( - opera10StackParser, - opera11StackParser, - chromeStackParser, - winjsStackParser, - geckoStackParser, - )(stacktrace, popSize); + return stackParser(stacktrace, popSize); } catch (e) { // no-empty } @@ -155,12 +142,13 @@ function extractMessage(ex: Error & { message: { error?: Error } }): string { * @hidden */ export function eventFromException( + stackParser: StackParser, exception: unknown, hint?: EventHint, attachStacktrace?: boolean, ): PromiseLike { const syntheticException = (hint && hint.syntheticException) || undefined; - const event = eventFromUnknownInput(exception, syntheticException, attachStacktrace); + const event = eventFromUnknownInput(stackParser, exception, syntheticException, attachStacktrace); addExceptionMechanism(event); // defaults to { type: 'generic', handled: true } event.level = Severity.Error; if (hint && hint.event_id) { @@ -174,13 +162,14 @@ export function eventFromException( * @hidden */ export function eventFromMessage( + stackParser: StackParser, message: string, level: Severity = Severity.Info, hint?: EventHint, attachStacktrace?: boolean, ): PromiseLike { const syntheticException = (hint && hint.syntheticException) || undefined; - const event = eventFromString(message, syntheticException, attachStacktrace); + const event = eventFromString(stackParser, message, syntheticException, attachStacktrace); event.level = level; if (hint && hint.event_id) { event.event_id = hint.event_id; @@ -192,6 +181,7 @@ export function eventFromMessage( * @hidden */ export function eventFromUnknownInput( + stackParser: StackParser, exception: unknown, syntheticException?: Error, attachStacktrace?: boolean, @@ -202,7 +192,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; - return eventFromError(errorEvent.error as Error); + return eventFromError(stackParser, 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 @@ -216,11 +206,11 @@ export function eventFromUnknownInput( const domException = exception as DOMException; if ('stack' in (exception as Error)) { - event = eventFromError(exception as Error); + event = eventFromError(stackParser, exception as Error); } else { const name = domException.name || (isDOMError(domException) ? 'DOMError' : 'DOMException'); const message = domException.message ? `${name}: ${domException.message}` : name; - event = eventFromString(message, syntheticException, attachStacktrace); + event = eventFromString(stackParser, message, syntheticException, attachStacktrace); addExceptionTypeValue(event, message); } if ('code' in domException) { @@ -231,14 +221,14 @@ export function eventFromUnknownInput( } if (isError(exception)) { // we have a real Error object, do nothing - return eventFromError(exception); + return eventFromError(stackParser, exception); } 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 // it manually. This will allow us to group events based on top-level keys which is much better than creating a new // group on any key/value change. const objectException = exception as Record; - event = eventFromPlainObject(objectException, syntheticException, isUnhandledRejection); + event = eventFromPlainObject(stackParser, objectException, syntheticException, isUnhandledRejection); addExceptionMechanism(event, { synthetic: true, }); @@ -254,7 +244,7 @@ export function eventFromUnknownInput( // - a plain Object // // So bail out and capture it as a simple message: - event = eventFromString(exception as string, syntheticException, attachStacktrace); + event = eventFromString(stackParser, exception as string, syntheticException, attachStacktrace); addExceptionTypeValue(event, `${exception}`, undefined); addExceptionMechanism(event, { synthetic: true, @@ -266,13 +256,18 @@ export function eventFromUnknownInput( /** * @hidden */ -export function eventFromString(input: string, syntheticException?: Error, attachStacktrace?: boolean): Event { +export function eventFromString( + stackParser: StackParser, + input: string, + syntheticException?: Error, + attachStacktrace?: boolean, +): Event { const event: Event = { message: input, }; if (attachStacktrace && syntheticException) { - const frames = parseStackFrames(syntheticException); + const frames = parseStackFrames(stackParser, syntheticException); if (frames.length) { event.exception = { values: [{ value: input, stacktrace: { frames } }], diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 6204a25f614e..704096fae520 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -42,6 +42,15 @@ export { } from '@sentry/core'; export { BrowserClient, BrowserOptions } from './client'; + +export { + defaultStackParsers, + chromeStackParser, + geckoStackParser, + opera10StackParser, + opera11StackParser, + winjsStackParser, +} from './stack-parsers'; export { injectReportDialog, ReportDialogOptions } from './helpers'; export { defaultIntegrations, forceLoad, init, lastEventId, onLoad, showReportDialog, flush, close, wrap } from './sdk'; export { SDK_NAME } from './version'; diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index 19ee594afaf3..70966e4f7ec5 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Hub, Integration, Primitive, Severity } from '@sentry/types'; +import { Event, EventHint, Hub, Integration, Primitive, Severity, StackParser } from '@sentry/types'; import { addExceptionMechanism, addInstrumentationHandler, @@ -9,8 +9,10 @@ import { isPrimitive, isString, logger, + stackParserFromOptions, } from '@sentry/utils'; +import { BrowserClient } from '../client'; import { eventFromUnknownInput } from '../eventbuilder'; import { IS_DEBUG_BUILD } from '../flags'; import { shouldIgnoreOnError } from '../helpers'; @@ -79,7 +81,7 @@ function _installGlobalOnErrorHandler(): void { 'error', // eslint-disable-next-line @typescript-eslint/no-explicit-any (data: { msg: any; url: any; line: any; column: any; error: any }) => { - const [hub, attachStacktrace] = getHubAndAttachStacktrace(); + const [hub, stackParser, attachStacktrace] = getHubAndOptions(); if (!hub.getIntegration(GlobalHandlers)) { return; } @@ -92,7 +94,7 @@ function _installGlobalOnErrorHandler(): void { error === undefined && isString(msg) ? _eventFromIncompleteOnError(msg, url, line, column) : _enhanceEventWithInitialFrame( - eventFromUnknownInput(error || msg, undefined, attachStacktrace, false), + eventFromUnknownInput(stackParser, error || msg, undefined, attachStacktrace, false), url, line, column, @@ -111,7 +113,7 @@ function _installGlobalOnUnhandledRejectionHandler(): void { 'unhandledrejection', // eslint-disable-next-line @typescript-eslint/no-explicit-any (e: any) => { - const [hub, attachStacktrace] = getHubAndAttachStacktrace(); + const [hub, stackParser, attachStacktrace] = getHubAndOptions(); if (!hub.getIntegration(GlobalHandlers)) { return; } @@ -142,7 +144,7 @@ function _installGlobalOnUnhandledRejectionHandler(): void { const event = isPrimitive(error) ? _eventFromRejectionWithPrimitive(error) - : eventFromUnknownInput(error, undefined, attachStacktrace, true); + : eventFromUnknownInput(stackParser, error, undefined, attachStacktrace, true); event.level = Severity.Error; @@ -250,9 +252,11 @@ function addMechanismAndCapture(hub: Hub, error: EventHint['originalException'], }); } -function getHubAndAttachStacktrace(): [Hub, boolean | undefined] { +function getHubAndOptions(): [Hub, StackParser, boolean | undefined] { const hub = getCurrentHub(); - const client = hub.getClient(); - const attachStacktrace = client && client.getOptions().attachStacktrace; - return [hub, attachStacktrace]; + const client = hub.getClient(); + const options = client?.getOptions(); + const parser = stackParserFromOptions(options); + const attachStacktrace = options?.attachStacktrace; + return [hub, parser, attachStacktrace]; } diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index fa197b48e681..1cc30c182950 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -1,7 +1,8 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types'; -import { isInstanceOf } from '@sentry/utils'; +import { Event, EventHint, Exception, ExtendedError, Integration, StackParser } from '@sentry/types'; +import { isInstanceOf, stackParserFromOptions } from '@sentry/utils'; +import { BrowserClient } from '../client'; import { exceptionFromError } from '../eventbuilder'; const DEFAULT_KEY = 'cause'; @@ -46,9 +47,12 @@ export class LinkedErrors implements Integration { * @inheritDoc */ public setupOnce(): void { + const options = getCurrentHub().getClient()?.getOptions(); + const parser = stackParserFromOptions(options); + addGlobalEventProcessor((event: Event, hint?: EventHint) => { const self = getCurrentHub().getIntegration(LinkedErrors); - return self ? _handler(self._key, self._limit, event, hint) : event; + return self ? _handler(parser, self._key, self._limit, event, hint) : event; }); } } @@ -56,11 +60,17 @@ export class LinkedErrors implements Integration { /** * @inheritDoc */ -export function _handler(key: string, limit: number, event: Event, hint?: EventHint): Event | null { +export function _handler( + parser: StackParser, + key: string, + limit: number, + event: Event, + hint?: EventHint, +): Event | null { if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { return event; } - const linkedErrors = _walkErrorTree(limit, hint.originalException as ExtendedError, key); + const linkedErrors = _walkErrorTree(parser, limit, hint.originalException as ExtendedError, key); event.exception.values = [...linkedErrors, ...event.exception.values]; return event; } @@ -68,10 +78,16 @@ export function _handler(key: string, limit: number, event: Event, hint?: EventH /** * JSDOC */ -export function _walkErrorTree(limit: number, error: ExtendedError, key: string, stack: Exception[] = []): Exception[] { +export function _walkErrorTree( + parser: StackParser, + limit: number, + error: ExtendedError, + key: string, + stack: Exception[] = [], +): Exception[] { if (!isInstanceOf(error[key], Error) || stack.length + 1 >= limit) { return stack; } - const exception = exceptionFromError(error[key]); - return _walkErrorTree(limit, error[key], key, [exception, ...stack]); + const exception = exceptionFromError(parser, error[key]); + return _walkErrorTree(parser, limit, error[key], key, [exception, ...stack]); } diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 44d66add18c3..60c53b364e9c 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -6,6 +6,7 @@ import { BrowserClient, BrowserOptions } from './client'; import { IS_DEBUG_BUILD } from './flags'; import { ReportDialogOptions, wrap as internalWrap } from './helpers'; import { Breadcrumbs, Dedupe, GlobalHandlers, LinkedErrors, TryCatch, UserAgent } from './integrations'; +import { defaultStackParsers } from './stack-parsers'; export const defaultIntegrations = [ new CoreIntegrations.InboundFilters(), @@ -92,6 +93,9 @@ export function init(options: BrowserOptions = {}): void { if (options.sendClientReports === undefined) { options.sendClientReports = true; } + if (options.stackParser === undefined) { + options.stackParser = defaultStackParsers; + } initAndBind(BrowserClient, options); diff --git a/packages/browser/src/stack-parsers.ts b/packages/browser/src/stack-parsers.ts index f54c1df803e9..5ee652001854 100644 --- a/packages/browser/src/stack-parsers.ts +++ b/packages/browser/src/stack-parsers.ts @@ -1,5 +1,4 @@ -import { StackFrame } from '@sentry/types'; -import { StackLineParser, StackLineParserFn } from '@sentry/utils'; +import { StackFrame, StackLineParser, StackLineParserFn } from '@sentry/types'; // global reference to slice const UNKNOWN_FUNCTION = '?'; @@ -131,6 +130,14 @@ const opera11: StackLineParserFn = line => { export const opera11StackParser: StackLineParser = [OPERA11_PRIORITY, opera11]; +export const defaultStackParsers = [ + chromeStackParser, + geckoStackParser, + opera10StackParser, + opera11StackParser, + winjsStackParser, +]; + /** * 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/integrations/linkederrors.test.ts b/packages/browser/test/unit/integrations/linkederrors.test.ts index 8178741d1210..2589487dbcac 100644 --- a/packages/browser/test/unit/integrations/linkederrors.test.ts +++ b/packages/browser/test/unit/integrations/linkederrors.test.ts @@ -1,7 +1,11 @@ import { ExtendedError } from '@sentry/types'; +import { createStackParser } from '@sentry/utils'; import { BrowserClient } from '../../../src/client'; import * as LinkedErrorsModule from '../../../src/integrations/linkederrors'; +import { defaultStackParsers } from '../../../src/stack-parsers'; + +const parser = createStackParser(...defaultStackParsers); describe('LinkedErrors', () => { describe('handler', () => { @@ -9,7 +13,7 @@ describe('LinkedErrors', () => { const event = { message: 'foo', }; - const result = LinkedErrorsModule._handler('cause', 5, event); + const result = LinkedErrorsModule._handler(parser, 'cause', 5, event); expect(result).toEqual(event); }); @@ -20,7 +24,7 @@ describe('LinkedErrors', () => { }, message: 'foo', }; - const result = LinkedErrorsModule._handler('cause', 5, event); + const result = LinkedErrorsModule._handler(parser, 'cause', 5, event); expect(result).toEqual(event); }); @@ -34,9 +38,9 @@ describe('LinkedErrors', () => { one.cause = two; const originalException = one; - const client = new BrowserClient({}); + const client = new BrowserClient({ stackParser: parser }); return client.eventFromException(originalException).then(event => { - const result = LinkedErrorsModule._handler('cause', 5, event, { + const result = LinkedErrorsModule._handler(parser, 'cause', 5, event, { originalException, }); @@ -64,9 +68,9 @@ describe('LinkedErrors', () => { one.reason = two; const originalException = one; - const client = new BrowserClient({}); + const client = new BrowserClient({ stackParser: parser }); return client.eventFromException(originalException).then(event => { - const result = LinkedErrorsModule._handler('reason', 5, event, { + const result = LinkedErrorsModule._handler(parser, 'reason', 5, event, { originalException, }); @@ -90,10 +94,10 @@ describe('LinkedErrors', () => { one.cause = two; two.cause = three; - const client = new BrowserClient({}); + const client = new BrowserClient({ stackParser: parser }); const originalException = one; return client.eventFromException(originalException).then(event => { - const result = LinkedErrorsModule._handler('cause', 2, event, { + const result = LinkedErrorsModule._handler(parser, 'cause', 2, event, { originalException, }); diff --git a/packages/browser/test/unit/tracekit/chromium.test.ts b/packages/browser/test/unit/tracekit/chromium.test.ts index e40abffd8d1f..67189984563a 100644 --- a/packages/browser/test/unit/tracekit/chromium.test.ts +++ b/packages/browser/test/unit/tracekit/chromium.test.ts @@ -1,9 +1,14 @@ +import { createStackParser } from '@sentry/utils'; + import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParsers } from '../../../src/stack-parsers'; + +const parser = createStackParser(...defaultStackParsers); 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 ex = exceptionFromError(NO_LOCATION); + const ex = exceptionFromError(parser, NO_LOCATION); expect(ex).toEqual({ value: 'foo', @@ -25,7 +30,7 @@ describe('Tracekit - Chrome Tests', () => { ' at http://path/to/file.js:24:4', }; - const ex = exceptionFromError(CHROME_15); + const ex = exceptionFromError(parser, CHROME_15); expect(ex).toEqual({ value: "Object # has no method 'undef'", @@ -52,7 +57,7 @@ describe('Tracekit - Chrome Tests', () => { ' at I.e.fn.(anonymous function) [as index] (http://localhost:8080/file.js:10:3651)', }; - const ex = exceptionFromError(CHROME_36); + const ex = exceptionFromError(parser, CHROME_36); expect(ex).toEqual({ value: 'Default error', @@ -98,7 +103,7 @@ describe('Tracekit - Chrome Tests', () => { ' at TESTTESTTEST.proxiedMethod(webpack:///./~/react-proxy/modules/createPrototypeProxy.js?:44:30)', }; - const ex = exceptionFromError(CHROME_XX_WEBPACK); + const ex = exceptionFromError(parser, CHROME_XX_WEBPACK); expect(ex).toEqual({ value: "Cannot read property 'error' of undefined", @@ -151,7 +156,7 @@ describe('Tracekit - Chrome Tests', () => { 'at http://localhost:8080/file.js:31:13\n', }; - const ex = exceptionFromError(CHROME_48_EVAL); + const ex = exceptionFromError(parser, CHROME_48_EVAL); expect(ex).toEqual({ value: 'message string', @@ -183,7 +188,7 @@ describe('Tracekit - Chrome Tests', () => { ' at n.handle (blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379:7:2863)', }; - const ex = exceptionFromError(CHROME_48_BLOB); + const ex = exceptionFromError(parser, CHROME_48_BLOB); expect(ex).toEqual({ value: 'Error: test', @@ -246,7 +251,7 @@ describe('Tracekit - Chrome Tests', () => { at examplescheme://examplehost/cd351f7250857e22ceaa.worker.js:70179:15`, }; - const ex = exceptionFromError(CHROMIUM_EMBEDDED_FRAMEWORK_CUSTOM_SCHEME); + const ex = exceptionFromError(parser, CHROMIUM_EMBEDDED_FRAMEWORK_CUSTOM_SCHEME); expect(ex).toEqual({ value: 'message string', @@ -276,7 +281,7 @@ describe('Tracekit - Chrome Tests', () => { at http://localhost:5000/test:24:7`, }; - const ex = exceptionFromError(CHROME73_NATIVE_CODE_EXCEPTION); + const ex = exceptionFromError(parser, CHROME73_NATIVE_CODE_EXCEPTION); expect(ex).toEqual({ value: 'test', @@ -309,7 +314,7 @@ describe('Tracekit - Chrome Tests', () => { at http://localhost:5000/:50:19`, }; - const ex = exceptionFromError(CHROME73_EVAL_EXCEPTION); + const ex = exceptionFromError(parser, CHROME73_EVAL_EXCEPTION); expect(ex).toEqual({ value: 'bad', @@ -342,7 +347,7 @@ describe('Tracekit - Chrome Tests', () => { at Global code (http://localhost:5000/test:24:7)`, }; - const ex = exceptionFromError(EDGE44_NATIVE_CODE_EXCEPTION); + const ex = exceptionFromError(parser, EDGE44_NATIVE_CODE_EXCEPTION); expect(ex).toEqual({ value: 'test', @@ -375,7 +380,7 @@ describe('Tracekit - Chrome Tests', () => { at Anonymous function (http://localhost:5000/:50:8)`, }; - const ex = exceptionFromError(EDGE44_EVAL_EXCEPTION); + const ex = exceptionFromError(parser, EDGE44_EVAL_EXCEPTION); expect(ex).toEqual({ value: 'aha', @@ -411,7 +416,7 @@ describe('Tracekit - Chrome Tests', () => { at TESTTESTTEST.someMethod (C:\\Users\\user\\path\\to\\file.js:295:108)`, }; - const ex = exceptionFromError(CHROME_ELECTRON_RENDERER); + const ex = exceptionFromError(parser, CHROME_ELECTRON_RENDERER); expect(ex).toEqual({ value: "Cannot read property 'error' of undefined", diff --git a/packages/browser/test/unit/tracekit/firefox.test.ts b/packages/browser/test/unit/tracekit/firefox.test.ts index a14fae1e38cc..87929568c857 100644 --- a/packages/browser/test/unit/tracekit/firefox.test.ts +++ b/packages/browser/test/unit/tracekit/firefox.test.ts @@ -1,4 +1,9 @@ +import { createStackParser } from '@sentry/utils'; + import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParsers } from '../../../src/stack-parsers'; + +const parser = createStackParser(...defaultStackParsers); describe('Tracekit - Firefox Tests', () => { it('should parse Firefox 3 error', () => { @@ -18,7 +23,7 @@ describe('Tracekit - Firefox Tests', () => { '', }; - const ex = exceptionFromError(FIREFOX_3); + const ex = exceptionFromError(parser, FIREFOX_3); expect(ex).toEqual({ value: 'this.undef is not a function', @@ -54,7 +59,7 @@ describe('Tracekit - Firefox Tests', () => { '', }; - const ex = exceptionFromError(FIREFOX_7); + const ex = exceptionFromError(parser, FIREFOX_7); expect(ex).toEqual({ value: 'bar', @@ -86,7 +91,7 @@ describe('Tracekit - Firefox Tests', () => { lineNumber: 48, }; - const ex = exceptionFromError(FIREFOX_14); + const ex = exceptionFromError(parser, FIREFOX_14); expect(ex).toEqual({ value: 'x is null', @@ -115,7 +120,7 @@ describe('Tracekit - Firefox Tests', () => { columnNumber: 12, }; - const ex = exceptionFromError(FIREFOX_31); + const ex = exceptionFromError(parser, FIREFOX_31); expect(ex).toEqual({ value: 'Default error', @@ -150,7 +155,7 @@ describe('Tracekit - Firefox Tests', () => { result: 2147500037, }; - const ex = exceptionFromError(FIREFOX_44_NS_EXCEPTION); + const ex = exceptionFromError(parser, FIREFOX_44_NS_EXCEPTION); expect(ex).toEqual({ value: 'No error message', @@ -185,7 +190,7 @@ describe('Tracekit - Firefox Tests', () => { name: 'TypeError', }; - const ex = exceptionFromError(FIREFOX_50_RESOURCE_URL); + const ex = exceptionFromError(parser, FIREFOX_50_RESOURCE_URL); expect(ex).toEqual({ value: 'this.props.raw[this.state.dataSource].rows is undefined', @@ -233,7 +238,7 @@ describe('Tracekit - Firefox Tests', () => { '@http://localhost:8080/file.js:33:9', }; - const ex = exceptionFromError(FIREFOX_43_EVAL); + const ex = exceptionFromError(parser, FIREFOX_43_EVAL); expect(ex).toEqual({ value: 'message string', @@ -259,7 +264,7 @@ describe('Tracekit - Firefox Tests', () => { @http://localhost:5000/test:24:7`, }; - const stacktrace = exceptionFromError(FIREFOX66_NATIVE_CODE_EXCEPTION); + const stacktrace = exceptionFromError(parser, FIREFOX66_NATIVE_CODE_EXCEPTION); expect(stacktrace).toEqual({ value: 'test', @@ -289,7 +294,7 @@ describe('Tracekit - Firefox Tests', () => { @http://localhost:5000/:50:19`, }; - const stacktrace = exceptionFromError(FIREFOX66_EVAL_EXCEPTION); + const stacktrace = exceptionFromError(parser, FIREFOX66_EVAL_EXCEPTION); expect(stacktrace).toEqual({ value: 'aha', diff --git a/packages/browser/test/unit/tracekit/ie.test.ts b/packages/browser/test/unit/tracekit/ie.test.ts index cfd60ab2e6c4..9a796060f1cc 100644 --- a/packages/browser/test/unit/tracekit/ie.test.ts +++ b/packages/browser/test/unit/tracekit/ie.test.ts @@ -1,4 +1,9 @@ +import { createStackParser } from '@sentry/utils'; + import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParsers } from '../../../src/stack-parsers'; + +const parser = createStackParser(...defaultStackParsers); describe('Tracekit - IE Tests', () => { it('should parse IE 10 error', () => { @@ -14,7 +19,7 @@ describe('Tracekit - IE Tests', () => { number: -2146823281, }; - const ex = exceptionFromError(IE_10); + const ex = exceptionFromError(parser, IE_10); // TODO: func should be normalized expect(ex).toEqual({ @@ -43,7 +48,7 @@ describe('Tracekit - IE Tests', () => { number: -2146823281, }; - const ex = exceptionFromError(IE_11); + const ex = exceptionFromError(parser, IE_11); // TODO: func should be normalized expect(ex).toEqual({ @@ -72,7 +77,7 @@ describe('Tracekit - IE Tests', () => { number: -2146823279, }; - const ex = exceptionFromError(IE_11_EVAL); + const ex = exceptionFromError(parser, IE_11_EVAL); expect(ex).toEqual({ value: "'getExceptionProps' is undefined", diff --git a/packages/browser/test/unit/tracekit/misc.test.ts b/packages/browser/test/unit/tracekit/misc.test.ts index 3aa59754cc9a..976f39e2449e 100644 --- a/packages/browser/test/unit/tracekit/misc.test.ts +++ b/packages/browser/test/unit/tracekit/misc.test.ts @@ -1,4 +1,9 @@ +import { createStackParser } from '@sentry/utils'; + import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParsers } from '../../../src/stack-parsers'; + +const parser = createStackParser(...defaultStackParsers); describe('Tracekit - Misc Tests', () => { it('should parse PhantomJS 1.19 error', () => { @@ -11,7 +16,7 @@ describe('Tracekit - Misc Tests', () => { ' at foo (http://path/to/file.js:4283)\n' + ' at http://path/to/file.js:4287', }; - const ex = exceptionFromError(PHANTOMJS_1_19); + const ex = exceptionFromError(parser, PHANTOMJS_1_19); expect(ex).toEqual({ value: 'bar', diff --git a/packages/browser/test/unit/tracekit/opera.test.ts b/packages/browser/test/unit/tracekit/opera.test.ts index 472c4a55e2ca..a97675824e18 100644 --- a/packages/browser/test/unit/tracekit/opera.test.ts +++ b/packages/browser/test/unit/tracekit/opera.test.ts @@ -1,4 +1,9 @@ +import { createStackParser } from '@sentry/utils'; + import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParsers } from '../../../src/stack-parsers'; + +const parser = createStackParser(...defaultStackParsers); describe('Tracekit - Opera Tests', () => { it('should parse Opera 10 error', () => { @@ -24,7 +29,7 @@ describe('Tracekit - Opera Tests', () => { '', }; - const ex = exceptionFromError(OPERA_10); + const ex = exceptionFromError(parser, OPERA_10); expect(ex).toEqual({ value: 'Statement on line 42: Type mismatch (usually non-object value supplied where object required)', @@ -70,7 +75,7 @@ describe('Tracekit - Opera Tests', () => { ' foo();', }; - const ex = exceptionFromError(OPERA_11); + const ex = exceptionFromError(parser, OPERA_11); expect(ex).toEqual({ value: "'this.undef' is not a function", @@ -107,7 +112,7 @@ describe('Tracekit - Opera Tests', () => { ' dumpException3();', }; - const ex = exceptionFromError(OPERA_12); + const ex = exceptionFromError(parser, OPERA_12); expect(ex).toEqual({ value: "Cannot convert 'x' to object", @@ -151,7 +156,7 @@ describe('Tracekit - Opera Tests', () => { ' at bar (http://path/to/file.js:108:168)', }; - const ex = exceptionFromError(OPERA_25); + const ex = exceptionFromError(parser, OPERA_25); expect(ex).toEqual({ value: "Cannot read property 'undef' of null", diff --git a/packages/browser/test/unit/tracekit/react-native.test.ts b/packages/browser/test/unit/tracekit/react-native.test.ts index 6935acd615fd..ac469a92246a 100644 --- a/packages/browser/test/unit/tracekit/react-native.test.ts +++ b/packages/browser/test/unit/tracekit/react-native.test.ts @@ -1,4 +1,9 @@ +import { createStackParser } from '@sentry/utils'; + import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParsers } from '../../../src/stack-parsers'; + +const parser = createStackParser(...defaultStackParsers); describe('Tracekit - React Native Tests', () => { it('should parse exceptions for react-native-v8', () => { @@ -14,7 +19,7 @@ describe('Tracekit - React Native Tests', () => { at Object.y(index.android.bundle:93:571) at P(index.android.bundle:93:714)`, }; - const stacktrace = exceptionFromError(REACT_NATIVE_V8_EXCEPTION); + const stacktrace = exceptionFromError(parser, REACT_NATIVE_V8_EXCEPTION); expect(stacktrace).toEqual({ value: 'Manually triggered crash to test Sentry reporting', @@ -61,7 +66,7 @@ describe('Tracekit - React Native Tests', () => { p@/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3:96:385 forEach@[native code]`, }; - const stacktrace = exceptionFromError(REACT_NATIVE_EXPO_EXCEPTION); + const stacktrace = exceptionFromError(parser, REACT_NATIVE_EXPO_EXCEPTION); expect(stacktrace).toEqual({ value: 'Test Error Expo', @@ -122,7 +127,7 @@ 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 ex = exceptionFromError(ANDROID_REACT_NATIVE); + const ex = exceptionFromError(parser, ANDROID_REACT_NATIVE); expect(ex).toEqual({ value: 'Error: test', @@ -241,7 +246,7 @@ describe('Tracekit - React Native Tests', () => { '[native code]', }; - const ex = exceptionFromError(ANDROID_REACT_NATIVE_PROD); + const ex = exceptionFromError(parser, ANDROID_REACT_NATIVE_PROD); expect(ex).toEqual({ value: 'Error: test', @@ -352,7 +357,7 @@ 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 ex = exceptionFromError(ANDROID_REACT_NATIVE_HERMES); + const ex = exceptionFromError(parser, ANDROID_REACT_NATIVE_HERMES); expect(ex).toEqual({ value: 'Error: lets throw!', diff --git a/packages/browser/test/unit/tracekit/react.test.ts b/packages/browser/test/unit/tracekit/react.test.ts index dba60cceab4f..55229b333403 100644 --- a/packages/browser/test/unit/tracekit/react.test.ts +++ b/packages/browser/test/unit/tracekit/react.test.ts @@ -1,4 +1,9 @@ +import { createStackParser } from '@sentry/utils'; + import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParsers } from '../../../src/stack-parsers'; + +const parser = createStackParser(...defaultStackParsers); describe('Tracekit - React Tests', () => { it('should correctly parse Invariant Violation errors and use framesToPop to drop info message', () => { @@ -14,7 +19,7 @@ describe('Tracekit - React Tests', () => { at f (http://localhost:5000/:1:980)`, }; - const ex = exceptionFromError(REACT_INVARIANT_VIOLATION_EXCEPTION); + const ex = exceptionFromError(parser, REACT_INVARIANT_VIOLATION_EXCEPTION); expect(ex).toEqual({ value: @@ -61,7 +66,7 @@ describe('Tracekit - React Tests', () => { at f (http://localhost:5000/:1:980)`, }; - const ex = exceptionFromError(REACT_PRODUCTION_ERROR); + const ex = exceptionFromError(parser, REACT_PRODUCTION_ERROR); expect(ex).toEqual({ value: @@ -109,7 +114,7 @@ describe('Tracekit - React Tests', () => { at f (http://localhost:5000/:1:980)`, }; - const ex = exceptionFromError(REACT_PRODUCTION_ERROR); + const ex = exceptionFromError(parser, REACT_PRODUCTION_ERROR); expect(ex).toEqual({ value: diff --git a/packages/browser/test/unit/tracekit/safari.test.ts b/packages/browser/test/unit/tracekit/safari.test.ts index 6342899e1ca4..beff492a6c1d 100644 --- a/packages/browser/test/unit/tracekit/safari.test.ts +++ b/packages/browser/test/unit/tracekit/safari.test.ts @@ -1,4 +1,9 @@ +import { createStackParser } from '@sentry/utils'; + import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParsers } from '../../../src/stack-parsers'; + +const parser = createStackParser(...defaultStackParsers); describe('Tracekit - Safari Tests', () => { it('should parse Safari 6 error', () => { @@ -14,7 +19,7 @@ describe('Tracekit - Safari Tests', () => { sourceURL: 'http://path/to/file.js', }; - const stackFrames = exceptionFromError(SAFARI_6); + const stackFrames = exceptionFromError(parser, SAFARI_6); expect(stackFrames).toEqual({ value: "'null' is not an object (evaluating 'x.undef')", @@ -40,7 +45,7 @@ describe('Tracekit - Safari Tests', () => { sourceURL: 'http://path/to/file.js', }; - const stackFrames = exceptionFromError(SAFARI_7); + const stackFrames = exceptionFromError(parser, SAFARI_7); expect(stackFrames).toEqual({ value: "'null' is not an object (evaluating 'x.undef')", @@ -66,7 +71,7 @@ describe('Tracekit - Safari Tests', () => { sourceURL: 'http://path/to/file.js', }; - const stackFrames = exceptionFromError(SAFARI_8); + const stackFrames = exceptionFromError(parser, SAFARI_8); expect(stackFrames).toEqual({ value: "null is not an object (evaluating 'x.undef')", @@ -96,7 +101,7 @@ describe('Tracekit - Safari Tests', () => { column: 18, }; - const stackFrames = exceptionFromError(SAFARI_8_EVAL); + const stackFrames = exceptionFromError(parser, SAFARI_8_EVAL); expect(stackFrames).toEqual({ value: "Can't find variable: getExceptionProps", @@ -121,7 +126,7 @@ describe('Tracekit - Safari Tests', () => { at safari-extension:(//3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js:3313:26)`, }; - const ex = exceptionFromError(SAFARI_EXTENSION_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI_EXTENSION_EXCEPTION); expect(ex).toEqual({ value: 'wat', @@ -155,7 +160,7 @@ describe('Tracekit - Safari Tests', () => { safari-extension://com.grammarly.safari.extension.ext2-W8F64X92K3/ee7759dd/Grammarly.js:2:1588410 promiseReactionJob@[native code]`, }; - const ex = exceptionFromError(SAFARI_EXTENSION_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI_EXTENSION_EXCEPTION); expect(ex).toEqual({ value: "undefined is not an object (evaluating 'e.groups.includes')", @@ -191,7 +196,7 @@ describe('Tracekit - Safari Tests', () => { at safari-web-extension:(//3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js:3313:26)`, }; - const ex = exceptionFromError(SAFARI_WEB_EXTENSION_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI_WEB_EXTENSION_EXCEPTION); expect(ex).toEqual({ value: 'wat', @@ -225,7 +230,7 @@ describe('Tracekit - Safari Tests', () => { safari-web-extension://46434E60-F5BD-48A4-80C8-A422C5D16897/scripts/content-script.js:29:56027 promiseReactionJob@[native code]`, }; - const ex = exceptionFromError(SAFARI_EXTENSION_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI_EXTENSION_EXCEPTION); expect(ex).toEqual({ value: "undefined is not an object (evaluating 'e.groups.includes')", @@ -263,7 +268,7 @@ describe('Tracekit - Safari Tests', () => { global code@http://localhost:5000/test:24:10`, }; - const ex = exceptionFromError(SAFARI12_NATIVE_CODE_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI12_NATIVE_CODE_EXCEPTION); expect(ex).toEqual({ value: 'test', @@ -297,7 +302,7 @@ describe('Tracekit - Safari Tests', () => { http://localhost:5000/:50:29`, }; - const ex = exceptionFromError(SAFARI12_EVAL_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI12_EVAL_EXCEPTION); expect(ex).toEqual({ value: 'aha', diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index 4401f4016520..81f808f5ef56 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -1,7 +1,7 @@ import { BaseClient, getEnvelopeEndpointWithUrlEncodedAuth, initAPIDetails, Scope, SDK_VERSION } from '@sentry/core'; import { SessionFlusher } from '@sentry/hub'; import { Event, EventHint, Severity, Transport, TransportOptions } from '@sentry/types'; -import { logger, makeDsn, resolvedSyncPromise } from '@sentry/utils'; +import { logger, makeDsn, resolvedSyncPromise, stackParserFromOptions } from '@sentry/utils'; import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; import { IS_DEBUG_BUILD } from './flags'; @@ -112,14 +112,16 @@ export class NodeClient extends BaseClient { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types public eventFromException(exception: any, hint?: EventHint): PromiseLike { - return resolvedSyncPromise(eventFromUnknownInput(exception, hint)); + return resolvedSyncPromise(eventFromUnknownInput(stackParserFromOptions(this._options), exception, hint)); } /** * @inheritDoc */ public eventFromMessage(message: string, level: Severity = Severity.Info, hint?: EventHint): PromiseLike { - return resolvedSyncPromise(eventFromMessage(message, level, hint, this._options.attachStacktrace)); + return resolvedSyncPromise( + eventFromMessage(stackParserFromOptions(this._options), message, level, hint, this._options.attachStacktrace), + ); } /** diff --git a/packages/node/src/eventbuilder.ts b/packages/node/src/eventbuilder.ts index 1287f2a47f48..e0b6b16261f0 100644 --- a/packages/node/src/eventbuilder.ts +++ b/packages/node/src/eventbuilder.ts @@ -1,34 +1,31 @@ import { getCurrentHub } from '@sentry/hub'; -import { Event, EventHint, Exception, Mechanism, Severity, StackFrame } from '@sentry/types'; +import { Event, EventHint, Exception, Mechanism, Severity, StackFrame, StackParser } from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, - createStackParser, extractExceptionKeysForMessage, isError, isPlainObject, normalizeToSize, } from '@sentry/utils'; -import { nodeStackParser } from './stack-parser'; - /** * Extracts stack frames from the error.stack string */ -export function parseStackFrames(error: Error): StackFrame[] { - return createStackParser(nodeStackParser)(error.stack || '', 1); +export function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] { + return stackParser(error.stack || '', 1); } /** * Extracts stack frames from the error and builds a Sentry Exception */ -export function exceptionFromError(error: Error): Exception { +export function exceptionFromError(stackParser: StackParser, error: Error): Exception { const exception: Exception = { type: error.name || error.constructor.name, value: error.message, }; - const frames = parseStackFrames(error); + const frames = parseStackFrames(stackParser, error); if (frames.length) { exception.stacktrace = { frames }; } @@ -40,7 +37,7 @@ export function exceptionFromError(error: Error): Exception { * Builds and Event from a Exception * @hidden */ -export function eventFromUnknownInput(exception: unknown, hint?: EventHint): Event { +export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event { // eslint-disable-next-line @typescript-eslint/no-explicit-any let ex: unknown = exception; const providedMechanism: Mechanism | undefined = @@ -73,7 +70,7 @@ export function eventFromUnknownInput(exception: unknown, hint?: EventHint): Eve const event = { exception: { - values: [exceptionFromError(ex as Error)], + values: [exceptionFromError(stackParser, ex as Error)], }, }; @@ -91,6 +88,7 @@ export function eventFromUnknownInput(exception: unknown, hint?: EventHint): Eve * @hidden */ export function eventFromMessage( + stackParser: StackParser, message: string, level: Severity = Severity.Info, hint?: EventHint, @@ -103,7 +101,7 @@ export function eventFromMessage( }; if (attachStacktrace && hint && hint.syntheticException) { - const frames = parseStackFrames(hint.syntheticException); + const frames = parseStackFrames(stackParser, hint.syntheticException); if (frames.length) { event.exception = { values: [ diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 591fd4ad911d..d6a924a04c2e 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -46,6 +46,7 @@ export { NodeClient } from './client'; export { defaultIntegrations, init, lastEventId, flush, close, getSentryRelease } from './sdk'; export { deepReadDirSync } from './utils'; export { SDK_NAME } from './version'; +export { nodeStackParser } from './stack-parser'; import { Integrations as CoreIntegrations } from '@sentry/core'; import { getMainCarrier } from '@sentry/hub'; diff --git a/packages/node/src/integrations/linkederrors.ts b/packages/node/src/integrations/linkederrors.ts index 701400594971..c1dfd285db84 100644 --- a/packages/node/src/integrations/linkederrors.ts +++ b/packages/node/src/integrations/linkederrors.ts @@ -1,7 +1,8 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types'; -import { isInstanceOf, resolvedSyncPromise, SyncPromise } from '@sentry/utils'; +import { Event, EventHint, Exception, ExtendedError, Integration, StackParser } from '@sentry/types'; +import { isInstanceOf, resolvedSyncPromise, stackParserFromOptions, SyncPromise } from '@sentry/utils'; +import { NodeClient } from '../client'; import { exceptionFromError } from '../eventbuilder'; import { ContextLines } from './contextlines'; @@ -42,12 +43,15 @@ export class LinkedErrors implements Integration { * @inheritDoc */ public setupOnce(): void { - addGlobalEventProcessor((event: Event, hint?: EventHint) => { - const self = getCurrentHub().getIntegration(LinkedErrors); + addGlobalEventProcessor(async (event: Event, hint?: EventHint) => { + const hub = getCurrentHub(); + const self = hub.getIntegration(LinkedErrors); + const stackParser = stackParserFromOptions(hub.getClient()?.getOptions()); + if (self) { - const handler = self._handler && self._handler.bind(self); - return typeof handler === 'function' ? handler(event, hint) : event; + await self._handler(stackParser, event, hint); } + return event; }); } @@ -55,13 +59,13 @@ export class LinkedErrors implements Integration { /** * @inheritDoc */ - private _handler(event: Event, hint?: EventHint): PromiseLike { + private _handler(stackParser: StackParser, event: Event, hint?: EventHint): PromiseLike { if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { return resolvedSyncPromise(event); } return new SyncPromise(resolve => { - void this._walkErrorTree(hint.originalException as Error, this._key) + void this._walkErrorTree(stackParser, hint.originalException as Error, this._key) .then((linkedErrors: Exception[]) => { if (event && event.exception && event.exception.values) { event.exception.values = [...linkedErrors, ...event.exception.values]; @@ -77,12 +81,17 @@ export class LinkedErrors implements Integration { /** * @inheritDoc */ - private async _walkErrorTree(error: ExtendedError, key: string, stack: Exception[] = []): Promise { + private async _walkErrorTree( + stackParser: StackParser, + error: ExtendedError, + key: string, + stack: Exception[] = [], + ): Promise { if (!isInstanceOf(error[key], Error) || stack.length + 1 >= this._limit) { return Promise.resolve(stack); } - const exception = exceptionFromError(error[key]); + const exception = exceptionFromError(stackParser, error[key]); // If the ContextLines integration is enabled, we add source code context to linked errors // because we can't guarantee the order that integrations are run. @@ -92,7 +101,7 @@ export class LinkedErrors implements Integration { } return new Promise((resolve, reject) => { - void this._walkErrorTree(error[key], key, [exception, ...stack]) + void this._walkErrorTree(stackParser, error[key], key, [exception, ...stack]) .then(resolve) .then(null, () => { reject(); diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 7dd660a44048..095bd3114144 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -7,6 +7,7 @@ import * as domain from 'domain'; import { NodeClient } from './client'; import { IS_DEBUG_BUILD } from './flags'; import { Console, ContextLines, Http, LinkedErrors, OnUncaughtException, OnUnhandledRejection } from './integrations'; +import { nodeStackParser } from './stack-parser'; import { NodeOptions } from './types'; export const defaultIntegrations = [ @@ -120,6 +121,10 @@ export function init(options: NodeOptions = {}): void { options.autoSessionTracking = true; } + if (options.stackParser === undefined) { + options.stackParser = [nodeStackParser]; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any if ((domain as any).active) { setHubOnCarrier(carrier, getCurrentHub()); diff --git a/packages/node/src/stack-parser.ts b/packages/node/src/stack-parser.ts index a37e38001da3..79a86e9018ee 100644 --- a/packages/node/src/stack-parser.ts +++ b/packages/node/src/stack-parser.ts @@ -1,4 +1,5 @@ -import { basename, dirname, StackLineParser, StackLineParserFn } from '@sentry/utils'; +import { StackLineParser, StackLineParserFn } from '@sentry/types'; +import { basename, dirname } from '@sentry/utils'; /** Gets the module */ function getModule(filename: string | undefined): string | undefined { diff --git a/packages/node/test/context-lines.test.ts b/packages/node/test/context-lines.test.ts index 821c95d7b6b0..55dce7be1615 100644 --- a/packages/node/test/context-lines.test.ts +++ b/packages/node/test/context-lines.test.ts @@ -1,10 +1,14 @@ import { StackFrame } from '@sentry/types'; +import { createStackParser } from '@sentry/utils'; import * as fs from 'fs'; import { parseStackFrames } from '../src/eventbuilder'; import { ContextLines, resetFileContentCache } from '../src/integrations/contextlines'; +import { nodeStackParser } from '../src/stack-parser'; import { getError } from './helper/error'; +const parser = createStackParser(nodeStackParser); + describe('ContextLines', () => { let readFileSpy: jest.SpyInstance; let contextLines: ContextLines; @@ -27,7 +31,7 @@ describe('ContextLines', () => { test('parseStack with same file', async () => { expect.assertions(1); - const frames = parseStackFrames(new Error('test')); + const frames = parseStackFrames(parser, new Error('test')); await addContext(Array.from(frames)); @@ -57,12 +61,12 @@ describe('ContextLines', () => { test('parseStack with adding different file', async () => { expect.assertions(1); - const frames = parseStackFrames(new Error('test')); + const frames = parseStackFrames(parser, new Error('test')); await addContext(frames); const numCalls = readFileSpy.mock.calls.length; - const parsedFrames = parseStackFrames(getError()); + const parsedFrames = parseStackFrames(parser, getError()); await addContext(parsedFrames); const newErrorCalls = readFileSpy.mock.calls.length; @@ -100,7 +104,7 @@ describe('ContextLines', () => { contextLines = new ContextLines({ frameContextLines: 0 }); expect.assertions(1); - const frames = parseStackFrames(new Error('test')); + const frames = parseStackFrames(parser, new Error('test')); await addContext(frames); expect(readFileSpy).toHaveBeenCalledTimes(0); diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 687465802a94..af12a4b35994 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -1,6 +1,7 @@ import { initAndBind, SDK_VERSION } from '@sentry/core'; import { getMainCarrier } from '@sentry/hub'; import { Integration } from '@sentry/types'; +import { createStackParser } from '@sentry/utils'; import * as domain from 'domain'; import { @@ -16,6 +17,9 @@ import { Scope, } from '../src'; import { ContextLines, LinkedErrors } from '../src/integrations'; +import { nodeStackParser } from '../src/stack-parser'; + +const stackParser = createStackParser(nodeStackParser); jest.mock('@sentry/core', () => { const original = jest.requireActual('@sentry/core'); @@ -86,6 +90,7 @@ describe('SentryNode', () => { test('record auto breadcrumbs', done => { const client = new NodeClient({ + stackParser, beforeSend: (event: Event) => { // TODO: It should be 3, but we don't capture a breadcrumb // for our own captureMessage/captureException calls yet @@ -117,6 +122,7 @@ describe('SentryNode', () => { expect.assertions(6); getCurrentHub().bindClient( new NodeClient({ + stackParser, beforeSend: (event: Event) => { expect(event.tags).toEqual({ test: '1' }); expect(event.exception).not.toBeUndefined(); @@ -144,6 +150,7 @@ describe('SentryNode', () => { expect.assertions(6); getCurrentHub().bindClient( new NodeClient({ + stackParser, beforeSend: (event: Event) => { expect(event.tags).toEqual({ test: '1' }); expect(event.exception).not.toBeUndefined(); @@ -171,6 +178,7 @@ describe('SentryNode', () => { expect.assertions(10); getCurrentHub().bindClient( new NodeClient({ + stackParser, beforeSend: (event: Event) => { expect(event.tags).toEqual({ test: '1' }); expect(event.exception).not.toBeUndefined(); @@ -202,6 +210,7 @@ describe('SentryNode', () => { expect.assertions(15); getCurrentHub().bindClient( new NodeClient({ + stackParser, integrations: [new ContextLines(), new LinkedErrors()], beforeSend: (event: Event) => { expect(event.exception).not.toBeUndefined(); @@ -242,6 +251,7 @@ describe('SentryNode', () => { expect.assertions(2); getCurrentHub().bindClient( new NodeClient({ + stackParser, beforeSend: (event: Event) => { expect(event.message).toBe('test'); expect(event.exception).toBeUndefined(); @@ -258,6 +268,7 @@ describe('SentryNode', () => { expect.assertions(2); getCurrentHub().bindClient( new NodeClient({ + stackParser, beforeSend: (event: Event) => { expect(event.message).toBe('test event'); expect(event.exception).toBeUndefined(); @@ -274,6 +285,7 @@ describe('SentryNode', () => { const d = domain.create(); const client = new NodeClient({ + stackParser, beforeSend: (event: Event) => { expect(event.message).toBe('test domain'); expect(event.exception).toBeUndefined(); @@ -294,6 +306,7 @@ describe('SentryNode', () => { expect.assertions(1); getCurrentHub().bindClient( new NodeClient({ + stackParser, beforeSend: (event: Event) => { expect( event.exception!.values![0].stacktrace!.frames![ diff --git a/packages/node/test/integrations/linkederrors.test.ts b/packages/node/test/integrations/linkederrors.test.ts index 47249d129716..296384a5f222 100644 --- a/packages/node/test/integrations/linkederrors.test.ts +++ b/packages/node/test/integrations/linkederrors.test.ts @@ -1,7 +1,11 @@ import { ExtendedError } from '@sentry/types'; +import { createStackParser } from '@sentry/utils'; import { Event, NodeClient } from '../../src'; import { LinkedErrors } from '../../src/integrations/linkederrors'; +import { nodeStackParser } from '../../src/stack-parser'; + +const stackParser = createStackParser(nodeStackParser); let linkedErrors: any; @@ -17,7 +21,7 @@ describe('LinkedErrors', () => { const event = { message: 'foo', }; - return linkedErrors._handler(event).then((result: any) => { + return linkedErrors._handler(stackParser, event).then((result: any) => { expect(spy.mock.calls.length).toEqual(0); expect(result).toEqual(event); }); @@ -27,13 +31,13 @@ describe('LinkedErrors', () => { expect.assertions(2); const spy = jest.spyOn(linkedErrors, '_walkErrorTree'); const one = new Error('originalException'); - const client = new NodeClient({}); + const client = new NodeClient({ stackParser }); let event: Event | undefined; return client .eventFromException(one) .then(eventFromException => { event = eventFromException; - return linkedErrors._handler(eventFromException); + return linkedErrors._handler(stackParser, eventFromException); }) .then(result => { expect(spy.mock.calls.length).toEqual(0); @@ -50,10 +54,10 @@ describe('LinkedErrors', () => { }), ); const one = new Error('originalException'); - const client = new NodeClient({}); + const client = new NodeClient({ stackParser }); return client.eventFromException(one).then(event => linkedErrors - ._handler(event, { + ._handler(stackParser, event, { originalException: one, }) .then((_: any) => { @@ -70,10 +74,10 @@ describe('LinkedErrors', () => { one.cause = two; two.cause = three; - const client = new NodeClient({}); + const client = new NodeClient({ stackParser }); return client.eventFromException(one).then(event => linkedErrors - ._handler(event, { + ._handler(stackParser, event, { originalException: one, }) .then((result: any) => { @@ -103,10 +107,10 @@ describe('LinkedErrors', () => { one.reason = two; two.reason = three; - const client = new NodeClient({}); + const client = new NodeClient({ stackParser }); return client.eventFromException(one).then(event => linkedErrors - ._handler(event, { + ._handler(stackParser, event, { originalException: one, }) .then((result: any) => { @@ -136,10 +140,10 @@ describe('LinkedErrors', () => { one.cause = two; two.cause = three; - const client = new NodeClient({}); + const client = new NodeClient({ stackParser }); return client.eventFromException(one).then(event => linkedErrors - ._handler(event, { + ._handler(stackParser, event, { originalException: one, }) .then((result: any) => { diff --git a/packages/node/test/stacktrace.test.ts b/packages/node/test/stacktrace.test.ts index 656ba1a69a9b..ec5ef8b94800 100644 --- a/packages/node/test/stacktrace.test.ts +++ b/packages/node/test/stacktrace.test.ts @@ -10,7 +10,12 @@ * @license MIT */ +import { createStackParser } from '@sentry/utils'; + import { parseStackFrames } from '../src/eventbuilder'; +import { nodeStackParser } from '../src/stack-parser'; + +const stackParser = createStackParser(nodeStackParser); function testBasic() { return new Error('something went wrong'); @@ -26,17 +31,17 @@ function evalWrapper() { describe('Stack parsing', () => { test('test basic error', () => { - const frames = parseStackFrames(testBasic()); + const frames = parseStackFrames(stackParser, testBasic()); const last = frames.length - 1; expect(frames[last].filename).toEqual(__filename); expect(frames[last].function).toEqual('testBasic'); - expect(frames[last].lineno).toEqual(16); + expect(frames[last].lineno).toEqual(21); expect(frames[last].colno).toEqual(10); }); test('test error with wrapper', () => { - const frames = parseStackFrames(testWrapper()); + const frames = parseStackFrames(stackParser, testWrapper()); const last = frames.length - 1; expect(frames[last].function).toEqual('testBasic'); @@ -44,7 +49,7 @@ describe('Stack parsing', () => { }); test('test error with eval wrapper', () => { - const frames = parseStackFrames(evalWrapper()); + const frames = parseStackFrames(stackParser, evalWrapper()); const last = frames.length - 1; expect(frames[last].function).toEqual('testBasic'); @@ -59,7 +64,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 = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -83,7 +88,7 @@ describe('Stack parsing', () => { test('parses undefined stack', () => { const err = { stack: undefined }; - const trace = parseStackFrames(err as Error); + const trace = parseStackFrames(stackParser, err as Error); expect(trace).toEqual([]); }); @@ -97,7 +102,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 = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -126,7 +131,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 = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -157,7 +162,7 @@ describe('Stack parsing', () => { ' at Array.0 (native)\n' + ' at EventEmitter._tickCallback (node.js:126:26)'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -212,7 +217,7 @@ describe('Stack parsing', () => { const err = new Error(); err.stack = 'AssertionError: true == false\n' + ' at /Users/felix/code/node-fast-or-slow/lib/test_case.js:80:10'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -232,7 +237,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 = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -252,7 +257,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 = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -273,7 +278,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 = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -309,7 +314,7 @@ describe('Stack parsing', () => { ' at async onBatch (/code/node_modules/kafkajs/src/consumer/runner.js:326:9)\n' + ' at async /code/node_modules/kafkajs/src/consumer/runner.js:376:15\n'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 13651b7cd78e..20856f52aa64 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -50,7 +50,7 @@ export { Severity } from './severity'; export { SeverityLevel, SeverityLevels } from './severity'; export { Span, SpanContext } from './span'; export { StackFrame } from './stackframe'; -export { Stacktrace } from './stacktrace'; +export { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; export { CustomSamplingContext, Measurements, diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index 3fefd41ca137..a7306acdd76f 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -3,6 +3,7 @@ import { Event, EventHint } from './event'; import { Integration } from './integration'; import { CaptureContext } from './scope'; import { SdkMetadata } from './sdkmetadata'; +import { StackLineParser, StackParser } from './stacktrace'; import { SamplingContext } from './transaction'; import { Transport, TransportClass, TransportOptions } from './transport'; @@ -149,6 +150,12 @@ export interface Options { */ initialScope?: CaptureContext; + /** + * A stack parser implementation or an array of stack line parsers + * By default, a stack parser is supplied for all supported browsers + */ + stackParser?: StackParser | StackLineParser[]; + /** * Set of metadata about the SDK that can be internally used to enhance envelopes and events, * and provide additional data about every request. diff --git a/packages/types/src/stacktrace.ts b/packages/types/src/stacktrace.ts index 120a1b471af6..ae2f350f716b 100644 --- a/packages/types/src/stacktrace.ts +++ b/packages/types/src/stacktrace.ts @@ -5,3 +5,7 @@ export interface Stacktrace { frames?: StackFrame[]; frames_omitted?: [number, number]; } + +export type StackParser = (stack: string, skipFirst?: number) => StackFrame[]; +export type StackLineParserFn = (line: string) => StackFrame | undefined; +export type StackLineParser = [number, StackLineParserFn]; diff --git a/packages/utils/src/stacktrace.ts b/packages/utils/src/stacktrace.ts index 7c9dddeee298..15c5401c09bd 100644 --- a/packages/utils/src/stacktrace.ts +++ b/packages/utils/src/stacktrace.ts @@ -1,11 +1,7 @@ -import { StackFrame } from '@sentry/types'; +import { StackFrame, StackLineParser, StackParser } from '@sentry/types'; const STACKTRACE_LIMIT = 50; -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 * @@ -34,6 +30,29 @@ export function createStackParser(...parsers: StackLineParser[]): StackParser { }; } +interface StackParserOptions { + stackParser?: StackParser | StackLineParser[]; +} + +/** + * Gets a stack parser implementation from options + * + * If options contains an array of line parsers, it is converted into a parser + */ +export function stackParserFromOptions(options: StackParserOptions | undefined): StackParser { + if (options) { + if (Array.isArray(options.stackParser)) { + options.stackParser = createStackParser(...options.stackParser); + } + + if (typeof options.stackParser === 'function') { + return options.stackParser; + } + } + + return _ => []; +} + /** * @hidden */