Skip to content

Adds autoBreadcrumbs config option ... #686

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
162 changes: 98 additions & 64 deletions src/raven.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -52,7 +54,8 @@ function Raven() {
crossOrigin: 'anonymous',
collectWindowErrors: true,
maxMessageLength: 0,
stackTraceLimit: 50
stackTraceLimit: 50,
autoBreadcrumbs: true
};
this._ignoreOnError = 0;
this._isRavenInstalled = false;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand All @@ -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')) {
Expand All @@ -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') {
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -867,7 +895,7 @@ Raven.prototype = {

return origOpen.apply(this, arguments);
};
});
}, wrappedBuiltIns);

fill(xhrproto, 'send', function(origSend) {
return function (data) { // preserve arity
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 () {
Expand All @@ -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
Expand All @@ -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 () {
Expand Down
18 changes: 17 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -252,5 +267,6 @@ module.exports = {
uuid4: uuid4,
htmlTreeAsString: htmlTreeAsString,
htmlElementAsString: htmlElementAsString,
parseUrl: parseUrl
parseUrl: parseUrl,
fill: fill
};
Loading