Jdrewniak has uploaded a new change for review. https://gerrit.wikimedia.org/r/251091
Change subject: wikipedia.org event logging ...................................................................... wikipedia.org event logging Change-Id: Ia52a523723dfbdd3db79a52e72e606c58351bd27 --- A dev/wikipedia.org/assets/js/event-logging-lite.js A dev/wikipedia.org/assets/js/wikimedia-org-event-logging.js M dev/wikipedia.org/index.html 3 files changed, 383 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/wikimedia/portals refs/changes/91/251091/1 diff --git a/dev/wikipedia.org/assets/js/event-logging-lite.js b/dev/wikipedia.org/assets/js/event-logging-lite.js new file mode 100644 index 0000000..e34c5f2 --- /dev/null +++ b/dev/wikipedia.org/assets/js/event-logging-lite.js @@ -0,0 +1,209 @@ +/* + +A slimmed down version of the event logging API. +Mostly copy & pasted from: +https://github.com/wikimedia/mediawiki-extensions-EventLogging/blob/master/modules/ext.eventLogging.core.js +without dependancies on jQuery or mediawiki.js. For use on wikipedia.org portal page. + +*/ + +( function () { + +'use strict'; + +// baseUrl = mw.config.get( 'wgEventLoggingBaseUri' ); +var baseUrl = '//www.wikipedia.org/beacon/event', +byteToHex = [], +self, helpers; + +helpers = { + // replaces $.extend + extend: function ( defaults, options ) { + var extended = {}, + prop; + + for ( prop in defaults ) { + if ( Object.prototype.hasOwnProperty.call( defaults, prop ) ) { + extended[ prop ] = defaults[ prop ]; + } + } + for ( prop in options ) { + if ( Object.prototype.hasOwnProperty.call( options, prop ) ) { + extended[ prop ] = options[ prop ]; + } + } + return extended; + }, + // replaces $.noop + noop: function () {}, + + // navigator.sendBeacon polyfill + sendBeacon: function ( url ) { + var xhr = ( 'XDomainRequest' in window ) ? new XDomainRequest() : new XMLHttpRequest(); + xhr.open( 'GET', url, false ); // synchronous request + xhr.responseType = 'text/plain'; + xhr.send(); + return true; + } +}; +//indexOf polyfill http://stackoverflow.com/questions/3629183/why-doesnt-indexof-work-on-an-array-ie8 +if ( !Array.prototype.indexOf ) { + Array.prototype.indexOf = function( elt /*, from*/) + { + var len = this.length >>> 0; + + var from = Number(arguments[1]) || 0; + from = (from < 0) + ? Math.ceil(from) + : Math.floor(from); + if (from < 0) + from += len; + + for (; from < len; from++) + { + if (from in this && + this[from] === elt) + return from; + } + return -1; + }; +} + + +// byte to hex from +// https://github.com/wikimedia/mediawiki/blob/e87668e86ce9ad20df05c1baa8e7cf3f58900524/resources/src/mediawiki/mediawiki.user.js +for ( var i = 0; i < 256; i++ ) { + // Padding: Add a full byte (0x100, 256) and strip the extra character + byteToHex[ i ] = ( i + 256 ).toString( 16 ).slice( 1 ); +} + +self = window.eventLoggingLite = { + + schema: {}, + + maxUrlSize: 2000, + + byteToHex: byteToHex, + + checkUrlSize: function ( schemaName, url ) { + var message; + if ( url.length > self.maxUrlSize ) { + message = 'Url exceeds maximum length'; + return message; + } + }, + + generateRandomSessionId: function () { + // copied from: + // https://github.com/wikimedia/mediawiki/blob/e87668e86ce9ad20df05c1baa8e7cf3f58900524/resources/src/mediawiki/mediawiki.user.js + + /*jshint bitwise:false */ + var rnds, i, r, + hexRnds = new Array( 8 ), + // Support: IE 11 + crypto = window.crypto || window.msCrypto; + + // Based on https://github.com/broofa/node-uuid/blob/bfd9f96127/uuid.js + if ( crypto && crypto.getRandomValues ) { + // Fill an array with 8 random values, each of which is 8 bits. + // Note that Uint8Array is array-like but does not implement Array. + rnds = new Uint8Array( 8 ); + crypto.getRandomValues( rnds ); + } else { + rnds = new Array( 8 ); + for ( i = 0; i < 8; i++ ) { + if ( ( i & 3 ) === 0 ) { + r = Math.random() * 0x100000000; + } + rnds[ i ] = r >>> ( ( i & 3 ) << 3 ) & 255; + } + } + // Convert from number to hex + for ( i = 0; i < 8; i++ ) { + hexRnds[ i ] = self.byteToHex[ rnds[ i ] ]; + } + + // Concatenation of two random integers with entrophy n and m + // returns a string with entrophy n+m if those strings are independent + return hexRnds.join( '' ); + + }, + + validate: function ( obj, schema ) { + var key, val, prop, + errors = []; + + if ( !schema || !schema.properties ) { + errors.push( 'Missing or empty schema' ); + return errors; + } + + for ( key in obj ) { + if ( !schema.properties.hasOwnProperty( key ) ) { + errors.push( 'Undeclared property: ' + key ); + } + } + + for ( key in schema.properties ) { + prop = schema.properties[ key ]; + + if ( !obj.hasOwnProperty( key ) ) { + if ( prop.required ) { + errors.push( 'Missing property:', key ); + } + continue; + } + val = obj[ key ]; + + if ( prop[ 'enum' ] && prop.required && prop[ 'enum' ].indexOf( val ) === -1 ) { + errors.push( 'Value "' + JSON.stringify( val ) + '" for property "' + key + '" is not one of ' + JSON.stringify( prop[ 'enum' ] ) ); + } + } + + return errors; + + }, + + prepare: function ( schema, eventData ) { + + var event = helpers.extend( schema.defaults, eventData ), + errors = self.validate( event, schema ); + + while ( errors.length ) { + console.log( errors[ errors.length - 1 ] ); + errors.pop(); + } + + return { + event: event, + revision: schema.revision || -1, + schema: schema.name, + webHost: location.hostname + /* wiki: mw.config.get( 'wgDBname' ) */ + }; + }, + + makeBeaconUrl: function ( data ) { + var queryString = encodeURIComponent( JSON.stringify( data ) ); + return baseUrl + '?' + queryString + ';'; + }, + + sendBeacon: ( /1|yes/.test( navigator.doNotTrack ) || !baseUrl ) + ? helpers.noop + : navigator.sendBeacon + ? function ( url ) { try { navigator.sendBeacon( url ); } catch ( e ) {} } + : function ( url ) { helpers.sendBeacon( url ); }, + + logEvent: function ( schemaName, eventData ) { + var event = self.prepare( schemaName, eventData ), + url = self.makeBeaconUrl( event ), + sizeError = self.checkUrlSize( schemaName, url ); + + if ( !sizeError ) { + self.sendBeacon( url ); + } + } + +}; // eventLoggingLite + +}() ); diff --git a/dev/wikipedia.org/assets/js/wikimedia-org-event-logging.js b/dev/wikipedia.org/assets/js/wikimedia-org-event-logging.js new file mode 100644 index 0000000..f189e4e --- /dev/null +++ b/dev/wikipedia.org/assets/js/wikimedia-org-event-logging.js @@ -0,0 +1,172 @@ +// jscs:disable requireCamelCaseOrUpperCaseIdentifiers + +( function ( eventLoggingLite ) { + +'use strict'; + +var portalSchema, linkSections, docLinks, docForms, i; + +portalSchema = { + name: 'WikipediaPortal', + // revision # from https://meta.wikimedia.org/wiki/Schema:WikipediaPortal + revision: 14377354, + defaults: { + session_id: eventLoggingLite.generateRandomSessionId(), + event_type: 'landing', + section_used: null, + destination: null, + referer: document.referrer || null, + country: null, + accept_language: ( navigator && navigator.language ) ? navigator.language : navigator.browserLanguage, + cohort: null + }, + description: 'Logs basic information about the use of the Wikipedia\nportal (www.wikipedia.org). Most of the things we want (user agent, say) are already tracked', + properties: { + session_id: { + type: 'string', + required: true, + description: 'A unique session ID that identifies events from a single user, resetting on every portal view. In other words, both "landing" and "clickthrough" events from a single user in a single session can be clumped together.' + }, + event_type: { + type: 'string', + required: true, + 'enum': [ + 'landing', + 'clickthrough' + ], + description: 'The type of event were logging. Options are "landing" - the start event, when someone shows up on a portal - and "clickthrough", indicating that they chose a link or the search bar or whatever and went somewhere else.' + }, + section_used: { + type: 'string', + required: false, + 'enum': [ + 'primary links', + 'search', + 'language search', + 'secondary links', + 'other languages', + 'other projects' + ], + description: 'The overall section interacted with. This could be\n \'primary links\' (the called-out project links around the globe\', \n \'search\' (the main search box), \'language search\' (\' Find Wikipedia in a language\'),\n \'secondary links\' (the less-prominent plaintext links)\n \'other languages\' (this page in other langauges)\n or \'other projects\' (links to wikisource, wiktionary, et al, portals). \n NULL in the case that \'event_type\' is landing' + }, + destination: { + type: 'string', + required: false, + description: 'Where the user clicked through to.\n This may be NULL if the event type is \'landing\'' + }, + referer: { + type: 'string', + required: false, + description: 'The referer the user came from, if known. May be NULL.' + }, + country: { + type: 'string', + required: false, + description: 'The ISO code for the country the user geolocates to. May be NULL.' + }, + accept_language: { + type: 'string', + required: true, + description: 'The accept_language header from the user\'s request' + }, + cohort: { + type: 'string', + required: false, + description: 'A cohort identifier. We can use this for running A/B tests or unrelated experiments, with the population who are *not* in any tests getting a NULL cohort field, meaning we can base our dashboards off those users and A/B test from the same schema in peace.' + } + } +}; + +linkSections = [ + { name: 'primary links', nodes: document.querySelectorAll( '.central-featured' ) }, + { name: 'search', nodes: document.querySelectorAll( '.search-form' ) }, + { name: 'language search', nodes: document.querySelectorAll( '.language-search' ) }, + { name: 'secondary links', nodes: document.querySelectorAll( '.langlist' ) }, + { name: 'other languages', nodes: document.querySelectorAll( '#langlist-other' ) }, + { name: 'other projects', nodes: document.querySelectorAll( '.otherprojects' ) } +]; + +function findLinkSection( clickNode ) { + + var linkSection = {}, + i, j; + + for ( i = 0; i < linkSections.length; i++ ) { + + //var nodes = Array.prototype.slice.call( linkSections[ i ].nodes, 0 ); + var nodes = linkSections[ i ].nodes; + + for ( j = 0; j < nodes.length; j++ ) { + + if ( nodes[ j ].contains( clickNode ) ) { + + linkSection = linkSections[ i ]; + } + } + } + return linkSection.name; +} + +function interceptLandingEvent() { + eventData = { + event_type: 'landing' + }; + eventLoggingLite.logEvent( portalSchema, eventData ); +} + +// polyfill element.matches https://developer.mozilla.org/en-US/docs/Web/API/Element/matches +// for eventual event delegation. +/* +var matches = function ( elm, selector ) { + var matches = ( elm.document || elm.ownerDocument ).querySelectorAll( selector ), + i = matches.length; + while ( --i >= 0 && matches.item( i ) !== elm ); + return i > -1; +} +*/ + +function interceptLinkClick( event ) { + + var event = window.event; + + // for IE6-8 + var target = event.target || event.srcElement; + + var eventData = { + event_type: 'clickthrough', + destination: target.href, + section_used: findLinkSection( target ) + }; + + + if ( eventData.section_used ) { + eventLoggingLite.logEvent( portalSchema, eventData ); + } + +} + +function interceptForms ( event ) { + var eventData = { + event_type: 'clickthrough', + section_used: findLinkSection( event.target ) + }; + eventLoggingLite.logEvent( portalSchema, eventData ); +} + +docLinks = document.links; + +docForms = document.getElementsByTagName('form'); + +for ( i = 0; i < docForms.length; i++ ) { + docForms[ i ].onsubmit = interceptForms; +} + +for ( i = 0; i < docLinks.length; i++ ) { + docLinks[ i ].onclick = interceptLinkClick; +} + +document.cookie = 'portal_session_id=' + portalSchema.defaults.session_id; + +window.onload = interceptLandingEvent; + +}( eventLoggingLite ) ); diff --git a/dev/wikipedia.org/index.html b/dev/wikipedia.org/index.html index 3688c88..c71f174 100644 --- a/dev/wikipedia.org/index.html +++ b/dev/wikipedia.org/index.html @@ -597,5 +597,7 @@ <div style="text-align:center"><span id="discovery-feedback-survey" style="display:none"><a href="//wikimedia.qualtrics.com/jfe/form/SV_3rZFiKhpb2JzWbb">Give Us Feedback</a> | </span><a href="//wikimediafoundation.org/wiki/Terms_of_Use">Terms of Use</a> | <a href="//wikimediafoundation.org/wiki/Privacy_policy">Privacy Policy</a></div> <script>if (Math.floor( Math.random() * 5 ) === 0) { document.getElementById( "discovery-feedback-survey" ).style.display="inline"; }</script> <script src="assets/js/wm-portal.js"></script> +<script src="assets/js/event-logging-lite.js"></script> +<script src="assets/js/wikimedia-org-event-logging.js"></script> </body> </html> -- To view, visit https://gerrit.wikimedia.org/r/251091 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ia52a523723dfbdd3db79a52e72e606c58351bd27 Gerrit-PatchSet: 1 Gerrit-Project: wikimedia/portals Gerrit-Branch: master Gerrit-Owner: Jdrewniak <[email protected]> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
