Skip to content

Fix line/column/url extraction from some eval frames #907

New issue

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

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

Already on GitHub? # to your account

Merged
merged 2 commits into from
Mar 28, 2017
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
1 change: 1 addition & 0 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<button onclick="divide(1, 0)">Sourcemap breakage</button>
<button onclick="derp()">window.onerror</button>
<button onclick="testOptions()">test options</button>
<button onclick="throwEval()">throw eval</button>
<button onclick="testSynthetic()">test synthetic</button>
<button onclick="throwString()">throw string</button>
<button onclick="showDialog()">show dialog</button>
Expand Down
4 changes: 4 additions & 0 deletions example/scratch.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ function throwString() {
throw 'oops';
}

function throwEval() {
eval('derp();');
}

function showDialog() {
broken();
Raven.showReportDialog();
Expand Down
45 changes: 34 additions & 11 deletions test/vendor/fixtures/captured-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,18 @@ CapturedExceptions.FIREFOX_31 = {
columnNumber: 12
};

CapturedExceptions.FIREFOX_43_EVAL = {
columnNumber: 30,
fileName: 'http://localhost:8080/file.js line 25 > eval line 2 > eval',
lineNumber: 1,
message: 'message string',
stack: 'baz@http://localhost:8080/file.js line 26 > eval line 2 > eval:1:30\n' +
'foo@http://localhost:8080/file.js line 26 > eval:2:96\n' +
'@http://localhost:8080/file.js line 26 > eval:4:18\n' +
'speak@http://localhost:8080/file.js:26:17\n' +
'@http://localhost:8080/file.js:33:9'
};

// Internal errors sometimes thrown by Firefox
// More here: https://developer.mozilla.org/en-US/docs/Mozilla/Errors
//
Expand All @@ -252,6 +264,17 @@ CapturedExceptions.FIREFOX_44_NS_EXCEPTION = {
result: 2147500037
};

CapturedExceptions.FIREFOX_50_RESOURCE_URL = {
stack: 'render@resource://path/data/content/bundle.js:5529:16\n' +
'dispatchEvent@resource://path/data/content/vendor.bundle.js:18:23028\n' +
'wrapped@resource://path/data/content/bundle.js:7270:25',
fileName: 'resource://path/data/content/bundle.js',
lineNumber: 5529,
columnNumber: 16,
message: 'this.props.raw[this.state.dataSource].rows is undefined',
name: 'TypeError'
};

CapturedExceptions.SAFARI_6 = {
message: "'null' is not an object (evaluating 'x.undef')",
stack: "@http://path/to/file.js:48\n" +
Expand Down Expand Up @@ -344,24 +367,24 @@ CapturedExceptions.CHROME_48_BLOB = {
" at n.handle (blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379:7:2863)"
};

CapturedExceptions.CHROME_48_EVAL = {
message: 'message string',
name: 'Error',
stack: 'Error: message string\n' +
'at baz (eval at foo (eval at speak (http://localhost:8080/file.js:21:17)), <anonymous>:1:30)\n' +
'at foo (eval at speak (http://localhost:8080/file.js:21:17), <anonymous>:2:96)\n' +
'at eval (eval at speak (http://localhost:8080/file.js:21:17), <anonymous>:4:18)\n' +
'at Object.speak (http://localhost:8080/file.js:21:17)\n' +
'at http://localhost:8080/file.js:31:13\n'
};

CapturedExceptions.PHANTOMJS_1_19 = {
stack: "Error: foo\n" +
" at file:///path/to/file.js:878\n" +
" at foo (http://path/to/file.js:4283)\n" +
" at http://path/to/file.js:4287"
};

CapturedExceptions.FIREFOX_50_RESOURCE_URL = {
stack: 'render@resource://path/data/content/bundle.js:5529:16\n' +
'dispatchEvent@resource://path/data/content/vendor.bundle.js:18:23028\n' +
'wrapped@resource://path/data/content/bundle.js:7270:25',
fileName: 'resource://path/data/content/bundle.js',
lineNumber: 5529,
columnNumber: 16,
message: 'this.props.raw[this.state.dataSource].rows is undefined',
name: 'TypeError'
};

CapturedExceptions.ANDROID_REACT_NATIVE = {
message: 'Error: test',
name: 'Error',
Expand Down
24 changes: 24 additions & 0 deletions test/vendor/tracekit-parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ describe('TraceKit', function () {
assert.deepEqual(stackFrames.stack[2], { url: 'http://localhost:8080/file.js', func: 'I.e.fn.(anonymous function) [as index]', args: [], line: 10, column: 3651 });
});


it('should parse Chrome error with webpack URLs', function () {
var stackFrames = TraceKit.computeStackTrace(CapturedExceptions.CHROME_XX_WEBPACK);
assert.ok(stackFrames);
Expand All @@ -133,6 +134,17 @@ describe('TraceKit', function () {
assert.deepEqual(stackFrames.stack[3], { url: 'webpack:///./~/react-proxy/modules/createPrototypeProxy.js?', func: 'TESTTESTTEST.proxiedMethod', args: [], line: 44, column: 30 });
});

it('should parse nested eval() from Chrome', function() {
var stackFrames = TraceKit.computeStackTrace(CapturedExceptions.CHROME_48_EVAL);
assert.ok(stackFrames);
assert.deepEqual(stackFrames.stack.length, 5);
assert.deepEqual(stackFrames.stack[0], { url: 'http://localhost:8080/file.js', func: 'baz', args: [], line: 21, column: 17});
assert.deepEqual(stackFrames.stack[1], { url: 'http://localhost:8080/file.js', func: 'foo', args: [], line: 21, column: 17});
assert.deepEqual(stackFrames.stack[2], { url: 'http://localhost:8080/file.js', func: 'eval', args: [], line: 21, column: 17});
assert.deepEqual(stackFrames.stack[3], { url: 'http://localhost:8080/file.js', func: 'Object.speak', args: [], line: 21, column: 17});
assert.deepEqual(stackFrames.stack[4], { url: 'http://localhost:8080/file.js', func: '?', args: [], line: 31, column: 13});
});

it('should parse Chrome error with blob URLs', function () {
var stackFrames = TraceKit.computeStackTrace(CapturedExceptions.CHROME_48_BLOB);
assert.ok(stackFrames);
Expand Down Expand Up @@ -226,6 +238,18 @@ describe('TraceKit', function () {
assert.deepEqual(stackFrames.stack.length, 3);
assert.deepEqual(stackFrames.stack[0], { url: 'resource://path/data/content/bundle.js', func: 'render', args: [], line: 5529, column: 16 });
});

it('should parse Firefox errors with eval URLs', function () {
var stackFrames = TraceKit.computeStackTrace(CapturedExceptions.FIREFOX_43_EVAL);
assert.ok(stackFrames);
assert.deepEqual(stackFrames.stack.length, 5);
assert.deepEqual(stackFrames.stack[0], { url: 'http://localhost:8080/file.js', func: 'baz', args:[], line: 26, column: null});
assert.deepEqual(stackFrames.stack[1], { url: 'http://localhost:8080/file.js', func: 'foo', args:[], line: 26, column: null});
assert.deepEqual(stackFrames.stack[2], { url: 'http://localhost:8080/file.js', func: '?', args:[], line: 26, column: null});
assert.deepEqual(stackFrames.stack[3], { url: 'http://localhost:8080/file.js', func: 'speak', args:[], line: 26, column: 17});
assert.deepEqual(stackFrames.stack[4], { url: 'http://localhost:8080/file.js', func: '?', args:[], line: 33, column: 9});
});

it('should parse React Native errors on Android', function () {
var stackFrames = TraceKit.computeStackTrace(CapturedExceptions.ANDROID_REACT_NATIVE);
assert.ok(stackFrames);
Expand Down
6 changes: 3 additions & 3 deletions test/vendor/tracekit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ describe('TraceKit', function(){

assert.equal(trace.stack[2].func, 'eval');
// TODO: fix nested evals
assert.equal(trace.stack[2].url, 'eval at <anonymous> (http://example.com/js/test.js:26:5), <anonymous>');
assert.equal(trace.stack[2].line, 1); // second set of line/column numbers used
assert.equal(trace.stack[2].column, 26);
assert.equal(trace.stack[2].url, 'http://example.com/js/test.js');
assert.equal(trace.stack[2].line, 26);
assert.equal(trace.stack[2].column, 5);
});
});

Expand Down
33 changes: 26 additions & 7 deletions vendor/TraceKit/tracekit.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,15 +393,28 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,
gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?)(?::(\d+))?(?::(\d+))?\s*$/i,
winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i,

// Used to additionally parse URL/line/column from eval frames
geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i,
chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/,

lines = ex.stack.split('\n'),
stack = [],
submatch,
parts,
element,
reference = /^(.*) is undefined$/.exec(ex.message);

for (var i = 0, j = lines.length; i < j; ++i) {
if ((parts = chrome.exec(lines[i]))) {
var isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line
var isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line
if (isEval && (submatch = chromeEval.exec(parts[2]))) {
// throw out eval line/column and use top-most line/column number
parts[2] = submatch[1]; // url
parts[3] = submatch[2]; // line
parts[4] = submatch[3]; // column
}
element = {
'url': !isNative ? parts[2] : null,
'func': parts[1] || UNKNOWN_FUNCTION,
Expand All @@ -418,6 +431,19 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
'column': parts[4] ? +parts[4] : null
};
} else if ((parts = gecko.exec(lines[i]))) {
var isEval = parts[3] && parts[3].indexOf(' > eval') > -1;
if (isEval && (submatch = geckoEval.exec(parts[3]))) {
// throw out eval line/column and use top-most line number
parts[3] = submatch[1];
parts[4] = submatch[2];
parts[5] = null; // no column when eval
} else if (i === 0 && !parts[5] && typeof ex.columnNumber !== 'undefined') {
// FireFox uses this awesome columnNumber property for its top frame
// Also note, Firefox's column number is 0-based and everything else expects 1-based,
// so adding 1
// NOTE: this hack doesn't work if top-most frame is eval
stack[0].column = ex.columnNumber + 1;
}
element = {
'url': parts[3],
'func': parts[1] || UNKNOWN_FUNCTION,
Expand All @@ -440,13 +466,6 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
return null;
}

if (!stack[0].column && typeof ex.columnNumber !== 'undefined') {
// FireFox uses this awesome columnNumber property for its top frame
// Also note, Firefox's column number is 0-based and everything else expects 1-based,
// so adding 1
stack[0].column = ex.columnNumber + 1;
}

return {
'name': ex.name,
'message': ex.message,
Expand Down