Skip to content

Commit 3827821

Browse files
authored
ref(stackparse): More stack parse improvements and consolidation (#4669)
- Removes browser SDK `parsers.ts` and moves code to `eventbuilder.ts` to match node SDK - There is no actual parsing occurring here any more so the filename didn't make much sense - As per the node SDK, renames browser parsers to be more clear `chrome` -> `chromeStackParser` - Adds priority to all stack parsers and sorts them when creating the parser - This is preliminary work to allow user to configure parsers - Renames `eventFromError` to `eventFromUnknownInput` to match browser SDK - Renames `extractStackFromError` to `parseStackFrames` to match browser SDK
1 parent 889024a commit 3827821

19 files changed

+213
-183
lines changed

packages/browser/src/eventbuilder.ts

+138-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,153 @@
1-
import { Event, EventHint, Severity } from '@sentry/types';
1+
import { Event, EventHint, Exception, Severity, StackFrame } from '@sentry/types';
22
import {
33
addExceptionMechanism,
44
addExceptionTypeValue,
5+
createStackParser,
6+
extractExceptionKeysForMessage,
57
isDOMError,
68
isDOMException,
79
isError,
810
isErrorEvent,
911
isEvent,
1012
isPlainObject,
13+
normalizeToSize,
1114
resolvedSyncPromise,
1215
} from '@sentry/utils';
1316

14-
import { eventFromError, eventFromPlainObject, parseStackFrames } from './parsers';
17+
import {
18+
chromeStackParser,
19+
geckoStackParser,
20+
opera10StackParser,
21+
opera11StackParser,
22+
winjsStackParser,
23+
} from './stack-parsers';
24+
25+
/**
26+
* This function creates an exception from an TraceKitStackTrace
27+
* @param stacktrace TraceKitStackTrace that will be converted to an exception
28+
* @hidden
29+
*/
30+
export function exceptionFromError(ex: Error): Exception {
31+
// Get the frames first since Opera can lose the stack if we touch anything else first
32+
const frames = parseStackFrames(ex);
33+
34+
const exception: Exception = {
35+
type: ex && ex.name,
36+
value: extractMessage(ex),
37+
};
38+
39+
if (frames.length) {
40+
exception.stacktrace = { frames };
41+
}
42+
43+
if (exception.type === undefined && exception.value === '') {
44+
exception.value = 'Unrecoverable error caught';
45+
}
46+
47+
return exception;
48+
}
49+
50+
/**
51+
* @hidden
52+
*/
53+
export function eventFromPlainObject(
54+
exception: Record<string, unknown>,
55+
syntheticException?: Error,
56+
isUnhandledRejection?: boolean,
57+
): Event {
58+
const event: Event = {
59+
exception: {
60+
values: [
61+
{
62+
type: isEvent(exception) ? exception.constructor.name : isUnhandledRejection ? 'UnhandledRejection' : 'Error',
63+
value: `Non-Error ${
64+
isUnhandledRejection ? 'promise rejection' : 'exception'
65+
} captured with keys: ${extractExceptionKeysForMessage(exception)}`,
66+
},
67+
],
68+
},
69+
extra: {
70+
__serialized__: normalizeToSize(exception),
71+
},
72+
};
73+
74+
if (syntheticException) {
75+
const frames = parseStackFrames(syntheticException);
76+
if (frames.length) {
77+
event.stacktrace = { frames };
78+
}
79+
}
80+
81+
return event;
82+
}
83+
84+
/**
85+
* @hidden
86+
*/
87+
export function eventFromError(ex: Error): Event {
88+
return {
89+
exception: {
90+
values: [exceptionFromError(ex)],
91+
},
92+
};
93+
}
94+
95+
/** Parses stack frames from an error */
96+
export function parseStackFrames(ex: Error & { framesToPop?: number; stacktrace?: string }): StackFrame[] {
97+
// Access and store the stacktrace property before doing ANYTHING
98+
// else to it because Opera is not very good at providing it
99+
// reliably in other circumstances.
100+
const stacktrace = ex.stacktrace || ex.stack || '';
101+
102+
const popSize = getPopSize(ex);
103+
104+
try {
105+
return createStackParser(
106+
opera10StackParser,
107+
opera11StackParser,
108+
chromeStackParser,
109+
winjsStackParser,
110+
geckoStackParser,
111+
)(stacktrace, popSize);
112+
} catch (e) {
113+
// no-empty
114+
}
115+
116+
return [];
117+
}
118+
119+
// Based on our own mapping pattern - https://github.com/getsentry/sentry/blob/9f08305e09866c8bd6d0c24f5b0aabdd7dd6c59c/src/sentry/lang/javascript/errormapping.py#L83-L108
120+
const reactMinifiedRegexp = /Minified React error #\d+;/i;
121+
122+
function getPopSize(ex: Error & { framesToPop?: number }): number {
123+
if (ex) {
124+
if (typeof ex.framesToPop === 'number') {
125+
return ex.framesToPop;
126+
}
127+
128+
if (reactMinifiedRegexp.test(ex.message)) {
129+
return 1;
130+
}
131+
}
132+
133+
return 0;
134+
}
135+
136+
/**
137+
* There are cases where stacktrace.message is an Event object
138+
* https://github.com/getsentry/sentry-javascript/issues/1949
139+
* In this specific case we try to extract stacktrace.message.error.message
140+
*/
141+
function extractMessage(ex: Error & { message: { error?: Error } }): string {
142+
const message = ex && ex.message;
143+
if (!message) {
144+
return 'No error message';
145+
}
146+
if (message.error && typeof message.error.message === 'string') {
147+
return message.error.message;
148+
}
149+
return message;
150+
}
15151

16152
/**
17153
* Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`.

packages/browser/src/integrations/linkederrors.ts

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

5-
import { exceptionFromError } from '../parsers';
5+
import { exceptionFromError } from '../eventbuilder';
66

77
const DEFAULT_KEY = 'cause';
88
const DEFAULT_LIMIT = 5;

packages/browser/src/parsers.ts

-128
This file was deleted.

packages/browser/src/stack-parsers.ts

+22-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { StackFrame } from '@sentry/types';
2-
import { StackLineParser } from '@sentry/utils';
2+
import { StackLineParser, StackLineParserFn } from '@sentry/utils';
33

44
// global reference to slice
55
const UNKNOWN_FUNCTION = '?';
66

7+
const OPERA10_PRIORITY = 10;
8+
const OPERA11_PRIORITY = 20;
9+
const CHROME_PRIORITY = 30;
10+
const WINJS_PRIORITY = 40;
11+
const GECKO_PRIORITY = 50;
12+
713
function createFrame(filename: string, func: string, lineno?: number, colno?: number): StackFrame {
814
const frame: StackFrame = {
915
filename,
@@ -28,7 +34,7 @@ const chromeRegex =
2834
/^\s*at (?:(.*?) ?\((?:address at )?)?((?:file|https?|blob|chrome-extension|address|native|eval|webpack|<anonymous>|[-a-z]+:|.*bundle|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
2935
const chromeEvalRegex = /\((\S*)(?::(\d+))(?::(\d+))\)/;
3036

31-
export const chrome: StackLineParser = line => {
37+
const chrome: StackLineParserFn = line => {
3238
const parts = chromeRegex.exec(line);
3339

3440
if (parts) {
@@ -55,14 +61,16 @@ export const chrome: StackLineParser = line => {
5561
return;
5662
};
5763

64+
export const chromeStackParser: StackLineParser = [CHROME_PRIORITY, chrome];
65+
5866
// gecko regex: `(?:bundle|\d+\.js)`: `bundle` is for react native, `\d+\.js` also but specifically for ram bundles because it
5967
// generates filenames without a prefix like `file://` the filenames in the stacktrace are just 42.js
6068
// We need this specific case for now because we want no other regex to match.
6169
const geckoREgex =
6270
/^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:file|https?|blob|chrome|webpack|resource|moz-extension|capacitor).*?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. /=]+)(?::(\d+))?(?::(\d+))?\s*$/i;
6371
const geckoEvalRegex = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i;
6472

65-
export const gecko: StackLineParser = line => {
73+
const gecko: StackLineParserFn = line => {
6674
const parts = geckoREgex.exec(line);
6775

6876
if (parts) {
@@ -89,32 +97,40 @@ export const gecko: StackLineParser = line => {
8997
return;
9098
};
9199

100+
export const geckoStackParser: StackLineParser = [GECKO_PRIORITY, gecko];
101+
92102
const winjsRegex =
93103
/^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i;
94104

95-
export const winjs: StackLineParser = line => {
105+
const winjs: StackLineParserFn = line => {
96106
const parts = winjsRegex.exec(line);
97107

98108
return parts
99109
? createFrame(parts[2], parts[1] || UNKNOWN_FUNCTION, +parts[3], parts[4] ? +parts[4] : undefined)
100110
: undefined;
101111
};
102112

113+
export const winjsStackParser: StackLineParser = [WINJS_PRIORITY, winjs];
114+
103115
const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i;
104116

105-
export const opera10: StackLineParser = line => {
117+
const opera10: StackLineParserFn = line => {
106118
const parts = opera10Regex.exec(line);
107119
return parts ? createFrame(parts[2], parts[3] || UNKNOWN_FUNCTION, +parts[1]) : undefined;
108120
};
109121

122+
export const opera10StackParser: StackLineParser = [OPERA10_PRIORITY, opera10];
123+
110124
const opera11Regex =
111125
/ line (\d+), column (\d+)\s*(?:in (?:<anonymous function: ([^>]+)>|([^)]+))\(.*\))? in (.*):\s*$/i;
112126

113-
export const opera11: StackLineParser = line => {
127+
const opera11: StackLineParserFn = line => {
114128
const parts = opera11Regex.exec(line);
115129
return parts ? createFrame(parts[5], parts[3] || parts[4] || UNKNOWN_FUNCTION, +parts[1], +parts[2]) : undefined;
116130
};
117131

132+
export const opera11StackParser: StackLineParser = [OPERA11_PRIORITY, opera11];
133+
118134
/**
119135
* Safari web extensions, starting version unknown, can produce "frames-only" stacktraces.
120136
* What it means, is that instead of format like:

packages/browser/test/unit/tracekit/chromium.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { exceptionFromError } from '../../../src/parsers';
1+
import { exceptionFromError } from '../../../src/eventbuilder';
22

33
describe('Tracekit - Chrome Tests', () => {
44
it('should parse Chrome error with no location', () => {

packages/browser/test/unit/tracekit/firefox.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { exceptionFromError } from '../../../src/parsers';
1+
import { exceptionFromError } from '../../../src/eventbuilder';
22

33
describe('Tracekit - Firefox Tests', () => {
44
it('should parse Firefox 3 error', () => {

packages/browser/test/unit/tracekit/ie.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { exceptionFromError } from '../../../src/parsers';
1+
import { exceptionFromError } from '../../../src/eventbuilder';
22

33
describe('Tracekit - IE Tests', () => {
44
it('should parse IE 10 error', () => {

0 commit comments

Comments
 (0)