AndyRussG has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/391208 )

Change subject: Add API to delay call to record impression
......................................................................

Add API to delay call to record impression

Change-Id: Ida5e777d435d157cfcf6ead7728b35b6beb330b2
Bug: T176334
---
M resources/subscribing/ext.centralNotice.display.js
M tests/qunit/subscribing/ext.centralNotice.display.tests.js
2 files changed, 264 insertions(+), 6 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/CentralNotice 
refs/changes/08/391208/1

diff --git a/resources/subscribing/ext.centralNotice.display.js 
b/resources/subscribing/ext.centralNotice.display.js
index 959b50b..9ced44b 100644
--- a/resources/subscribing/ext.centralNotice.display.js
+++ b/resources/subscribing/ext.centralNotice.display.js
@@ -39,8 +39,11 @@
                // For providing a jQuery.Promise to signal when a banner has 
loaded
                bannerLoadedDeferredObj,
 
-               // Name of a requested banner; see cn.requestBanner(), below
-               requestedBannerName = null;
+               // Name of a requested banner; see cn.requestBanner(), below.
+               requestedBannerName = null,
+
+               // Maximum time to delay the record impression call, in 
milliseconds
+               MAX_RECORD_IMPRESSION_DELAY = 250;
 
        // TODO: make data.result options explicit via constants
 
@@ -199,7 +202,44 @@
                        .prepend( bannerHtml );
        }
 
+       /**
+        * Adds reallyRecordImpression() as the last handler for 
cn.recordImpressionDeferredObj,
+        * then resolves.
+        */
+       function resolveRecordImpressionDeferred() {
+               cn.recordImpressionDeferredObj.done( reallyRecordImpression );
+               cn.recordImpressionDeferredObj.resolve();
+       }
+
        function recordImpression() {
+               var timeout,
+                       timeoutHasRun = false;
+
+               if ( cn.recordImpressionDelayPromises.length === 0 ) {
+                       reallyRecordImpression();
+                       return;
+               }
+
+               // If there are promises in cn.recordImpressionDelayPromises, 
then
+               // cn.recordImpressionDeferredObj (used in 
resolveRecordImpressionDeferred())
+               // should already have been set.
+
+               timeout = setTimeout( function() {
+                       timeoutHasRun = true;
+                       resolveRecordImpressionDeferred();
+               }, MAX_RECORD_IMPRESSION_DELAY );
+
+               // This function can only run once, so checking that the 
timeout hasn't run yet
+               // should be sufficient to prevent extra record impression 
calls.
+               $.when.apply( $, cn.recordImpressionDelayPromises ).always( 
function () {
+                       if ( !timeoutHasRun ) {
+                               clearTimeout( timeout );
+                               resolveRecordImpressionDeferred();
+                       }
+               } );
+       }
+
+       function reallyRecordImpression() {
                var state = cn.internal.state,
                        url;
 
@@ -321,7 +361,7 @@
 
                } else {
                        // Otherwise, use a random number and banner weights to 
choose from among
-                       // banners available to the user in this campiagn, in 
this bucket. (Most
+                       // banners available to the user in this campaign, in 
this bucket. (Most
                        // of the time, there's only one.)
                        banner = chooser.chooseBanner(
                                campaign,
@@ -343,7 +383,7 @@
                        return;
                }
 
-               // Pass more info about following banner selection
+               // Pass more info following banner selection
                state.setBanner( banner );
 
                if ( cn.kvStore ) {
@@ -470,6 +510,21 @@
 
                        processAfterBannerFetch();
                },
+
+               /**
+                * Promises to delay the record impression call, if possible; 
see
+                * cn.requestRecordImpressionDelay(), below. Only exposed for 
use in tests.
+                *
+                * @private
+                */
+               recordImpressionDelayPromises: [],
+
+               /**
+                * For providing a jQuery.Promise to signal when the record 
impression call is
+                * about to be sent. (Value will be set to a new deferred 
object only as needed.)
+                * @private
+                */
+               recordImpressionDeferredObj: null,
 
                /**
                 * Attachment point for other objects in this module that are 
not meant
@@ -657,6 +712,24 @@
                },
 
                /**
+                * Request that, if possible, the record impression call be 
delayed until a
+                * promise is resolved. If the promise does not resolve before
+                * MAX_RECORD_IMPRESSION_DELAY milliseconds after the banner is 
injected,
+                * the call will be made in any case.
+                *
+                * Returns another promise that will resolve immediately before 
the record
+                * impression call is made.
+                *
+                * @param {jquery.Promise} promise
+                * @returns {jquery.Promise}
+                */
+               requestRecordImpressionDelay: function ( promise ) {
+                       cn.recordImpressionDelayPromises.push( promise );
+                       cn.recordImpressionDeferredObj = 
cn.recordImpressionDeferredObj || $.Deferred();
+                       return cn.recordImpressionDeferredObj.promise();
+               },
+
+               /**
                 * Get the value of a property used in campaign/banner 
selection and
                 * display, and for recording the results of that process.
                 */
diff --git a/tests/qunit/subscribing/ext.centralNotice.display.tests.js 
b/tests/qunit/subscribing/ext.centralNotice.display.tests.js
index f0a858c..03e288e 100644
--- a/tests/qunit/subscribing/ext.centralNotice.display.tests.js
+++ b/tests/qunit/subscribing/ext.centralNotice.display.tests.js
@@ -6,6 +6,7 @@
                realGeoIP = mw.geoIP,
                realBucketCookie = $.cookie( 'CN' ),
                realHideCookie = $.cookie( 'centralnotice_hide_fundraising' ),
+               realSendBeacon = navigator.sendBeacon,
                bannerData = {
                        bannerName: 'test_banner',
                        campaign: 'test_campaign',
@@ -154,10 +155,16 @@
                                        return deferred.promise();
                                }
                        };
+
+                       // Reset record impression deferred object and array of 
promises for delaying
+                       // record impression call
+                       mw.centralNotice.recordImpressionDeferredObj = null;
+                       mw.centralNotice.recordImpressionDelayPromises = [];
                },
                teardown: function () {
                        $.ajax = realAjax;
                        mw.geoIP = realGeoIP;
+                       navigator.sendBeacon = realSendBeacon;
                        $.cookie( 'centralnotice_hide_fundraising', 
realHideCookie, { path: '/' } );
                        $.cookie( 'CN', realBucketCookie, { path: '/' } );
                        mw.centralNotice.internal.state.data = {};
@@ -177,6 +184,184 @@
                assert.equal( $( 'div#test_banner' ).length, 1 );
        } );
 
+       /**
+        * Create the required state in CN for the record impression call to 
occur. The first
+        * campaign in choiceData2Campaigns will be chosen.
+        */
+       function setupForRecordImpressionCall() {
+
+               // Request 100% sample rate for record impression
+               
mw.centralNotice.internal.state.urlParams.recordImpressionSampleRate = 1;
+
+               // Use the mock choice data with two campaigns, and force the 
first campaign
+               mw.centralNotice.choiceData = choiceData2Campaigns;
+               mw.centralNotice.internal.state.urlParams.randomcampaign = 0.25;
+               mw.centralNotice.chooseAndMaybeDisplay();
+       }
+
+       QUnit.test( 'call record impression', function ( assert ) {
+               assert.expect( 2 );
+
+               // Mock navigator.sendBeacon to capture calls and check data 
points sent
+               navigator.sendBeacon = function ( urlString ) {
+
+                       // mediawiki.Uri is already a dependency of 
ext.centralNotice.display
+                       var url = new mw.Uri( urlString );
+                       assert.strictEqual( url.query.campaign, 'campaign1', 
'record impression campaign' );
+                       assert.strictEqual( url.query.banner, 'banner1', 
'record impression banner' );
+               };
+
+               setupForRecordImpressionCall();
+
+               // Call reallyInsertBanner() instead of insertBanner() to avoid 
the async
+               // DOM-waiting of the latter. This triggers the call to record 
impression.
+               mw.centralNotice.reallyInsertBanner( bannerData );
+       } );
+
+       QUnit.test( 'delay record impression call and register tests', function 
( assert ) {
+               var deferred = $.Deferred(),
+                       recordImpresionPromise,
+                       signalTestDone = assert.async();
+
+               assert.expect( 3 );
+
+               // Mock navigator.sendBeacon to check record impression doesn't 
fire early
+               navigator.sendBeacon = function () {
+                       // If we get here, it's a failure
+                       assert.ok( false, 'record impression waits, as 
requested' );
+               };
+
+               setupForRecordImpressionCall();
+
+               // Request a delay and capture the promise that should run 
right before the
+               // record impression call
+               recordImpresionPromise =
+                       mw.centralNotice.requestRecordImpressionDelay( 
deferred.promise() );
+
+               recordImpresionPromise.done( function () {
+                       mw.centralNotice.registerTest( 'test_test' );
+               } );
+
+               // Call reallyInsertBanner() instead of insertBanner() to avoid 
the async
+               // DOM-waiting of the latter. This doesn't trigger the call to 
record impression
+               // yet because we requested a delay.
+               mw.centralNotice.reallyInsertBanner( bannerData );
+
+               // Mock navigator.sendBeacon to capture calls and check data 
points sent
+               navigator.sendBeacon = function ( urlString ) {
+                       var url = new mw.Uri( urlString );
+                       assert.strictEqual( url.query.campaign, 'campaign1', 
'record impression campaign' );
+                       assert.strictEqual( url.query.banner, 'banner1', 
'record impression banner' );
+
+                       assert.strictEqual(
+                               url.query.testIdentifiers,
+                               'test_test',
+                               'record impression test identifier'
+                       );
+
+                       signalTestDone();
+               };
+
+               // Resolve the promise to let the record impression call go 
ahead
+               deferred.resolve();
+       } );
+
+       QUnit.test( 'record impression timeout and register tests', function ( 
assert ) {
+               var recordImpresionPromise,
+                       start = new Date().getTime(),
+                       MAX_RECORD_IMPRESSION_DELAY = 250, // Coordinate with 
ext.centralnotice.display.js
+                       signalTestDone = assert.async();
+
+               assert.expect( 4 );
+               setupForRecordImpressionCall();
+
+               // Request a delay and capture the promise that should run 
right before the
+               // record impression call
+               recordImpresionPromise =
+                       mw.centralNotice.requestRecordImpressionDelay( 
$.Deferred().promise() );
+
+               recordImpresionPromise.done( function () {
+                       mw.centralNotice.registerTest( 'test_timeouted_test' );
+               } );
+
+               // Mock navigator.sendBeacon to capture calls and check time 
and data points sent
+               navigator.sendBeacon = function ( urlString ) {
+                       var url = new mw.Uri( urlString ),
+                               delay = new Date().getTime() - start;
+
+                       assert.strictEqual( url.query.campaign, 'campaign1', 
'record impression campaign' );
+                       assert.strictEqual( url.query.banner, 'banner1', 
'record impression banner' );
+
+                       // 50 ms leewway is bit arbitrary
+                       assert.ok(
+                               ( delay > MAX_RECORD_IMPRESSION_DELAY - 50 ) &&
+                               ( delay < MAX_RECORD_IMPRESSION_DELAY + 50 ),
+                               'record impression called by timeout'
+                       );
+
+                       assert.strictEqual(
+                               url.query.testIdentifiers,
+                               'test_timeouted_test',
+                               'record impression test identifier'
+                       );
+
+                       signalTestDone();
+               };
+
+               // Call reallyInsertBanner() instead of insertBanner() to avoid 
the async
+               // DOM-waiting of the latter. This starts the record impression 
timeout.
+               mw.centralNotice.reallyInsertBanner( bannerData );
+
+               // We don't resolve the promise to delay record impression. The 
call should
+               // be made by MAX_RECORD_IMPRESSION_DELAY milliseconds (give or 
take a bit).
+       } );
+
+       QUnit.test( 'record impression called only once', function ( assert ) {
+               var deferred = $.Deferred(),
+                       MAX_RECORD_IMPRESSION_DELAY = 250, // Coordinate with 
ext.centralnotice.display.js
+                       recordImpresionPromise,
+                       signalTestDone = assert.async();
+
+               assert.expect( 2 );
+               setupForRecordImpressionCall();
+
+               // Request a delay and capture the promise that should run 
right before the
+               // record impression call
+               recordImpresionPromise =
+                       mw.centralNotice.requestRecordImpressionDelay( 
deferred.promise() );
+
+               // Mock navigator.sendBeacon to capture calls
+               navigator.sendBeacon = function () {
+                       assert.ok( true, 'record impression called once' );
+
+                       // Re-mock to fail test if called again
+                       navigator.sendBeacon = function () {
+                               assert.ok( false, 'record impression not called 
twice' );
+                       };
+               };
+
+               // Call reallyInsertBanner() instead of insertBanner() to avoid 
the async
+               // DOM-waiting of the latter. This starts the record impression 
timeout.
+               mw.centralNotice.reallyInsertBanner( bannerData );
+
+               // Set another timeout to resolve the promise requesting a 
delay after the timeout
+               // to call record impression has run. By starting this timeout 
after the record
+               // impression timeout (above) and making it a bit longer, we 
ensure it runs second.
+               setTimeout( function () {
+                       assert.equal(
+                               recordImpresionPromise.state(),
+                               'resolved',
+                               'record impression call was from timeout'
+                       );
+
+                       // Resolve the promise that delays the call only up 
until the timeout, to
+                       // test that record impression isn't called a second 
time.
+                       deferred.resolve();
+               }, MAX_RECORD_IMPRESSION_DELAY + 1 );
+
+               setTimeout( signalTestDone, MAX_RECORD_IMPRESSION_DELAY + 2 );
+       } );
+
        QUnit.test( 'banner= override param', function ( assert ) {
                assert.expect( 2 );
                mw.centralNotice.internal.state.urlParams.banner = 
'test_banner';
@@ -193,7 +378,7 @@
                assert.expect( 2 );
                mw.centralNotice.choiceData = choiceData2Campaigns;
 
-               // Get the first banner
+               // Get the first campaign
                mw.centralNotice.internal.state.urlParams.randomcampaign = 0.25;
 
                $.ajax = function ( params ) {
@@ -202,7 +387,7 @@
                };
                mw.centralNotice.chooseAndMaybeDisplay();
 
-               // Get the second banner
+               // Get the second campaign
                mw.centralNotice.internal.state.urlParams.randomcampaign = 0.75;
 
                $.ajax = function ( params ) {

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ida5e777d435d157cfcf6ead7728b35b6beb330b2
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/CentralNotice
Gerrit-Branch: wmf_deploy
Gerrit-Owner: AndyRussG <andrew.green...@gmail.com>

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

Reply via email to