From 044316ba5115e93a7e44e22888ad4f6c2334a0a7 Mon Sep 17 00:00:00 2001 From: ssafejava Date: Thu, 19 Sep 2013 11:15:16 +0800 Subject: [PATCH 1/8] Add support for extended window.onerror (#48). Chrome Canary now supports sending a column number & stacktrace to window.onerror. This patch enables TraceKit's window.onerror handler to use that stack trace if it exists. Additionally, it adds checks preventing the default setTimeout/setInterval wrapper from executing if this extended onerror arity is present. Since this wrapping has been extended out to a plugin, I resurrected the old jQuery event/ajax wrappers as a plugin. Along with the async plugin, it now exists in /plugins. Building the project will result in a tracekit.min.js and tracekit.noplugins.min.js. --- plugins/wrapAsync.js | 37 +++++++++++++++++++ plugins/wrapJquery.js | 86 +++++++++++++++++++++++++++++++++++++++++++ tracekit.js | 74 ++++++++++++++++++++++--------------- 3 files changed, 167 insertions(+), 30 deletions(-) create mode 100644 plugins/wrapAsync.js create mode 100644 plugins/wrapJquery.js diff --git a/plugins/wrapAsync.js b/plugins/wrapAsync.js new file mode 100644 index 0000000..1b28db3 --- /dev/null +++ b/plugins/wrapAsync.js @@ -0,0 +1,37 @@ +/** + * Extends support for global error handling for asynchronous browser + * functions. Adopted from Closure Library's errorhandler.js + */ +(function extendToAsynchronousCallbacks(window) { + var _helper = function _helper(fnName) { + var originalFn = window[fnName]; + window[fnName] = function traceKitAsyncExtension() { + // Make a copy of the arguments + var args = [].slice.call(arguments); + var originalCallback = args[0]; + if (typeof (originalCallback) === 'function') { + args[0] = window.TraceKit.wrap(originalCallback); + } + // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it + // also only supports 2 argument and doesn't care what "this" is, so we + // can just call the original function directly. + if (originalFn.apply) { + return originalFn.apply(this, args); + } else { + return originalFn(args[0], args[1]); + } + }; + }; + + window.TraceKit.supportsExtendedWindowOnError(function(supported){ + if(!supported) { + installHelpers(); + } + }); + + function installHelpers() { + _helper('setTimeout'); + _helper('setInterval'); + } + +}(this)); \ No newline at end of file diff --git a/plugins/wrapJquery.js b/plugins/wrapJquery.js new file mode 100644 index 0000000..84d8678 --- /dev/null +++ b/plugins/wrapJquery.js @@ -0,0 +1,86 @@ +/** + * Extended support for backtraces and global error handling for most + * asynchronous jQuery functions. + */ +(function traceKitAsyncForjQuery(window) { + + // quit if jQuery or TraceKit isn't on the page + var $ = window.$; + var TraceKit = window.TraceKit; + if (!$ || !TraceKit) { + if (window.console && !TraceKit.suppressWarnings) { + window.console.warn('Unable to load TraceKit jQuery plugin: TraceKit or jQuery is not available ' + + 'at the time of invocation.'); + } + return; + } + + window.TraceKit.supportsExtendedWindowOnError(function(supported){ + if(!supported) { + installHelpers(); + } + }); + + function installHelpers() { + var _oldEventAdd = $.event.add; + $.event.add = function traceKitEventAdd(elem, types, handler, data, selector) { + var _handler; + + if (handler.handler) { + _handler = handler.handler; + handler.handler = TraceKit.wrap(handler.handler); + } else { + _handler = handler; + handler = TraceKit.wrap(handler); + } + + // If the handler we are attaching doesn’t have the same guid as + // the original, it will never be removed when someone tries to + // unbind the original function later. Technically as a result of + // this our guids are no longer globally unique, but whatever, that + // never hurt anybody RIGHT?! + if (_handler.guid) { + handler.guid = _handler.guid; + } else { + handler.guid = _handler.guid = $.guid++; + } + + return _oldEventAdd.call(this, elem, types, handler, data, selector); + }; + + var _oldReady = $.fn.ready; + $.fn.ready = function traceKitjQueryReadyWrapper(fn) { + return _oldReady.call(this, TraceKit.wrap(fn)); + }; + + var _oldAjax = $.ajax; + $.ajax = function traceKitAjaxWrapper(url, options) { + var keys = ['complete', 'error', 'success'], key; + + // Taken from https://github.com/jquery/jquery/blob/eee2eaf1d7a189d99106423a4206c224ebd5b848/src/ajax.js#L311-L318 + // If url is an object, simulate pre-1.5 signature + if (typeof url === 'object') { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + while(key = keys.pop()) { + if ($.isFunction(options[key])) { + options[key] = TraceKit.wrap(options[key]); + } + } + + try { + return _oldAjax.call(this, url, options); + } catch (e) { + TraceKit.report(e); + throw e; + } + }; + } + + +}(this)); \ No newline at end of file diff --git a/tracekit.js b/tracekit.js index cea78b3..3254c5a 100644 --- a/tracekit.js +++ b/tracekit.js @@ -8,6 +8,7 @@ var TraceKit = {}; var _oldTraceKit = window.TraceKit; +var _supportsExtendedOnError; // global reference to slice var _slice = [].slice; @@ -57,6 +58,41 @@ TraceKit.wrap = function traceKitWrapper(func) { return wrapped; }; +/** + * TraceKit.supportsExtendedWindowOnError: Calls back with a boolean indicating + * support for the extended window.onerror handler, part of the HTML5 spec as of + * August 2013. (https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror) + * + * Call this function from a plugin to determine if you should wrap a function or not. + * If the browser supports extended window.onerror, there is no need to wrap jQuery, + * setTimeout, etc. as they will receive decent stacks from window.onerror itself. + * + * @param {Function} cb Callback with support enabled/disabled boolean. + */ +TraceKit.supportsExtendedWindowOnError = function supportsExtendedWindowOnError(cb) { + if (typeof _supportsExtendedOnError !== 'undefined') { + return cb(_supportsExtendedOnError); + } + + var oldOnError = window.onerror; + window.onerror = function(message, filename, lineno, colno, error) { + // Return window.onerror to its rightful owner. + window.onerror = oldOnError; + // Cache this result. + _supportsExtendedOnError = (typeof colno !== 'undefined' && typeof error !== 'undefined'); + // Call back with the result, but don't do it inside this window.onerror handler; + // otherwise, you may see weird results if you throw an error from whatever handles the callback. + setTimeout(function() { cb(_supportsExtendedOnError); }, 0); + // Prevent the error from hitting the console. + return true; + }; + + // Throw an error so we can detect window.onerror's arity + setTimeout(function() { + throw new Error('Testing Error'); + }); +}; + /** * TraceKit.report: cross-browser processing of unhandled exceptions * @@ -156,7 +192,7 @@ TraceKit.report = (function reportModuleWrapper() { * @param {(number|string)} lineNo The line number at which the error * occurred. */ - function traceKitWindowOnError(message, url, lineNo) { + function traceKitWindowOnError(message, url, lineNo, colNo, error) { var stack = null; if (lastExceptionStack) { @@ -164,6 +200,9 @@ TraceKit.report = (function reportModuleWrapper() { stack = lastExceptionStack; lastExceptionStack = null; lastException = null; + } else if (error) { + // New HTML5 spec (Aug 2013) actually passes an error to window.onerror + stack = TraceKit.computeStackTrace(error); } else { var location = { 'url': url, @@ -1063,35 +1102,6 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { return computeStackTrace; }()); -/** - * Extends support for global error handling for asynchronous browser - * functions. Adopted from Closure Library's errorhandler.js - */ -(function extendToAsynchronousCallbacks() { - var _helper = function _helper(fnName) { - var originalFn = window[fnName]; - window[fnName] = function traceKitAsyncExtension() { - // Make a copy of the arguments - var args = _slice.call(arguments); - var originalCallback = args[0]; - if (typeof (originalCallback) === 'function') { - args[0] = TraceKit.wrap(originalCallback); - } - // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it - // also only supports 2 argument and doesn't care what "this" is, so we - // can just call the original function directly. - if (originalFn.apply) { - return originalFn.apply(this, args); - } else { - return originalFn(args[0], args[1]); - } - }; - }; - - _helper('setTimeout'); - _helper('setInterval'); -}()); - //Default options: if (!TraceKit.remoteFetching) { TraceKit.remoteFetching = true; @@ -1103,6 +1113,10 @@ if (!TraceKit.linesOfContext || TraceKit.linesOfContext < 1) { // 5 lines before, the offending line, 5 lines after TraceKit.linesOfContext = 11; } +// Set to true to ignore e.g. plugin warnings +if (typeof TraceKit.suppressWarnings === 'undefined') { + TraceKit.suppressWarnings = false; +} From 86c6097f7f881b31186a74352cdef78fc73a28f9 Mon Sep 17 00:00:00 2001 From: ssafejava Date: Thu, 19 Sep 2013 11:17:44 +0800 Subject: [PATCH 2/8] Add documentation for previous (#48) --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d5aac0..883c15c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ bower install tracekit ``` This places TraceKit at `components/tracekit/tracekit.js`. Install [bower](http://twitter.github.com/bower/): `npm install bower -g`, download npm with Node: http://nodejs.org -Then include the ` + + + +

TraceKit OnError Test

+

If your browser supports the new onError signature, you should see column numbers on traces + and headers starting with 'Error', not 'undefined'.

+
+ + \ No newline at end of file diff --git a/tests/recursion.html b/tests/recursion.html index 4a6762b..5c49e72 100644 --- a/tests/recursion.html +++ b/tests/recursion.html @@ -51,7 +51,7 @@ + + + + + + + +

TraceKit Build Test

+

You should see output below and no errors (just logging) in the console. If not, the build has broken.

+
+ + + \ No newline at end of file From 0b580d6aa19c23283106653f91abb16614ca36f5 Mon Sep 17 00:00:00 2001 From: ssafejava Date: Mon, 23 Sep 2013 16:46:54 +0800 Subject: [PATCH 4/8] Add build note. --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 883c15c..70203e6 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,19 @@ Error object along. In this case, TraceKit has the function `TraceKit.supportsEx with a boolean. If true, `window.onerror` has superpowers and wrapping plugins should halt as their functionality is no longer needed. +## Building + +To get minified versions of source, clone the project, and: + +```bash +npm install +wget http://closure-compiler.googlecode.com/files/compiler-latest.zip +unzip compiler-latest.zip -d closure +grunt +``` + +Built files will be stored in /dist. + `tracekit.noplugins.min.js` does not wrap setTimeout/setInterval or jQuery. `tracekit.min.js` contains setTimeout/setInterval & jQuery wrapping. If you need one but not the other, From 9961f32bd00eb47d8d4172c83e396997c2948232 Mon Sep 17 00:00:00 2001 From: ssafejava Date: Tue, 24 Sep 2013 17:19:22 +0800 Subject: [PATCH 5/8] Only execute supportsExtendedWindowOnError test if we know our handler is still intact. --- tracekit.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tracekit.js b/tracekit.js index 3254c5a..9e3dbb1 100644 --- a/tracekit.js +++ b/tracekit.js @@ -75,7 +75,7 @@ TraceKit.supportsExtendedWindowOnError = function supportsExtendedWindowOnError( } var oldOnError = window.onerror; - window.onerror = function(message, filename, lineno, colno, error) { + var testOnError = function(message, filename, lineno, colno, error) { // Return window.onerror to its rightful owner. window.onerror = oldOnError; // Cache this result. @@ -87,9 +87,19 @@ TraceKit.supportsExtendedWindowOnError = function supportsExtendedWindowOnError( return true; }; - // Throw an error so we can detect window.onerror's arity + window.onerror = testOnError; + + // Throw an error so we can detect window.onerror's arity. + // Do this within a setTimeout so we don't stop execution of whatever called this. setTimeout(function() { - throw new Error('Testing Error'); + // Only execute this test if our overridden onerror still stands. + // If not, somebody has switched it out from under us & we can't test this properly. + if (window.onerror === testOnError){ + throw new Error('Testing Error'); + } else { + _supportsExtendedOnError = false; + cb(_supportsExtendedOnError); + } }); }; From 0854956b836ea886812c7025cebf5fb0c68c69ce Mon Sep 17 00:00:00 2001 From: ssafejava Date: Mon, 7 Oct 2013 13:09:29 +0800 Subject: [PATCH 6/8] Don't throw an error to detect extended onError, use ErrorEvent object. --- plugins/wrapAsync.js | 19 ++++---- plugins/wrapJquery.js | 108 ++++++++++++++++++++---------------------- tracekit.js | 46 +++++------------- 3 files changed, 73 insertions(+), 100 deletions(-) diff --git a/plugins/wrapAsync.js b/plugins/wrapAsync.js index 1b28db3..7f1ab3d 100644 --- a/plugins/wrapAsync.js +++ b/plugins/wrapAsync.js @@ -3,6 +3,12 @@ * functions. Adopted from Closure Library's errorhandler.js */ (function extendToAsynchronousCallbacks(window) { + + // Bail out if window.onerror can do this for us. + if (window.TraceKit.supportsExtendedWindowOnError()) { + return; + } + var _helper = function _helper(fnName) { var originalFn = window[fnName]; window[fnName] = function traceKitAsyncExtension() { @@ -23,15 +29,6 @@ }; }; - window.TraceKit.supportsExtendedWindowOnError(function(supported){ - if(!supported) { - installHelpers(); - } - }); - - function installHelpers() { - _helper('setTimeout'); - _helper('setInterval'); - } - + _helper('setTimeout'); + _helper('setInterval'); }(this)); \ No newline at end of file diff --git a/plugins/wrapJquery.js b/plugins/wrapJquery.js index 84d8678..e1ba851 100644 --- a/plugins/wrapJquery.js +++ b/plugins/wrapJquery.js @@ -14,73 +14,69 @@ } return; } + // Bail out if window.onerror can do this work for us + if (window.TraceKit.supportsExtendedWindowOnError()) { + return; + } - window.TraceKit.supportsExtendedWindowOnError(function(supported){ - if(!supported) { - installHelpers(); - } - }); - - function installHelpers() { - var _oldEventAdd = $.event.add; - $.event.add = function traceKitEventAdd(elem, types, handler, data, selector) { - var _handler; + var _oldEventAdd = $.event.add; + $.event.add = function traceKitEventAdd(elem, types, handler, data, selector) { + var _handler; - if (handler.handler) { - _handler = handler.handler; - handler.handler = TraceKit.wrap(handler.handler); - } else { - _handler = handler; - handler = TraceKit.wrap(handler); - } + if (handler.handler) { + _handler = handler.handler; + handler.handler = TraceKit.wrap(handler.handler); + } else { + _handler = handler; + handler = TraceKit.wrap(handler); + } - // If the handler we are attaching doesn’t have the same guid as - // the original, it will never be removed when someone tries to - // unbind the original function later. Technically as a result of - // this our guids are no longer globally unique, but whatever, that - // never hurt anybody RIGHT?! - if (_handler.guid) { - handler.guid = _handler.guid; - } else { - handler.guid = _handler.guid = $.guid++; - } + // If the handler we are attaching doesn’t have the same guid as + // the original, it will never be removed when someone tries to + // unbind the original function later. Technically as a result of + // this our guids are no longer globally unique, but whatever, that + // never hurt anybody RIGHT?! + if (_handler.guid) { + handler.guid = _handler.guid; + } else { + handler.guid = _handler.guid = $.guid++; + } - return _oldEventAdd.call(this, elem, types, handler, data, selector); - }; + return _oldEventAdd.call(this, elem, types, handler, data, selector); + }; - var _oldReady = $.fn.ready; - $.fn.ready = function traceKitjQueryReadyWrapper(fn) { - return _oldReady.call(this, TraceKit.wrap(fn)); - }; + var _oldReady = $.fn.ready; + $.fn.ready = function traceKitjQueryReadyWrapper(fn) { + return _oldReady.call(this, TraceKit.wrap(fn)); + }; - var _oldAjax = $.ajax; - $.ajax = function traceKitAjaxWrapper(url, options) { - var keys = ['complete', 'error', 'success'], key; + var _oldAjax = $.ajax; + $.ajax = function traceKitAjaxWrapper(url, options) { + var keys = ['complete', 'error', 'success'], key; - // Taken from https://github.com/jquery/jquery/blob/eee2eaf1d7a189d99106423a4206c224ebd5b848/src/ajax.js#L311-L318 - // If url is an object, simulate pre-1.5 signature - if (typeof url === 'object') { - options = url; - url = undefined; - } + // Taken from https://github.com/jquery/jquery/blob/eee2eaf1d7a189d99106423a4206c224ebd5b848/src/ajax.js#L311-L318 + // If url is an object, simulate pre-1.5 signature + if (typeof url === 'object') { + options = url; + url = undefined; + } - // Force options to be an object - options = options || {}; + // Force options to be an object + options = options || {}; - while(key = keys.pop()) { - if ($.isFunction(options[key])) { - options[key] = TraceKit.wrap(options[key]); - } + while(key = keys.pop()) { + if ($.isFunction(options[key])) { + options[key] = TraceKit.wrap(options[key]); } + } - try { - return _oldAjax.call(this, url, options); - } catch (e) { - TraceKit.report(e); - throw e; - } - }; - } + try { + return _oldAjax.call(this, url, options); + } catch (e) { + TraceKit.report(e); + throw e; + } + }; }(this)); \ No newline at end of file diff --git a/tracekit.js b/tracekit.js index 9e3dbb1..19aa2e1 100644 --- a/tracekit.js +++ b/tracekit.js @@ -8,7 +8,6 @@ var TraceKit = {}; var _oldTraceKit = window.TraceKit; -var _supportsExtendedOnError; // global reference to slice var _slice = [].slice; @@ -59,48 +58,29 @@ TraceKit.wrap = function traceKitWrapper(func) { }; /** - * TraceKit.supportsExtendedWindowOnError: Calls back with a boolean indicating + * TraceKit.supportsExtendedWindowOnError: Returns a boolean indicating * support for the extended window.onerror handler, part of the HTML5 spec as of * August 2013. (https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror) * * Call this function from a plugin to determine if you should wrap a function or not. * If the browser supports extended window.onerror, there is no need to wrap jQuery, * setTimeout, etc. as they will receive decent stacks from window.onerror itself. + * + * An easy way to test for support is to check for an error & colno attribute + * on a created ErrorEvent. See + * https://src.chromium.org/viewvc/blink/trunk/LayoutTests/fast/events/constructors/error-event-constructor.html?r1=155454&r2=155453&pathrev=155454 * - * @param {Function} cb Callback with support enabled/disabled boolean. + * @returns {Boolean} supportedExtendedOnError Support enabled/disabled boolean. */ -TraceKit.supportsExtendedWindowOnError = function supportsExtendedWindowOnError(cb) { - if (typeof _supportsExtendedOnError !== 'undefined') { - return cb(_supportsExtendedOnError); - } +TraceKit.supportsExtendedWindowOnError = function supportsExtendedWindowOnError() { + if (!window.ErrorEvent) return false; + + var testError = new window.ErrorEvent('eventType', {error: {foo: 12345}}); - var oldOnError = window.onerror; - var testOnError = function(message, filename, lineno, colno, error) { - // Return window.onerror to its rightful owner. - window.onerror = oldOnError; - // Cache this result. - _supportsExtendedOnError = (typeof colno !== 'undefined' && typeof error !== 'undefined'); - // Call back with the result, but don't do it inside this window.onerror handler; - // otherwise, you may see weird results if you throw an error from whatever handles the callback. - setTimeout(function() { cb(_supportsExtendedOnError); }, 0); - // Prevent the error from hitting the console. + if (testError.error && testError.error.foo === 12345 && typeof testError.colno === 'number'){ return true; - }; - - window.onerror = testOnError; - - // Throw an error so we can detect window.onerror's arity. - // Do this within a setTimeout so we don't stop execution of whatever called this. - setTimeout(function() { - // Only execute this test if our overridden onerror still stands. - // If not, somebody has switched it out from under us & we can't test this properly. - if (window.onerror === testOnError){ - throw new Error('Testing Error'); - } else { - _supportsExtendedOnError = false; - cb(_supportsExtendedOnError); - } - }); + } + return false; }; /** From d482ccee7099fd73f25092f82bf87e2e976b0cb3 Mon Sep 17 00:00:00 2001 From: ssafejava Date: Mon, 25 Nov 2013 09:36:56 +0800 Subject: [PATCH 7/8] Fix jslint. --- tracekit.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tracekit.js b/tracekit.js index 19aa2e1..cf0bda9 100644 --- a/tracekit.js +++ b/tracekit.js @@ -73,7 +73,9 @@ TraceKit.wrap = function traceKitWrapper(func) { * @returns {Boolean} supportedExtendedOnError Support enabled/disabled boolean. */ TraceKit.supportsExtendedWindowOnError = function supportsExtendedWindowOnError() { - if (!window.ErrorEvent) return false; + if (!window.ErrorEvent){ + return false; + } var testError = new window.ErrorEvent('eventType', {error: {foo: 12345}}); From 87273280a1580411bdeb6ebcca9e2688b86fc864 Mon Sep 17 00:00:00 2001 From: ssafejava Date: Fri, 24 Jan 2014 18:00:58 +0800 Subject: [PATCH 8/8] Fix IE10 explosion on instantiation of ErrorEvent. --- tracekit.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tracekit.js b/tracekit.js index cf0bda9..1565763 100644 --- a/tracekit.js +++ b/tracekit.js @@ -77,7 +77,13 @@ TraceKit.supportsExtendedWindowOnError = function supportsExtendedWindowOnError( return false; } - var testError = new window.ErrorEvent('eventType', {error: {foo: 12345}}); + var testError; + try { + testError = new window.ErrorEvent('eventType', {error: {foo: 12345}}); + } catch(e){ + // IE breaks with "Object doesn't support this action" + return false; + } if (testError.error && testError.error.foo === 12345 && typeof testError.colno === 'number'){ return true;