jenkins-bot has submitted this change and it was merged.

Change subject: Refactor search satisfaction to allow reusing code for 
autocomplete satisfaction
......................................................................


Refactor search satisfaction to allow reusing code for autocomplete satisfaction

Change-Id: I1cc59ddcae4ae65c962860caaca5bc00026eeedf
---
M modules/ext.wikimediaEvents.searchSatisfaction.js
1 file changed, 265 insertions(+), 205 deletions(-)

Approvals:
  MaxSem: Looks good to me, approved
  JGirault: Looks good to me, but someone else must approve
  jenkins-bot: Verified



diff --git a/modules/ext.wikimediaEvents.searchSatisfaction.js 
b/modules/ext.wikimediaEvents.searchSatisfaction.js
index 35e3ba3..b8575db 100644
--- a/modules/ext.wikimediaEvents.searchSatisfaction.js
+++ b/modules/ext.wikimediaEvents.searchSatisfaction.js
@@ -19,241 +19,301 @@
  * @author Erik Bernhardson <ebernhard...@wikimedia.org>
  */
 ( function ( mw, $, undefined ) {
-
+       'use strict';
        // reject mobile users
-       if ( mw.config.get( 'wgMFMode' ) !== null ) {
+       if ( mw.config.get( 'skin' ) === 'minerva' ) {
                return;
        }
 
-       var isSearchResultPage = mw.config.get( 'wgIsSearchResultPage' ),
+       var search, session,
+               isSearchResultPage = mw.config.get( 'wgIsSearchResultPage' ),
                uri = new mw.Uri( location.href ),
-       // wprov attached to all search result links. If available
-       // indicates user got here directly from Special:Search
-               wprovPrefix = 'srpw1_',
-       // srpw1 has the position (including offset) of the search
-       // result appended.
-               searchResultPosition = parseInt( uri.query.wprov &&
-                       uri.query.wprov.substr( 0, wprovPrefix.length ) === 
wprovPrefix &&
-                       uri.query.wprov.substr( wprovPrefix.length ), 10 ),
-               cameFromSearchResult = !isNaN( searchResultPosition ),
+               checkinTimes = [ 10, 20, 30, 40, 50, 60, 90, 120, 150, 180, 
210, 240, 300, 360, 420 ],
                lastScrollTop = 0,
-               storageNamespace = 'wmE-sS-';
+               articleId = mw.config.get( 'wgArticleId' )
+               ;
+
+       function extractResultPosition( uri, wprovPrefix ) {
+               return parseInt( uri.query.wprov &&
+                       uri.query.wprov.substr( 0, wprovPrefix.length ) === 
wprovPrefix &&
+                       uri.query.wprov.substr( wprovPrefix.length ), 10 );
+       }
+
+       function initFromWprov( wprovPrefix ) {
+               var res = {
+                       wprovPrefix: wprovPrefix,
+                       resultPosition: extractResultPosition( uri, wprovPrefix 
)
+               };
+               res.cameFromSearch = !isNaN( res.resultPosition );
+               return res;
+       }
+
+       search = initFromWprov( 'srpw1_' );
+
+       // Cleanup the location bar in supported browsers.
+       if ( uri.query.wprov && window.history.replaceState ) {
+               delete uri.query.wprov;
+               window.history.replaceState( {}, '', uri.toString() );
+       }
+
+       function SessionState() {
+               // currently loaded state
+               var state = {},
+                       storageNamespace = 'wmE-sS-',
+               // persistent state keys that have a lifetime
+                       ttl = {
+                               sessionId: 10 * 60 * 1000,
+                               token: 24 * 60 * 60 * 1000
+                       },
+                       now = new Date().getTime();
+
+               /**
+                * Generates a cache key specific to this session and key type.
+                *
+                * @param {string} type
+                * @return {string}
+                */
+               function key( type ) {
+                       return storageNamespace + '-' + type;
+               }
+
+               /**
+                * Generate a unique token. Appends timestamp in base 36 to 
increase
+                * uniqueness of the token.
+                *
+                * @return {string}
+                */
+               function randomToken() {
+                       return mw.user.generateRandomSessionId() + new 
Date().getTime().toString( 36 );
+               }
+
+               /**
+                * Initializes the session.
+                *
+                * @param {SessionState} session
+                * @private
+                */
+               function initialize( session ) {
+
+                       var sessionId = session.get( 'sessionId' ),
+                               /**
+                                * Determines whether the user is part of the 
population size.
+                                *
+                                * @param {number} populationSize
+                                * @return {boolean}
+                                * @private
+                                */
+                               oneIn = function ( populationSize ) {
+                                       var rand = 
mw.user.generateRandomSessionId(),
+                                       // take the first 52 bits of the rand 
value
+                                               parsed = parseInt( rand.slice( 
0, 13 ), 16 );
+                                       return parsed % populationSize === 0;
+                               };
+
+                       if ( sessionId === 'rejected' ) {
+                               // User was previously rejected
+                               return false;
+                       }
+                       // If a sessionId exists the user was previously 
accepted into the test
+                       if ( !sessionId ) {
+                               if ( !oneIn( 200 ) ) {
+                                       // user was not chosen in a sampling of 
search results
+                                       session.set( 'sessionId', 'rejected' );
+                                       return false;
+                               }
+                               // User was chosen to participate in the test 
and does not yet
+                               // have a search session id, generate one.
+                               if ( !session.set( 'sessionId', randomToken() ) 
) {
+                                       return false;
+                               }
+                       }
+
+                       if ( session.get( 'token' ) === null ) {
+                               if ( !session.set( 'token', randomToken() ) ) {
+                                       return false;
+                               }
+                       } else {
+                               session.refresh( 'token' );
+                       }
+
+                       // Unique token per page load to know which events 
occured
+                       // within the exact same page.
+                       session.set( 'pageViewId', randomToken() );
+
+                       return true;
+               }
+
+               this.get = function ( type ) {
+                       if ( !state.hasOwnProperty( type ) ) {
+                               if ( ttl.hasOwnProperty( type ) ) {
+                                       var endTime = parseInt( mw.storage.get( 
key( type + 'EndTime' ) ), 10 );
+                                       if ( endTime && endTime > now ) {
+                                               state[ type ] = mw.storage.get( 
key( type ) );
+                                       } else {
+                                               mw.storage.remove( key( type ) 
);
+                                               mw.storage.remove( key( type + 
'EndTime' ) );
+                                               state[ type ] = null;
+                                       }
+                               } else {
+                                       state[ type ] = null;
+                               }
+                       }
+                       return state[ type ];
+               };
+
+               this.set = function ( type, value ) {
+                       if ( ttl.hasOwnProperty( type ) ) {
+                               if ( !mw.storage.set( key( type + 'EndTime' ), 
now + ttl[ type ] ) ) {
+                                       return false;
+                               }
+                               if ( !mw.storage.set( key( type ), value ) ) {
+                                       mw.storage.remove( key( type + 
'EndTime' ) );
+                                       return false;
+                               }
+                       }
+                       state[ type ] = value;
+                       return true;
+               };
+
+               this.refresh = function ( type ) {
+                       if ( ttl.hasOwnProperty( type ) ) {
+                               mw.storage.set( key( type + 'EndTime' ), now + 
ttl[ type ] );
+                       }
+               };
+
+               state.enabled = initialize( this );
+
+               return this;
+       }
 
        /**
-        * Initializes the test.
+        * Adds an attribute to the link to track the offset
+        * of the result in the SERP.
         *
-        * @return {boolean} `true` if the user is selected for the test, 
`false`
-        *  otherwise.
+        * Expects to be run with an html anchor as `this`.
+        *
+        * @param {Event} evt jQuery Event object
         * @private
         */
-       function initializeTest() {
-
-               var sessionStartTime = mw.storage.get( storageNamespace + 
'sessionStartTime' ),
-                       tokenStartTime = mw.storage.get( storageNamespace + 
'tokenStartTime' ),
-                       now = new Date().getTime(),
-                       maxSessionLifetimeMs = 10 * 60 * 1000,
-                       maxTokenLifetimeMs = 24 * 60 * 60 * 1000,
-                       searchSessionId,
-                       searchToken,
-                       isSessionValid,
-                       isTokenValid,
-                       /**
-                        * Determines whether the user is part of the 
population size.
-                        *
-                        * @param {number} populationSize
-                        * @return {boolean}
-                        * @private
-                        */
-                       oneIn = function ( populationSize ) {
-                               var rand = mw.user.generateRandomSessionId(),
-                               // take the first 52 bits of the rand value
-                                       parsed = parseInt( rand.slice( 0, 13 ), 
16 );
-                               return parsed % populationSize === 0;
-                       };
-
-               // Retrieving values from cache only if they are still valid.
-               isSessionValid = sessionStartTime && ( now - parseInt( 
sessionStartTime, 10 ) ) < maxSessionLifetimeMs;
-               if ( isSessionValid ) {
-                       searchSessionId = mw.storage.get( storageNamespace + 
'sessionId' );
+       function updateSearchHref( evt ) {
+               var uri = new mw.Uri( evt.target.href ),
+                       offset = $( evt.target ).data( 'serp-pos' );
+               if ( offset ) {
+                       uri.query.wprov = evt.data.wprovPrefix + offset;
+                       evt.target.href = uri.toString();
                }
-               isTokenValid = tokenStartTime && ( now - parseInt( 
tokenStartTime, 10 ) ) < maxTokenLifetimeMs;
-               if ( isTokenValid ) {
-                       searchToken = mw.storage.get( storageNamespace + 
'token' );
-               }
-
-               if ( searchSessionId === 'rejected' ) {
-                       // User was previously rejected
-                       return false;
-               }
-
-               if ( searchSessionId ) {
-                       // User was previously chosen to participate in the 
test.
-                       // When a new search is performed reset the session 
lifetime.
-                       if ( isSearchResultPage ) {
-                               mw.storage.set( storageNamespace + 
'sessionStartTime', now );
-                       }
-               } else if ( !oneIn( 200 ) ) {
-                       // user was not chosen in a sampling of search results
-                       mw.storage.set( storageNamespace + 'sessionId', 
'rejected' );
-                       mw.storage.set( storageNamespace + 'sessionStartTime', 
now + maxSessionLifetimeMs );
-                       return false;
-               } else {
-                       // User was chosen to participate in the test and does 
not yet
-                       // have a search session id, generate one.
-                       searchSessionId = mw.user.generateRandomSessionId();
-                       // If storage is full we can't reliably correlate 
events from the SERP to the target
-                       // pages.
-                       if ( !mw.storage.set( storageNamespace + 'sessionId', 
searchSessionId ) || !mw.storage.set( storageNamespace + 'sessionStartTime', 
now )
-                       ) {
-                               return false;
-                       }
-               }
-
-               if ( !searchToken ) {
-                       searchToken = mw.user.generateRandomSessionId();
-                       if ( !mw.storage.set( storageNamespace + 'token', 
searchToken ) || !mw.storage.set( storageNamespace + 'tokenStartTime', now )
-                       ) {
-                               return false;
-                       }
-               }
-
-               return true;
        }
 
        /**
-        * Sets up the test.
+        * Executes an action at the given times.
         *
-        * This is assuming the user passed {@link #initializeTest}.
+        * @param {number[]} checkinTimes Times (in seconds from start) when the
+        *  action should be executed.
+        * @param {Function} fn The action to execute.
+        * @private
+        */
+       function interval( checkinTimes, fn ) {
+               var checkin = checkinTimes.shift(),
+                       timeout = checkin;
+
+               function action() {
+                       var current = checkin;
+                       fn( current );
+
+                       checkin = checkinTimes.shift();
+                       if ( checkin ) {
+                               timeout = checkin - current;
+                               setTimeout( action, 1000 * timeout );
+                       }
+               }
+
+               setTimeout( action, 1000 * timeout );
+       }
+
+       function genLogEventFn( session ) {
+               return function ( action, extraData ) {
+                       var scrollTop = $( window ).scrollTop(),
+                               evt = {
+                                       // searchResultPage, visitPage or 
checkin
+                                       action: action,
+                                       // identifies a single user performing 
searches within
+                                       // a limited time span.
+                                       searchSessionId: session.get( 
'sessionId' ),
+                                       // used to correlate actions that 
happen on the same
+                                       // page. Otherwise a user opening 
multiple search results
+                                       // in tabs would make their events 
overlap and the dwell
+                                       // time per page uncertain.
+                                       pageViewId: session.get( 'pageViewId' ),
+                                       // identifies if a user has scrolled 
the page since the
+                                       // last event
+                                       scroll: scrollTop !== lastScrollTop,
+                                       // identifies a single user over a 24 
hour timespan,
+                                       // allowing to tie together multiple 
search sessions
+                                       searchToken: session.get( 'token' )
+                               };
+
+                       lastScrollTop = scrollTop;
+
+                       if ( articleId > 0 ) {
+                               evt.articleId = articleId;
+                       }
+
+                       // add any schema specific data
+                       if ( extraData ) {
+                               $.extend( evt, extraData );
+                       }
+
+                       // ship the event
+                       mw.loader.using( [ 'schema.TestSearchSatisfaction2' ] 
).then( function () {
+                               mw.eventLog.logEvent( 
'TestSearchSatisfaction2', evt );
+                       } );
+               };
+       }
+
+       /**
+        * Sets up the full text search test.
+        *
         * It will log events and will put an attribute on some links
         * to track user satisfaction.
+        *
+        * @param {SessionState} session
         */
-       function setupTest() {
-
-               var checkinTimes = [ 10, 20, 30, 40, 50, 60, 90, 120, 150, 180, 
210, 240, 300, 360, 420 ],
-                       articleId = mw.config.get( 'wgArticleId' ),
-                       searchSessionId = mw.storage.get( storageNamespace + 
'sessionId' ),
-                       pageViewId = mw.user.generateRandomSessionId(),
-                       searchToken = mw.storage.get( storageNamespace + 
'token' ),
-                       logEvent = function ( action, checkinTime ) {
-                               var scrollTop = $( window ).scrollTop(),
-                                       evt = {
-                                               // searchResultPage, visitPage 
or checkin
-                                               action: action,
-                                               // identifies a single user 
performing searches within
-                                               // a limited time span.
-                                               searchSessionId: 
searchSessionId,
-                                               // identifies a single user 
over a 24 hour timespan,
-                                               // allowing to tie together 
multiple search sessions
-                                               searchToken: searchToken,
-                                               // used to correlate actions 
that happen on the same
-                                               // page. Otherwise a user 
opening multiple search results
-                                               // in tabs would make their 
events overlap and the dwell
-                                               // time per page uncertain.
-                                               pageViewId: pageViewId,
-                                               // identifies if a user has 
scrolled the page since the
-                                               // last event
-                                               scroll: scrollTop !== 
lastScrollTop
-                                       };
-                               lastScrollTop = scrollTop;
-                               if ( checkinTime !== undefined ) {
-                                       // identifies how long the user has 
been on this page
-                                       evt.checkin = checkinTime;
-                               }
-                               if ( isSearchResultPage ) {
-                                       // the users actual search term
-                                       evt.query = mw.config.get( 'searchTerm' 
);
-                                       // the number of results shown on this 
page.
-                                       evt.hitsReturned = $( 
'.mw-search-result-heading' ).length;
-                               }
-                               if ( articleId > 0 ) {
-                                       evt.articleId = articleId;
-                               }
-                               if ( cameFromSearchResult ) {
-                                       // this is only available on article 
pages linked
-                                       // directly from a search result.
-                                       evt.position = searchResultPosition;
-                               }
-                               mw.loader.using( [ 
'schema.TestSearchSatisfaction2' ] ).then( function () {
-                                       mw.eventLog.logEvent( 
'TestSearchSatisfaction2', evt );
-                               } );
-                       },
-                       /**
-                        * Adds an attribute to the link to track the offset
-                        * of the result in the SERP.
-                        *
-                        * Expects to be run with an html anchor as `this`.
-                        *
-                        * @private
-                        */
-                       updateSearchHref = function () {
-                               var uri = new mw.Uri( this.href ),
-                                       offset = $( this ).data( 'serp-pos' );
-                               if ( offset ) {
-                                       uri.query.wprov = 'srpw1_' + offset;
-                                       this.href = uri.toString();
-                               }
-                       },
-                       /**
-                        * Executes an action at the given times.
-                        *
-                        * @param {number[]} checkinTimes Times (in seconds 
from start) when the
-                        *  action should be executed.
-                        * @param {Function} fn The action to execute.
-                        * @private
-                        */
-                       interval = function ( checkinTimes, fn ) {
-                               var checkin = checkinTimes.shift(),
-                                       timeout = checkin;
-
-                               function action() {
-                                       var current = checkin;
-                                       fn( current );
-
-                                       checkin = checkinTimes.shift();
-                                       if ( checkin ) {
-                                               timeout = checkin - current;
-                                               setTimeout( action, 1000 * 
timeout );
-                                       }
-                               }
-
-                               setTimeout( action, 1000 * timeout );
-                       };
+       function setupSearchTest( session ) {
+               var logEvent = genLogEventFn( session );
 
                if ( isSearchResultPage ) {
-                       $( '#mw-content-text' ).on( 'click', 
'.mw-search-result-heading a', updateSearchHref );
-                       logEvent( 'searchResultPage' );
-               } else if ( cameFromSearchResult ) {
-                       logEvent( 'visitPage' );
-                       interval( checkinTimes, function ( checkin ) {
-                               logEvent( 'checkin', checkin );
-                       } );
-               }
-       }
+                       // When a new search is performed reset the session 
lifetime.
+                       session.refresh( 'sessionId' );
 
-       /**
-        * Cleanup the location bar in supported browsers.
-        */
-       function cleanupHistoryState() {
-               if ( window.history.replaceState ) {
-                       delete uri.query.wprov;
-                       window.history.replaceState( {}, '', uri.toString() );
+                       $( '#mw-content-text' ).on(
+                               'click',
+                               '.mw-search-result-heading a',
+                               { wprovPrefix: search.wprovPrefix },
+                               updateSearchHref
+                       );
+                       logEvent( 'searchResultPage', {
+                               query: mw.config.get( 'searchTerm' ),
+                               hitsReturned: $( '.mw-search-result-heading' 
).length
+                       } );
+               } else if ( search.cameFromSearch ) {
+                       logEvent( 'visitPage', {
+                               position: search.resultPosition
+                       } );
+                       interval( checkinTimes, function ( checkin ) {
+                               logEvent( 'checkin', { checkin: checkin } );
+                       } );
                }
        }
 
        /**
         * Logic starts here.
         */
-       if ( isSearchResultPage || cameFromSearchResult ) {
-
-               if ( cameFromSearchResult ) {
-                       cleanupHistoryState();
-               }
-
+       if ( isSearchResultPage || search.cameFromSearch ) {
                $( document ).ready( function () {
-                       if ( !initializeTest() ) {
-                               return;
+                       session = new SessionState();
+                       if ( session.get( 'enabled' ) ) {
+                               setupSearchTest( session );
                        }
-                       setupTest();
                } );
        }
 

-- 
To view, visit https://gerrit.wikimedia.org/r/269879
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I1cc59ddcae4ae65c962860caaca5bc00026eeedf
Gerrit-PatchSet: 8
Gerrit-Project: mediawiki/extensions/WikimediaEvents
Gerrit-Branch: master
Gerrit-Owner: EBernhardson <ebernhard...@wikimedia.org>
Gerrit-Reviewer: EBernhardson <ebernhard...@wikimedia.org>
Gerrit-Reviewer: JGirault <jgira...@wikimedia.org>
Gerrit-Reviewer: Jdrewniak <jdrewn...@wikimedia.org>
Gerrit-Reviewer: MaxSem <maxsem.w...@gmail.com>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to