Move VirtualPageViews back to mw.track

Use mw.track instead of dedicated tracker for VirtualPageViews.
This way we can migrate it to the new Event Platform client.
The new client does not observe DNT, which was the reason this
instrument was moved to a dedicated tracker on the first place.

Bug: T279382
Change-Id: I8bb515eab337ffed686ba7522bc6153cfdd8ca8d
This commit is contained in:
Marcel Ruiz Forns 2021-04-28 16:28:11 +02:00
parent acb5d1d8e9
commit a6a06f70bf
7 changed files with 56 additions and 275 deletions

Binary file not shown.

Binary file not shown.

View file

@ -17,20 +17,21 @@ export default function pageviews(
boundActions, pageviewTracker
) {
return ( oldState, newState ) => {
let page;
let page, pageview;
if ( newState.pageviews && newState.pageviews.pageview && newState.pageviews.page ) {
page = newState.pageviews.page;
pageviewTracker( 'event.VirtualPageView', $.extend( {},
{
/* eslint-disable camelcase */
source_page_id: page.id,
source_namespace: page.namespaceId,
source_title: page.title,
source_url: page.url
/* eslint-enable camelcase */
},
newState.pageviews.pageview )
);
pageview = newState.pageviews.pageview;
pageviewTracker( 'event.VirtualPageView', {
/* eslint-disable camelcase */
source_page_id: page.id,
source_namespace: page.namespaceId,
source_title: mw.Title.newFromText( page.title ).getPrefixedDb(),
source_url: page.url,
page_id: pageview.page_id,
page_namespace: pageview.page_namespace,
page_title: mw.Title.newFromText( pageview.page_title ).getPrefixedDb()
/* eslint-enable camelcase */
} );
// Clear the pageview now its been logged.
boundActions.pageviewLogged();
}

View file

@ -1,111 +0,0 @@
/**
* @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 );
}
/**
* Truncates a string to a maximum length based on its URI encoded value.
*
* @param {string} sourceUrl source string
* @param {number} maxLength maximum length
* @return {string} string is returned in the same encoding as the input
*/
function limitByEncodedURILength( sourceUrl, maxLength ) {
let truncatedUrl = '';
sourceUrl.split( '' ).every( ( char ) => {
return ( encodeURIComponent( truncatedUrl + char ).length < maxLength ) ?
( truncatedUrl += char ) :
false;
} );
return truncatedUrl;
}
/**
* Convert Title properties into mediawiki canonical form
* and limit the length of source_url.
*
* @param {Object} eventData
* @return {Object}
*/
function prepareEventData( eventData ) {
const data = eventData;
/* eslint-disable camelcase */
data.source_title = mw.Title.newFromText( eventData.source_title )
.getPrefixedDb();
data.page_title = mw.Title.newFromText( eventData.page_title )
.getPrefixedDb();
// prevent source_url from exceeding varnish max-url size - T196904
data.source_url = limitByEncodedURILength( eventData.source_url, 1000 );
/* eslint-enable camelcase */
return data;
}
/**
* 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' ];
return loader( dependencies ).then( function () {
const evLog = trackerGetter();
const payload = evLog.prepare( schema, prepareEventData( eventData ) );
const url = evLog.makeBeaconUrl( payload );
sendBeacon( url );
} );
};
return config.get( 'wgPopupsVirtualPageViews' ) ? pageviewTracker : () => {};
}
/**
* 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, limitByEncodedURILength };
export default getPageviewTracker;

View file

@ -21,7 +21,6 @@ import changeListeners from './changeListeners';
import * as actions from './actions';
import reducers from './reducers';
import createMediaWikiPopupsObject from './integrations/mwpopups';
import getPageviewTracker, { getSendBeacon } from './getPageviewTracker';
import { previewTypes, getPreviewType } from './preview/model';
import isReferencePreviewsEnabled from './isReferencePreviewsEnabled';
import setUserConfigFlags from './setUserConfigFlags';
@ -68,6 +67,18 @@ function getStatsvTracker( user, config, experiments ) {
return isStatsvEnabled( user, config, experiments ) ? mw.track : () => {};
}
/**
* Gets the appropriate analytics event tracker for logging virtual pageviews.
*
* @param {Object} config
* @return {EventTracker}
*/
function getPageviewTracker( config ) {
return config.get( 'wgPopupsVirtualPageViews' ) ? mw.track : () => {
// NOP
};
}
/**
* Gets the appropriate analytics event tracker for logging EventLogging events
* via [the "EventLogging subscriber" analytics event protocol][0].
@ -173,12 +184,7 @@ function registerChangeListeners(
settingsDialog = createSettingsDialogRenderer( mw.config ),
experiments = createExperiments( mw.experiments ),
statsvTracker = getStatsvTracker( mw.user, mw.config, experiments ),
// Virtual pageviews are always tracked.
pageviewTracker = getPageviewTracker( mw.config,
mw.loader.using,
() => mw.eventLog,
getSendBeacon( window.navigator )
),
pageviewTracker = getPageviewTracker( mw.config ),
eventLoggingTracker = getEventLoggingTracker(
mw.user,
mw.config,

View file

@ -1,11 +1,22 @@
import pageviews from '../../../src/changeListeners/pageviews';
const REFERRER = 'https://en.m.wikipedia.org/wiki/Kittens',
page = {
namespaceId: 1,
id: 42,
title: 'Kittens',
url: REFERRER
newState = {
pageviews: {
page: {
id: 42,
namespaceId: 1,
title: 'Kittens',
url: REFERRER
},
pageview: {
/* eslint-disable camelcase */
page_id: 43,
page_namespace: 1,
page_title: 'Rainbows'
/* eslint-enable camelcase */
}
}
};
QUnit.module( 'ext.popups/pageviews', {
@ -19,27 +30,17 @@ QUnit.module( 'ext.popups/pageviews', {
this.boundActions,
this.pageviewTracker
);
// Stub internal usage of mw.Title.newFromText
mw.Title.newFromText = ( str ) => {
return {
getPrefixedDb: () => { return str; }
};
};
}
} );
function createState( title ) {
return title ? {
pageviews: {
page,
pageview: {
page_title: title // eslint-disable-line camelcase
}
}
} : {
pageviews: {
page,
pageview: undefined
}
};
}
QUnit.test( 'it should log the queued event', function ( assert ) {
const newState = createState( 'Rainbows' );
this.changeListener( undefined, newState );
assert.ok(
@ -47,11 +48,13 @@ QUnit.test( 'it should log the queued event', function ( assert ) {
'event.VirtualPageView',
{
/* eslint-disable camelcase */
page_title: 'Rainbows',
source_url: REFERRER,
source_page_id: 42,
source_namespace: 1,
source_title: 'Kittens'
source_title: 'Kittens',
source_url: REFERRER,
page_id: 43,
page_namespace: 1,
page_title: 'Rainbows'
/* eslint-enable camelcase */
}
),
@ -64,7 +67,8 @@ QUnit.test( 'it should log the queued event', function ( assert ) {
} );
QUnit.test( 'it should not log something that is not a pageview', function ( assert ) {
const newState = createState();
const noPageviewState = $.extend( {}, newState );
delete noPageviewState.pageviews.pageview;
this.changeListener( undefined, newState );

View file

@ -1,119 +0,0 @@
import getPageviewTracker, { getSendBeacon, limitByEncodedURILength } from '../../src/getPageviewTracker';
QUnit.module( 'ext.popups#getPageviewTracker', {
beforeEach() {
this.makeBeaconUrl = this.sandbox.stub();
this.prepare = this.sandbox.stub();
this.trackerGetter = () => ( { makeBeaconUrl: this.makeBeaconUrl,
prepare: this.prepare } );
this.loader = () => Promise.resolve();
this.Title = {
newFromText: this.sandbox.stub()
};
mw.Title = this.Title;
},
afterEach() {
mw.Title = null;
}
} );
const enabledConfig = {
get: () => true
};
QUnit.test( 'getPageviewTracker', function ( assert ) {
const loader = this.sandbox.stub();
const sendBeacon = this.sandbox.stub();
/* eslint-disable camelcase */
const data = {
page_title: 'Test title',
source_title: 'Source title',
page_namespace: 1,
source_url: 'http://some/url'
};
const eventData = {
page_title: 'Test_title',
source_title: 'Source_title',
page_namespace: 1,
source_url: 'http://some/url'
};
this.Title.newFromText.withArgs( data.page_title ).returns( {
getPrefixedDb: () => eventData.page_title
} );
this.Title.newFromText.withArgs( data.source_title ).returns( {
getPrefixedDb: () => eventData.source_title
} );
/* eslint-enable camelcase */
const tracker = getPageviewTracker( enabledConfig,
loader, this.trackerGetter, sendBeacon );
loader.resolves();
return tracker( 'event.VirtualPageView', data ).then( () => {
assert.strictEqual( loader.callCount, 1, 'loader called once' );
assert.ok( loader.calledWith( [ 'ext.eventLogging' ] ),
'appropriate code is loaded' );
assert.strictEqual(
this.Title.newFromText.callCount,
2,
'The title factory was invoked twice.'
);
assert.ok( this.prepare.calledWith( 'VirtualPageView', eventData ),
'mw.eventLog.prepare called appropriately' );
assert.strictEqual( this.makeBeaconUrl.callCount, 1,
'makeBeacon called with result of prepare' );
assert.strictEqual( sendBeacon.callCount, 1,
'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.strictEqual( spy.callCount, 1, 'an img element is used as fallback' );
} );
QUnit.test( 'limitByEncodedURILength', function ( assert ) {
const shortUrl = 'https://en.wikipedia.org/wiki/banana',
longEncodedUrl = 'https://ka.wikipedia.org/wiki/%E1%83%95%E1%83%98%E1%83%99%E1%83%98%E1%83%9E%E1%83%94%E1%83%93%E1%83%98%E1%83%90:%E1%83%95%E1%83%98%E1%83%99%E1%83%98%E1%83%A1_%E1%83%A3%E1%83%A7%E1%83%95%E1%83%90%E1%83%A0%E1%83%A1_%E1%83%AB%E1%83%94%E1%83%92%E1%83%9A%E1%83%94%E1%83%91%E1%83%98/%E1%83%AB%E1%83%94%E1%83%92%E1%83%9A%E1%83%94%E1%83%91%E1%83%98%E1%83%A1_%E1%83%A1%E1%83%98%E1%83%90/%E1%83%99%E1%83%90%E1%83%AE%E1%83%94%E1%83%97%E1%83%98/%E1%83%A1%E1%83%90%E1%83%92%E1%83%90%E1%83%A0%E1%83%94%E1%83%AF%E1%83%9D%E1%83%A1_%E1%83%9B%E1%83%A3%E1%83%9C%E1%83%98%E1%83%AA%E1%83%98%E1%83%9E%E1%83%90%E1%83%9A%E1%83%98%E1%83%A2%E1%83%94%E1%83%A2%E1%83%98',
randomSequence = 'チプロ للا المتحة Добро пожаловат פעילה למען שוווв ВикипедиюA %20%F0%9F%A4%A8%20 IJ@_#*($PJOWR',
contentRg = new RegExp( limitByEncodedURILength( randomSequence, 326 ) );
assert.strictEqual(
limitByEncodedURILength( shortUrl, 1000 ), shortUrl,
'short url is not truncated' );
assert.strictEqual(
encodeURIComponent(
limitByEncodedURILength( longEncodedUrl, 1000 )
).length < 1000, true,
'long url is truncated to the correct length' );
assert.strictEqual(
encodeURIComponent(
limitByEncodedURILength( randomSequence, 50 )
).length < 50, true,
'Random string is truncated to the correct length' );
assert.strictEqual(
contentRg.test( randomSequence ), true,
'Truncated string contains the same content as the original'
);
} );