VirtualPageViews bypass EventLogging for logging virtual events

We are using EventLogging to track page views not user behaviour.
This is an exception to the rule and requires special handling.

Bug: T190188
Change-Id: If096ccaf0ac884d57744ed57e2f26b51446de2d7
This commit is contained in:
jdlrobson 2018-03-27 12:42:01 -07:00 committed by Your Name
parent 9eccf1c103
commit e6202b6ce4
5 changed files with 137 additions and 11 deletions

Binary file not shown.

Binary file not shown.

72
src/getPageviewTracker.js Normal file
View file

@ -0,0 +1,72 @@
/**
* @module getPageviewTracker
*/
/**
* @typedef {Object} MwCodeLoader
*
* Loads code from the server to the client on demand.
*
* @param {array} dependencies to load
* @return {jQuery.Deferred} resolving when the code is loaded and
* can be used by the client.
*
* @global
*/
/**
* Convert the first letter of a string to uppercase.
*
* @param {string} word
* @return {string}
*/
function titleCase( word ) {
return word[ 0 ].toUpperCase() + word.slice( 1 );
}
/**
* Gets the appropriate analytics event tracker for logging virtual pageviews.
* Note this bypasses EventLogging in order to track virtual pageviews
* for pages where the DNT header (do not track) has been added.
* This is explained in https://phabricator.wikimedia.org/T187277.
*
* @param {Object} config
* @param {MwCodeLoader} loader that can source code that obeys the
* EventLogging api specification.
* @param {Function} trackerGetter when called returns an instance
* of MediaWiki's EventLogging client
* @param {Function} sendBeacon see
* https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon
* @return {EventTracker}
*/
function getPageviewTracker( config, loader, trackerGetter, sendBeacon ) {
const pageviewTracker = function ( topic, eventData ) {
const schema = titleCase( topic.slice( topic.indexOf( '.' ) + 1 ) );
const dependencies = [ 'ext.eventLogging', `schema.${schema}` ];
return loader( dependencies ).then( function () {
const evLog = trackerGetter();
const payload = evLog.prepare( schema, eventData );
const url = evLog.makeBeaconUrl( payload );
sendBeacon( url );
} );
};
return config.get( 'wgPopupsVirtualPageViews' ) ? pageviewTracker : $.noop;
}
/**
* Gets a function that can asynchronously transfer a small amount of data
* over HTTP to a web server.
*
* @param {Window.Navigator} navigatorObj
* @return {Function}
*/
function getSendBeacon( navigatorObj ) {
return navigatorObj.sendBeacon ?
navigatorObj.sendBeacon.bind( navigatorObj ) :
( url ) => {
document.createElement( 'img' ).src = url;
};
}
export { getSendBeacon };
export default getPageviewTracker;

View file

@ -22,6 +22,7 @@ import * as actions from './actions';
import reducers from './reducers';
import createMediaWikiPopupsObject from './integrations/mwpopups';
import getUserBucket from './getUserBucket';
import getPageviewTracker, { getSendBeacon } from './getPageviewTracker';
const mw = mediaWiki,
$ = jQuery,
@ -66,16 +67,6 @@ function getStatsvTracker( user, config, experiments ) {
return isStatsvEnabled( user, config, experiments ) ? mw.track : $.noop;
}
/**
* Gets the appropriate analytics event tracker for logging virtual pageviews.
*
* @param {Object} config
* @return {EventTracker}
*/
function getPageViewTracker( config ) {
return config.get( 'wgPopupsVirtualPageViews' ) ? mw.track : $.noop;
}
/**
* Gets the appropriate analytics event tracker for logging EventLogging events
* via [the "EventLogging subscriber" analytics event protocol][0].
@ -185,7 +176,11 @@ mw.requestIdleCallback( function () {
experiments = createExperiments( mw.experiments ),
statsvTracker = getStatsvTracker( mw.user, mw.config, experiments ),
// Virtual pageviews are always tracked.
pageviewTracker = getPageViewTracker( mw.config ),
pageviewTracker = getPageviewTracker( mw.config,
mw.loader.using,
() => mw.eventLog,
getSendBeacon( window.navigator )
),
eventLoggingTracker = getEventLoggingTracker(
mw.user,
mw.config,

View file

@ -0,0 +1,59 @@
/* global Promise */
import getPageviewTracker, { getSendBeacon } from '../../src/getPageviewTracker';
QUnit.module( 'ext.popups#getPageviewTracker', {
beforeEach: function () {
this.makeBeaconUrl = this.sandbox.stub();
this.prepare = this.sandbox.stub();
this.trackerGetter = () => ( { makeBeaconUrl: this.makeBeaconUrl,
prepare: this.prepare } );
this.loader = () => Promise.resolve();
}
} );
const enabledConfig = {
get: () => true
};
QUnit.test( 'getPageviewTracker', function ( assert ) {
const loader = this.sandbox.stub();
const sendBeacon = this.sandbox.stub();
const data = { foo: 1 };
const tracker = getPageviewTracker( enabledConfig,
loader, this.trackerGetter, sendBeacon );
loader.resolves();
return tracker( 'event.VirtualPageView', data ).then( () => {
assert.ok( loader.calledOnce, 'loader called once' );
assert.ok( loader.calledWith( [ 'ext.eventLogging', 'schema.VirtualPageView' ] ),
'appropriate code is loaded' );
assert.ok( this.prepare.calledWith( 'VirtualPageView', data ),
'mw.eventLog.prepare called appropriately' );
assert.ok( this.makeBeaconUrl.calledOnce,
'makeBeacon called with result of prepare' );
assert.ok( sendBeacon.calledOnce,
'sendBeacon called with url from makeBeaconUrl' );
} );
} );
QUnit.test( 'getSendBeacon', function ( assert ) {
let success = false;
const navtr = {
successful: true,
sendBeacon: function () {
// This local variable helps test context. Don't refactor.
success = this.successful;
}
};
const sendBeacon = getSendBeacon( navtr );
sendBeacon();
assert.ok( success, 'native sendBeacon is used when available and run in appropriate context' );
} );
QUnit.test( 'getSendBeacon (fallback)', function ( assert ) {
const spy = this.sandbox.spy( document, 'createElement' );
const sendBeacon = getSendBeacon( {} );
sendBeacon();
assert.ok( spy.calledOnce, 'an img element is used as fallback' );
} );