summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/UploadWizard/resources/uw.EventFlowLogger.js
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/UploadWizard/resources/uw.EventFlowLogger.js')
-rw-r--r--www/wiki/extensions/UploadWizard/resources/uw.EventFlowLogger.js297
1 files changed, 297 insertions, 0 deletions
diff --git a/www/wiki/extensions/UploadWizard/resources/uw.EventFlowLogger.js b/www/wiki/extensions/UploadWizard/resources/uw.EventFlowLogger.js
new file mode 100644
index 00000000..0d4f8110
--- /dev/null
+++ b/www/wiki/extensions/UploadWizard/resources/uw.EventFlowLogger.js
@@ -0,0 +1,297 @@
+/*
+ * This file is part of the MediaWiki extension UploadWizard.
+ *
+ * UploadWizard is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * UploadWizard is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with UploadWizard. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, uw ) {
+ /**
+ * Event logging helper for funnel analysis. Should be instantiated at the very beginning; uses internal state
+ * to link events together.
+ * @class uw.EventFlowLogger
+ * @constructor
+ */
+ uw.EventFlowLogger = function UWEventFlowLogger() {
+ };
+
+ /**
+ * Returns a string identifying this upload session for analytics purposes.
+ * Since UploadWizard is currently implemented as a single-page application, this is just
+ * a number regenerated on every pageview. It's stored as a string to avoid overflow problems
+ * on the backend.
+ *
+ * @private
+ * @return {string}
+ */
+ uw.EventFlowLogger.prototype.getFlowId = function () {
+ var rnd;
+
+ if ( !uw.EventFlowLogger.flowId ) {
+ rnd = '00' + Math.floor( Math.random() * 1000 );
+ uw.EventFlowLogger.flowId = new Date().getTime() + rnd.substr( rnd.length - 3, 3 );
+ }
+ return uw.EventFlowLogger.flowId;
+ };
+
+ /**
+ * Returns a number identifying this event's position in the event flow.
+ * (I.e. (flowId, flowPosition) will uniquely identify an event, with the positions for a given
+ * flowId going 1..N.)
+ *
+ * @private
+ * @return {number}
+ */
+ uw.EventFlowLogger.prototype.getFlowPosition = function () {
+ uw.EventFlowLogger.flowPosition = ( uw.EventFlowLogger.flowPosition || 0 ) + 1;
+ return uw.EventFlowLogger.flowPosition;
+ };
+
+ /**
+ * Does the work of logging a step.
+ *
+ * @private
+ * @param {'tutorial'|'file'|'deeds'|'details'|'thanks'} step
+ * @param {boolean} [skipped=false]
+ * @param {Object} [extraData] Extra data passed to the log.
+ */
+ uw.EventFlowLogger.prototype.performStepLog = function ( step, skipped, extraData ) {
+ var data = extraData || {};
+
+ data.step = step;
+
+ if ( skipped === true ) {
+ data.skipped = true;
+ }
+
+ this.log( 'UploadWizardStep', data );
+ };
+
+ /**
+ * Logs arbitrary data. This is for internal use, you should call one of the more specific functions.
+ *
+ * @protected
+ * @param {string} schema EventLogger schema name
+ * @param {Object} data event data (without flowId)
+ */
+ uw.EventFlowLogger.prototype.log = function ( schema, data ) {
+ data.flowId = this.getFlowId();
+ data.flowPosition = this.getFlowPosition();
+ mw.track( 'event.' + schema, data );
+ };
+
+ /**
+ * Logs entering into a given step of the upload process.
+ *
+ * @param {'tutorial'|'file'|'deeds'|'details'|'thanks'} step
+ * @param {Object} [extraData] Extra data to pass along in the log.
+ */
+ uw.EventFlowLogger.prototype.logStep = function ( step, extraData ) {
+ this.performStepLog( step, false, extraData );
+ };
+
+ /**
+ * Logs skipping a given step of the upload process.
+ *
+ * @param {'tutorial'|'file'|'deeds'|'details'|'thanks'} step
+ */
+ uw.EventFlowLogger.prototype.logSkippedStep = function ( step ) {
+ this.performStepLog( step, true );
+ };
+
+ /**
+ * Logs user action on the tutorial page for UploadWizard.
+ *
+ * @param {'load'|'helpdesk-click'|'skip-check'|'skip-uncheck'|'continue'} type
+ */
+ uw.EventFlowLogger.prototype.logTutorialAction = function ( type ) {
+ var payload, thisUri;
+
+ payload = {};
+ payload.username = mw.config.get( 'wgUserName' );
+ payload.language = mw.config.get( 'wgUserLanguage' );
+
+ thisUri = new mw.Uri( window.location.href, { overrideKeys: true } );
+ if ( thisUri.query.uselang ) {
+ payload.language = thisUri.query.uselang;
+ }
+
+ payload.action = type;
+ mw.track( 'event.UploadWizardTutorialActions', payload );
+ };
+
+ /**
+ * Logs an event.
+ *
+ * @param {string} name Event name. Recognized names:
+ * - upload-button-clicked
+ * - flickr-upload-button-clicked
+ * - retry-uploads-button-clicked
+ * - continue-clicked
+ * - continue-anyway-clicked
+ * - leave-page
+ */
+ uw.EventFlowLogger.prototype.logEvent = function ( name ) {
+ this.log( 'UploadWizardFlowEvent', { event: name } );
+ };
+
+ uw.EventFlowLogger.prototype.logError = function ( step, data ) {
+ this.log( 'UploadWizardErrorFlowEvent', {
+ step: step,
+ code: data.code,
+ message: String( data.message ) // could be a function which kills EventLogging
+ } );
+ };
+
+ uw.EventFlowLogger.prototype.logApiError = function ( step, result ) {
+ var code, message;
+ if ( !result ) {
+ code = 'api/empty';
+ } else if ( result.error ) {
+ code = 'api/error/' + result.error.code;
+ } else if ( result.upload ) {
+ code = 'api/warning/' + Object.keys( result.upload.warnings || {} ).sort().join( ',' );
+ } else {
+ code = '???';
+ }
+ if ( result && result.upload && result.upload.imageinfo ) {
+ // This can contain stupid amounts of data and exceed the length of what EventLogging can log.
+ // Let's hope we won't need to look at anything in there.
+ result = OO.copy( result );
+ delete result.upload.imageinfo;
+ }
+ try {
+ message = JSON.stringify( result );
+ } catch ( er ) {
+ message = String( result );
+ }
+ this.log( 'UploadWizardErrorFlowEvent', {
+ step: step,
+ code: code,
+ message: message
+ } );
+ };
+
+ /**
+ * Sets up logging for global javascript errors.
+ */
+ uw.EventFlowLogger.prototype.installExceptionLogger = function () {
+ var self = this;
+
+ function toNumber( val ) {
+ var num = parseInt( val, 10 );
+ if ( isNaN( num ) ) {
+ return undefined;
+ }
+ return num;
+ }
+
+ mw.trackSubscribe( 'global.error', function ( topic, data ) {
+ self.log( 'UploadWizardExceptionFlowEvent', {
+ message: data.errorMessage,
+ url: data.url,
+ line: toNumber( data.lineNumber ),
+ column: toNumber( data.columnNumber ),
+ stack: undefined // T91347
+ } );
+ } );
+
+ mw.trackSubscribe( 'resourceloader.exception', function ( topic, data ) {
+ // Ignore noise about 'localStorage' being undefined or module store exceeding the quota
+ if (
+ data.source === 'store-localstorage-init' ||
+ data.source === 'store-localstorage-json' ||
+ data.source === 'store-localstorage-update'
+ ) {
+ return;
+ }
+
+ self.log( 'UploadWizardExceptionFlowEvent', {
+ message: data.exception.message,
+ url: 'resourceLoader://' + data.source + '/' + data.module, // Bleh
+ line: 0,
+ column: 0,
+ stack: undefined // T91347
+ } );
+ } );
+
+ mw.trackSubscribe( 'mediawiki.jqueryMsg', function ( topic, data ) {
+ self.log( 'UploadWizardExceptionFlowEvent', {
+ message: data.errorMessage,
+ url: 'jqueryMsg://' + data.messageKey, // Yeah, we're abusing the schema
+ line: 0,
+ column: 0,
+ stack: undefined
+ } );
+ } );
+ };
+
+ /**
+ * Logs an upload event.
+ *
+ * @param {string} name Event name. Recognized names:
+ * - upload-started
+ * - upload-succeeded
+ * - upload-failed
+ * - upload-removed
+ * - uploads-added
+ * @param {Object} data
+ * @param {string} data.extension file extension
+ * @param {number} data.quantity number of files added
+ * @param {number} data.size file size in bytes (will be anonymized)
+ * @param {number} data.duration upload duration in seconds
+ * @param {string} data.error upload error string
+ */
+ uw.EventFlowLogger.prototype.logUploadEvent = function ( name, data ) {
+ data.event = name;
+
+ if ( 'size' in data ) {
+ // anonymize size by rounding to closest number with 1 significant digit
+ data.size = parseFloat( Number( data.size ).toPrecision( 1 ), 10 );
+ }
+
+ this.log( 'UploadWizardUploadFlowEvent', data );
+ };
+
+ /**
+ * If `err` is a Firefox 'NS_ERROR_NOT_AVAILABLE' exception, such as those occasionally spuriously
+ * throw when calling `canvas.getContext( '2d' ).drawImage( … )`, log it for future debugging
+ * along with more data about the image that failed to be drawn. See T136831.
+ *
+ * @param {Error} err
+ * @param {Object} img
+ */
+ uw.EventFlowLogger.prototype.maybeLogFirefoxCanvasException = function ( err, img ) {
+ if ( err.name !== 'NS_ERROR_NOT_AVAILABLE' ) {
+ return;
+ }
+
+ this.log( 'UploadWizardExceptionFlowEvent', {
+ message: ( err.message || '' ),
+ url: 'debug://NS_ERROR_NOT_AVAILABLE',
+ line: 0,
+ column: 0,
+ stack: JSON.stringify( {
+ type: img.tagName,
+ url: String( img.src ).slice( 0, 100 ),
+ // Both of these should be truthy if the image is actually loaded
+ complete: img.complete,
+ naturalWidth: img.naturalWidth
+ } )
+ } );
+ };
+
+ // FIXME
+ uw.eventFlowLogger = new uw.EventFlowLogger();
+ uw.eventFlowLogger.installExceptionLogger();
+}( mediaWiki, mediaWiki.uploadWizard ) );