Skip to content

Commit

Permalink
Errors: Loader Snippet to capture errors before Boomerang arrives
Browse files Browse the repository at this point in the history
  • Loading branch information
nicjansma committed Apr 3, 2018
1 parent c5fbfc5 commit becae71
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 8 deletions.
30 changes: 25 additions & 5 deletions plugins/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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");
Expand Down
3 changes: 3 additions & 0 deletions tests/page-template-snippets/captureErrorsSnippet.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script>
<%= captureErrorsSnippetNoScript %>
</script>
56 changes: 56 additions & 0 deletions tests/page-template-snippets/captureErrorsSnippetNoScript.tpl
Original file line number Diff line number Diff line change
@@ -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);
12 changes: 9 additions & 3 deletions tests/page-template-snippets/instrumentXHRSnippetNoScript.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
43 changes: 43 additions & 0 deletions tests/page-templates/14-errors/26-loader-snippet.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<%= header %>
<script>
window.errorsLogged = 0;
window.onerror = function() {
window.errorsLogged++;
};
</script>
<%= captureErrorsSnippet %>
<script>
function errorFunction1() {
// a is not defined
a.foo = 1;
}
function errorFunction2() {
// a is not defined
a.foo = 1;
}
function errorFunction3() {
// a is not defined
a.foo = 2;
}
errorFunction1();
</script>
<script>
errorFunction2();
</script>
<%= boomerangScriptMin %>
<script src="26-loader-snippet.js" type="text/javascript"></script>
<script>
BOOMR_test.init({
testAfterOnBeacon: true,
Errors: {
enabled: true,
monitorGlobal: true
}
});
</script>
<!-- delay the page by 1second so an error can fire -->
<img src="/delay?delay=1000&amp;file=/assets/img.jpg" style="width: 100px" />
<script>
errorFunction3();
</script>
<%= footer %>
126 changes: 126 additions & 0 deletions tests/page-templates/14-errors/26-loader-snippet.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
51 changes: 51 additions & 0 deletions tests/page-templates/14-errors/27-loader-snippet-overwritten.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<%= header %>
<script>
window.errorsLogged1 = 0;
window.onerror = function() {
window.errorsLogged1++;
};
</script>
<%= captureErrorsSnippet %>
<script>
// now have another snippet override ours
window.oldOnError = window.onerror;
window.errorsLogged2 = 0;
window.onerror = function() {
window.errorsLogged2++;
window.oldOnError.apply(window, arguments);
};

function errorFunction1() {
// a is not defined
a.foo = 1;
}
function errorFunction2() {
// a is not defined
a.foo = 1;
}
function errorFunction3() {
// a is not defined
a.foo = 2;
}
errorFunction1();
</script>
<script>
errorFunction2();
</script>
<%= boomerangScriptMin %>
<script src="27-loader-snippet-overwritten.js" type="text/javascript"></script>
<script>
BOOMR_test.init({
testAfterOnBeacon: true,
Errors: {
enabled: true,
monitorGlobal: true
}
});
</script>
<!-- delay the page by 1second so an error can fire -->
<img src="/delay?delay=1000&amp;file=/assets/img.jpg" style="width: 100px" />
<script>
errorFunction3();
</script>
<%= footer %>
30 changes: 30 additions & 0 deletions tests/page-templates/14-errors/27-loader-snippet-overwritten.js
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit becae71

Please sign in to comment.