Report uncaught errors via mw.track
authorGergő Tisza <gtisza@wikimedia.org>
Thu, 29 Jan 2015 01:21:01 +0000 (01:21 +0000)
committerOri.livneh <ori@wikimedia.org>
Fri, 27 Mar 2015 20:53:25 +0000 (20:53 +0000)
Adds a global error handler that sends errors to mw.track as
an errorLogging.windowOnerror event.

Bug: T88874
Change-Id: Ic091c9f93c59bda47bda2cfd609c64cd1d014b39

.jshintrc
maintenance/jsduck/categories.json
maintenance/jsduck/eg-iframe.html
resources/Resources.php
resources/src/mediawiki/mediawiki.errorLogger.js [new file with mode: 0644]
tests/qunit/QUnitTestResources.php
tests/qunit/suites/resources/mediawiki/mediawiki.errorLogger.test.js [new file with mode: 0644]

index d77ffb8..4bb2440 100644 (file)
--- a/.jshintrc
+++ b/.jshintrc
@@ -22,6 +22,7 @@
                "mediaWiki": true,
                "JSON": true,
                "jQuery": false,
-               "QUnit": false
+               "QUnit": false,
+               "sinon": false
        }
 }
index c0d0499..732bdc0 100644 (file)
@@ -14,7 +14,8 @@
                                        "mw.html.Cdata",
                                        "mw.html.Raw",
                                        "mw.hook",
-                                       "mw.template"
+                                       "mw.template",
+                                       "mw.errorLogger"
                                ]
                        },
                        {
index 4e61140..fca839d 100644 (file)
@@ -41,6 +41,7 @@
        </script>
        <script src="modules/lib/jquery/jquery.js"></script>
        <script src="modules/src/mediawiki/mediawiki.js"></script>
+       <script src="modules/src/mediawiki/mediawiki.errorLogger.js"></script>
        <script src="modules/src/mediawiki/mediawiki.startUp.js"></script>
        <style>
                .mw-jsduck-log {
index 4464e4b..1ae76a0 100644 (file)
@@ -775,6 +775,7 @@ return array(
                // Keep maintenance/jsduck/eg-iframe.html in sync
                'scripts' => array(
                        'resources/src/mediawiki/mediawiki.js',
+                       'resources/src/mediawiki/mediawiki.errorLogger.js',
                        'resources/src/mediawiki/mediawiki.startUp.js',
                ),
                'debugScripts' => 'resources/src/mediawiki/mediawiki.log.js',
diff --git a/resources/src/mediawiki/mediawiki.errorLogger.js b/resources/src/mediawiki/mediawiki.errorLogger.js
new file mode 100644 (file)
index 0000000..9f4f19d
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * Try to catch errors in modules which don't do their own error handling.
+ * @class mw.errorLogger
+ * @singleton
+ */
+( function ( mw ) {
+       'use strict';
+
+       mw.errorLogger = {
+               /**
+                * Fired via mw.track when an error is not handled by local code and is caught by the
+                * window.onerror handler.
+                *
+                * @event global_error
+                * @param {string} errorMessage Error errorMessage.
+                * @param {string} url URL where error was raised.
+                * @param {number} lineNumber Line number where error was raised.
+                * @param {number} [columnNumber] Line number where error was raised. Not all browsers
+                *   support this.
+                * @param {Error|Mixed} [errorObject] The error object. Typically an instance of Error, but anything
+                *   (even a primitive value) passed to a throw clause will end up here.
+                */
+
+               /**
+                * Install a window.onerror handler that will report via mw.track, while preserving
+                * any previous handler.
+                * @param {Object} window
+                */
+               installGlobalHandler: function ( window ) {
+                       // We will preserve the return value of the previous handler. window.onerror works the
+                       // opposite way than normal event handlers (returning true will prevent the default
+                       // action, returning false will let the browser handle the error normally, by e.g.
+                       // logging to the console), so our fallback old handler needs to return false.
+                       var oldHandler = window.onerror || function () { return false; };
+
+                       /**
+                        * Dumb window.onerror handler which forwards the errors via mw.track.
+                        * @fires global_error
+                        */
+                       window.onerror = function ( errorMessage, url, lineNumber, columnNumber, errorObject ) {
+                               mw.track( 'global.error', { errorMessage: errorMessage, url: url,
+                                       lineNumber: lineNumber, columnNumber: columnNumber, errorObject: errorObject } );
+                               return oldHandler.apply( this, arguments );
+                       };
+               }
+       };
+
+       mw.errorLogger.installGlobalHandler( window );
+}( mediaWiki ) );
index 8430413..d73f679 100644 (file)
@@ -62,6 +62,7 @@ return array(
                        'tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js',
                        'tests/qunit/suites/resources/jquery/jquery.textSelection.test.js',
                        'tests/qunit/data/mediawiki.jqueryMsg.data.js',
+                       'tests/qunit/suites/resources/mediawiki/mediawiki.errorLogger.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.errorLogger.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.errorLogger.test.js
new file mode 100644 (file)
index 0000000..7c3f1ec
--- /dev/null
@@ -0,0 +1,42 @@
+( function ( $, mw ) {
+       QUnit.module( 'mediawiki.errorLogger', QUnit.newMwEnvironment() );
+
+       QUnit.test( 'installGlobalHandler', 7, function ( assert ) {
+               var w = {},
+                       errorMessage = 'Foo',
+                       errorUrl = 'http://example.com',
+                       errorLine = '123',
+                       errorColumn = '45',
+                       errorObject = new Error( 'Foo'),
+                       oldHandler = this.sandbox.stub();
+
+               this.sandbox.stub( mw, 'track' );
+
+               mw.errorLogger.installGlobalHandler( w );
+
+               assert.ok( w.onerror, 'Global handler has been installed' );
+               assert.strictEqual( w.onerror( errorMessage, errorUrl, errorLine ), false,
+                       'Global handler returns false when there is no previous handler' );
+               sinon.assert.calledWithExactly( mw.track, 'global.error',
+                       sinon.match( { errorMessage: errorMessage, url: errorUrl, lineNumber: errorLine } ) );
+
+               mw.track.reset();
+               w.onerror( errorMessage, errorUrl, errorLine, errorColumn, errorObject );
+               sinon.assert.calledWithExactly( mw.track, 'global.error',
+                       sinon.match( { errorMessage: errorMessage, url: errorUrl, lineNumber: errorLine,
+                       columnNumber: errorColumn, errorObject: errorObject } ) );
+
+               w = { onerror: oldHandler };
+
+               mw.errorLogger.installGlobalHandler( w );
+               w.onerror( errorMessage, errorUrl, errorLine );
+               sinon.assert.calledWithExactly( oldHandler, errorMessage, errorUrl, errorLine );
+
+               oldHandler.returns( false );
+               assert.strictEqual( w.onerror( errorMessage, errorUrl, errorLine ), false,
+                       'Global handler preserves false return from previous handler' );
+               oldHandler.returns( true );
+               assert.strictEqual( w.onerror( errorMessage, errorUrl, errorLine ), true,
+                       'Global handler preserves true return from previous handler' );
+       } );
+}( jQuery, mediaWiki ) );