Merge "Add instrumentation for edit schema"

This commit is contained in:
jenkins-bot 2014-11-22 00:26:31 +00:00 committed by Gerrit Code Review
commit 0ed86dd9da
6 changed files with 229 additions and 113 deletions

View file

@ -297,6 +297,7 @@ $wgResourceModules += array(
'modules/ve-mw/init/ve.init.mw.Platform.js',
'modules/ve-mw/init/ve.init.mw.Target.js',
'modules/ve-mw/init/ve.init.mw.TargetEvents.js',
'modules/ve-mw/init/ve.init.mw.trackSubscriber.js',
),
'dependencies' => array(
'jquery.visibleText',

View file

@ -31,13 +31,6 @@
.then( function () {
var target = new ve.init.mw.ViewPageTarget();
// Tee tracked events to MediaWiki firehose, if available (1.23+).
if ( mw.track ) {
ve.trackSubscribeAll( function ( topic, data ) {
mw.track.call( null, 've.' + topic, data );
} );
}
// Transfer methods
ve.init.mw.ViewPageTarget.prototype.setupSectionEditLinks = init.setupSectionLinks;
@ -342,6 +335,7 @@
}
init.showLoading();
ve.track( 'mwedit.init', { type: 'page', mechanism: 'click' } );
if ( history.pushState && uri.query.veaction !== 'edit' ) {
// Replace the current state with one that is tagged as ours, to prevent the
@ -357,8 +351,11 @@
e.preventDefault();
getTarget().done( function ( target ) {
ve.track( 'Edit', { action: 'edit-link-click' } );
target.activate().always( init.hideLoading );
target.activate()
.done( function () {
ve.track( 'mwedit.ready' );
} )
.always( init.hideLoading );
} );
},
@ -368,6 +365,7 @@
}
init.showLoading();
ve.track( 'mwedit.init', { type: 'section', mechanism: 'click' } );
if ( history.pushState && uri.query.veaction !== 'edit' ) {
// Replace the current state with one that is tagged as ours, to prevent the
@ -382,9 +380,12 @@
e.preventDefault();
getTarget().done( function ( target ) {
ve.track( 'Edit', { action: 'section-edit-link-click' } );
target.saveEditSection( $( e.target ).closest( 'h1, h2, h3, h4, h5, h6' ).get( 0 ) );
target.activate().always( init.hideLoading );
target.activate()
.done( function () {
ve.track( 'mwedit.ready' );
} )
.always( init.hideLoading );
} );
},
@ -476,9 +477,16 @@
$( function () {
if ( init.isAvailable ) {
if ( isViewPage && uri.query.veaction === 'edit' ) {
var isSection = uri.query.vesection !== undefined;
init.showLoading();
ve.track( 'mwedit.init', { type: isSection ? 'section' : 'page', mechanism: 'url' } );
getTarget().done( function ( target ) {
target.activate().always( init.hideLoading );
target.activate()
.done( function () {
ve.track( 'mwedit.ready' );
} )
.always( init.hideLoading );
} );
}
}

View file

@ -88,7 +88,6 @@ ve.init.mw.ViewPageTarget = function VeInitMwViewPageTarget() {
saveErrorEmpty: 'onSaveErrorEmpty',
saveErrorSpamBlacklist: 'onSaveErrorSpamBlacklist',
saveErrorAbuseFilter: 'onSaveErrorAbuseFilter',
saveErrorBadToken: 'onSaveErrorBadToken',
saveErrorNewUser: 'onSaveErrorNewUser',
saveErrorCaptcha: 'onSaveErrorCaptcha',
saveErrorUnknown: 'onSaveErrorUnknown',
@ -155,7 +154,7 @@ ve.init.mw.ViewPageTarget.compatibility = {
/**
* @event saveWorkflowBegin
* Fired when user enters the save workflow
* Fired when user clicks the button to open the save dialog.
*/
/**
@ -276,19 +275,20 @@ ve.init.mw.ViewPageTarget.prototype.activate = function () {
* Determines whether we want to switch to view mode or not (displaying a dialog if necessary)
* Then, if we do, actually switches to view mode.
*
* @method
* @param {boolean} [override] Do not display a dialog
* @param {string} [trackMechanism] Abort mechanism; used for event tracking if present
*/
ve.init.mw.ViewPageTarget.prototype.deactivate = function ( override ) {
ve.init.mw.ViewPageTarget.prototype.deactivate = function ( override, trackMechanism ) {
var target = this;
if ( override || ( this.active && !this.deactivating ) ) {
if ( override || !this.edited ) {
this.cancel();
this.cancel( trackMechanism );
} else {
this.surface.dialogs.openWindow( 'cancelconfirm' ).then( function ( opened ) {
opened.then( function ( closing ) {
closing.then( function ( data ) {
if ( data.action === 'discard' ) {
target.cancel();
target.cancel( trackMechanism );
}
} );
} );
@ -300,10 +300,29 @@ ve.init.mw.ViewPageTarget.prototype.deactivate = function ( override ) {
/**
* Switch to view mode
*
* @method
* @param {string} [trackMechanism] Abort mechanism; used for event tracking if present
*/
ve.init.mw.ViewPageTarget.prototype.cancel = function () {
var promises = [];
ve.init.mw.ViewPageTarget.prototype.cancel = function ( trackMechanism ) {
var abortType, promises = [];
// Event tracking
if ( trackMechanism ) {
if ( this.activating ) {
abortType = 'preinit';
} else if ( !this.edited ) {
abortType = 'nochange';
} else if ( this.saving ) {
abortType = 'abandonMidsave';
} else {
// switchwith and switchwithout do not go through this code path,
// they go through switchToWikitextEditor() instead
abortType = 'abandon';
}
ve.track( 'mwedit.abort', {
type: abortType,
mechanism: trackMechanism
} );
}
this.deactivating = true;
// User interface changes
@ -380,8 +399,12 @@ ve.init.mw.ViewPageTarget.prototype.onLoadError = function ( jqXHR, status ) {
this.currentUri.query.action = 'edit';
location.href = this.currentUri.toString();
} else {
// Something weird happened? Deactivate
// TODO: how does this handle load errors triggered from
// calling this.loading.abort()?
this.activating = false;
// User interface changes
// Not passing trackMechanism because we don't know what happened
// and this is not a user action
this.deactivate( true );
}
};
@ -497,6 +520,8 @@ ve.init.mw.ViewPageTarget.prototype.onSave = function (
contentSub
);
this.setupSectionEditLinks();
// Tear down the target now that we're done saving
// Not passing trackMechanism because this isn't an abort action
this.deactivate( true );
if ( newid !== undefined ) {
mw.hook( 'postEdit' ).fire( {
@ -513,7 +538,6 @@ ve.init.mw.ViewPageTarget.prototype.onSave = function (
*/
ve.init.mw.ViewPageTarget.prototype.onSaveErrorEmpty = function () {
this.showSaveError( ve.msg( 'visualeditor-saveerror', 'Empty server response' ), false /* prevents reapply */ );
this.events.trackSaveError( 'empty' );
};
/**
@ -530,7 +554,6 @@ ve.init.mw.ViewPageTarget.prototype.onSaveErrorSpamBlacklist = function ( editAp
),
false // prevents reapply
);
this.events.trackSaveError( 'spamblacklist' );
};
/**
@ -544,16 +567,6 @@ ve.init.mw.ViewPageTarget.prototype.onSaveErrorAbuseFilter = function ( editApi
// Don't disable the save button. If the action is not disallowed the user may save the
// edit by pressing Save again. The AbuseFilter API currently has no way to distinguish
// between filter triggers that are and aren't disallowing the action.
this.events.trackSaveError( 'abusefilter' );
};
/**
* Track when there is a bad edit token on save
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSaveErrorBadToken = function () {
this.events.trackSaveError( 'badtoken' );
};
/**
@ -625,8 +638,6 @@ ve.init.mw.ViewPageTarget.prototype.onSaveErrorCaptcha = function ( editApi ) {
this.saveDialog.clearMessage( 'api-save-error' );
this.saveDialog.showMessage( 'api-save-error', $captchaDiv );
this.saveDialog.popPending();
this.events.trackSaveError( 'captcha' );
};
/**
@ -647,7 +658,6 @@ ve.init.mw.ViewPageTarget.prototype.onSaveErrorUnknown = function ( editApi, dat
) ),
false // prevents reapply
);
this.events.trackSaveError( 'unknown' );
};
/**
@ -738,11 +748,11 @@ ve.init.mw.ViewPageTarget.prototype.onViewTabClick = function ( e ) {
return;
}
if ( this.active ) {
this.deactivate();
this.deactivate( false, 'navigate-read' );
// Prevent the edit tab's normal behavior
e.preventDefault();
} else if ( this.activating ) {
this.deactivate( true );
this.deactivate( true, 'navigate-read' );
this.activating = false;
e.preventDefault();
}
@ -1188,6 +1198,7 @@ ve.init.mw.ViewPageTarget.prototype.attachToolbarSaveButton = function () {
* @fires saveWorkflowBegin
*/
ve.init.mw.ViewPageTarget.prototype.showSaveDialog = function () {
this.emit( 'saveWorkflowBegin' );
this.surface.getDialogs().getWindow( 'mwSave' ).then( function ( win ) {
var currentWindow = this.surface.getContext().getInspectors().getCurrentWindow(),
target = this;
@ -1227,7 +1238,6 @@ ve.init.mw.ViewPageTarget.prototype.showSaveDialog = function () {
.always( function ( opened ) {
opened.always( target.onSaveDialogClose.bind( target ) );
} );
this.emit( 'saveWorkflowBegin' );
}.bind( this ) );
};
@ -1525,7 +1535,7 @@ ve.init.mw.ViewPageTarget.prototype.onWindowPopState = function ( e ) {
}
if ( this.active && newUri.query.veaction !== 'edit' ) {
this.actFromPopState = true;
this.deactivate();
this.deactivate( false, 'navigate-back' );
}
};
@ -1748,6 +1758,7 @@ ve.init.mw.ViewPageTarget.prototype.onBeforeUnload = function () {
*/
ve.init.mw.ViewPageTarget.prototype.switchToWikitextEditor = function ( discardChanges ) {
if ( discardChanges ) {
ve.track( 'mwedit.abort', { type: 'switchwithout', mechanism: 'navigate' } );
this.submitting = true;
location.href = this.viewUri.clone().extend( {
action: 'edit',
@ -1756,7 +1767,10 @@ ve.init.mw.ViewPageTarget.prototype.switchToWikitextEditor = function ( discardC
} else {
this.serialize(
this.docToSave || ve.dm.converter.getDomFromModel( this.surface.getModel().getDocument() ),
this.submitWithSaveFields.bind( this, { wpDiff: 1, veswitched: 1 } )
function () {
ve.track( 'mwedit.abort', { type: 'switchwith', mechanism: 'navigate' } );
this.submitWithSaveFields( { wpDiff: 1, veswitched: 1 } );
}.bind( this )
);
}
};

View file

@ -277,6 +277,14 @@ ve.init.mw.Target.static.iconModuleStyles = [
*/
ve.init.mw.Target.static.name = 'mwTarget';
/**
* Type of integration. Used by ve.init.mw.trackSubscriber.js for event tracking.
* @static
* @property {string}
* @inheritable
*/
ve.init.mw.Target.static.integrationType = 'page';
/* Static Methods */
/**

View file

@ -23,15 +23,15 @@ ve.init.mw.TargetEvents = function ( target ) {
saveInitiated: 'onSaveInitated',
save: 'onSaveComplete',
saveReview: 'onSaveReview',
saveErrorEmpty: 'onSaveErrorEmpty',
saveErrorSpamBlacklist: 'onSaveErrorSpamBlacklist',
saveErrorAbuseFilter: 'onSaveErrorAbuseFilter',
saveErrorBadToken: 'onSaveErrorBadToken',
saveErrorNewUser: 'onSaveErrorNewUser',
saveErrorCaptcha: 'onSaveErrorCaptcha',
saveErrorUnknown: 'onSaveErrorUnknown',
saveErrorEmpty: [ 'trackSaveError', 'empty' ],
saveErrorSpamBlacklist: [ 'trackSaveError', 'spamblacklist' ],
saveErrorAbuseFilter: [ 'trackSaveError', 'abusefilter' ],
saveErrorBadToken: [ 'trackSaveError', 'badtoken' ],
saveErrorNewUser: [ 'trackSaveError', 'newuser' ],
saveErrorCaptcha: [ 'trackSaveError', 'captcha' ],
saveErrorUnknown: [ 'trackSaveError', 'unknown' ],
editConflict: [ 'trackSaveError', 'editconflict' ],
surfaceReady: 'onSurfaceReady',
editConflict: 'onEditConflict',
showChanges: 'onShowChanges',
showChangesError: 'onShowChangesError',
noChanges: 'onNoChanges',
@ -48,7 +48,12 @@ ve.init.mw.TargetEvents = function ( target ) {
*/
ve.init.mw.TargetEvents.prototype.track = function ( topic, data ) {
data.targetName = this.target.constructor.static.name;
ve.track( topic, data );
ve.track( 'mwtiming.' + 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;
}
};
/**
@ -59,6 +64,7 @@ ve.init.mw.TargetEvents.prototype.onSaveWorkflowBegin = function () {
this.track( 'behavior.lastTransactionTillSaveDialogOpen', {
duration: this.timings.saveWorkflowBegin - this.timings.lastTransaction
} );
ve.track( 'mwedit.saveIntent' );
};
/**
@ -78,14 +84,22 @@ ve.init.mw.TargetEvents.prototype.onSaveInitated = function () {
this.track( 'behavior.saveDialogOpenTillSave', {
duration: this.timings.saveInitiated - this.timings.saveWorkflowBegin
} );
ve.track( 'mwedit.saveAttempt' );
};
/**
* Track when document save is complete
* Track when the save is complete
* @param {string} content
* @param {string} categoriesHtml
* @param {number} newRevId
*/
ve.init.mw.TargetEvents.prototype.onSaveComplete = function () {
ve.init.mw.TargetEvents.prototype.onSaveComplete = function ( content, categoriesHtml, newRevId ) {
this.track( 'performance.user.saveComplete', { duration: ve.now() - this.timings.saveInitiated } );
this.timings.saveRetries = 0;
ve.track( 'mwedit.saveSuccess', {
timing: ve.now() - this.timings.saveInitiated + ( this.timings.serializeForCache || 0 ),
'page.revid': newRevId
} );
};
/**
@ -95,11 +109,36 @@ ve.init.mw.TargetEvents.prototype.onSaveComplete = function () {
* @param {string} type Text for error type
*/
ve.init.mw.TargetEvents.prototype.trackSaveError = function ( type ) {
this.track( 'performance.user.saveError', {
var key,
// Maps mwtiming types to mwedit types
typeMap = {
badtoken: 'userBadToken',
newuser: 'userNewUser',
abusefilter: 'extensionAbuseFilter',
captcha: 'extensionCaptcha',
spamblacklist: 'extensionSpamBlacklist',
empty: 'responseEmpty',
unknown: 'responseUnknown',
editconflict: 'editConflict'
},
// Types that are logged as performance.user.saveError.{type}
// (for historical reasons; this sucks)
specialTypes = [ 'editconflict' ];
key = 'performance.user.saveError';
if ( specialTypes.indexOf( type ) !== -1 ) {
key += '.' + type;
}
this.track( key, {
duration: ve.now() - this.timings.saveInitiated,
retries: this.timings.saveRetries,
type: type
} );
ve.track( 'mwedit.saveFailure', {
type: typeMap[type] || 'responseUnknown',
timing: ve.now() - this.timings.saveInitiated + ( this.timings.serializeForCache || 0 )
} );
};
/**
@ -119,55 +158,6 @@ ve.init.mw.TargetEvents.prototype.onSaveReview = function () {
} );
};
/**
* Track when save api returns no data
*/
ve.init.mw.TargetEvents.prototype.onSaveErrorEmpty = function () {
this.trackSaveError( 'empty' );
};
/**
* Track when spamblacklist save error occurs
*/
ve.init.mw.TargetEvents.prototype.onSaveErrorSpamBlacklist = function () {
this.trackSaveError( 'spamblacklist' );
};
/**
* Track when abusefilter save error occurs
*/
ve.init.mw.TargetEvents.prototype.onSaveErrorAbuseFilter = function () {
this.trackSaveError( 'abusefilter' );
};
/**
* Track when the save request requires a new edit token
*/
ve.init.mw.TargetEvents.prototype.onSaveErrorBadToken = function () {
this.trackSaveError( 'badtoken' );
};
/**
* Track when the save request detects a new user session
*/
ve.init.mw.TargetEvents.prototype.onSaveErrorNewUser = function () {
this.trackSaveError( 'newuser' );
};
/**
* Track when the save request requires about captcha
*/
ve.init.mw.TargetEvents.prototype.onSaveErrorCaptcha = function () {
this.trackSaveError( 'captcha' );
};
/**
* Track when save request has an unknown error
*/
ve.init.mw.TargetEvents.prototype.onSaveErrorUnknown = function () {
this.trackSaveError( 'unknown' );
};
ve.init.mw.TargetEvents.prototype.onSurfaceReady = function () {
this.track( 'performance.system.activation', { duration: ve.now() - this.timings.activationStart } );
this.target.surface.getModel().getDocument().connect( this, {
@ -175,16 +165,6 @@ ve.init.mw.TargetEvents.prototype.onSurfaceReady = function () {
} );
};
/**
* Track when save request results in an edit conflict
*/
ve.init.mw.TargetEvents.prototype.onEditConflict = function () {
this.track( 'performance.user.saveError.editconflict', {
duration: ve.now() - this.timings.saveInitiated,
retries: this.timings.saveRetries
} );
};
/**
* Track when the user enters the review workflow
*/

View file

@ -0,0 +1,105 @@
/*!
* VisualEditor MediaWiki event subscriber.
*
* Subscribes to ve.track() events and routes them to mw.track().
*
* @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
( function () {
var lastEventWithAction = {},
editingSessionId = mw.user.generateRandomSessionId();
function getDefaultTiming( action, data, now ) {
switch ( action ) {
case 'init':
// Account for second opening
return now - Math.max(
Math.floor( window.mediaWikiLoadStart ),
lastEventWithAction.saveSuccess || 0,
lastEventWithAction.abort || 0
);
case 'ready':
return now - lastEventWithAction.init;
case 'saveIntent':
return now - lastEventWithAction.ready;
case 'saveAttempt':
return now - lastEventWithAction.saveIntent;
case 'saveSuccess':
case 'saveFailure':
// HERE BE DRAGONS: the caller must compute these themselves
// for sensible results. Deliberately sabotage any attempts to
// use the default by returning -1
mw.log.warn( 've.init.mw.trackSubscriber: Do not rely on default timing value for saveSuccess/saveFailure' );
return -1;
case 'abort':
switch ( data.type ) {
case 'preinit':
return now - lastEventWithAction.init;
case 'nochange':
case 'switchwith':
case 'switchwithout':
case 'abandon':
return now - lastEventWithAction.ready;
case 'abandonMidsave':
return now - lastEventWithAction.saveAttempt;
}
}
mw.log.warn( 've.init.mw.trackSubscriber: Unrecognized action', action );
return -1;
}
ve.trackSubscribeAll( function ( topic, data ) {
data = data || {};
var newData, action, now = Math.floor( ve.now() ), prefix = topic.substr( 0, topic.indexOf( '.' ) );
if ( prefix === 'mwtiming' ) {
// Legacy TimingData events
// Map timing.foo --> ve.foo
topic = 've.' + topic.substr( prefix.length + 1 );
} else if ( prefix === 'mwedit' ) {
// Edit schema
action = topic.split( '.' )[1];
if ( action === 'init' ) {
// Regenerate editingSessionId
editingSessionId = mw.user.generateRandomSessionId();
}
newData = $.extend( {
version: 1,
action: action,
editor: 'visualeditor',
platform: 'desktop', // FIXME
integration: ve.init.target && ve.init.target.constructor.static.integrationType || 'page',
'page.id': mw.config.get( 'wgArticleId' ),
'page.title': mw.config.get( 'wgPageName' ),
'page.ns': mw.config.get( 'wgNamespaceNumber' ),
'page.revid': mw.config.get( 'wgRevisionId' ),
'page.length': -1, // FIXME
editingSessionId: editingSessionId,
'user.id': mw.user.getId(),
'user.editCount': mw.config.get( 'wgUserEditCount', 0 )
}, data );
if ( mw.user.isAnon() ) {
newData['user.class'] = 'IP';
}
newData['action.' + action + '.type'] = data.type;
newData['action.' + action + '.mechanism'] = data.mechanism;
newData['action.' + action + '.timing'] = data.timing !== undefined ?
Math.floor( data.timing ) : getDefaultTiming( action, data, now );
// Remove renamed properties
delete newData.type;
delete newData.mechanism;
delete newData.timing;
data = newData;
topic = 'event.Edit';
lastEventWithAction[action] = now;
}
mw.track( topic, data );
} );
} )();