From 86dec327e6068379bd9bf5a10216f14f29e8835f Mon Sep 17 00:00:00 2001 From: Walt Jones Date: Sat, 11 May 2019 16:01:54 -0700 Subject: [PATCH] feat: allow enable/disable uncaught errors and rejections in Rollbar.configure --- sdks/rollbar.js/examples/error.html | 23 ++ sdks/rollbar.js/src/browser/rollbar.js | 42 +++- sdks/rollbar.js/src/server/rollbar.js | 26 ++- sdks/rollbar.js/test/browser.rollbar.test.js | 212 +++++++++++++++++++ sdks/rollbar.js/test/server.rollbar.test.js | 179 ++++++++++++++++ 5 files changed, 466 insertions(+), 16 deletions(-) create mode 100644 sdks/rollbar.js/examples/error.html diff --git a/sdks/rollbar.js/examples/error.html b/sdks/rollbar.js/examples/error.html new file mode 100644 index 000000000..a5c9ab5e0 --- /dev/null +++ b/sdks/rollbar.js/examples/error.html @@ -0,0 +1,23 @@ + + + + + Generate errors for test automation + + + + +
+

+ Generate errors for test automation +

+
+ + diff --git a/sdks/rollbar.js/src/browser/rollbar.js b/sdks/rollbar.js/src/browser/rollbar.js index 9c086c652..0428ed877 100644 --- a/sdks/rollbar.js/src/browser/rollbar.js +++ b/sdks/rollbar.js/src/browser/rollbar.js @@ -19,18 +19,11 @@ function Rollbar(options, client) { var api = new API(this.options, transport, urllib); this.client = client || new Client(this.options, api, logger, 'browser'); - var gWindow = ((typeof window != 'undefined') && window) || ((typeof self != 'undefined') && self); + var gWindow = _gWindow(); var gDocument = (typeof document != 'undefined') && document; addTransformsToNotifier(this.client.notifier, gWindow); addPredicatesToQueue(this.client.queue); - if (this.options.captureUncaught || this.options.handleUncaughtExceptions) { - globals.captureUncaughtExceptions(gWindow, this); - globals.wrapGlobals(gWindow, this); - } - if (this.options.captureUnhandledRejections || this.options.handleUnhandledRejections) { - globals.captureUnhandledRejections(gWindow, this); - } - + this.setupUnhandledCapture(); this.instrumenter = new Instrumenter(this.options, this.client.telemeter, this, gWindow, gDocument); this.instrumenter.instrument(); } @@ -73,6 +66,7 @@ Rollbar.prototype.configure = function(options, payloadData) { this.options = _.handleOptions(oldOptions, options, payload); this.client.configure(this.options, payloadData); this.instrumenter.configure(this.options); + this.setupUnhandledCapture(); return this; }; Rollbar.configure = function(options, payloadData) { @@ -221,7 +215,29 @@ Rollbar.sendJsonPayload = function() { } }; +Rollbar.prototype.setupUnhandledCapture = function() { + var gWindow = _gWindow(); + + if (!this.unhandledExceptionsInitialized) { + if (this.options.captureUncaught || this.options.handleUncaughtExceptions) { + globals.captureUncaughtExceptions(gWindow, this); + globals.wrapGlobals(gWindow, this); + this.unhandledExceptionsInitialized = true; + } + } + if (!this.unhandledRejectionsInitialized) { + if (this.options.captureUnhandledRejections || this.options.handleUnhandledRejections) { + globals.captureUnhandledRejections(gWindow, this); + this.unhandledRejectionsInitialized = true; + } + } +}; + Rollbar.prototype.handleUncaughtException = function(message, url, lineno, colno, error, context) { + if (!this.options.captureUncaught && !this.options.handleUncaughtExceptions) { + return; + } + var item; var stackInfo = _.makeUnhandledStackInfo( message, @@ -249,6 +265,10 @@ Rollbar.prototype.handleUncaughtException = function(message, url, lineno, colno }; Rollbar.prototype.handleUnhandledRejection = function(reason, promise) { + if (!this.options.captureUnhandledRejections && !this.options.handleUnhandledRejections) { + return; + } + var message = 'unhandled rejection was null or undefined!'; if (reason) { if (reason.message) { @@ -418,6 +438,10 @@ function _getFirstFunction(args) { return undefined; } +function _gWindow() { + return ((typeof window != 'undefined') && window) || ((typeof self != 'undefined') && self); +} + /* global __NOTIFIER_VERSION__:false */ /* global __DEFAULT_BROWSER_SCRUB_FIELDS__:false */ /* global __DEFAULT_LOG_LEVEL__:false */ diff --git a/sdks/rollbar.js/src/server/rollbar.js b/sdks/rollbar.js/src/server/rollbar.js index d11fba416..037fe6d65 100644 --- a/sdks/rollbar.js/src/server/rollbar.js +++ b/sdks/rollbar.js/src/server/rollbar.js @@ -36,13 +36,7 @@ function Rollbar(options, client) { this.client = client || new Client(this.options, api, logger, 'server'); addTransformsToNotifier(this.client.notifier); addPredicatesToQueue(this.client.queue); - - if (this.options.captureUncaught || this.options.handleUncaughtExceptions) { - this.handleUncaughtExceptions(); - } - if (this.options.captureUnhandledRejections || this.options.handleUnhandledRejections) { - this.handleUnhandledRejections(); - } + this.setupUnhandledCapture(); } var _instance = null; @@ -88,6 +82,7 @@ Rollbar.prototype.configure = function(options, payloadData) { delete this.options.maxItems; logger.setVerbose(this.options.verbose); this.client.configure(options, payloadData); + this.setupUnhandledCapture(); return this; }; Rollbar.configure = function(options, payloadData) { @@ -523,11 +518,24 @@ function _getFirstFunction(args) { return undefined; } +Rollbar.prototype.setupUnhandledCapture = function() { + if (this.options.captureUncaught || this.options.handleUncaughtExceptions) { + this.handleUncaughtExceptions(); + } + if (this.options.captureUnhandledRejections || this.options.handleUnhandledRejections) { + this.handleUnhandledRejections(); + } +}; + Rollbar.prototype.handleUncaughtExceptions = function() { var exitOnUncaught = !!this.options.exitOnUncaughtException; delete this.options.exitOnUncaughtException; addOrReplaceRollbarHandler('uncaughtException', function(err) { + if (!this.options.captureUncaught && !this.options.handleUncaughtExceptions) { + return; + } + this._uncaughtError(err, function(err) { if (err) { logger.error('Encountered error while handling an uncaught exception.'); @@ -546,6 +554,10 @@ Rollbar.prototype.handleUncaughtExceptions = function() { Rollbar.prototype.handleUnhandledRejections = function() { addOrReplaceRollbarHandler('unhandledRejection', function(reason) { + if (!this.options.captureUnhandledRejections && !this.options.handleUnhandledRejections) { + return; + } + this._uncaughtError(reason, function(err) { if (err) { logger.error('Encountered error while handling an uncaught exception.'); diff --git a/sdks/rollbar.js/test/browser.rollbar.test.js b/sdks/rollbar.js/test/browser.rollbar.test.js index 8c250436e..8fbbeb8f7 100644 --- a/sdks/rollbar.js/test/browser.rollbar.test.js +++ b/sdks/rollbar.js/test/browser.rollbar.test.js @@ -204,6 +204,218 @@ describe('configure', function() { }); }); +describe('options.captureUncaught', function() { + before(function (done) { + // Load the HTML page, so errors can be generated. + document.write(window.__html__['examples/error.html']); + + window.server = sinon.createFakeServer(); + done(); + }); + + after(function () { + window.server.restore(); + }); + + function stubResponse(server) { + server.respondWith('POST', 'api/1/item', + [ + 200, + { 'Content-Type': 'application/json' }, + '{"err": 0, "result":{ "uuid": "d4c7acef55bf4c9ea95e4fe9428a8287"}}' + ] + ); + } + + it('should capture when enabled in constructor', function(done) { + var server = window.server; + stubResponse(server); + server.requests.length = 0; + + var options = { + accessToken: 'POST_CLIENT_ITEM_TOKEN', + captureUncaught: true + }; + var rollbar = new Rollbar(options); + + var element = document.getElementById('throw-error'); + element.click(); + server.respond(); + + var body = JSON.parse(server.requests[0].requestBody); + + expect(body.access_token).to.eql('POST_CLIENT_ITEM_TOKEN'); + expect(body.data.body.trace.exception.message).to.eql('test error'); + + // karma doesn't unload the browser between tests, so the onerror handler + // will remain installed. Unset captureUncaught so the onerror handler + // won't affect other tests. + rollbar.configure({ + captureUncaught: false + }); + + done(); + }); + + it('should respond to enable/disable in configure', function(done) { + var server = window.server; + var element = document.getElementById('throw-error'); + stubResponse(server); + server.requests.length = 0; + + var options = { + accessToken: 'POST_CLIENT_ITEM_TOKEN', + captureUncaught: false + }; + var rollbar = new Rollbar(options); + + element.click(); + server.respond(); + expect(server.requests.length).to.eql(0); // Disabled, no event + server.requests.length = 0; + + rollbar.configure({ + captureUncaught: true + }); + + element.click(); + server.respond(); + + var body = JSON.parse(server.requests[0].requestBody); + + expect(body.access_token).to.eql('POST_CLIENT_ITEM_TOKEN'); + expect(body.data.body.trace.exception.message).to.eql('test error'); + + server.requests.length = 0; + + rollbar.configure({ + captureUncaught: false + }); + + element.click(); + server.respond(); + expect(server.requests.length).to.eql(0); // Disabled, no event + + done(); + }); +}); + +describe('options.captureUnhandledRejections', function() { + before(function (done) { + window.server = sinon.createFakeServer(); + done(); + }); + + after(function () { + window.server.restore(); + }); + + function stubResponse(server) { + server.respondWith('POST', 'api/1/item', + [ + 200, + { 'Content-Type': 'application/json' }, + '{"err": 0, "result":{ "uuid": "d4c7acef55bf4c9ea95e4fe9428a8287"}}' + ] + ); + } + + it('should capture when enabled in constructor', function(done) { + var server = window.server; + stubResponse(server); + server.requests.length = 0; + + var options = { + accessToken: 'POST_CLIENT_ITEM_TOKEN', + captureUnhandledRejections: true + }; + var rollbar = new Rollbar(options); + + Promise.reject(new Error('test reject')); + + setTimeout(function() { + server.respond(); + + var body = JSON.parse(server.requests[0].requestBody); + + expect(body.access_token).to.eql('POST_CLIENT_ITEM_TOKEN'); + expect(body.data.body.trace.exception.message).to.eql('test reject'); + + rollbar.configure({ + captureUnhandledRejections: false + }); + window.removeEventListener('unhandledrejection', window._rollbarURH); + + done(); + }, 500); + }); + + it('should respond to enable in configure', function(done) { + var server = window.server; + stubResponse(server); + server.requests.length = 0; + + var options = { + accessToken: 'POST_CLIENT_ITEM_TOKEN', + captureUnhandledRejections: false + }; + var rollbar = new Rollbar(options); + + rollbar.configure({ + captureUnhandledRejections: true + }); + + Promise.reject(new Error('test reject')); + + setTimeout(function() { + server.respond(); + + var body = JSON.parse(server.requests[0].requestBody); + + expect(body.access_token).to.eql('POST_CLIENT_ITEM_TOKEN'); + expect(body.data.body.trace.exception.message).to.eql('test reject'); + + server.requests.length = 0; + + rollbar.configure({ + captureUnhandledRejections: false + }); + window.removeEventListener('unhandledrejection', window._rollbarURH); + + done(); + }, 500); + }); + + it('should respond to disable in configure', function(done) { + var server = window.server; + stubResponse(server); + server.requests.length = 0; + + var options = { + accessToken: 'POST_CLIENT_ITEM_TOKEN', + captureUnhandledRejections: true + }; + var rollbar = new Rollbar(options); + + rollbar.configure({ + captureUnhandledRejections: false + }); + + Promise.reject(new Error('test reject')); + + setTimeout(function() { + server.respond(); + + expect(server.requests.length).to.eql(0); // Disabled, no event + server.requests.length = 0; + + window.removeEventListener('unhandledrejection', window._rollbarURH); + + done(); + }, 500); + }) +}); + describe('captureEvent', function() { it('should handle missing/default type and level', function(done) { var options = {}; diff --git a/sdks/rollbar.js/test/server.rollbar.test.js b/sdks/rollbar.js/test/server.rollbar.test.js index 85755fdfe..771f9e936 100644 --- a/sdks/rollbar.js/test/server.rollbar.test.js +++ b/sdks/rollbar.js/test/server.rollbar.test.js @@ -2,6 +2,7 @@ var assert = require('assert'); var vows = require('vows'); +var sinon = require('sinon'); process.env.NODE_ENV = process.env.NODE_ENV || 'test-node-env'; var Rollbar = require('../src/server/rollbar'); @@ -36,6 +37,26 @@ function TestClientGen() { return TestClient; } +async function wait(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + +async function nodeReject(rollbar, callback) { + Promise.reject(new Error('node reject')); + await wait(500); + callback(rollbar); +} + +async function nodeThrow(rollbar, callback) { + setTimeout(function () { + throw new Error('node error'); + }, 10); + await wait(500); + callback(rollbar); +} + vows.describe('rollbar') .addBatch({ 'constructor': { @@ -277,6 +298,164 @@ vows.describe('rollbar') } } }, + // captureUncaught tests are set up using subtopics because Rollbar's node.js handlers + // are treated as global and concurrent access will lead to handlers being removed + // unexpectedly. (Subtopics execute sequentially.) + 'captureUncaught': { + 'enabled in constructor': { + topic: function() { + var rollbar = new Rollbar({ + accessToken: 'abc123', + captureUncaught: true + }); + var notifier = rollbar.client.notifier; + rollbar.logStub = sinon.stub(notifier, 'log'); + + nodeThrow(rollbar, this.callback); + }, + 'should log': function(r) { + var logStub = r.logStub; + + assert.isTrue(logStub.called); + if (logStub.called) { + assert.equal(logStub.getCall(0).args[0].err.message, 'node error'); + } + logStub.reset(); + }, + 'disabled in configure': { + topic: function(r) { + r.configure({ + captureUncaught: false + }); + + nodeThrow(r, this.callback); + }, + 'should not log': function(r) { + var notifier = r.client.notifier; + var logStub = r.logStub; + + assert.isFalse(logStub.called); + notifier.log.restore(); + }, + 'disabled in constructor': { + topic: function() { + var rollbar = new Rollbar({ + accessToken: 'abc123', + captureUncaught: false + }); + var notifier = rollbar.client.notifier; + rollbar.logStub = sinon.stub(notifier, 'log'); + + nodeThrow(rollbar, this.callback); + }, + 'should not log': function(r) { + var logStub = r.logStub; + + assert.isFalse(logStub.called); + logStub.reset(); + }, + 'enabled in configure': { + topic: function(r) { + r.configure({ + captureUncaught: true + }); + + nodeThrow(r, this.callback); + }, + 'should log': function(r) { + var notifier = r.client.notifier; + var logStub = r.logStub; + + assert.isTrue(logStub.called); + if (logStub.called) { + assert.equal(logStub.getCall(0).args[0].err.message, 'node error'); + } + notifier.log.restore(); + } + } + } + } + } + }, + // captureUnhandledRejections tests are set up using subtopics because Rollbar's node.js handlers + // are treated as global and concurrent access will lead to handlers being removed + // unexpectedly. (Subtopics execute sequentially.) + 'captureUnhandledRejections': { + 'enabled in constructor': { + topic: function() { + var rollbar = new Rollbar({ + accessToken: 'abc123', + captureUnhandledRejections: true + }); + var notifier = rollbar.client.notifier; + rollbar.logStub = sinon.stub(notifier, 'log'); + + nodeReject(rollbar, this.callback); + }, + 'should log': function(r) { + var logStub = r.logStub; + + assert.isTrue(logStub.called); + if (logStub.called) { + assert.equal(logStub.getCall(0).args[0].err.message, 'node reject'); + } + logStub.reset(); + }, + 'disabled in configure': { + topic: function(r) { + r.configure({ + captureUnhandledRejections: false + }); + + nodeReject(r, this.callback); + }, + 'should not log': function(r) { + var notifier = r.client.notifier; + var logStub = r.logStub; + + assert.isFalse(logStub.called); + notifier.log.restore(); + }, + 'disabled in constructor': { + topic: function() { + var rollbar = new Rollbar({ + accessToken: 'abc123', + captureUnhandledRejections: false + }); + var notifier = rollbar.client.notifier; + rollbar.logStub = sinon.stub(notifier, 'log'); + + nodeReject(rollbar, this.callback); + }, + 'should not log': function(r) { + var logStub = r.logStub; + + assert.isFalse(logStub.called); + logStub.reset(); + }, + 'enabled in configure': { + topic: function(r) { + r.configure({ + captureUnhandledRejections: true + }); + + nodeReject(r, this.callback); + }, + 'should log': function(r) { + var notifier = r.client.notifier; + var logStub = r.logStub; + + assert.isTrue(logStub.called); + if (logStub.called) { + assert.equal(logStub.getCall(0).args[0].err.message, 'node reject'); + } + notifier.log.restore(); + } + } + } + } + } + }, 'buildJsonPayload': { topic: function() { var client = new (TestClientGen())();