Skip to content

Commit 2f6b99a

Browse files
authored
Adds autoBreadcrumbs config option ... (#686)
Can disable automatic collection of breadcrumbs entirely, or enable/disable individual breadcrumb loggers (dom, location, etc).
1 parent 2210615 commit 2f6b99a

File tree

4 files changed

+198
-70
lines changed

4 files changed

+198
-70
lines changed

docs/config.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,23 @@ Those configuration options are documented below:
167167
By default, Raven does not truncate messages. If you need to truncate
168168
characters for whatever reason, you may set this to limit the length.
169169

170+
.. describe:: autoBreadcrumbs
171+
172+
Enables/disables automatic collection of breadcrumbs. Possible values are:
173+
174+
* `true` (default)
175+
* `false` - all automatic breadcrumb collection disabled
176+
* A dictionary of individual breadcrumb types that can be enabled/disabled:
177+
178+
..code-block:: javascript
179+
180+
autoBreadcrumbs: {
181+
'xhr': false, // XMLHttpRequest
182+
'console': false, // console logging
183+
'dom': true, // DOM interactions, i.e. clicks/typing
184+
'location': false // url changes, including pushState/popState
185+
}
186+
170187
.. describe:: maxBreadcrumbs
171188

172189
By default, Raven captures as many as 100 breadcrumb entries. If you find this too noisy, you can reduce this

src/raven.js

Lines changed: 98 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var uuid4 = utils.uuid4;
2020
var htmlTreeAsString = utils.htmlTreeAsString;
2121
var parseUrl = utils.parseUrl;
2222
var isString = utils.isString;
23+
var fill = utils.fill;
2324

2425
var wrapConsoleMethod = require('./console').wrapMethod;
2526

@@ -30,6 +31,7 @@ function now() {
3031
return +new Date();
3132
}
3233

34+
3335
// First, check for JSON support
3436
// If there is no JSON, we no-op the core features of Raven
3537
// since JSON is required to encode the payload
@@ -52,7 +54,8 @@ function Raven() {
5254
crossOrigin: 'anonymous',
5355
collectWindowErrors: true,
5456
maxMessageLength: 0,
55-
stackTraceLimit: 50
57+
stackTraceLimit: 50,
58+
autoBreadcrumbs: true
5659
};
5760
this._ignoreOnError = 0;
5861
this._isRavenInstalled = false;
@@ -138,6 +141,21 @@ Raven.prototype = {
138141
this._globalOptions.includePaths = joinRegExp(this._globalOptions.includePaths);
139142
this._globalOptions.maxBreadcrumbs = Math.max(0, Math.min(this._globalOptions.maxBreadcrumbs || 100, 100)); // default and hard limit is 100
140143

144+
var autoBreadcrumbDefaults = {
145+
xhr: true,
146+
console: true,
147+
dom: true,
148+
location: true
149+
};
150+
151+
var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs;
152+
if ({}.toString.call(autoBreadcrumbs) === '[object Object]') {
153+
autoBreadcrumbs = objectMerge(autoBreadcrumbDefaults, autoBreadcrumbs);
154+
} else if (autoBreadcrumbs !== false) {
155+
autoBreadcrumbs = autoBreadcrumbDefaults;
156+
}
157+
this._globalOptions.autoBreadcrumbs = autoBreadcrumbs;
158+
141159
this._globalKey = uri.user;
142160
this._globalSecret = uri.pass && uri.pass.substr(1);
143161
this._globalProject = uri.path.substr(lastSlash + 1);
@@ -167,7 +185,9 @@ Raven.prototype = {
167185
TraceKit.report.subscribe(function () {
168186
self._handleOnErrorStackInfo.apply(self, arguments);
169187
});
170-
this._wrapBuiltIns();
188+
this._instrumentTryCatch();
189+
if (self._globalOptions.autoBreadcrumbs)
190+
this._instrumentBreadcrumbs();
171191

172192
// Install all of the plugins
173193
this._drainPlugins();
@@ -742,16 +762,10 @@ Raven.prototype = {
742762
/**
743763
* Install any queued plugins
744764
*/
745-
_wrapBuiltIns: function() {
765+
_instrumentTryCatch: function() {
746766
var self = this;
747767

748-
function fill(obj, name, replacement, noUndo) {
749-
var orig = obj[name];
750-
obj[name] = replacement(orig);
751-
if (!noUndo) {
752-
self._wrappedBuiltIns.push([obj, name, orig]);
753-
}
754-
}
768+
var wrappedBuiltIns = self._wrappedBuiltIns;
755769

756770
function wrapTimeFn(orig) {
757771
return function (fn, t) { // preserve arity
@@ -777,6 +791,8 @@ Raven.prototype = {
777791
};
778792
}
779793

794+
var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs;
795+
780796
function wrapEventTarget(global) {
781797
var proto = window[global] && window[global].prototype;
782798
if (proto && proto.hasOwnProperty && proto.hasOwnProperty('addEventListener')) {
@@ -790,10 +806,10 @@ Raven.prototype = {
790806
// can sometimes get 'Permission denied to access property "handle Event'
791807
}
792808

793-
794-
// TODO: more than just click
809+
// More breadcrumb DOM capture ... done here and not in `_instrumentBreadcrumbs`
810+
// so that we don't have more than one wrapper function
795811
var before;
796-
if (global === 'EventTarget' || global === 'Node') {
812+
if (autoBreadcrumbs && autoBreadcrumbs.dom && (global === 'EventTarget' || global === 'Node')) {
797813
if (evtName === 'click'){
798814
before = self._breadcrumbEventHandler(evtName);
799815
} else if (evtName === 'keypress') {
@@ -802,46 +818,24 @@ Raven.prototype = {
802818
}
803819
return orig.call(this, evtName, self.wrap(fn, undefined, before), capture, secure);
804820
};
805-
});
821+
}, wrappedBuiltIns);
806822
fill(proto, 'removeEventListener', function (orig) {
807823
return function (evt, fn, capture, secure) {
808824
fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn);
809825
return orig.call(this, evt, fn, capture, secure);
810826
};
811-
});
827+
}, wrappedBuiltIns);
812828
}
813829
}
814830

815-
function wrapProp(prop, xhr) {
816-
if (prop in xhr && isFunction(xhr[prop])) {
817-
fill(xhr, prop, function (orig) {
818-
return self.wrap(orig);
819-
}, true /* noUndo */); // don't track filled methods on XHR instances
820-
}
821-
}
822-
823-
fill(window, 'setTimeout', wrapTimeFn);
824-
fill(window, 'setInterval', wrapTimeFn);
831+
fill(window, 'setTimeout', wrapTimeFn, wrappedBuiltIns);
832+
fill(window, 'setInterval', wrapTimeFn, wrappedBuiltIns);
825833
if (window.requestAnimationFrame) {
826834
fill(window, 'requestAnimationFrame', function (orig) {
827835
return function (cb) {
828836
return orig(self.wrap(cb));
829837
};
830-
});
831-
}
832-
833-
// Capture breadcrubms from any click that is unhandled / bubbled up all the way
834-
// to the document. Do this before we instrument addEventListener.
835-
if (this._hasDocument) {
836-
if (document.addEventListener) {
837-
document.addEventListener('click', self._breadcrumbEventHandler('click'), false);
838-
document.addEventListener('keypress', self._keypressEventHandler(), false);
839-
}
840-
else {
841-
// IE8 Compatibility
842-
document.attachEvent('onclick', self._breadcrumbEventHandler('click'));
843-
document.attachEvent('onkeypress', self._keypressEventHandler());
844-
}
838+
}, wrappedBuiltIns);
845839
}
846840

847841
// event targets borrowed from bugsnag-js:
@@ -851,7 +845,41 @@ Raven.prototype = {
851845
wrapEventTarget(eventTargets[i]);
852846
}
853847

854-
if ('XMLHttpRequest' in window) {
848+
var $ = window.jQuery || window.$;
849+
if ($ && $.fn && $.fn.ready) {
850+
fill($.fn, 'ready', function (orig) {
851+
return function (fn) {
852+
return orig.call(this, self.wrap(fn));
853+
};
854+
}, wrappedBuiltIns);
855+
}
856+
},
857+
858+
859+
/**
860+
* Instrument browser built-ins w/ breadcrumb capturing
861+
* - XMLHttpRequests
862+
* - DOM interactions (click/typing)
863+
* - window.location changes
864+
* - console
865+
*
866+
* Can be disabled or individually configured via the `autoBreadcrumbs` config option
867+
*/
868+
_instrumentBreadcrumbs: function () {
869+
var self = this;
870+
var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs;
871+
872+
var wrappedBuiltIns = self._wrappedBuiltIns;
873+
874+
function wrapProp(prop, xhr) {
875+
if (prop in xhr && isFunction(xhr[prop])) {
876+
fill(xhr, prop, function (orig) {
877+
return self.wrap(orig);
878+
}); // intentionally don't track filled methods on XHR instances
879+
}
880+
}
881+
882+
if (autoBreadcrumbs.xhr && 'XMLHttpRequest' in window) {
855883
var xhrproto = XMLHttpRequest.prototype;
856884
fill(xhrproto, 'open', function(origOpen) {
857885
return function (method, url) { // preserve arity
@@ -867,7 +895,7 @@ Raven.prototype = {
867895

868896
return origOpen.apply(this, arguments);
869897
};
870-
});
898+
}, wrappedBuiltIns);
871899

872900
fill(xhrproto, 'send', function(origSend) {
873901
return function (data) { // preserve arity
@@ -896,7 +924,7 @@ Raven.prototype = {
896924
if ('onreadystatechange' in xhr && isFunction(xhr.onreadystatechange)) {
897925
fill(xhr, 'onreadystatechange', function (orig) {
898926
return self.wrap(orig, undefined, onreadystatechangeHandler);
899-
}, true /* noUndo */);
927+
} /* intentionally don't track this instrumentation */);
900928
} else {
901929
// if onreadystatechange wasn't actually set by the page on this xhr, we
902930
// are free to set our own and capture the breadcrumb
@@ -905,7 +933,21 @@ Raven.prototype = {
905933

906934
return origSend.apply(this, arguments);
907935
};
908-
});
936+
}, wrappedBuiltIns);
937+
}
938+
939+
// Capture breadcrumbs from any click that is unhandled / bubbled up all the way
940+
// to the document. Do this before we instrument addEventListener.
941+
if (autoBreadcrumbs.dom && this._hasDocument) {
942+
if (document.addEventListener) {
943+
document.addEventListener('click', self._breadcrumbEventHandler('click'), false);
944+
document.addEventListener('keypress', self._keypressEventHandler(), false);
945+
}
946+
else {
947+
// IE8 Compatibility
948+
document.attachEvent('onclick', self._breadcrumbEventHandler('click'));
949+
document.attachEvent('onkeypress', self._keypressEventHandler());
950+
}
909951
}
910952

911953
// record navigation (URL) changes
@@ -915,7 +957,7 @@ Raven.prototype = {
915957
var chrome = window.chrome;
916958
var isChromePackagedApp = chrome && chrome.app && chrome.app.runtime;
917959
var hasPushState = !isChromePackagedApp && window.history && history.pushState;
918-
if (hasPushState) {
960+
if (autoBreadcrumbs.location && hasPushState) {
919961
// TODO: remove onpopstate handler on uninstall()
920962
var oldOnPopState = window.onpopstate;
921963
window.onpopstate = function () {
@@ -930,7 +972,7 @@ Raven.prototype = {
930972
fill(history, 'pushState', function (origPushState) {
931973
// note history.pushState.length is 0; intentionally not declaring
932974
// params to preserve 0 arity
933-
return function(/* state, title, url */) {
975+
return function (/* state, title, url */) {
934976
var url = arguments.length > 2 ? arguments[2] : undefined;
935977

936978
// url argument is optional
@@ -941,32 +983,24 @@ Raven.prototype = {
941983

942984
return origPushState.apply(this, arguments);
943985
};
944-
});
986+
}, wrappedBuiltIns);
945987
}
946988

947-
// console
948-
var consoleMethodCallback = function (msg, data) {
949-
self.captureBreadcrumb({
950-
message: msg,
951-
level: data.level,
952-
category: 'console'
953-
});
954-
};
989+
if (autoBreadcrumbs.console && 'console' in window && console.log) {
990+
// console
991+
var consoleMethodCallback = function (msg, data) {
992+
self.captureBreadcrumb({
993+
message: msg,
994+
level: data.level,
995+
category: 'console'
996+
});
997+
};
955998

956-
if ('console' in window && console.log) {
957999
each(['debug', 'info', 'warn', 'error', 'log'], function (_, level) {
9581000
wrapConsoleMethod(console, level, consoleMethodCallback);
9591001
});
9601002
}
9611003

962-
var $ = window.jQuery || window.$;
963-
if ($ && $.fn && $.fn.ready) {
964-
fill($.fn, 'ready', function (orig) {
965-
return function (fn) {
966-
return orig.call(this, self.wrap(fn));
967-
};
968-
});
969-
}
9701004
},
9711005

9721006
_restoreBuiltIns: function () {

src/utils.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,21 @@ function htmlElementAsString(elem) {
236236
return out.join('');
237237
}
238238

239+
/**
240+
* Polyfill a method
241+
* @param obj object e.g. `document`
242+
* @param name method name present on object e.g. `addEventListener`
243+
* @param replacement replacement function
244+
* @param track {optional} record instrumentation to an array
245+
*/
246+
function fill(obj, name, replacement, track) {
247+
var orig = obj[name];
248+
obj[name] = replacement(orig);
249+
if (track) {
250+
track.push([obj, name, orig]);
251+
}
252+
}
253+
239254
module.exports = {
240255
isUndefined: isUndefined,
241256
isFunction: isFunction,
@@ -252,5 +267,6 @@ module.exports = {
252267
uuid4: uuid4,
253268
htmlTreeAsString: htmlTreeAsString,
254269
htmlElementAsString: htmlElementAsString,
255-
parseUrl: parseUrl
270+
parseUrl: parseUrl,
271+
fill: fill
256272
};

0 commit comments

Comments
 (0)