Skip to content

Commit 4077204

Browse files
timfishAbhiPrasad
authored andcommitted
feat: 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`
1 parent 0d99333 commit 4077204

30 files changed

+330
-173
lines changed

packages/browser/src/client.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BaseClient, getEnvelopeEndpointWithUrlEncodedAuth, initAPIDetails, Scope, SDK_VERSION } from '@sentry/core';
22
import { Event, EventHint, Options, Severity, Transport, TransportOptions } from '@sentry/types';
3-
import { getGlobalObject, logger, supportsFetch } from '@sentry/utils';
3+
import { getGlobalObject, logger, stackParserFromOptions, supportsFetch } from '@sentry/utils';
44

55
import { eventFromException, eventFromMessage } from './eventbuilder';
66
import { IS_DEBUG_BUILD } from './flags';
@@ -83,14 +83,20 @@ export class BrowserClient extends BaseClient<BrowserOptions> {
8383
* @inheritDoc
8484
*/
8585
public eventFromException(exception: unknown, hint?: EventHint): PromiseLike<Event> {
86-
return eventFromException(exception, hint, this._options.attachStacktrace);
86+
return eventFromException(stackParserFromOptions(this._options), exception, hint, this._options.attachStacktrace);
8787
}
8888

8989
/**
9090
* @inheritDoc
9191
*/
9292
public eventFromMessage(message: string, level: Severity = Severity.Info, hint?: EventHint): PromiseLike<Event> {
93-
return eventFromMessage(message, level, hint, this._options.attachStacktrace);
93+
return eventFromMessage(
94+
stackParserFromOptions(this._options),
95+
message,
96+
level,
97+
hint,
98+
this._options.attachStacktrace,
99+
);
94100
}
95101

96102
/**

packages/browser/src/eventbuilder.ts

+30-35
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { Event, EventHint, Exception, Severity, StackFrame } from '@sentry/types';
1+
import { Event, EventHint, Exception, Severity, StackFrame, StackParser } from '@sentry/types';
22
import {
33
addExceptionMechanism,
44
addExceptionTypeValue,
5-
createStackParser,
65
extractExceptionKeysForMessage,
76
isDOMError,
87
isDOMException,
@@ -14,22 +13,12 @@ import {
1413
resolvedSyncPromise,
1514
} from '@sentry/utils';
1615

17-
import {
18-
chromeStackParser,
19-
geckoStackParser,
20-
opera10StackParser,
21-
opera11StackParser,
22-
winjsStackParser,
23-
} from './stack-parsers';
24-
2516
/**
2617
* This function creates an exception from an TraceKitStackTrace
27-
* @param stacktrace TraceKitStackTrace that will be converted to an exception
28-
* @hidden
2918
*/
30-
export function exceptionFromError(ex: Error): Exception {
19+
export function exceptionFromError(stackParser: StackParser, ex: Error): Exception {
3120
// Get the frames first since Opera can lose the stack if we touch anything else first
32-
const frames = parseStackFrames(ex);
21+
const frames = parseStackFrames(stackParser, ex);
3322

3423
const exception: Exception = {
3524
type: ex && ex.name,
@@ -51,6 +40,7 @@ export function exceptionFromError(ex: Error): Exception {
5140
* @hidden
5241
*/
5342
export function eventFromPlainObject(
43+
stackParser: StackParser,
5444
exception: Record<string, unknown>,
5545
syntheticException?: Error,
5646
isUnhandledRejection?: boolean,
@@ -72,7 +62,7 @@ export function eventFromPlainObject(
7262
};
7363

7464
if (syntheticException) {
75-
const frames = parseStackFrames(syntheticException);
65+
const frames = parseStackFrames(stackParser, syntheticException);
7666
if (frames.length) {
7767
// event.exception.values[0] has been set above
7868
(event.exception as { values: Exception[] }).values[0].stacktrace = { frames };
@@ -85,16 +75,19 @@ export function eventFromPlainObject(
8575
/**
8676
* @hidden
8777
*/
88-
export function eventFromError(ex: Error): Event {
78+
export function eventFromError(stackParser: StackParser, ex: Error): Event {
8979
return {
9080
exception: {
91-
values: [exceptionFromError(ex)],
81+
values: [exceptionFromError(stackParser, ex)],
9282
},
9383
};
9484
}
9585

9686
/** Parses stack frames from an error */
97-
export function parseStackFrames(ex: Error & { framesToPop?: number; stacktrace?: string }): StackFrame[] {
87+
export function parseStackFrames(
88+
stackParser: StackParser,
89+
ex: Error & { framesToPop?: number; stacktrace?: string },
90+
): StackFrame[] {
9891
// Access and store the stacktrace property before doing ANYTHING
9992
// else to it because Opera is not very good at providing it
10093
// reliably in other circumstances.
@@ -103,13 +96,7 @@ export function parseStackFrames(ex: Error & { framesToPop?: number; stacktrace?
10396
const popSize = getPopSize(ex);
10497

10598
try {
106-
return createStackParser(
107-
opera10StackParser,
108-
opera11StackParser,
109-
chromeStackParser,
110-
winjsStackParser,
111-
geckoStackParser,
112-
)(stacktrace, popSize);
99+
return stackParser(stacktrace, popSize);
113100
} catch (e) {
114101
// no-empty
115102
}
@@ -155,12 +142,13 @@ function extractMessage(ex: Error & { message: { error?: Error } }): string {
155142
* @hidden
156143
*/
157144
export function eventFromException(
145+
stackParser: StackParser,
158146
exception: unknown,
159147
hint?: EventHint,
160148
attachStacktrace?: boolean,
161149
): PromiseLike<Event> {
162150
const syntheticException = (hint && hint.syntheticException) || undefined;
163-
const event = eventFromUnknownInput(exception, syntheticException, attachStacktrace);
151+
const event = eventFromUnknownInput(stackParser, exception, syntheticException, attachStacktrace);
164152
addExceptionMechanism(event); // defaults to { type: 'generic', handled: true }
165153
event.level = Severity.Error;
166154
if (hint && hint.event_id) {
@@ -174,13 +162,14 @@ export function eventFromException(
174162
* @hidden
175163
*/
176164
export function eventFromMessage(
165+
stackParser: StackParser,
177166
message: string,
178167
level: Severity = Severity.Info,
179168
hint?: EventHint,
180169
attachStacktrace?: boolean,
181170
): PromiseLike<Event> {
182171
const syntheticException = (hint && hint.syntheticException) || undefined;
183-
const event = eventFromString(message, syntheticException, attachStacktrace);
172+
const event = eventFromString(stackParser, message, syntheticException, attachStacktrace);
184173
event.level = level;
185174
if (hint && hint.event_id) {
186175
event.event_id = hint.event_id;
@@ -192,6 +181,7 @@ export function eventFromMessage(
192181
* @hidden
193182
*/
194183
export function eventFromUnknownInput(
184+
stackParser: StackParser,
195185
exception: unknown,
196186
syntheticException?: Error,
197187
attachStacktrace?: boolean,
@@ -202,7 +192,7 @@ export function eventFromUnknownInput(
202192
if (isErrorEvent(exception as ErrorEvent) && (exception as ErrorEvent).error) {
203193
// If it is an ErrorEvent with `error` property, extract it to get actual Error
204194
const errorEvent = exception as ErrorEvent;
205-
return eventFromError(errorEvent.error as Error);
195+
return eventFromError(stackParser, errorEvent.error as Error);
206196
}
207197

208198
// 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(
216206
const domException = exception as DOMException;
217207

218208
if ('stack' in (exception as Error)) {
219-
event = eventFromError(exception as Error);
209+
event = eventFromError(stackParser, exception as Error);
220210
} else {
221211
const name = domException.name || (isDOMError(domException) ? 'DOMError' : 'DOMException');
222212
const message = domException.message ? `${name}: ${domException.message}` : name;
223-
event = eventFromString(message, syntheticException, attachStacktrace);
213+
event = eventFromString(stackParser, message, syntheticException, attachStacktrace);
224214
addExceptionTypeValue(event, message);
225215
}
226216
if ('code' in domException) {
@@ -231,14 +221,14 @@ export function eventFromUnknownInput(
231221
}
232222
if (isError(exception)) {
233223
// we have a real Error object, do nothing
234-
return eventFromError(exception);
224+
return eventFromError(stackParser, exception);
235225
}
236226
if (isPlainObject(exception) || isEvent(exception)) {
237227
// If it's a plain object or an instance of `Event` (the built-in JS kind, not this SDK's `Event` type), serialize
238228
// it manually. This will allow us to group events based on top-level keys which is much better than creating a new
239229
// group on any key/value change.
240230
const objectException = exception as Record<string, unknown>;
241-
event = eventFromPlainObject(objectException, syntheticException, isUnhandledRejection);
231+
event = eventFromPlainObject(stackParser, objectException, syntheticException, isUnhandledRejection);
242232
addExceptionMechanism(event, {
243233
synthetic: true,
244234
});
@@ -254,7 +244,7 @@ export function eventFromUnknownInput(
254244
// - a plain Object
255245
//
256246
// So bail out and capture it as a simple message:
257-
event = eventFromString(exception as string, syntheticException, attachStacktrace);
247+
event = eventFromString(stackParser, exception as string, syntheticException, attachStacktrace);
258248
addExceptionTypeValue(event, `${exception}`, undefined);
259249
addExceptionMechanism(event, {
260250
synthetic: true,
@@ -266,13 +256,18 @@ export function eventFromUnknownInput(
266256
/**
267257
* @hidden
268258
*/
269-
export function eventFromString(input: string, syntheticException?: Error, attachStacktrace?: boolean): Event {
259+
export function eventFromString(
260+
stackParser: StackParser,
261+
input: string,
262+
syntheticException?: Error,
263+
attachStacktrace?: boolean,
264+
): Event {
270265
const event: Event = {
271266
message: input,
272267
};
273268

274269
if (attachStacktrace && syntheticException) {
275-
const frames = parseStackFrames(syntheticException);
270+
const frames = parseStackFrames(stackParser, syntheticException);
276271
if (frames.length) {
277272
event.exception = {
278273
values: [{ value: input, stacktrace: { frames } }],

packages/browser/src/exports.ts

+9
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ export {
4242
} from '@sentry/core';
4343

4444
export { BrowserClient, BrowserOptions } from './client';
45+
46+
export {
47+
defaultStackParsers,
48+
chromeStackParser,
49+
geckoStackParser,
50+
opera10StackParser,
51+
opera11StackParser,
52+
winjsStackParser,
53+
} from './stack-parsers';
4554
export { injectReportDialog, ReportDialogOptions } from './helpers';
4655
export { defaultIntegrations, forceLoad, init, lastEventId, onLoad, showReportDialog, flush, close, wrap } from './sdk';
4756
export { SDK_NAME } from './version';

packages/browser/src/integrations/globalhandlers.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
22
import { getCurrentHub } from '@sentry/core';
3-
import { Event, EventHint, Hub, Integration, Primitive, Severity } from '@sentry/types';
3+
import { Event, EventHint, Hub, Integration, Primitive, Severity, StackParser } from '@sentry/types';
44
import {
55
addExceptionMechanism,
66
addInstrumentationHandler,
@@ -9,8 +9,10 @@ import {
99
isPrimitive,
1010
isString,
1111
logger,
12+
stackParserFromOptions,
1213
} from '@sentry/utils';
1314

15+
import { BrowserClient } from '../client';
1416
import { eventFromUnknownInput } from '../eventbuilder';
1517
import { IS_DEBUG_BUILD } from '../flags';
1618
import { shouldIgnoreOnError } from '../helpers';
@@ -79,7 +81,7 @@ function _installGlobalOnErrorHandler(): void {
7981
'error',
8082
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8183
(data: { msg: any; url: any; line: any; column: any; error: any }) => {
82-
const [hub, attachStacktrace] = getHubAndAttachStacktrace();
84+
const [hub, stackParser, attachStacktrace] = getHubAndOptions();
8385
if (!hub.getIntegration(GlobalHandlers)) {
8486
return;
8587
}
@@ -92,7 +94,7 @@ function _installGlobalOnErrorHandler(): void {
9294
error === undefined && isString(msg)
9395
? _eventFromIncompleteOnError(msg, url, line, column)
9496
: _enhanceEventWithInitialFrame(
95-
eventFromUnknownInput(error || msg, undefined, attachStacktrace, false),
97+
eventFromUnknownInput(stackParser, error || msg, undefined, attachStacktrace, false),
9698
url,
9799
line,
98100
column,
@@ -111,7 +113,7 @@ function _installGlobalOnUnhandledRejectionHandler(): void {
111113
'unhandledrejection',
112114
// eslint-disable-next-line @typescript-eslint/no-explicit-any
113115
(e: any) => {
114-
const [hub, attachStacktrace] = getHubAndAttachStacktrace();
116+
const [hub, stackParser, attachStacktrace] = getHubAndOptions();
115117
if (!hub.getIntegration(GlobalHandlers)) {
116118
return;
117119
}
@@ -142,7 +144,7 @@ function _installGlobalOnUnhandledRejectionHandler(): void {
142144

143145
const event = isPrimitive(error)
144146
? _eventFromRejectionWithPrimitive(error)
145-
: eventFromUnknownInput(error, undefined, attachStacktrace, true);
147+
: eventFromUnknownInput(stackParser, error, undefined, attachStacktrace, true);
146148

147149
event.level = Severity.Error;
148150

@@ -250,9 +252,11 @@ function addMechanismAndCapture(hub: Hub, error: EventHint['originalException'],
250252
});
251253
}
252254

253-
function getHubAndAttachStacktrace(): [Hub, boolean | undefined] {
255+
function getHubAndOptions(): [Hub, StackParser, boolean | undefined] {
254256
const hub = getCurrentHub();
255-
const client = hub.getClient();
256-
const attachStacktrace = client && client.getOptions().attachStacktrace;
257-
return [hub, attachStacktrace];
257+
const client = hub.getClient<BrowserClient>();
258+
const options = client?.getOptions();
259+
const parser = stackParserFromOptions(options);
260+
const attachStacktrace = options?.attachStacktrace;
261+
return [hub, parser, attachStacktrace];
258262
}

packages/browser/src/integrations/linkederrors.ts

+24-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
2-
import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types';
3-
import { isInstanceOf } from '@sentry/utils';
2+
import { Event, EventHint, Exception, ExtendedError, Integration, StackParser } from '@sentry/types';
3+
import { isInstanceOf, stackParserFromOptions } from '@sentry/utils';
44

5+
import { BrowserClient } from '../client';
56
import { exceptionFromError } from '../eventbuilder';
67

78
const DEFAULT_KEY = 'cause';
@@ -46,32 +47,47 @@ export class LinkedErrors implements Integration {
4647
* @inheritDoc
4748
*/
4849
public setupOnce(): void {
50+
const options = getCurrentHub().getClient<BrowserClient>()?.getOptions();
51+
const parser = stackParserFromOptions(options);
52+
4953
addGlobalEventProcessor((event: Event, hint?: EventHint) => {
5054
const self = getCurrentHub().getIntegration(LinkedErrors);
51-
return self ? _handler(self._key, self._limit, event, hint) : event;
55+
return self ? _handler(parser, self._key, self._limit, event, hint) : event;
5256
});
5357
}
5458
}
5559

5660
/**
5761
* @inheritDoc
5862
*/
59-
export function _handler(key: string, limit: number, event: Event, hint?: EventHint): Event | null {
63+
export function _handler(
64+
parser: StackParser,
65+
key: string,
66+
limit: number,
67+
event: Event,
68+
hint?: EventHint,
69+
): Event | null {
6070
if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) {
6171
return event;
6272
}
63-
const linkedErrors = _walkErrorTree(limit, hint.originalException as ExtendedError, key);
73+
const linkedErrors = _walkErrorTree(parser, limit, hint.originalException as ExtendedError, key);
6474
event.exception.values = [...linkedErrors, ...event.exception.values];
6575
return event;
6676
}
6777

6878
/**
6979
* JSDOC
7080
*/
71-
export function _walkErrorTree(limit: number, error: ExtendedError, key: string, stack: Exception[] = []): Exception[] {
81+
export function _walkErrorTree(
82+
parser: StackParser,
83+
limit: number,
84+
error: ExtendedError,
85+
key: string,
86+
stack: Exception[] = [],
87+
): Exception[] {
7288
if (!isInstanceOf(error[key], Error) || stack.length + 1 >= limit) {
7389
return stack;
7490
}
75-
const exception = exceptionFromError(error[key]);
76-
return _walkErrorTree(limit, error[key], key, [exception, ...stack]);
91+
const exception = exceptionFromError(parser, error[key]);
92+
return _walkErrorTree(parser, limit, error[key], key, [exception, ...stack]);
7793
}

packages/browser/src/sdk.ts

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { BrowserClient, BrowserOptions } from './client';
66
import { IS_DEBUG_BUILD } from './flags';
77
import { ReportDialogOptions, wrap as internalWrap } from './helpers';
88
import { Breadcrumbs, Dedupe, GlobalHandlers, LinkedErrors, TryCatch, UserAgent } from './integrations';
9+
import { defaultStackParsers } from './stack-parsers';
910

1011
export const defaultIntegrations = [
1112
new CoreIntegrations.InboundFilters(),
@@ -92,6 +93,9 @@ export function init(options: BrowserOptions = {}): void {
9293
if (options.sendClientReports === undefined) {
9394
options.sendClientReports = true;
9495
}
96+
if (options.stackParser === undefined) {
97+
options.stackParser = defaultStackParsers;
98+
}
9599

96100
initAndBind(BrowserClient, options);
97101

0 commit comments

Comments
 (0)