From ee6f6c760af91ee72d47d7ef037be473626cc432 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Fri, 12 Aug 2016 16:09:00 -0700 Subject: [PATCH] Adds `autoBreadcrumbs` config option ... Can disable automatic collection of breadcrumbs entirely, or enable/disable individual breadcrumb loggers (dom, location, etc). --- docs/config.rst | 17 +++++ src/raven.js | 162 +++++++++++++++++++++++++++------------------ src/utils.js | 18 ++++- test/raven.test.js | 71 ++++++++++++++++++-- 4 files changed, 198 insertions(+), 70 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 28f6487d7f76..9cf738fb8723 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -167,6 +167,23 @@ Those configuration options are documented below: By default, Raven does not truncate messages. If you need to truncate characters for whatever reason, you may set this to limit the length. +.. describe:: autoBreadcrumbs + + Enables/disables automatic collection of breadcrumbs. Possible values are: + + * `true` (default) + * `false` - all automatic breadcrumb collection disabled + * A dictionary of individual breadcrumb types that can be enabled/disabled: + + ..code-block:: javascript + + autoBreadcrumbs: { + 'xhr': false, // XMLHttpRequest + 'console': false, // console logging + 'dom': true, // DOM interactions, i.e. clicks/typing + 'location': false // url changes, including pushState/popState + } + .. describe:: maxBreadcrumbs By default, Raven captures as many as 100 breadcrumb entries. If you find this too noisy, you can reduce this diff --git a/src/raven.js b/src/raven.js index b40b36c13358..203ca4ba6b9a 100644 --- a/src/raven.js +++ b/src/raven.js @@ -20,6 +20,7 @@ var uuid4 = utils.uuid4; var htmlTreeAsString = utils.htmlTreeAsString; var parseUrl = utils.parseUrl; var isString = utils.isString; +var fill = utils.fill; var wrapConsoleMethod = require('./console').wrapMethod; @@ -30,6 +31,7 @@ function now() { return +new Date(); } + // First, check for JSON support // If there is no JSON, we no-op the core features of Raven // since JSON is required to encode the payload @@ -52,7 +54,8 @@ function Raven() { crossOrigin: 'anonymous', collectWindowErrors: true, maxMessageLength: 0, - stackTraceLimit: 50 + stackTraceLimit: 50, + autoBreadcrumbs: true }; this._ignoreOnError = 0; this._isRavenInstalled = false; @@ -138,6 +141,21 @@ Raven.prototype = { this._globalOptions.includePaths = joinRegExp(this._globalOptions.includePaths); this._globalOptions.maxBreadcrumbs = Math.max(0, Math.min(this._globalOptions.maxBreadcrumbs || 100, 100)); // default and hard limit is 100 + var autoBreadcrumbDefaults = { + xhr: true, + console: true, + dom: true, + location: true + }; + + var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs; + if ({}.toString.call(autoBreadcrumbs) === '[object Object]') { + autoBreadcrumbs = objectMerge(autoBreadcrumbDefaults, autoBreadcrumbs); + } else if (autoBreadcrumbs !== false) { + autoBreadcrumbs = autoBreadcrumbDefaults; + } + this._globalOptions.autoBreadcrumbs = autoBreadcrumbs; + this._globalKey = uri.user; this._globalSecret = uri.pass && uri.pass.substr(1); this._globalProject = uri.path.substr(lastSlash + 1); @@ -167,7 +185,9 @@ Raven.prototype = { TraceKit.report.subscribe(function () { self._handleOnErrorStackInfo.apply(self, arguments); }); - this._wrapBuiltIns(); + this._instrumentTryCatch(); + if (self._globalOptions.autoBreadcrumbs) + this._instrumentBreadcrumbs(); // Install all of the plugins this._drainPlugins(); @@ -742,16 +762,10 @@ Raven.prototype = { /** * Install any queued plugins */ - _wrapBuiltIns: function() { + _instrumentTryCatch: function() { var self = this; - function fill(obj, name, replacement, noUndo) { - var orig = obj[name]; - obj[name] = replacement(orig); - if (!noUndo) { - self._wrappedBuiltIns.push([obj, name, orig]); - } - } + var wrappedBuiltIns = self._wrappedBuiltIns; function wrapTimeFn(orig) { return function (fn, t) { // preserve arity @@ -777,6 +791,8 @@ Raven.prototype = { }; } + var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs; + function wrapEventTarget(global) { var proto = window[global] && window[global].prototype; if (proto && proto.hasOwnProperty && proto.hasOwnProperty('addEventListener')) { @@ -790,10 +806,10 @@ Raven.prototype = { // can sometimes get 'Permission denied to access property "handle Event' } - - // TODO: more than just click + // More breadcrumb DOM capture ... done here and not in `_instrumentBreadcrumbs` + // so that we don't have more than one wrapper function var before; - if (global === 'EventTarget' || global === 'Node') { + if (autoBreadcrumbs && autoBreadcrumbs.dom && (global === 'EventTarget' || global === 'Node')) { if (evtName === 'click'){ before = self._breadcrumbEventHandler(evtName); } else if (evtName === 'keypress') { @@ -802,46 +818,24 @@ Raven.prototype = { } return orig.call(this, evtName, self.wrap(fn, undefined, before), capture, secure); }; - }); + }, wrappedBuiltIns); fill(proto, 'removeEventListener', function (orig) { return function (evt, fn, capture, secure) { fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn); return orig.call(this, evt, fn, capture, secure); }; - }); + }, wrappedBuiltIns); } } - function wrapProp(prop, xhr) { - if (prop in xhr && isFunction(xhr[prop])) { - fill(xhr, prop, function (orig) { - return self.wrap(orig); - }, true /* noUndo */); // don't track filled methods on XHR instances - } - } - - fill(window, 'setTimeout', wrapTimeFn); - fill(window, 'setInterval', wrapTimeFn); + fill(window, 'setTimeout', wrapTimeFn, wrappedBuiltIns); + fill(window, 'setInterval', wrapTimeFn, wrappedBuiltIns); if (window.requestAnimationFrame) { fill(window, 'requestAnimationFrame', function (orig) { return function (cb) { return orig(self.wrap(cb)); }; - }); - } - - // Capture breadcrubms from any click that is unhandled / bubbled up all the way - // to the document. Do this before we instrument addEventListener. - if (this._hasDocument) { - if (document.addEventListener) { - document.addEventListener('click', self._breadcrumbEventHandler('click'), false); - document.addEventListener('keypress', self._keypressEventHandler(), false); - } - else { - // IE8 Compatibility - document.attachEvent('onclick', self._breadcrumbEventHandler('click')); - document.attachEvent('onkeypress', self._keypressEventHandler()); - } + }, wrappedBuiltIns); } // event targets borrowed from bugsnag-js: @@ -851,7 +845,41 @@ Raven.prototype = { wrapEventTarget(eventTargets[i]); } - if ('XMLHttpRequest' in window) { + var $ = window.jQuery || window.$; + if ($ && $.fn && $.fn.ready) { + fill($.fn, 'ready', function (orig) { + return function (fn) { + return orig.call(this, self.wrap(fn)); + }; + }, wrappedBuiltIns); + } + }, + + + /** + * Instrument browser built-ins w/ breadcrumb capturing + * - XMLHttpRequests + * - DOM interactions (click/typing) + * - window.location changes + * - console + * + * Can be disabled or individually configured via the `autoBreadcrumbs` config option + */ + _instrumentBreadcrumbs: function () { + var self = this; + var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs; + + var wrappedBuiltIns = self._wrappedBuiltIns; + + function wrapProp(prop, xhr) { + if (prop in xhr && isFunction(xhr[prop])) { + fill(xhr, prop, function (orig) { + return self.wrap(orig); + }); // intentionally don't track filled methods on XHR instances + } + } + + if (autoBreadcrumbs.xhr && 'XMLHttpRequest' in window) { var xhrproto = XMLHttpRequest.prototype; fill(xhrproto, 'open', function(origOpen) { return function (method, url) { // preserve arity @@ -867,7 +895,7 @@ Raven.prototype = { return origOpen.apply(this, arguments); }; - }); + }, wrappedBuiltIns); fill(xhrproto, 'send', function(origSend) { return function (data) { // preserve arity @@ -896,7 +924,7 @@ Raven.prototype = { if ('onreadystatechange' in xhr && isFunction(xhr.onreadystatechange)) { fill(xhr, 'onreadystatechange', function (orig) { return self.wrap(orig, undefined, onreadystatechangeHandler); - }, true /* noUndo */); + } /* intentionally don't track this instrumentation */); } else { // if onreadystatechange wasn't actually set by the page on this xhr, we // are free to set our own and capture the breadcrumb @@ -905,7 +933,21 @@ Raven.prototype = { return origSend.apply(this, arguments); }; - }); + }, wrappedBuiltIns); + } + + // Capture breadcrumbs from any click that is unhandled / bubbled up all the way + // to the document. Do this before we instrument addEventListener. + if (autoBreadcrumbs.dom && this._hasDocument) { + if (document.addEventListener) { + document.addEventListener('click', self._breadcrumbEventHandler('click'), false); + document.addEventListener('keypress', self._keypressEventHandler(), false); + } + else { + // IE8 Compatibility + document.attachEvent('onclick', self._breadcrumbEventHandler('click')); + document.attachEvent('onkeypress', self._keypressEventHandler()); + } } // record navigation (URL) changes @@ -915,7 +957,7 @@ Raven.prototype = { var chrome = window.chrome; var isChromePackagedApp = chrome && chrome.app && chrome.app.runtime; var hasPushState = !isChromePackagedApp && window.history && history.pushState; - if (hasPushState) { + if (autoBreadcrumbs.location && hasPushState) { // TODO: remove onpopstate handler on uninstall() var oldOnPopState = window.onpopstate; window.onpopstate = function () { @@ -930,7 +972,7 @@ Raven.prototype = { fill(history, 'pushState', function (origPushState) { // note history.pushState.length is 0; intentionally not declaring // params to preserve 0 arity - return function(/* state, title, url */) { + return function (/* state, title, url */) { var url = arguments.length > 2 ? arguments[2] : undefined; // url argument is optional @@ -941,32 +983,24 @@ Raven.prototype = { return origPushState.apply(this, arguments); }; - }); + }, wrappedBuiltIns); } - // console - var consoleMethodCallback = function (msg, data) { - self.captureBreadcrumb({ - message: msg, - level: data.level, - category: 'console' - }); - }; + if (autoBreadcrumbs.console && 'console' in window && console.log) { + // console + var consoleMethodCallback = function (msg, data) { + self.captureBreadcrumb({ + message: msg, + level: data.level, + category: 'console' + }); + }; - if ('console' in window && console.log) { each(['debug', 'info', 'warn', 'error', 'log'], function (_, level) { wrapConsoleMethod(console, level, consoleMethodCallback); }); } - var $ = window.jQuery || window.$; - if ($ && $.fn && $.fn.ready) { - fill($.fn, 'ready', function (orig) { - return function (fn) { - return orig.call(this, self.wrap(fn)); - }; - }); - } }, _restoreBuiltIns: function () { diff --git a/src/utils.js b/src/utils.js index 967a98f34d8b..adbc62aafe7b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -236,6 +236,21 @@ function htmlElementAsString(elem) { return out.join(''); } +/** + * Polyfill a method + * @param obj object e.g. `document` + * @param name method name present on object e.g. `addEventListener` + * @param replacement replacement function + * @param track {optional} record instrumentation to an array + */ +function fill(obj, name, replacement, track) { + var orig = obj[name]; + obj[name] = replacement(orig); + if (track) { + track.push([obj, name, orig]); + } +} + module.exports = { isUndefined: isUndefined, isFunction: isFunction, @@ -252,5 +267,6 @@ module.exports = { uuid4: uuid4, htmlTreeAsString: htmlTreeAsString, htmlElementAsString: htmlElementAsString, - parseUrl: parseUrl + parseUrl: parseUrl, + fill: fill }; diff --git a/test/raven.test.js b/test/raven.test.js index eb6ef786417b..8e1dba5f615b 100644 --- a/test/raven.test.js +++ b/test/raven.test.js @@ -1600,6 +1600,39 @@ describe('Raven (public API)', function() { assert.equal(Raven._globalOptions.maxBreadcrumbs, 100); }); }); + + describe('autoBreadcrumbs', function () { + it('should convert `true` to a dictionary of enabled breadcrumb features', function () { + Raven.config(SENTRY_DSN); + assert.deepEqual(Raven._globalOptions.autoBreadcrumbs, { + xhr: true, + console: true, + dom: true, + location: true + }); + }); + + it('should leave false as-is', function () { + Raven.config(SENTRY_DSN, { + autoBreadcrumbs: false + }); + assert.equal(Raven._globalOptions.autoBreadcrumbs, false); + }); + + it('should merge objects with the default autoBreadcrumb settings', function () { + Raven.config(SENTRY_DSN, { + autoBreadcrumbs: { + location: false + } + }); + assert.deepEqual(Raven._globalOptions.autoBreadcrumbs, { + xhr: true, + console: true, + dom: true, + location: false /* ! */ + }); + }); + }); }); describe('.wrap', function() { @@ -2225,9 +2258,12 @@ describe('install/uninstall', function () { }); describe('.install', function() { + beforeEach(function () { + this.sinon.stub(TraceKit.report, 'subscribe'); + }); + it('should check `Raven.isSetup`', function() { this.sinon.stub(Raven, 'isSetup').returns(false); - this.sinon.stub(TraceKit.report, 'subscribe'); Raven.install(); assert.isTrue(Raven.isSetup.calledOnce); assert.isFalse(TraceKit.report.subscribe.calledOnce); @@ -2235,7 +2271,6 @@ describe('install/uninstall', function () { it('should register itself with TraceKit', function() { this.sinon.stub(Raven, 'isSetup').returns(true); - this.sinon.stub(TraceKit.report, 'subscribe'); this.sinon.stub(Raven, '_handleStackInfo'); assert.equal(Raven, Raven.install()); assert.isTrue(TraceKit.report.subscribe.calledOnce); @@ -2249,19 +2284,21 @@ describe('install/uninstall', function () { it('should not register itself more than once', function() { this.sinon.stub(Raven, 'isSetup').returns(true); - this.sinon.stub(TraceKit.report, 'subscribe'); Raven.install(); Raven.install(); assert.isTrue(TraceKit.report.subscribe.calledOnce); }); - it('should use attachEvent instead of addEventListener in IE8', function () { + it('_instrumentBreadcrumbs should use attachEvent instead of addEventListener in IE8', function () { + Raven._globalOptions.autoBreadcrumbs = { + dom: true + }; + // Maintain a ref to the old function so we can restore it later. var temp = document.addEventListener; // Test setup. this.sinon.stub(Raven, 'isSetup').returns(true); - this.sinon.stub(TraceKit.report, 'subscribe'); document.addEventListener = false; document.attachEvent = this.sinon.stub(); @@ -2273,6 +2310,30 @@ describe('install/uninstall', function () { // Cleanup. document.addEventListener = temp; }); + + it('should instrument breadcrumbs by default', function () { + this.sinon.stub(Raven, '_instrumentBreadcrumbs'); + Raven.config(SENTRY_DSN).install(); + assert.isTrue(Raven._instrumentBreadcrumbs.calledOnce); + }); + + it('should instrument breadcrumbs if autoBreadcrumbs is an object', function () { + this.sinon.stub(Raven, '_instrumentBreadcrumbs'); + Raven.config(SENTRY_DSN, { + dom: true, + location: false + }).install(); + + assert.isTrue(Raven._instrumentBreadcrumbs.calledOnce); + }); + + it('should not instrument breadcrumbs if autoBreadcrumbs is false', function () { + this.sinon.stub(Raven, '_instrumentBreadcrumbs'); + Raven.config(SENTRY_DSN, { + autoBreadcrumbs: false + }).install(); + assert.isFalse(Raven._instrumentBreadcrumbs.called); + }); }); describe('.uninstall', function() {