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

Reply via email to