Gergő Tisza has uploaded a new change for review.
https://gerrit.wikimedia.org/r/188328
Change subject: Wrap asynchronous callbacks with error logging
......................................................................
Wrap asynchronous callbacks with error logging
Ensures that callback passed to asynchronous functions such as $.ajax
or setTimeout are always wrapped in a try..catch block. Reporting
functions can subscribe to the exceptions via mw.trackSubscribe.
Bug: T513
Change-Id: Ic7bb771e4b94ffe3a91af330351f24b1aeb50514
---
M includes/resourceloader/ResourceLoaderStartUpModule.php
M resources/Resources.php
A resources/src/mediawiki/mediawiki.errorLogging.js
M resources/src/mediawiki/mediawiki.js
M tests/qunit/QUnitTestResources.php
A tests/qunit/suites/resources/mediawiki/mediawiki.errorLogging.test.js
6 files changed, 181 insertions(+), 1 deletion(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core
refs/changes/28/188328/1
diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php
b/includes/resourceloader/ResourceLoaderStartUpModule.php
index a2d2fe0..28d7d83 100644
--- a/includes/resourceloader/ResourceLoaderStartUpModule.php
+++ b/includes/resourceloader/ResourceLoaderStartUpModule.php
@@ -295,7 +295,7 @@
* @return array
*/
public static function getStartupModules() {
- return array( 'jquery', 'mediawiki', 'mediawiki.startUp' );
+ return array( 'jquery', 'mediawiki', 'mediawiki.errorLogging',
'mediawiki.startUp' );
}
/**
diff --git a/resources/Resources.php b/resources/Resources.php
index 2e66ec5..2d37f10 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -852,6 +852,10 @@
// must be loaded on the bottom
'position' => 'bottom',
),
+ 'mediawiki.errorLogging' => array(
+ 'scripts' =>
'resources/src/mediawiki/mediawiki.errorLogging.js',
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
'mediawiki.feedback' => array(
'templates' => array(
'dialog.html' =>
'resources/src/mediawiki/templates/dialog.html',
diff --git a/resources/src/mediawiki/mediawiki.errorLogging.js
b/resources/src/mediawiki/mediawiki.errorLogging.js
new file mode 100644
index 0000000..563c068
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.errorLogging.js
@@ -0,0 +1,119 @@
+/*
+ * Try to catch errors in modules which don't do their own error handling.
+ * Based partially on some raven.js ( https://github.com/getsentry/raven-js )
plugins.
+ */
+( function ( mw, $ ) {
+ 'use strict';
+
+ mw.errorLogging = {
+ /**
+ * Wrap a function in a try-catch block and report any errors.
+ * @param {Function} fn
+ * @param {string} name name of the function, for logging
+ * @return {*}
+ */
+ wrap: function ( fn, name ) {
+ var wrappedFn;
+
+ if ( !$.isFunction( fn ) ) {
+ return fn;
+ }
+
+ // use named function expression so this is easy to
identify in the stack trace
+ wrappedFn = function mediawikiErrorLoggingWrapper() {
+ try {
+ if ( fn.apply ) {
+ return fn.apply( this,
arguments );
+ } else { // IE8 host object compat
+ return
Function.prototype.apply.call( fn, this, arguments );
+ }
+ } catch ( e ) {
+ mw.track( 'errorLogging.exception', {
exception: e, source: name } );
+ throw e ;
+ }
+ };
+
+ // raven.js needs these
+ wrappedFn.__raven__ = true;
+ wrappedFn.__inner__ = fn;
+
+ return wrappedFn;
+ },
+
+ /**
+ * Decorates a function so that it executes a callback on its
arguments before
+ * executing the function itself.
+ * @param {Function} fn
+ * @param {function(Array)} callback a callback which receives
the arguments (as a real array).
+ * Changes in the arguments will effect the original function.
+ */
+ decorateWithArgsCallback: function ( fn, callback ) {
+ var _; // http://kangax.github.io/nfe/#safari-bug
+ return ( _ = function
mediaWikiErrorLoggingDecoratedFunction() {
+ var args = [].slice.call( arguments );
+
+ callback( args );
+
+ if ( fn.apply ) {
+ return fn.apply( this, args );
+ } else { // IE8 host object compat
+ return Function.prototype.apply.call(
fn, this, args );
+ }
+ } );
+ },
+
+ /**
+ * Decorate async functions to wrap their callbacks
+ */
+ register: function () {
+ $.each( ['setTimeout', 'setInterval'], function ( _,
name ) {
+ window[name] =
mw.errorLogging.decorateWithArgsCallback( window[name], function ( args ) {
+ if ( args[0] ) {
+ args[0] = mw.errorLogging.wrap(
args[0], name );
+ }
+ } );
+ } );
+
+ $.fn.ready = mw.errorLogging.decorateWithArgsCallback(
$.fn.ready, function ( args ) {
+ if ( args[0] ) {
+ args[0] = mw.errorLogging.wrap(
args[0], '$.ready' );
+ }
+ } );
+
+ $.event.add = mw.errorLogging.decorateWithArgsCallback(
$.event.add, function ( args ) {
+ var handler, wrappedHandler;
+
+ // $.event.add can be called with a config
object instead of a handler
+ if ( args[2] && args[2].handler ) {
+ handler = args[2].handler;
+ wrappedHandler = mw.errorLogging.wrap(
handler, '$.event.add' );
+ args[2].handler = wrappedHandler;
+ } else if ( args[2] ) {
+ handler = args[2];
+ wrappedHandler = mw.errorLogging.wrap(
handler, '$.event.add' );
+ args[2] = wrappedHandler;
+ }
+
+ // emulate jQuery.proxy() behavior
+ wrappedHandler.guid = handler.guid =
handler.guid || $.guid++;
+ } );
+
+ $.ajax = mw.errorLogging.decorateWithArgsCallback(
$.ajax, function ( args ) {
+ // can be called as $.ajax( options ) or
$.ajax( url, options )
+ var options = typeof args[0] === 'object' ?
args[0] : args[1];
+
+ // don't die if no options present
+ options = options || {};
+
+ $.each( ['complete', 'error', 'success'],
function ( _, name ) {
+ if ( options[name] ) {
+ options[name] =
mw.errorLogging.wrap( options[name], '$.ajax.' + name );
+ }
+ } );
+ } );
+ }
+ };
+
+ mw.errorLogging.register();
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki/mediawiki.js
b/resources/src/mediawiki/mediawiki.js
index 169c0f5..cb1ed7d 100644
--- a/resources/src/mediawiki/mediawiki.js
+++ b/resources/src/mediawiki/mediawiki.js
@@ -2536,4 +2536,5 @@
// Attach to window and globally alias
window.mw = window.mediaWiki = mw;
+
}( jQuery ) );
diff --git a/tests/qunit/QUnitTestResources.php
b/tests/qunit/QUnitTestResources.php
index 29834c1..8262132 100644
--- a/tests/qunit/QUnitTestResources.php
+++ b/tests/qunit/QUnitTestResources.php
@@ -62,6 +62,7 @@
'tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js',
'tests/qunit/suites/resources/jquery/jquery.textSelection.test.js',
'tests/qunit/data/mediawiki.jqueryMsg.data.js',
+
'tests/qunit/suites/resources/mediawiki/mediawiki.errorLogging.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.test.js',
diff --git
a/tests/qunit/suites/resources/mediawiki/mediawiki.errorLogging.test.js
b/tests/qunit/suites/resources/mediawiki/mediawiki.errorLogging.test.js
new file mode 100644
index 0000000..b492025
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.errorLogging.test.js
@@ -0,0 +1,55 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mediawiki.errorLogging', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'wrap', 6, function ( assert ) {
+ var wrapper, stub,
+ exception = new Error(),
+ context = {};
+
+ assert.strictEqual( mw.errorLogging.wrap( context, '?' ),
context, 'Wrapping a non-function is a noop.' );
+
+ stub = this.sandbox.stub();
+ wrapper = mw.errorLogging.wrap( stub, 'stub' );
+ wrapper();
+ sinon.assert.called( stub );
+
+ stub = this.sandbox.stub();
+ wrapper = mw.errorLogging.wrap( stub, 'stub' );
+ wrapper.call( context, 'foo', 'bar' );
+ sinon.assert.calledOn( stub, context );
+ sinon.assert.calledWithExactly( stub, 'foo', 'bar' );
+
+ stub = this.sandbox.stub().throws( exception );
+ wrapper = mw.errorLogging.wrap( stub, 'name' );
+ this.sandbox.stub( mw, 'track' );
+ try {
+ wrapper();
+ } catch ( e ) {
+ assert.strictEqual( e, exception, 'Exceptions are not
swallowed and still appear on the console.' );
+ }
+ sinon.assert.calledWith( mw.track, 'errorLogging.exception',
sinon.match( { exception: exception, source: 'name' } ) );
+
+ } );
+
+ QUnit.test( 'decorateWithArgsCallback', 5, function ( assert ) {
+ var decorated, stub, callback,
+ context = {};
+
+ stub = this.sandbox.stub();
+ callback = this.sandbox.stub();
+ decorated = mw.errorLogging.decorateWithArgsCallback( stub,
callback );
+
+ decorated.call( context, 'foo', 'bar' );
+ sinon.assert.called( stub );
+ sinon.assert.calledOn( stub, context );
+ sinon.assert.calledWithExactly( stub, 'foo', 'bar' );
+ sinon.assert.calledWithExactly( callback, ['foo', 'bar'] );
+
+ stub = this.sandbox.stub();
+ callback = function( args ) { args.pop(); args.push( 'baz' );};
+ decorated = mw.errorLogging.decorateWithArgsCallback( stub,
callback );
+
+ decorated.call( context, 'foo', 'bar' );
+ sinon.assert.calledWithExactly( stub, 'foo', 'baz' );
+ } );
+}( mediaWiki, jQuery ) );
--
To view, visit https://gerrit.wikimedia.org/r/188328
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ic7bb771e4b94ffe3a91af330351f24b1aeb50514
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Gergő Tisza <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits