mediawiki-extensions-Visual.../modules/ve-mw/init/ve.init.mw.trackSubscriber.js
David Lynch b59625158d Update schema logging of bucket for new DiscussionTools A/B test
Bug: T304030
Depends-On: I5e57bb9b7958576f3a04373748331a86f4626fb5
Change-Id: I2ef4c64a55eebc0d2eeca7a57b84b8ba0ded1499
2022-05-19 01:20:33 -05:00

285 lines
8.7 KiB
JavaScript

/*!
* VisualEditor MediaWiki event subscriber.
*
* Subscribes to ve.track() events and routes them to mw.track().
*
* @copyright 2011-2020 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
( function () {
var actionPrefixMap = {
firstChange: 'first_change',
saveIntent: 'save_intent',
saveAttempt: 'save_attempt',
saveSuccess: 'save_success',
saveFailure: 'save_failure'
},
trackdebug = !!mw.Uri().query.trackdebug,
firstInitDone = false;
function getEditingSessionIdFromRequest() {
return mw.config.get( 'wgWMESchemaEditAttemptStepSessionId' ) ||
mw.Uri().query.editingStatsId;
}
var timing = {};
var editingSessionId = getEditingSessionIdFromRequest() || mw.user.generateRandomSessionId();
ve.init.editingSessionId = editingSessionId;
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 addABTestData( data ) {
// DiscussionTools New Topic A/B test for logged out users
if ( !mw.config.get( 'wgDiscussionToolsABTest' ) ) {
return;
}
if ( mw.config.get( 'wgDiscussionToolsABTestBucket' ) ) {
data.bucket = mw.config.get( 'wgDiscussionToolsABTestBucket' );
}
}
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 'firstChange':
return timeStamp - timing.ready;
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;
if ( action === 'init' ) {
if ( firstInitDone ) {
// Regenerate editingSessionId
editingSessionId = mw.user.generateRandomSessionId();
ve.init.editingSessionId = editingSessionId;
}
firstInitDone = true;
}
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 */
var event = ve.extendObject( {
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' ),
// eslint-disable-next-line no-jquery/no-global-selector
revision_id: mw.config.get( 'wgRevisionId' ) || +$( 'input[name=parentRevId]' ).val() || 0,
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;
}
// Remove renamed properties
delete event.type;
delete event.mechanism;
delete event.timing;
delete event.message;
if ( action === 'abort' ) {
timing = {};
} else {
timing[ action ] = timeStamp;
}
/* eslint-enable camelcase */
addABTestData( event );
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 ];
if ( !inSample() && !mw.config.get( 'wgWMESchemaEditAttemptStepOversample' ) && !trackdebug ) {
return;
}
if ( ve.init.target && (
ve.init.target.constructor.static.platformType !== 'desktop'
) ) {
// We want to log activity events when we're also logging to
// EditAttemptStep. The EAS events are only fired from DesktopArticleTarget
// in this repo. As such, we suppress this unless the current target is at
// least inheriting that. (Other tools may fire their own instances of
// those events, but probably need to reimplement this anyway for
// session-identification reasons.)
return;
}
/* eslint-disable camelcase */
var event = {
feature: feature,
action: data.action,
editingSessionId: editingSessionId,
is_oversample: !inSample(),
user_id: mw.user.getId(),
user_editcount: mw.config.get( 'wgUserEditCount', 0 ),
editor_interface: ve.getProp( ve, 'init', 'target', 'surface', 'mode' ) === 'source' ? 'wikitext-2017' : 'visualeditor',
integration: ve.getProp( ve, 'init', 'target', 'constructor', 'static', 'integrationType' ) || 'page',
platform: ve.getProp( ve, 'init', 'target', 'constructor', 'static', 'platformType' ) || 'other'
};
/* eslint-enable camelcase */
addABTestData( data );
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' first, it provides mw.eventLog.randomTokenMatch.
mw.loader.using( 'ext.eventLogging' ).done( function () {
ve.trackSubscribe( 'mwedit.', mwEditHandler );
ve.trackSubscribe( 'mwtiming.', mwTimingHandler );
ve.trackSubscribe( 'activity.', activityHandler );
} );
}
}() );