From 589ac173fc4231267cc8d6a73e2a516924a7a9f1 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Thu, 23 Mar 2017 15:32:28 -0700 Subject: [PATCH 1/2] Better extraction of eval frames on Firefox, Chrome --- example/index.html | 1 + example/scratch.js | 4 +++ test/vendor/fixtures/captured-errors.js | 45 +++++++++++++++++++------ test/vendor/tracekit-parser.test.js | 24 +++++++++++++ test/vendor/tracekit.test.js | 6 ++-- vendor/TraceKit/tracekit.js | 29 ++++++++++++---- 6 files changed, 88 insertions(+), 21 deletions(-) diff --git a/example/index.html b/example/index.html index b9dff45eae46..c6f5ba19a5fc 100644 --- a/example/index.html +++ b/example/index.html @@ -33,6 +33,7 @@ + diff --git a/example/scratch.js b/example/scratch.js index 9d164ac9bf14..ca7c44607ac3 100644 --- a/example/scratch.js +++ b/example/scratch.js @@ -41,6 +41,10 @@ function throwString() { throw 'oops'; } +function throwEval() { + eval('derp();'); +} + function showDialog() { broken(); Raven.showReportDialog(); diff --git a/test/vendor/fixtures/captured-errors.js b/test/vendor/fixtures/captured-errors.js index 58ef2f987e94..0e04bc7a355c 100644 --- a/test/vendor/fixtures/captured-errors.js +++ b/test/vendor/fixtures/captured-errors.js @@ -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 // @@ -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" + @@ -344,6 +367,17 @@ 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)), :1:30)\n' + + 'at foo (eval at speak (http://localhost:8080/file.js:21:17), :2:96)\n' + + 'at eval (eval at speak (http://localhost:8080/file.js:21:17), :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" + @@ -351,17 +385,6 @@ CapturedExceptions.PHANTOMJS_1_19 = { " 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', diff --git a/test/vendor/tracekit-parser.test.js b/test/vendor/tracekit-parser.test.js index d87e1ea3ae5e..ce1a61dd0ca7 100644 --- a/test/vendor/tracekit-parser.test.js +++ b/test/vendor/tracekit-parser.test.js @@ -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); @@ -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); @@ -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); diff --git a/test/vendor/tracekit.test.js b/test/vendor/tracekit.test.js index 28de5c43b810..50b04dd2be02 100644 --- a/test/vendor/tracekit.test.js +++ b/test/vendor/tracekit.test.js @@ -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 (http://example.com/js/test.js:26:5), '); - 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); }); }); diff --git a/vendor/TraceKit/tracekit.js b/vendor/TraceKit/tracekit.js index f3d46d98e60b..45d912c9d06d 100644 --- a/vendor/TraceKit/tracekit.js +++ b/vendor/TraceKit/tracekit.js @@ -392,9 +392,11 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i, gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?)(?::(\d+))?(?::(\d+))?\s*$/i, + geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i, winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i, lines = ex.stack.split('\n'), stack = [], + submatch, parts, element, reference = /^(.*) is undefined$/.exec(ex.message); @@ -402,6 +404,13 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { 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 = /\((\S*)(?::(\d+))(?::(\d+))\)/.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, @@ -418,6 +427,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, @@ -440,13 +462,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, From e8fa25c0eacc60cfa099de79c049da1931bf7465 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Sun, 26 Mar 2017 09:38:35 -0700 Subject: [PATCH 2/2] Declare regex at top --- vendor/TraceKit/tracekit.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vendor/TraceKit/tracekit.js b/vendor/TraceKit/tracekit.js index 45d912c9d06d..35119a880839 100644 --- a/vendor/TraceKit/tracekit.js +++ b/vendor/TraceKit/tracekit.js @@ -392,8 +392,12 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i, gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?)(?::(\d+))?(?::(\d+))?\s*$/i, - geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/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, @@ -405,7 +409,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { 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 = /\((\S*)(?::(\d+))(?::(\d+))\)/.exec(parts[2]))) { + 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