From becae71e6733510239e16165636a3701b2a7d4dd Mon Sep 17 00:00:00 2001 From: Nic Jansma Date: Fri, 5 May 2017 13:08:51 -0400 Subject: [PATCH] Errors: Loader Snippet to capture errors before Boomerang arrives --- plugins/errors.js | 30 ++++- .../captureErrorsSnippet.tpl | 3 + .../captureErrorsSnippetNoScript.tpl | 56 ++++++++ .../instrumentXHRSnippetNoScript.tpl | 12 +- .../14-errors/26-loader-snippet.html | 43 ++++++ .../14-errors/26-loader-snippet.js | 126 ++++++++++++++++++ .../27-loader-snippet-overwritten.html | 51 +++++++ .../27-loader-snippet-overwritten.js | 30 +++++ 8 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 tests/page-template-snippets/captureErrorsSnippet.tpl create mode 100644 tests/page-template-snippets/captureErrorsSnippetNoScript.tpl create mode 100644 tests/page-templates/14-errors/26-loader-snippet.html create mode 100644 tests/page-templates/14-errors/26-loader-snippet.js create mode 100644 tests/page-templates/14-errors/27-loader-snippet-overwritten.html create mode 100644 tests/page-templates/14-errors/27-loader-snippet-overwritten.js diff --git a/plugins/errors.js b/plugins/errors.js index 30301df2d..293e0508f 100644 --- a/plugins/errors.js +++ b/plugins/errors.js @@ -608,7 +608,7 @@ error.generatedStack = true; // set the time when it was created - error.timestamp = now; + error.timestamp = error.timestamp || now; impl.addError(error, via, source); } @@ -618,7 +618,7 @@ } else { // add the timestamp - error.timestamp = now; + error.timestamp = error.timestamp || now; // send (or queue) the error impl.addError(error, via, source); @@ -1459,7 +1459,18 @@ // hook into window.onError if configured if (impl.monitorGlobal) { try { - var globalOnError = BOOMR.window.onerror; + // globalOnError might be set by loader snippet + if (!BOOMR.globalOnError) { + BOOMR.globalOnError = BOOMR.window.onerror; + } + else { + // Another error wrapper came in after us - call this new onerror first. Since + // it presumably wrapped our original handler, that will likely be called but + // will detect Boomerang has loaded and will call *its* original onerror handler. + if (BOOMR.window.onerror && !BOOMR.window.onerror._bmr) { + BOOMR.globalOnError = BOOMR.window.onerror; + } + } BOOMR.window.onerror = function BOOMR_plugins_errors_onerror(message, fileName, lineNumber, columnNumber, error) { // a SyntaxError can produce a null error @@ -1476,10 +1487,19 @@ }, E.VIA_GLOBAL_EXCEPTION_HANDLER); } - if (typeof globalOnError === "function") { - globalOnError.apply(window, arguments); + if (typeof BOOMR.globalOnError === "function") { + BOOMR.globalOnError.apply(window, arguments); } }; + + // send any errors from the loader snippet + if (BOOMR.globalErrors) { + for (var i = 0; i < BOOMR.globalErrors.length; i++) { + impl.send(BOOMR.globalErrors[i], E.VIA_GLOBAL_EXCEPTION_HANDLER); + } + + delete BOOMR.globalErrors; + } } catch (e) { BOOMR.debug("Exception in the window.onerror handler", "Errors"); diff --git a/tests/page-template-snippets/captureErrorsSnippet.tpl b/tests/page-template-snippets/captureErrorsSnippet.tpl new file mode 100644 index 000000000..7dfc7e4aa --- /dev/null +++ b/tests/page-template-snippets/captureErrorsSnippet.tpl @@ -0,0 +1,3 @@ + diff --git a/tests/page-template-snippets/captureErrorsSnippetNoScript.tpl b/tests/page-template-snippets/captureErrorsSnippetNoScript.tpl new file mode 100644 index 000000000..df9b7701a --- /dev/null +++ b/tests/page-template-snippets/captureErrorsSnippetNoScript.tpl @@ -0,0 +1,56 @@ +(function(w){ + w.BOOMR = w.BOOMR || {}; + + w.BOOMR.globalOnErrorOrig = w.BOOMR.globalOnError = w.onerror; + w.BOOMR.globalErrors = []; + + var now = (function() { + try { + if ("performance" in w) { + return function() { + return Math.round(w.performance.now() + performance.timing.navigationStart); + }; + } + } + catch (ignore) {} + + return Date.now || function() { + return new Date().getTime(); + }; + })(); + + w.onerror = function BOOMR_plugins_errors_onerror(message, fileName, lineNumber, columnNumber, error) { + if (w.BOOMR.version) { + // If Boomerang has already loaded, the only reason this function would still be alive would be if + // we're in the chain from another handler that overwrote window.onerror. In that case, we should + // run globalOnErrorOrig which presumably hasn't been overwritten by Boomerang. + if (typeof w.BOOMR.globalOnErrorOrig === "function") { + w.BOOMR.globalOnErrorOrig.apply(w, arguments); + } + + return; + } + + if (typeof error !== "undefined" && error !== null) { + error.timestamp = now(); + w.BOOMR.globalErrors.push(error); + } + else { + w.BOOMR.globalErrors.push({ + message: message, + fileName: fileName, + lineNumber: lineNumber, + columnNumber: columnNumber, + noStack: true, + timestamp: now() + }); + } + + if (typeof w.BOOMR.globalOnError === "function") { + w.BOOMR.globalOnError.apply(w, arguments); + } + }; + + // make it easier to detect this is our wrapped handler + w.onerror._bmr = true; +})(window); diff --git a/tests/page-template-snippets/instrumentXHRSnippetNoScript.tpl b/tests/page-template-snippets/instrumentXHRSnippetNoScript.tpl index 26e494234..79944111a 100644 --- a/tests/page-template-snippets/instrumentXHRSnippetNoScript.tpl +++ b/tests/page-template-snippets/instrumentXHRSnippetNoScript.tpl @@ -25,11 +25,17 @@ var now = (function() { try { - if ("performance" in w) - return function() { return Math.round(performance.now() + performance.timing.navigationStart); }; + if ("performance" in w) { + return function() { + return Math.round(w.performance.now() + performance.timing.navigationStart); + }; + } } catch (ignore) {} - return Date.now || function() { return new Date().getTime(); }; + + return Date.now || function() { + return new Date().getTime(); + }; })(); w.XMLHttpRequest = function() { diff --git a/tests/page-templates/14-errors/26-loader-snippet.html b/tests/page-templates/14-errors/26-loader-snippet.html new file mode 100644 index 000000000..0500afa09 --- /dev/null +++ b/tests/page-templates/14-errors/26-loader-snippet.html @@ -0,0 +1,43 @@ +<%= header %> + +<%= captureErrorsSnippet %> + + +<%= boomerangScriptMin %> + + + + + +<%= footer %> diff --git a/tests/page-templates/14-errors/26-loader-snippet.js b/tests/page-templates/14-errors/26-loader-snippet.js new file mode 100644 index 000000000..dc3cd1f82 --- /dev/null +++ b/tests/page-templates/14-errors/26-loader-snippet.js @@ -0,0 +1,126 @@ +/*eslint-env mocha*/ +/*global BOOMR_test,assert*/ + +describe("e2e/14-errors/26-loader-snippet", function() { + var tf = BOOMR.plugins.TestFramework; + var t = BOOMR_test; + var C = BOOMR.utils.Compression; + + it("Should have sent a single beacon validation", function(done) { + t.validateBeaconWasSent(done); + }); + + it("Should have put the err on the beacon", function() { + var b = tf.lastBeacon(); + assert.isDefined(b.err); + }); + + it("Should have had 3 errors", function() { + var b = tf.lastBeacon(); + assert.equal(C.jsUrlDecompress(b.err).length, 3); + }); + + it("Should have count = 1 for each error", function() { + var b = tf.lastBeacon(); + var errs = BOOMR.plugins.Errors.decompressErrors(C.jsUrlDecompress(b.err)); + for (var i = 0; i < 3; i++) { + var err = errs[i]; + assert.equal(err.count, 1); + } + }); + + it("Should have fileName of the page (if set) for each error", function() { + var b = tf.lastBeacon(); + var errs = BOOMR.plugins.Errors.decompressErrors(C.jsUrlDecompress(b.err)); + for (var i = 0; i < 3; i++) { + var err = errs[i]; + + if (err.fileName) { + assert.include(err.fileName, "26-loader-snippet.html"); + } + } + }); + + it("Should have functionName of 'errorFunction' for each error", function() { + var b = tf.lastBeacon(); + var errs = BOOMR.plugins.Errors.decompressErrors(C.jsUrlDecompress(b.err)); + for (var i = 0; i < 3; i++) { + var err = errs[i]; + + if (err.functionName) { + assert.include(err.functionName, "errorFunction"); + } + } + }); + + it("Should have message = 'a is not defined' or 'Can't find variable: a' or ''a' is undefined' for each error", function() { + var b = tf.lastBeacon(); + var errs = BOOMR.plugins.Errors.decompressErrors(C.jsUrlDecompress(b.err)); + for (var i = 0; i < 3; i++) { + var err = errs[i]; + + // Chrome, Firefox == a is not defined, Safari = Can't find variable + assert.isTrue( + err.message.indexOf("a is not defined") !== -1 || + err.message.indexOf("Can't find variable: a") !== -1 || + err.message.indexOf("'a' is undefined") !== -1); + } + }); + + it("Should have source = APP for each error", function() { + var b = tf.lastBeacon(); + var errs = BOOMR.plugins.Errors.decompressErrors(C.jsUrlDecompress(b.err)); + for (var i = 0; i < 3; i++) { + var err = errs[i]; + + assert.equal(err.source, BOOMR.plugins.Errors.SOURCE_APP); + } + }); + + it("Should have stack with the stack for each error", function() { + var b = tf.lastBeacon(); + var errs = BOOMR.plugins.Errors.decompressErrors(C.jsUrlDecompress(b.err)); + for (var i = 0; i < 3; i++) { + var err = errs[i]; + + assert.isDefined(err.stack); + } + }); + + it("Should have type = 'ReferenceError' or 'Error' for each error", function() { + var b = tf.lastBeacon(); + var errs = BOOMR.plugins.Errors.decompressErrors(C.jsUrlDecompress(b.err)); + for (var i = 0; i < 3; i++) { + var err = errs[i]; + + // Chrome, Firefox == ReferenceError, Safari = Error + assert.isTrue(err.type === "ReferenceError" || err.type === "Error"); + } + }); + + it("Should have via = GLOBAL_EXCEPTION_HANDLER for each error", function() { + var b = tf.lastBeacon(); + var errs = BOOMR.plugins.Errors.decompressErrors(C.jsUrlDecompress(b.err)); + for (var i = 0; i < 3; i++) { + var err = errs[i]; + + assert.equal(err.via, BOOMR.plugins.Errors.VIA_GLOBAL_EXCEPTION_HANDLER); + } + }); + + it("Should have columNumber to be a number if specified for each error", function() { + var b = tf.lastBeacon(); + var errs = BOOMR.plugins.Errors.decompressErrors(C.jsUrlDecompress(b.err)); + for (var i = 0; i < 3; i++) { + var err = errs[i]; + + if (typeof err.columnNumber !== "undefined") { + assert.isTrue(err.columnNumber >= 0); + } + } + }); + + it("Should have called the original window.onerror for each error", function() { + assert.equal(window.errorsLogged, 3); + }); +}); diff --git a/tests/page-templates/14-errors/27-loader-snippet-overwritten.html b/tests/page-templates/14-errors/27-loader-snippet-overwritten.html new file mode 100644 index 000000000..50211be93 --- /dev/null +++ b/tests/page-templates/14-errors/27-loader-snippet-overwritten.html @@ -0,0 +1,51 @@ +<%= header %> + +<%= captureErrorsSnippet %> + + +<%= boomerangScriptMin %> + + + + + +<%= footer %> diff --git a/tests/page-templates/14-errors/27-loader-snippet-overwritten.js b/tests/page-templates/14-errors/27-loader-snippet-overwritten.js new file mode 100644 index 000000000..fc1f7a38f --- /dev/null +++ b/tests/page-templates/14-errors/27-loader-snippet-overwritten.js @@ -0,0 +1,30 @@ +/*eslint-env mocha*/ +/*global BOOMR_test,assert*/ + +describe("e2e/14-errors/26-loader-snippet", function() { + var tf = BOOMR.plugins.TestFramework; + var t = BOOMR_test; + var C = BOOMR.utils.Compression; + + it("Should have sent a single beacon validation", function(done) { + t.validateBeaconWasSent(done); + }); + + it("Should have put the err on the beacon", function() { + var b = tf.lastBeacon(); + assert.isDefined(b.err); + }); + + it("Should have had 3 errors", function() { + var b = tf.lastBeacon(); + assert.equal(C.jsUrlDecompress(b.err).length, 3); + }); + + it("Should have called the original window.onerror for each error", function() { + assert.equal(window.errorsLogged1, 3); + }); + + it("Should have called the second window.onerror for each error", function() { + assert.equal(window.errorsLogged2, 3); + }); +});