Skip to content

ref(stackparse): More stack parse improvements and consolidation #4669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 138 additions & 2 deletions packages/browser/src/eventbuilder.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,153 @@
import { Event, EventHint, Severity } from '@sentry/types';
import { Event, EventHint, Exception, Severity, StackFrame } from '@sentry/types';
import {
addExceptionMechanism,
addExceptionTypeValue,
createStackParser,
extractExceptionKeysForMessage,
isDOMError,
isDOMException,
isError,
isErrorEvent,
isEvent,
isPlainObject,
normalizeToSize,
resolvedSyncPromise,
} from '@sentry/utils';

import { eventFromError, eventFromPlainObject, parseStackFrames } from './parsers';
import {
chromeStackParser,
geckoStackParser,
opera10StackParser,
opera11StackParser,
winjsStackParser,
} from './stack-parsers';

/**
* This function creates an exception from an TraceKitStackTrace
* @param stacktrace TraceKitStackTrace that will be converted to an exception
* @hidden
*/
export function exceptionFromError(ex: Error): Exception {
// Get the frames first since Opera can lose the stack if we touch anything else first
const frames = parseStackFrames(ex);

const exception: Exception = {
type: ex && ex.name,
value: extractMessage(ex),
};

if (frames.length) {
exception.stacktrace = { frames };
}

if (exception.type === undefined && exception.value === '') {
exception.value = 'Unrecoverable error caught';
}

return exception;
}

/**
* @hidden
*/
export function eventFromPlainObject(
exception: Record<string, unknown>,
syntheticException?: Error,
isUnhandledRejection?: boolean,
): Event {
const event: Event = {
exception: {
values: [
{
type: isEvent(exception) ? exception.constructor.name : isUnhandledRejection ? 'UnhandledRejection' : 'Error',
value: `Non-Error ${
isUnhandledRejection ? 'promise rejection' : 'exception'
} captured with keys: ${extractExceptionKeysForMessage(exception)}`,
},
],
},
extra: {
__serialized__: normalizeToSize(exception),
},
};

if (syntheticException) {
const frames = parseStackFrames(syntheticException);
if (frames.length) {
event.stacktrace = { frames };
}
}

return event;
}

/**
* @hidden
*/
export function eventFromError(ex: Error): Event {
return {
exception: {
values: [exceptionFromError(ex)],
},
};
}

/** Parses stack frames from an error */
export function parseStackFrames(ex: Error & { framesToPop?: number; stacktrace?: string }): StackFrame[] {
// Access and store the stacktrace property before doing ANYTHING
// else to it because Opera is not very good at providing it
// reliably in other circumstances.
const stacktrace = ex.stacktrace || ex.stack || '';

const popSize = getPopSize(ex);

try {
return createStackParser(
opera10StackParser,
opera11StackParser,
chromeStackParser,
winjsStackParser,
geckoStackParser,
)(stacktrace, popSize);
} catch (e) {
// no-empty
}

return [];
}

// Based on our own mapping pattern - https://github.com/getsentry/sentry/blob/9f08305e09866c8bd6d0c24f5b0aabdd7dd6c59c/src/sentry/lang/javascript/errormapping.py#L83-L108
const reactMinifiedRegexp = /Minified React error #\d+;/i;

function getPopSize(ex: Error & { framesToPop?: number }): number {
if (ex) {
if (typeof ex.framesToPop === 'number') {
return ex.framesToPop;
}

if (reactMinifiedRegexp.test(ex.message)) {
return 1;
}
}

return 0;
}

/**
* There are cases where stacktrace.message is an Event object
* https://github.com/getsentry/sentry-javascript/issues/1949
* In this specific case we try to extract stacktrace.message.error.message
*/
function extractMessage(ex: Error & { message: { error?: Error } }): string {
const message = ex && ex.message;
if (!message) {
return 'No error message';
}
if (message.error && typeof message.error.message === 'string') {
return message.error.message;
}
return message;
}

/**
* Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`.
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/integrations/linkederrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types';
import { isInstanceOf } from '@sentry/utils';

import { exceptionFromError } from '../parsers';
import { exceptionFromError } from '../eventbuilder';

const DEFAULT_KEY = 'cause';
const DEFAULT_LIMIT = 5;
Expand Down
128 changes: 0 additions & 128 deletions packages/browser/src/parsers.ts

This file was deleted.

28 changes: 22 additions & 6 deletions packages/browser/src/stack-parsers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { StackFrame } from '@sentry/types';
import { StackLineParser } from '@sentry/utils';
import { StackLineParser, StackLineParserFn } from '@sentry/utils';

// global reference to slice
const UNKNOWN_FUNCTION = '?';

const OPERA10_PRIORITY = 10;
const OPERA11_PRIORITY = 20;
const CHROME_PRIORITY = 30;
const WINJS_PRIORITY = 40;
const GECKO_PRIORITY = 50;

function createFrame(filename: string, func: string, lineno?: number, colno?: number): StackFrame {
const frame: StackFrame = {
filename,
Expand All @@ -28,7 +34,7 @@ const chromeRegex =
/^\s*at (?:(.*?) ?\((?:address at )?)?((?:file|https?|blob|chrome-extension|address|native|eval|webpack|<anonymous>|[-a-z]+:|.*bundle|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
const chromeEvalRegex = /\((\S*)(?::(\d+))(?::(\d+))\)/;

export const chrome: StackLineParser = line => {
const chrome: StackLineParserFn = line => {
const parts = chromeRegex.exec(line);

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

export const chromeStackParser: StackLineParser = [CHROME_PRIORITY, chrome];

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

export const gecko: StackLineParser = line => {
const gecko: StackLineParserFn = line => {
const parts = geckoREgex.exec(line);

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

export const geckoStackParser: StackLineParser = [GECKO_PRIORITY, gecko];

const winjsRegex =
/^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i;

export const winjs: StackLineParser = line => {
const winjs: StackLineParserFn = line => {
const parts = winjsRegex.exec(line);

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

export const winjsStackParser: StackLineParser = [WINJS_PRIORITY, winjs];

const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i;

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

export const opera10StackParser: StackLineParser = [OPERA10_PRIORITY, opera10];

const opera11Regex =
/ line (\d+), column (\d+)\s*(?:in (?:<anonymous function: ([^>]+)>|([^)]+))\(.*\))? in (.*):\s*$/i;

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

export const opera11StackParser: StackLineParser = [OPERA11_PRIORITY, opera11];

/**
* Safari web extensions, starting version unknown, can produce "frames-only" stacktraces.
* What it means, is that instead of format like:
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/test/unit/tracekit/chromium.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { exceptionFromError } from '../../../src/parsers';
import { exceptionFromError } from '../../../src/eventbuilder';

describe('Tracekit - Chrome Tests', () => {
it('should parse Chrome error with no location', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/test/unit/tracekit/firefox.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { exceptionFromError } from '../../../src/parsers';
import { exceptionFromError } from '../../../src/eventbuilder';

describe('Tracekit - Firefox Tests', () => {
it('should parse Firefox 3 error', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/test/unit/tracekit/ie.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { exceptionFromError } from '../../../src/parsers';
import { exceptionFromError } from '../../../src/eventbuilder';

describe('Tracekit - IE Tests', () => {
it('should parse IE 10 error', () => {
Expand Down
Loading