wikimedia/mediawiki-extensions-VisualEditor

View on GitHub
modules/ve-mw/init/ve.init.mw.ArticleTargetEvents.js

Summary

Maintainability
C
1 day
Test Coverage
/*!
 * VisualEditor MediaWiki Initialization class.
 *
 * @copyright See AUTHORS.txt
 * @license The MIT License (MIT); see LICENSE.txt
 */

/**
 * Initialization MediaWiki Article Target Analytics.
 *
 * @class
 *
 * @constructor
 * @param {ve.init.mw.ArticleTarget} target Target class to log events for
 */
ve.init.mw.ArticleTargetEvents = function VeInitMwArticleTargetEvents( target ) {
    this.target = target;
    this.timings = { saveRetries: 0 };
    // Events
    this.target.connect( this, {
        saveWorkflowBegin: 'onSaveWorkflowBegin',
        saveWorkflowEnd: 'onSaveWorkflowEnd',
        saveInitiated: 'onSaveInitiated',
        save: 'onSaveComplete',
        saveReview: 'onSaveReview',
        saveError: 'trackSaveError',
        surfaceReady: 'onSurfaceReady',
        showChanges: 'onShowChanges',
        showChangesError: 'onShowChangesError',
        noChanges: 'onNoChanges',
        serializeComplete: 'onSerializeComplete',
        serializeError: 'onSerializeError'
    } );
};

/**
 * Target specific ve.track wrapper
 *
 * @param {string} topic Event name
 * @param {Object} data Additional data describing the event, encoded as an object
 */
ve.init.mw.ArticleTargetEvents.prototype.track = function ( topic, data ) {
    ve.track( topic, ve.extendObject( {
        mode: this.target.surface ? this.target.surface.getMode() : this.target.getDefaultMode()
    }, data ) );
};

/**
 * Target specific ve.track wrapper, focused on timing
 *
 * @param {string} topic Event name
 * @param {Object} data Additional data describing the event, encoded as an object
 */
ve.init.mw.ArticleTargetEvents.prototype.trackTiming = function ( topic, data ) {
    if ( topic.indexOf( 'performance.system.serializeforcache' ) === 0 ) {
        // HACK: track serializeForCache duration here, because there's no event for that
        this.timings.serializeForCache = data.duration;
    }

    // Add type for save errors; not in the topic for stupid historical reasons
    if ( topic === 'performance.user.saveError' ) {
        topic = topic + '.' + data.type;
    }

    topic = 'timing.ve.' + this.target.constructor.static.trackingName + '.' + topic;

    mw.track( topic, data.duration );
};

/**
 * Track when the user makes their first transaction
 */
ve.init.mw.ArticleTargetEvents.prototype.onFirstTransaction = function () {
    this.track( 'editAttemptStep', { action: 'firstChange' } );

    this.trackTiming( 'behavior.firstTransaction', {
        duration: ve.now() - this.timings.surfaceReady
    } );
};

/**
 * Track when user begins the save workflow
 */
ve.init.mw.ArticleTargetEvents.prototype.onSaveWorkflowBegin = function () {
    this.timings.saveWorkflowBegin = ve.now();
    this.trackTiming( 'behavior.lastTransactionTillSaveDialogOpen', {
        duration: this.timings.saveWorkflowBegin - this.timings.lastTransaction
    } );
    this.track( 'editAttemptStep', { action: 'saveIntent' } );
};

/**
 * Track when user ends the save workflow
 */
ve.init.mw.ArticleTargetEvents.prototype.onSaveWorkflowEnd = function () {
    this.trackTiming( 'behavior.saveDialogClose', { duration: ve.now() - this.timings.saveWorkflowBegin } );
    this.timings.saveWorkflowBegin = null;
};

/**
 * Track when document save is initiated
 */
ve.init.mw.ArticleTargetEvents.prototype.onSaveInitiated = function () {
    this.timings.saveInitiated = ve.now();
    this.timings.saveRetries++;
    this.trackTiming( 'behavior.saveDialogOpenTillSave', {
        duration: this.timings.saveInitiated - this.timings.saveWorkflowBegin
    } );
    this.track( 'editAttemptStep', { action: 'saveAttempt' } );
};

/**
 * Track when the save is complete
 *
 * @param {Object} data Save data from the API, see ve.init.mw.ArticleTarget#saveComplete
 */
ve.init.mw.ArticleTargetEvents.prototype.onSaveComplete = function ( data ) {
    this.trackTiming( 'performance.user.saveComplete', { duration: ve.now() - this.timings.saveInitiated } );
    this.timings.saveRetries = 0;
    this.track( 'editAttemptStep', {
        action: 'saveSuccess',
        timing: ve.now() - this.timings.saveInitiated + ( this.timings.serializeForCache || 0 ),
        // eslint-disable-next-line camelcase
        revision_id: data.newrevid
    } );
};

/**
 * Track a save error by type
 *
 * @param {string} code Error code
 */
ve.init.mw.ArticleTargetEvents.prototype.trackSaveError = function ( code ) {
    // Maps error codes to editAttemptStep types
    const typeMap = {
            badtoken: 'userBadToken',
            assertanonfailed: 'userNewUser',
            assertuserfailed: 'userNewUser',
            assertnameduserfailed: 'userNewUser',
            'abusefilter-disallowed': 'extensionAbuseFilter',
            'abusefilter-warning': 'extensionAbuseFilter',
            captcha: 'extensionCaptcha',
            spamblacklist: 'extensionSpamBlacklist',
            'titleblacklist-forbidden': 'extensionTitleBlacklist',
            pagedeleted: 'editPageDeleted',
            editconflict: 'editConflict'
        },
        // Types that are logged as performance.user.saveError.{code}
        // (for historical reasons; this sucks)
        specialTypes = [ 'editconflict' ];

    let key = 'performance.user.saveError';
    if ( specialTypes.indexOf( code ) !== -1 ) {
        key += '.' + code;
    }
    this.trackTiming( key, {
        duration: ve.now() - this.timings.saveInitiated,
        type: code
    } );

    this.track( 'editAttemptStep', {
        action: 'saveFailure',
        message: code,
        type: typeMap[ code ] || 'responseUnknown',
        timing: ve.now() - this.timings.saveInitiated + ( this.timings.serializeForCache || 0 )
    } );
};

/**
 * Record activation having started.
 *
 * @param {number} [startTime] Timestamp activation started. Defaults to current time
 */
ve.init.mw.ArticleTargetEvents.prototype.trackActivationStart = function ( startTime ) {
    this.timings.activationStart = startTime || ve.now();
};

/**
 * Record activation being complete.
 */
ve.init.mw.ArticleTargetEvents.prototype.trackActivationComplete = function () {
    this.trackTiming( 'performance.system.activation', { duration: ve.now() - this.timings.activationStart } );
};

/**
 * Record the time of the last transaction in response to a 'transact' event on the document.
 */
ve.init.mw.ArticleTargetEvents.prototype.recordLastTransactionTime = function () {
    this.timings.lastTransaction = ve.now();
};

/**
 * Track time elapsed from beginning of save workflow to review
 */
ve.init.mw.ArticleTargetEvents.prototype.onSaveReview = function () {
    this.timings.saveReview = ve.now();
    this.trackTiming( 'behavior.saveDialogOpenTillReview', {
        duration: this.timings.saveReview - this.timings.saveWorkflowBegin
    } );
};

ve.init.mw.ArticleTargetEvents.prototype.onSurfaceReady = function () {
    this.timings.surfaceReady = ve.now();
    this.target.surface.getModel().getDocument().connect( this, {
        transact: 'recordLastTransactionTime'
    } ).once( 'transact', this.onFirstTransaction.bind( this ) );
};

/**
 * Track when the user enters the review workflow
 */
ve.init.mw.ArticleTargetEvents.prototype.onShowChanges = function () {
    this.trackTiming( 'performance.user.reviewComplete', { duration: ve.now() - this.timings.saveReview } );
};

/**
 * Track when the diff request fails in the review workflow
 */
ve.init.mw.ArticleTargetEvents.prototype.onShowChangesError = function () {
    this.trackTiming( 'performance.user.reviewError', { duration: ve.now() - this.timings.saveReview } );
};

/**
 * Track when the diff request detects no changes
 */
ve.init.mw.ArticleTargetEvents.prototype.onNoChanges = function () {
    this.trackTiming( 'performance.user.reviewComplete', { duration: ve.now() - this.timings.saveReview } );
};

/**
 * Track when serialization is complete in review workflow
 */
ve.init.mw.ArticleTargetEvents.prototype.onSerializeComplete = function () {
    this.trackTiming( 'performance.user.reviewComplete', { duration: ve.now() - this.timings.saveReview } );
};

/**
 * Track when there is a serialization error
 */
ve.init.mw.ArticleTargetEvents.prototype.onSerializeError = function () {
    if ( this.timings.saveWorkflowBegin ) {
        // This function can be called by the switch to wikitext button as well, so only log
        // reviewError if we actually got here from the save workflow
        this.trackTiming( 'performance.user.reviewError', { duration: ve.now() - this.timings.saveReview } );
    }
};