mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-15 10:35:48 +00:00
ddb5ac3a14
The VisualEditorFeatureUsage instrument is a candidate for migration to the Metrics Platform [0]. The first step of the migration is to log events both using the Event Platform directly (i.e. via mw.eventLog.submit()) and using the Metrics Platform client (i.e. via mw.eventLog.dispatch()). The Metrics Platform Client can mix in additional information - so-called context attributes [1] - based on the stream configuration. The majority of the default values mixed into each event are already known to the Metrics Platform Client. Note well that the Metrics Platform client will not log an event without one or more streams being configured to receive that event. Therefore, this change is a NOP. An example stream configuration is given in [2]. [0] https://wikitech.wikimedia.org/wiki/Metrics_Platform [1] https://gerrit.wikimedia.org/g/mediawiki/libs/metrics-platform/ +/aed6738b845/js/src/StreamConfig.d.ts#31 [2] https://phabricator.wikimedia.org/T309602#7973206 Bug: T309602 Change-Id: If40fcc6cae371788b98365953218300a5c0b3ca1
359 lines
11 KiB
JavaScript
359 lines
11 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 inEASSample() {
|
|
// 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 inVEFUSample() {
|
|
return mw.eventLog.randomTokenMatch(
|
|
1 / mw.config.get( 'wgWMESchemaVisualEditorFeatureUseSamplingRate' ),
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Log the equivalent of an EditAttemptStep event via
|
|
* [the Metrics Platform](https://wikitech.wikimedia.org/wiki/Metrics_Platform).
|
|
*
|
|
* See https://phabricator.wikimedia.org/T309013.
|
|
*
|
|
* @param {Object} data
|
|
* @param {string} actionPrefix
|
|
*/
|
|
function logEditViaMetricsPlatform( data, actionPrefix ) {
|
|
var eventName = 'eas.ve.' + actionPrefix;
|
|
|
|
/* eslint-disable camelcase */
|
|
var customData = ve.extendObject(
|
|
{
|
|
editor_interface: 'visualeditor',
|
|
integration: ve.init && ve.init.target && ve.init.target.constructor.static.integrationType || 'page',
|
|
editing_session_id: editingSessionId
|
|
},
|
|
data
|
|
);
|
|
|
|
if ( !mw.config.get( 'wgRevisionId' ) ) {
|
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector
|
|
customData.revision_id = +$( 'input[name=parentRevId]' ).val() || 0;
|
|
}
|
|
|
|
/* eslint-enable camelcase */
|
|
|
|
delete customData.action;
|
|
|
|
// Sampling rate (and therefore whether a stream should oversample) is captured in the
|
|
// stream config ($wgEventStreams).
|
|
delete customData.is_oversample;
|
|
|
|
// Platform can be derived from the agent_client_platform_family context attribute mixed in
|
|
// by the JavaScript Metrics Platform Client. The context attribute will be
|
|
// "desktop_browser" or "mobile_browser" depending on whether the MobileFrontend extension
|
|
// has signalled that it is enabled.
|
|
delete customData.platform;
|
|
|
|
mw.eventLog.dispatch( eventName, customData );
|
|
}
|
|
|
|
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 ( !inEASSample() && !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 );
|
|
}
|
|
}
|
|
|
|
// Schema's kind of a mess of special properties
|
|
if ( action === 'init' || action === 'abort' || action === 'saveFailure' ) {
|
|
data[ actionPrefix + '_type' ] = data.type;
|
|
}
|
|
if ( action === 'init' || action === 'abort' ) {
|
|
data[ actionPrefix + '_mechanism' ] = data.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, data, timeStamp ) );
|
|
data[ actionPrefix + '_timing' ] = duration;
|
|
}
|
|
if ( action === 'saveFailure' ) {
|
|
data[ actionPrefix + '_message' ] = data.message;
|
|
}
|
|
|
|
// Remove renamed properties
|
|
delete data.type;
|
|
delete data.mechanism;
|
|
delete data.timing;
|
|
delete data.message;
|
|
|
|
if ( action === 'abort' ) {
|
|
timing = {};
|
|
} else {
|
|
timing[ action ] = timeStamp;
|
|
}
|
|
/* eslint-enable camelcase */
|
|
|
|
addABTestData( data );
|
|
|
|
logEditViaMetricsPlatform( data, actionPrefix );
|
|
|
|
/* eslint-disable camelcase */
|
|
var event = ve.extendObject( {
|
|
version: 1,
|
|
action: action,
|
|
is_oversample: !inEASSample(),
|
|
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';
|
|
}
|
|
|
|
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 ( !inVEFUSample() && !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: !inVEFUSample(),
|
|
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 );
|
|
|
|
// T309602: Also log via the Metrics Platform:
|
|
var eventName = 'vefu.' + event.action;
|
|
|
|
/* eslint-disable camelcase */
|
|
var customData = {
|
|
feature: event.feature,
|
|
editing_session_id: event.editingSessionId,
|
|
editor_interface: event.editor_interface,
|
|
integration: event.integration
|
|
};
|
|
/* eslint-enable camelcase */
|
|
|
|
mw.eventLog.dispatch( eventName, customData );
|
|
}
|
|
}
|
|
|
|
// 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 and
|
|
// mw.eventLog.dispatch.
|
|
mw.loader.using( 'ext.eventLogging' ).done( function () {
|
|
ve.trackSubscribe( 'mwedit.', mwEditHandler );
|
|
ve.trackSubscribe( 'mwtiming.', mwTimingHandler );
|
|
} );
|
|
}
|
|
|
|
if ( mw.config.exists( 'wgWMESchemaVisualEditorFeatureUseSamplingRate' ) ) {
|
|
mw.loader.using( 'ext.eventLogging' ).done( function () {
|
|
ve.trackSubscribe( 'activity.', activityHandler );
|
|
} );
|
|
}
|
|
|
|
}() );
|