Skip to content

Commit

Permalink
ref: Expose configurable stack parser (#4902)
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
timfish committed Apr 12, 2022
1 parent 0f10358 commit f3e9384
Show file tree
Hide file tree
Showing 30 changed files with 330 additions and 173 deletions.
12 changes: 9 additions & 3 deletions 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';
Expand Down Expand Up @@ -83,14 +83,20 @@ export class BrowserClient extends BaseClient<BrowserOptions> {
* @inheritDoc
*/
public eventFromException(exception: unknown, hint?: EventHint): PromiseLike<Event> {
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<Event> {
return eventFromMessage(message, level, hint, this._options.attachStacktrace);
return eventFromMessage(
stackParserFromOptions(this._options),
message,
level,
hint,
this._options.attachStacktrace,
);
}

/**
Expand Down
65 changes: 30 additions & 35 deletions 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,
Expand All @@ -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,
Expand All @@ -51,6 +40,7 @@ export function exceptionFromError(ex: Error): Exception {
* @hidden
*/
export function eventFromPlainObject(
stackParser: StackParser,
exception: Record<string, unknown>,
syntheticException?: Error,
isUnhandledRejection?: boolean,
Expand All @@ -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 };
Expand All @@ -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.
Expand All @@ -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
}
Expand Down Expand Up @@ -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<Event> {
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) {
Expand All @@ -174,13 +162,14 @@ export function eventFromException(
* @hidden
*/
export function eventFromMessage(
stackParser: StackParser,
message: string,
level: Severity = Severity.Info,
hint?: EventHint,
attachStacktrace?: boolean,
): PromiseLike<Event> {
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;
Expand All @@ -192,6 +181,7 @@ export function eventFromMessage(
* @hidden
*/
export function eventFromUnknownInput(
stackParser: StackParser,
exception: unknown,
syntheticException?: Error,
attachStacktrace?: boolean,
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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<string, unknown>;
event = eventFromPlainObject(objectException, syntheticException, isUnhandledRejection);
event = eventFromPlainObject(stackParser, objectException, syntheticException, isUnhandledRejection);
addExceptionMechanism(event, {
synthetic: true,
});
Expand All @@ -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,
Expand All @@ -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 } }],
Expand Down
9 changes: 9 additions & 0 deletions packages/browser/src/exports.ts
Expand Up @@ -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';
22 changes: 13 additions & 9 deletions 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,
Expand All @@ -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';
Expand Down Expand Up @@ -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;
}
Expand All @@ -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,
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<BrowserClient>();
const options = client?.getOptions();
const parser = stackParserFromOptions(options);
const attachStacktrace = options?.attachStacktrace;
return [hub, parser, attachStacktrace];
}
32 changes: 24 additions & 8 deletions 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';
Expand Down Expand Up @@ -46,32 +47,47 @@ export class LinkedErrors implements Integration {
* @inheritDoc
*/
public setupOnce(): void {
const options = getCurrentHub().getClient<BrowserClient>()?.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;
});
}
}

/**
* @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;
}

/**
* 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]);
}
4 changes: 4 additions & 0 deletions packages/browser/src/sdk.ts
Expand Up @@ -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(),
Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit f3e9384

Please sign in to comment.