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 <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits