VisualEditor/Adding instrumentation how-to

This short guide describes how to add instrumentation to VisualEditor. Adding instrumentation to VisualEditor means modifying VisualEditor's code to make it send events and metrics to a log aggregator for plotting and analysis.

Step 1: Identify the code path that you want to instrument edit

For this example, we want VisualEditor to report the time it took to retrieve a wikitext diff from the Parsoid API. If you are already familiar with VisualEditor's codebase, you might already know that the relevant code is in ve.init.mw.Target.prototype.showChanges. Otherwise you'll have to ask or grep around.

Step 2: Modify the code to add a ve.track call edit

The relevant code (ve.init.mw.Target.js@760ef37e2b) looks like this:

$.ajax( {
    'url': this.apiUrl,
    'data': {
        'format': 'json',
        'action': 'visualeditor',
        'paction': 'diff',
        'page': this.pageName,
        'oldid': this.revid,
        'html': this.getHtml( doc )
    },
    'dataType': 'json',
    'type': 'POST',
    // Wait up to 100 seconds before giving up
    'timeout': 100000
} )
    .done( ve.init.mw.Target.onShowChanges.bind( this ) )
    .fail( ve.init.mw.Target.onShowChangesError.bind( this ) );

If we want to measure the duration of the API call, we're going to need to make a few small changes. First, we'll want to capture the time immediately before the API request is fired, so we can compute the time it took the request to complete. This can be done by calling ve.now() and storing the result in a variable. (ve.now provides access to a high-precision, monotonic clock in browsers that implement the High Resolution Time API falling back to a standard new Date().getTime() on older browsers.)

Next, we'll want to bind an additional success handler for the AJAX request that will handle logging. The handler will generate an event object representing the successful API call and will dispatch it to event subscribers by calling ve.track with an event name and the object as arguments.

Event names comprise dot-separated, lowerCamelCase components, ordered from most general to most specific. The event name you choose should be consistent with the format and scheme of other ve.track calls. The conventions are not fully formalized, but they are easy enough to intuit -- just grep the VisualEditor codebase for 've.track'. In the case of API calls, we already log the timing of DOM load and DOM save as 'performance.domLoad' and 'performance.domSave', so it seems logical to name this new event 'performance.domPreview'.

The final code looks like this:

var start = ve.now();
$.ajax( {
    'url': this.apiUrl,
    'data': {
        'format': 'json',
        'action': 'visualeditor',
        'paction': 'diff',
        'page': this.pageName,
        'oldid': this.revid,
        'html': this.getHtml( doc )
    },
    'dataType': 'json',
    'type': 'POST',
    // Wait up to 100 seconds before giving up
    'timeout': 100000
} )
    .done( ve.init.mw.Target.onShowChanges.bind( this ) )
    .fail( ve.init.mw.Target.onShowChangesError.bind( this ) )
    .done( function () {
        ve.track( 'performance.domPreview', {
            'duration': ve.now() - start,
        } );
    } );

Read the in-line documentation for ve.track for more information.

Step 3: Use EventLogging to log the tracked event edit

One of the design goals for VisualEditor is for the implementation to be usable in a variety of contexts, not just MediaWiki. To meet that goal, VisualEditor makes as few assumptions as possible about the configuration of unrelated software components. This is reflected in the design of ve.track. Rather than tie ve.track to a particular web analytics provider, ve.track provides a means for analytics frameworks to subscribe to VisualEditor events.

Wikimedia wikis use EventLogging to log analytic events from client-side code. If you are not familiar with EventLogging, you should read the EventLogging guide. The remainder of this tutorial will assume that you have created an appropriate schema for your event (or have identified an existing schema you can re-use). For this tutorial, the schema is provided for you: meta:Schema:VisualEditorDOMPreviewed. It contains just three properties: page ID, revision ID, and the AJAX call duration.

The WikimediaEvents extension provides a generic, centralized place where handlers for VisualEditor analytic events can be declared. First, edit the file WikimediaEvents.php and copy the schema registration code from the schema page. Next, edit the file modules/ext.wikimediaEvents.ve.js in that extension's repository and add the following code:

mw.hook( 've.activationComplete' ).add( function () {
    ve.trackSubscribe( 'performance.domPreview', function ( topic, data ) {
        var event = {
            pageId: mw.config.get('wgArticleId'),
            revId: mw.config.get('wgCurRevisionId'),
            duration: data.duration
        };
        mw.eventLog.logEvent( 'VisualEditorDOMPreviewed', event );
    } );
} );

The outer mw.hook call ensures that we only attempt to call ve.trackSubscribe when ve has loaded. (Otherwise 've' is undefined.) The ve.trackSubscribe call itself registers a handler for the 'performance.domPreview' event. It constructs an event object by grabbing the request duration from the data provided to ve.track and it gets the page and revision IDs from the environment provided by MediaWiki. Finally, the mw.eventLog.logEvent calls passes the event object to EventLogging to be dispatched to the server for aggregation and analysis.

That's it! If you have VisualEditor, EventLogging & WikimediaEvents configured, you should start seeing VisualEditorDOMPreview events in your EventLogging event stream.

TODO: graphing VE analytic data using Ganglia / Graphite