mediawiki-extensions-Visual.../modules/ve-mw/init/ve.init.mw.trackSubscriber.js
Timo Tijhof fda2729ebc trackSubscriber: Migrate away from deprecated 'schema.' modules
The EventLogging extension no longer uses these internal modules.
They were phased out as part of last year's "lightweight EventLogging"
project (detailed at T187207). Migration notes at T205744.

VisualEditor has migrated already, mostly. It still depended
on the existence of these module names for some condition guards.

* The subscriber for 'mwTimingHandler' was guarded by 'schema.EditAttemptStep',
  but did not emit events of that schema. What 'mwTimingHandler' really
  needs is the '*SamplingRate' variable for its call to 'inSample()'.
  This previously worked because the variable and the schema are both
  provided by the WikimediaEvents extension.

* The subscriber for 'activityHandler' had a separate schema guard. This
  might suggest an intent for the code to silently degrade if WikimediaEvents
  were to be changed to no longer supply the second schema, or for the code
  to work for third-parties without WikimediaEvents if they register only
  the schema. However, this subscriber too calls 'inSample()' and needs those
  variables.

I've assumed for now that it is okay for these to all be guarded together.
Even if the schemas were to be removed and we were to forget updating this
code, the new EventLogging client degrades gracefully from this (no errors).

Bug: T221281
Change-Id: I260c25752c3becfe6e499813197fbf7a3dba88c3
2019-06-13 18:28:49 +00:00

246 lines
7 KiB
JavaScript

/*!
* VisualEditor MediaWiki event subscriber.
*
* Subscribes to ve.track() events and routes them to mw.track().
*
* @copyright 2011-2019 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
( function () {
var timing, editingSessionId,
actionPrefixMap = {
saveIntent: 'save_intent',
saveAttempt: 'save_attempt',
saveSuccess: 'save_success',
saveFailure: 'save_failure'
},
trackdebug = !!mw.Uri().query.trackdebug;
timing = {};
editingSessionId = mw.user.generateRandomSessionId();
function log() {
// mw.log is a no-op unless resource loader is in debug mode, so
// this allows trackdebug to work independently (T211698)
// eslint-disable-next-line no-console
console.log.apply( console, arguments );
}
function inSample() {
// Not using mw.eventLog.inSample() because we need to be able to pass our own editingSessionId
return mw.eventLog.randomTokenMatch(
1 / mw.config.get( 'wgWMESchemaEditAttemptStepSamplingRate' ),
editingSessionId
);
}
function computeDuration( action, event, timeStamp ) {
if ( event.timing !== undefined ) {
return event.timing;
}
switch ( action ) {
case 'ready':
return timeStamp - timing.init;
case 'loaded':
return timeStamp - timing.init;
case 'saveIntent':
return timeStamp - timing.ready;
case 'saveAttempt':
return timeStamp - timing.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 ( event.type ) {
case 'preinit':
return timeStamp - timing.init;
case 'nochange':
case 'switchwith':
case 'switchwithout':
case 'switchnochange':
case 'abandon':
return timeStamp - timing.ready;
case 'abandonMidsave':
return timeStamp - timing.saveAttempt;
}
}
mw.log.warn( 've.init.mw.trackSubscriber: Unrecognized action', action );
return -1;
}
function mwEditHandler( topic, data, timeStamp ) {
var action = topic.split( '.' )[ 1 ],
actionPrefix = actionPrefixMap[ action ] || action,
duration = 0,
event;
timeStamp = timeStamp || this.timeStamp; // I8e82acc12 back-compat
if ( action === 'init' ) {
// Regenerate editingSessionId
editingSessionId = mw.user.generateRandomSessionId();
}
if ( !inSample() && !mw.config.get( 'wgWMESchemaEditAttemptStepOversample' ) && !trackdebug ) {
return;
}
if (
action === 'abort' &&
( data.type === 'unknown' || data.type === 'unknown-edited' )
) {
if (
timing.saveAttempt &&
timing.saveSuccess === undefined &&
timing.saveFailure === undefined
) {
data.type = 'abandonMidsave';
} else if (
timing.init &&
timing.ready === undefined
) {
data.type = 'preinit';
} else if ( data.type === 'unknown' ) {
data.type = 'nochange';
} else {
data.type = 'abandon';
}
}
// Convert mode=source/visual to interface name
if ( data && data.mode ) {
// eslint-disable-next-line camelcase
data.editor_interface = data.mode === 'source' ? 'wikitext-2017' : 'visualeditor';
delete data.mode;
}
if ( !data.platform ) {
if ( ve.init && ve.init.target && ve.init.target.constructor.static.platformType ) {
data.platform = ve.init.target.constructor.static.platformType;
} else {
data.platform = 'other';
// TODO: outright abort in this case, once we think we've caught everything
mw.log.warn( 've.init.mw.trackSubscriber: no target available and no platform specified', action );
}
}
/* eslint-disable camelcase */
event = $.extend( {
version: 1,
action: action,
is_oversample: !inSample(),
editor_interface: 'visualeditor',
integration: ve.init && 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' ),
revision_id: mw.config.get( 'wgRevisionId' ),
editing_session_id: editingSessionId,
page_token: mw.user.getPageviewToken(),
session_token: mw.user.sessionId(),
user_id: mw.user.getId(),
user_editcount: mw.config.get( 'wgUserEditCount', 0 ),
mw_version: mw.config.get( 'wgVersion' )
}, data );
if ( mw.user.isAnon() ) {
event.user_class = 'IP';
}
// Schema's kind of a mess of special properties
if ( action === 'init' || action === 'abort' || action === 'saveFailure' ) {
event[ actionPrefix + '_type' ] = event.type;
}
if ( action === 'init' || action === 'abort' ) {
event[ actionPrefix + '_mechanism' ] = event.mechanism;
}
if ( action !== 'init' ) {
// Schema actually does have an init_timing field, but we don't want to
// store it because it's not meaningful.
duration = Math.round( computeDuration( action, event, timeStamp ) );
event[ actionPrefix + '_timing' ] = duration;
}
if ( action === 'saveFailure' ) {
event[ actionPrefix + '_message' ] = event.message;
}
/* eslint-enable camelcase */
// Remove renamed properties
delete event.type;
delete event.mechanism;
delete event.timing;
delete event.message;
if ( action === 'abort' ) {
timing = {};
} else {
timing[ action ] = timeStamp;
}
if ( trackdebug ) {
log( topic, duration + 'ms', event );
} else {
mw.track( 'event.EditAttemptStep', event );
}
}
function mwTimingHandler( topic, data ) {
// Add type for save errors; not in the topic for stupid historical reasons
if ( topic === 'mwtiming.performance.user.saveError' ) {
topic = topic + '.' + data.type;
}
// Map mwtiming.foo --> timing.ve.foo.mobile
topic = topic.replace( /^mwtiming/, 'timing.ve.' + data.targetName );
if ( trackdebug ) {
log( topic, Math.round( data.duration ) + 'ms' );
} else {
mw.track( topic, data.duration );
}
}
function activityHandler( topic, data ) {
var feature = topic.split( '.' )[ 1 ],
event;
if ( !inSample() && !trackdebug ) {
return;
}
if ( ve.init.target.constructor.static.platformType === 'phone' ) {
// handled in MobileFrontend for session-identification reasons
return;
}
event = {
feature: feature,
action: data.action,
editingSessionId: editingSessionId
};
if ( trackdebug ) {
log( topic, event );
} else {
mw.track( 'event.VisualEditorFeatureUse', event );
}
}
// Only log events if the WikimediaEvents extension is installed.
// It provides variables that the above code depends on and registers the schemas.
if ( mw.config.exists( 'wgWMESchemaEditAttemptStepSamplingRate' ) ) {
// Ensure 'ext.eventLogging.subscriber' first, it provides mw.eventLog.randomTokenMatch.
mw.loader.using( 'ext.eventLogging.subscriber' ).done( function () {
ve.trackSubscribe( 'mwedit.', mwEditHandler );
ve.trackSubscribe( 'mwtiming.', mwTimingHandler );
ve.trackSubscribe( 'activity.', activityHandler );
} );
}
}() );