diff --git a/package.json b/package.json index e276124c89bc..ddd91c7f4243 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "grunt-release": "~0.6.0", "grunt-s3": "~0.2.0-alpha.3", "grunt-sri": "mattrobenolt/grunt-sri#pretty", + "jquery": "^2.1.4", "lodash": "~2.4.0", "proxyquireify": "^3.0.0", "sinon": "~1.7.3", diff --git a/src/raven.js b/src/raven.js index f5fbacc75665..909e42e07f15 100644 --- a/src/raven.js +++ b/src/raven.js @@ -55,6 +55,7 @@ function Raven() { this._originalConsoleMethods = {}; this._plugins = []; this._startTime = now(); + this._wrappedBuiltIns = []; for (var method in this._originalConsole) { this._originalConsoleMethods[method] = this._originalConsole[method]; @@ -267,6 +268,9 @@ Raven.prototype = { */ uninstall: function() { TraceKit.report.uninstall(); + + this._restoreBuiltIns(); + this._isRavenInstalled = false; return this; @@ -545,9 +549,12 @@ Raven.prototype = { _wrapBuiltIns: function() { var self = this; - function fill(obj, name, replacement) { + function fill(obj, name, replacement, noUndo) { var orig = obj[name]; obj[name] = replacement(orig); + if (!noUndo) { + self._wrappedBuiltIns.push([obj, name, orig]); + } } function wrapTimeFn(orig) { @@ -607,26 +614,42 @@ Raven.prototype = { var origOpen; if ('XMLHttpRequest' in window) { origOpen = XMLHttpRequest.prototype.open; - XMLHttpRequest.prototype.open = function (data) { // preserve arity - var xhr = this; - 'onreadystatechange onload onerror onprogress'.replace(/\w+/g, function (prop) { - if (prop in xhr && Object.prototype.toString.call(xhr[prop]) === '[object Function]') { - fill(xhr, prop, function (orig) { - return self.wrap(orig); - }); - } - }); - origOpen.apply(this, arguments); - }; + fill(XMLHttpRequest.prototype, 'open', function(origOpen) { + return function (data) { // preserve arity + var xhr = this; + 'onreadystatechange onload onerror onprogress'.replace(/\w+/g, function (prop) { + if (prop in xhr && Object.prototype.toString.call(xhr[prop]) === '[object Function]') { + fill(xhr, prop, function (orig) { + return self.wrap(orig); + }, true /* noUndo */); // don't track filled methods on XHR instances + } + }); + origOpen.apply(this, arguments); + }; + }); } var $ = window.jQuery || window.$; - var origReady; if ($ && $.fn && $.fn.ready) { - origReady = $.fn.ready; - $.fn.ready = function ravenjQueryReadyWrapper(fn) { - return origReady.call(this, self.wrap(fn)); - }; + fill($.fn, 'ready', function (orig) { + return function (fn) { + orig.call(this, self.wrap(fn)); + }; + }); + } + }, + + _restoreBuiltIns: function () { + // restore any wrapped builtins + var builtin; + while (this._wrappedBuiltIns.length) { + builtin = this._wrappedBuiltIns.shift(); + + var obj = builtin[0], + name = builtin[1], + orig = builtin[2]; + + obj[name] = orig; } }, diff --git a/test/integration/frame.html b/test/integration/frame.html index a902ccfca1b1..ae2d6603e164 100644 --- a/test/integration/frame.html +++ b/test/integration/frame.html @@ -36,14 +36,25 @@ }; }()); + + + diff --git a/test/integration/test.js b/test/integration/test.js index dacf6848d2b7..1ca4b101e212 100644 --- a/test/integration/test.js +++ b/test/integration/test.js @@ -7,9 +7,9 @@ function iframeExecute(iframe, done, execute, assertCallback) { } catch (e) { done(e); } - } + }; // use setTimeout so stack trace doesn't go all the way back to mocha test runner - iframe.contentWindow.eval('origSetTimeout(' + execute.toString() + ');'); + iframe.contentWindow.eval('window.originalBuiltIns.setTimeout.call(window, ' + execute.toString() + ');'); } function createIframe(done) { @@ -248,7 +248,7 @@ describe('integration', function () { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { foo(); - } + }; xhr.open('GET', 'example.json'); xhr.send(); }, @@ -259,5 +259,77 @@ describe('integration', function () { } ); }); + + it('should capture exceptions from $.fn.ready (jQuery)', function (done) { + var iframe = this.iframe; + + iframeExecute(iframe, done, + function () { + setTimeout(done); + + $(function () { + foo(); + }); + }, + function () { + var ravenData = iframe.contentWindow.ravenData[0]; + // # of frames alter significantly between chrome/firefox & safari + assert.isAbove(ravenData.exception.values[0].stacktrace.frames.length, 2); + } + ); + }); + }); + + describe('uninstall', function () { + it('should restore original built-ins', function (done) { + var iframe = this.iframe; + + iframeExecute(iframe, done, + function () { + setTimeout(done); + Raven.uninstall(); + + window.isRestored = { + setTimeout: originalBuiltIns.setTimeout === setTimeout, + setInterval: originalBuiltIns.setInterval === setInterval, + requestAnimationFrame: originalBuiltIns.requestAnimationFrame === requestAnimationFrame, + xhrProtoOpen: originalBuiltIns.xhrProtoOpen === XMLHttpRequest.prototype.open, + headAddEventListener: originalBuiltIns.headAddEventListener === document.body.addEventListener, + headRemoveEventListener: originalBuiltIns.headRemoveEventListener === document.body.removeEventListener + }; + }, + function () { + var isRestored = iframe.contentWindow.isRestored; + assert.isTrue(isRestored.setTimeout); + assert.isTrue(isRestored.setInterval); + assert.isTrue(isRestored.requestAnimationFrame); + assert.isTrue(isRestored.xhrProtoOpen); + assert.isTrue(isRestored.headAddEventListener); + assert.isTrue(isRestored.headRemoveEventListener); + } + ); + }); + + it('should not restore XMLHttpRequest instance methods', function (done) { + var iframe = this.iframe; + + iframeExecute(iframe, done, + function () { + setTimeout(done); + + var xhr = new XMLHttpRequest(); + var origOnReadyStateChange = xhr.onreadystatechange = function () {}; + xhr.open('GET', '/foo/'); + xhr.abort(); + + Raven.uninstall(); + + window.isOnReadyStateChangeRestored = xhr.onready === origOnReadyStateChange; + }, + function () { + assert.isFalse(iframe.contentWindow.isOnReadyStateChangeRestored); + } + ); + }); }); });