jenkins-bot has submitted this change and it was merged.
Change subject: Choose banner on client
......................................................................
Choose banner on client
This is part of a series of changes to shift banner selection partly
to the client. This change only modifies CN functionality when
serving banners if $wgCentralNoticeChooseBannerOnClient is set to
true.
Introduces a new global:
$wgCentralSelectedBannerDispatcher: URL for fetching a banner that
is chosen on the client. This is the new banner-choosing
mechanism's version of $wgCentralBannerDispatcher.
Change-Id: I04e2d4bfdd43529a83d32998e20d4bd885b9033d
---
M CentralNotice.hooks.php
M CentralNotice.php
M modules/ext.centralNotice.bannerController/bannerController.js
M modules/ext.centralNotice.bannerController/bannerController.lib.js
M special/SpecialBannerLoader.php
M special/SpecialCentralNotice.php
6 files changed, 342 insertions(+), 29 deletions(-)
Approvals:
Awight: Looks good to me, approved
jenkins-bot: Verified
diff --git a/CentralNotice.hooks.php b/CentralNotice.hooks.php
index 30667ac..9bef6f9 100644
--- a/CentralNotice.hooks.php
+++ b/CentralNotice.hooks.php
@@ -309,13 +309,17 @@
$wgNoticeInfrastructure, $wgNoticeCloseButton,
$wgCentralBannerDispatcher,
$wgCentralBannerRecorder, $wgNoticeNumberOfBuckets,
$wgNoticeBucketExpiry,
$wgNoticeNumberOfControllerBuckets,
$wgNoticeCookieDurations, $wgScript,
- $wgNoticeHideUrls, $wgNoticeOldCookieApocalypse;
+ $wgNoticeHideUrls, $wgNoticeOldCookieApocalypse,
+ $wgCentralNoticeChooseBannerOnClient,
$wgCentralSelectedBannerDispatcher;
// Making these calls too soon will causes issues with the namespace
localisation cache. This seems
// to be just right. We require them at all because MW will 302 page
requests made to non localised
// namespaces which results in wasteful extra calls.
if ( !$wgCentralBannerDispatcher ) {
$wgCentralBannerDispatcher = SpecialPage::getTitleFor(
'BannerRandom' )->getLocalUrl();
+ }
+ if ( !$wgCentralSelectedBannerDispatcher ) {
+ $wgCentralSelectedBannerDispatcher = SpecialPage::getTitleFor(
'BannerLoader' )->getLocalUrl();
}
if ( !$wgCentralBannerRecorder ) {
$wgCentralBannerRecorder = SpecialPage::getTitleFor(
'RecordImpression' )->getLocalUrl();
@@ -345,6 +349,8 @@
$vars[ 'wgNoticeCookieDurations' ] = $wgNoticeCookieDurations;
$vars[ 'wgNoticeHideUrls' ] = $wgNoticeHideUrls;
$vars[ 'wgNoticeOldCookieApocalypse' ] = $wgNoticeOldCookieApocalypse;
+ $vars[ 'wgCentralNoticeChooseBannerOnClient' ] =
$wgCentralNoticeChooseBannerOnClient;
+ $vars[ 'wgCentralSelectedBannerDispatcher' ] =
$wgCentralSelectedBannerDispatcher;
if ( $wgNoticeInfrastructure ) {
$vars[ 'wgNoticeCloseButton' ] = $wgNoticeCloseButton;
diff --git a/CentralNotice.php b/CentralNotice.php
index fb177e7..550e139 100644
--- a/CentralNotice.php
+++ b/CentralNotice.php
@@ -62,10 +62,8 @@
// The name of the database which hosts the centralized campaign data
$wgCentralDBname = false;
-// URL where BannerRandom is hosted, where FALSE will default to the
-// Special:BannerRandom on the machine serving ResourceLoader requests (meta
-// in the case of WMF). To use our reverse proxy, for example, set this
-// variable to 'http://banners.wikimedia.org/banner_load'.
+// URL where BannerRandom is hosted, where false will default to the
+// Special:BannerRandom on the machine serving ResourceLoader requests.
$wgCentralBannerDispatcher = false;
// URL which is hit after a banner is loaded, for compatibility with analytics.
@@ -98,6 +96,13 @@
// Enable the new mechanism for making the banner selection on the client
$wgCentralNoticeChooseBannerOnClient = false;
+// URL for BannerLoader, for requests to fetch a banner that is already
+// known (using the banner URL param). If false, it will default to
+// Special:BannerLoader on the machine serving ResourceLoader requests. This
+// value is used when for fetching banners that are chosen on the client (i.e.,
+// when $wgCentralNoticeChooseBannerOnClient is set to true).
+$wgCentralSelectedBannerDispatcher = false;
+
// Enable the loader itself
// Allows to control the loader visibility, without destroying infrastructure
// for cached content
diff --git a/modules/ext.centralNotice.bannerController/bannerController.js
b/modules/ext.centralNotice.bannerController/bannerController.js
index 8aca1f2..1069335 100644
--- a/modules/ext.centralNotice.bannerController/bannerController.js
+++ b/modules/ext.centralNotice.bannerController/bannerController.js
@@ -140,6 +140,8 @@
debug: mw.centralNotice.data.getVars.debug
};
+ // TODO use the new $wgCentralSelectedBannerDispatcher
here instead
+
$.ajax({
url: mw.config.get( 'wgCentralPagePath' ) + '?'
+ $.param( bannerPageQuery ),
dataType: 'script',
@@ -147,25 +149,69 @@
});
},
loadRandomBanner: function () {
- var RAND_MAX = 30;
- var bannerDispatchQuery = {
- uselang: mw.config.get( 'wgUserLanguage' ),
- sitename: mw.config.get( 'wgSiteName' ),
- project: mw.config.get( 'wgNoticeProject' ),
- anonymous: mw.config.get( 'wgUserName' ) ===
null,
- bucket: mw.centralNotice.data.bucket,
- country: mw.centralNotice.data.country,
- device: mw.centralNotice.data.device,
- slot: Math.floor( Math.random() * RAND_MAX ) +
1,
- debug: mw.centralNotice.data.getVars.debug
- };
- var scriptUrl = mw.config.get(
'wgCentralBannerDispatcher' ) + '?' + $.param( bannerDispatchQuery );
- $.ajax({
- url: scriptUrl,
- dataType: 'script',
- cache: true
- });
+ var fetchBannerQueryParams = {
+ uselang: mw.config.get(
'wgUserLanguage' ),
+ project: mw.config.get(
'wgNoticeProject' ),
+ anonymous: mw.config.get( 'wgUserName'
) === null,
+ bucket: mw.centralNotice.data.bucket,
+ country: mw.centralNotice.data.country,
+ device: mw.centralNotice.data.device,
+ debug:
mw.centralNotice.data.getVars.debug
+ },
+ scriptUrl;
+
+ // Check if we're configured to get choose banners on
the client,
+ // and do a few sanity checks.
+ if ( mw.config.get(
'wgCentralNoticeChooseBannerOnClient' ) &&
+ mw.cnBannerControllerLib &&
+ mw.cnBannerControllerLib.choiceData !== null ) {
+
+ // Filter choice data and calculate allocations
+ mw.cnBannerControllerLib.filterChoiceData();
+
mw.cnBannerControllerLib.calculateBannerAllocations();
+
+ // Get a random seed or use the random=
parameter from the URL,
+ // and choose the banner.
+ var random =
mw.centralNotice.data.getVars.random || Math.random();
+ mw.cnBannerControllerLib.chooseBanner( random );
+
+ // Only fetch a banner if we need to :)
+ if ( mw.centralNotice.data.banner ) {
+ fetchBannerQueryParams.banner =
mw.centralNotice.data.banner;
+ fetchBannerQueryParams.campaign =
mw.centralNotice.data.campaign;
+
+ scriptUrl = mw.config.get(
'wgCentralSelectedBannerDispatcher' ) +
+ '?' + $.param(
fetchBannerQueryParams );
+
+ // This will call insertBanner() after
the banner is retrieved
+ $.ajax( {
+ url: scriptUrl,
+ dataType: 'script',
+ cache: true
+ } );
+
+ } else {
+ // Call insertBanner to trigger a call
to
+ // Special:RecordImpression to register
the empty result.
+ // TODO Refactor and register that the
banner wasn't even
+ // fetched.
+ mw.centralNotice.insertBanner( false );
+ }
+
+ } else {
+ var RAND_MAX = 30;
+ fetchBannerQueryParams.slot = Math.floor(
Math.random() * RAND_MAX ) + 1;
+
+ scriptUrl = mw.config.get(
'wgCentralBannerDispatcher' ) +
+ '?' + $.param( fetchBannerQueryParams );
+
+ $.ajax( {
+ url: scriptUrl,
+ dataType: 'script',
+ cache: true
+ } );
+ }
},
// TODO: move function definitions once controller cache has
cleared
insertBanner: function( bannerJson ) {
diff --git a/modules/ext.centralNotice.bannerController/bannerController.lib.js
b/modules/ext.centralNotice.bannerController/bannerController.lib.js
index bd53bfb..8e7fcaa 100644
--- a/modules/ext.centralNotice.bannerController/bannerController.lib.js
+++ b/modules/ext.centralNotice.bannerController/bannerController.lib.js
@@ -8,11 +8,267 @@
* Set possible campaign and banner choices. Called by
* ext.centralNotice.bannerChoices.
*/
- 'setChoiceData': function ( choices ) {
+ setChoiceData: function ( choices ) {
this.choiceData = choices;
},
- 'choiceData': null
+ choiceData: null,
+
+ possibleBanners: null,
+
+ /**
+ * Filter the choice data and create a flat list of possible
banners
+ * to chose from. Add some additional data on to each banner
entry. The
+ * result is placed in possibleBanners.
+ *
+ * Note that if the same actual banner is assigned to more than
one
+ * campaign it can have more than one entry in that list.
That's the
+ * desired result; here "banners" would be more accurately
called
+ * "banner assignments".
+ *
+ * The procedure followed here closely resembles legacy PHP
code in
+ * BannerChooser and current PHP code in
BannerAllocationCalculator.
+ *
+ * FIXME Re-organize code in all those places to make it easier
to
+ * understand.
+ */
+ filterChoiceData: function() {
+
+ var i, campaign, j, banner;
+ this.possibleBanners = [];
+
+ for ( i = 0; i < this.choiceData.length; i++ ) {
+ campaign = this.choiceData[i];
+
+ // Filter for country if geotargetted
+ if ( campaign.geotargetted &&
+ ( $.inArray(
+ mw.centralNotice.data.country,
campaign.countries )
+ === -1 ) ) {
+
+ continue;
+ }
+
+ // Now filter by banner properties
+ for ( j = 0; j < campaign.banners.length; j++ )
{
+ banner = campaign.banners[j];
+
+ // Device
+ if ( $.inArray(
+ mw.centralNotice.data.device,
banner.devices ) === -1 ) {
+ continue;
+ }
+
+ // Bucket
+ if ( parseInt(
mw.centralNotice.data.bucket, 10) %
+ campaign.bucket_count !==
banner.bucket ) {
+ continue;
+ }
+
+ // Add in data about the campaign the
banner is part of.
+ // This will be used in the
calculateBannerAllocations(),
+ // the next step in choosing a banner.
+ banner.campaignName = campaign.name;
+ banner.campaignThrottle =
campaign.throttle;
+ banner.campaignZIndex =
campaign.preferred;
+
+ this.possibleBanners.push( banner );
+ }
+ }
+ },
+
+ /**
+ * Calculate the allocation of possible banners (i.e., the
required
+ * relative distribution among users that meet the same
criteria as
+ * this user). This calculation will be used to randomly select
a
+ * banner. This method operates on the values of
this.possibleBanners.
+ *
+ * The procedure followed here closely resembles legacy PHP
code in
+ * BannerChooser and current PHP code in
BannerAllocationCalculator.
+ *
+ * FIXME Re-organize code in all those places to make it easier
to
+ * understand.
+ */
+ calculateBannerAllocations: function() {
+
+ var campaignTotalWeights = [],
+ priorityTotalAllocations = [],
+ bannersByPriority = [],
+ priorities = [],
+ remainingAllocation = 1,
+ i, j,
+ banner, campaignName, campaignZIndex, priority,
+ bannersAtThisPriority,
totalAllocationAtThisPriority, scaling;
+
+ // Calculate the sum of the weight properties for
banners in each
+ // campaign. This will be used to calculate their
proportional
+ // weight within each campaign and normalize weights
across
+ // campaigns.
+ for ( i = 0; i < this.possibleBanners.length ; i ++ ) {
+
+ banner = this.possibleBanners[i];
+ campaignName = banner.campaignName;
+
+ if ( !campaignTotalWeights[campaignName] ) {
+ campaignTotalWeights[campaignName] = 0;
+ }
+
+ campaignTotalWeights[campaignName] +=
banner.weight;
+ }
+
+ // Calculate the normalized maximum allocation of each
banner
+ // within the campaign it's assigned to. First we
normalize the
+ // banner's weight, then scale down as necessary if the
campaign
+ // is throttled. This is the maximum allocation because
it may
+ // be scaled down further in subsequent steps.
+ for ( i = 0; i < this.possibleBanners.length ; i ++ ) {
+ banner = this.possibleBanners[i];
+ banner.maxAllocation =
+ ( banner.weight /
campaignTotalWeights[banner.campaignName] )
+ * ( banner.campaignThrottle / 100 );
+ }
+
+ // Make an index of banners by priority level, and find
the sum of
+ // all maximum allocations for each priority level.
+
+ // Note that here we are using a variety of terms for
the same thing.
+ // Priority level = priority = preferred (DB column) =
z-index
+ // (as copied in filterChoiceData()). This needs to be
fixed, but
+ // it's being left as-is for the transition to choosing
banners on
+ // the client, to make it easier to compare legacy and
new code.
+
+ // Note also that the actual values of priority levels
are integers,
+ // and higher integers represent higher priority. These
values are
+ // defined by class constants in the CentralNotice PHP
class.
+
+ for ( i = 0; i < this.possibleBanners.length ; i ++ ) {
+
+ banner = this.possibleBanners[i];
+ campaignZIndex = banner.campaignZIndex;
+
+ // Initialize index vars the first time we hit
this priority
+ // level/zIndex
+ if ( !bannersByPriority[campaignZIndex] ) {
+ bannersByPriority[campaignZIndex] = [];
+
priorityTotalAllocations[campaignZIndex] = 0;
+ }
+
+ bannersByPriority[campaignZIndex].push( banner
);
+ priorityTotalAllocations[campaignZIndex] +=
banner.maxAllocation;
+ }
+
+ // Dole out chunks of allocation to create the final
allocation
+ // values. Full allocation is 1 and no allocation is 0;
this is
+ // tracked by the remainingAllocation variable.
+
+ // First, make an array of priority levels and sort in
descending
+ // order.
+ for ( priority in bannersByPriority ) {
+ priorities.push( priority );
+ }
+ priorities.sort();
+ priorities.reverse();
+
+ // Now go through the priority levels from highest to
lowest. If
+ // campaigns are not throttled, then campaigns with a
higher
+ // priority level will eclipse all campaigns with lower
priority.
+ // Only if some campaigns are throttled will they allow
some space
+ // for campaigns at the next level down.
+
+ // Also note that since priority and throttling are set
at the
+ // campaign level, there will never be banners from a
single
+ // campaign at more than one priority level. (This is
important for
+ // the per-campaign normalizations performed above to
be useful
+ // here.)
+
+ for ( i = 0; i < priorities.length; i++ ) {
+
+ bannersAtThisPriority =
bannersByPriority[priorities[i]];
+ totalAllocationAtThisPriority =
+ priorityTotalAllocations[priorities[i]];
+
+ // If we fully allocated at a previous level,
set allocations
+ // at this level to zero. (We check with 0.01
instead of 0 in
+ // case of issues due to finite precision.)
+ if ( remainingAllocation <= 0.01 ) {
+ for ( j = 0; j <
bannersAtThisPriority.length; j++ ) {
+
bannersAtThisPriority[j].allocation = 0;
+ }
+ continue;
+ }
+
+ // If we are here, there is some allocation
remaining.
+
+ // First see if the total allocation for this
level is greater
+ // than the remaining allocation. This can
happen in two
+ // circumstances: (1) if there is more than one
campaign
+ // at this level, and (2) if we are using up
some remaining
+ // allocation left over from a higher priority
level. In both
+ // cases, we'll scale the allocation of banners
at this level to
+ // exactly fill the remaining allocation.
+ if ( totalAllocationAtThisPriority >
remainingAllocation ) {
+ scaling = remainingAllocation /
totalAllocationAtThisPriority;
+ remainingAllocation = 0;
+
+ } else {
+ // If we are here, it means that
whatever allocations there
+ // are at this level will either not
fully take up or will
+ // exactly take up the remaining
allocation. The former
+ // case means some campaigns are
throttled, so we don't want
+ // to scale up, but rather leave some
allocation tidbits for
+ // the next level. In the latter case,
also, no scaling
+ // is needed. So we set scaling to 1,
and take the chunk
+ // we are due from remainingAllocation.
+
+ scaling = 1;
+ remainingAllocation -=
totalAllocationAtThisPriority;
+ }
+
+ // Set the allocation property of all the
banners at this level,
+ // scaling the previously set maxAllocation
property as required.
+ for ( j = 0; j < bannersAtThisPriority.length;
j++ ) {
+ banner = bannersAtThisPriority[j];
+ banner.allocation =
banner.maxAllocation * scaling;
+ }
+ }
+ },
+
+ /**
+ * Choose a banner (or choose not to show one) as determined by
random
+ * and the allocations in this.possibleBanners. If a banner is
chosen,
+ * set the banner's name in mw.centralNotice.data.banner and the
+ * campaign it's associated with in
mw.centralNotice.data.campaign. If
+ * no banner is chosen, set mw.centralNotice.data.banner to
null.
+ *
+ * @param random float A random number, greater or equal to 0
and less
+ * than 1, to use in choosing a banner.
+ */
+ chooseBanner: function( random ) {
+ var blockStart = 0,
+ i, banner, blockEnd;
+
+ // Cycle through possible banners, calculating which
piece of
+ // the allocation pie they should get. When random is
in the piece,
+ // choose the banner. Note that the order of the
contents of
+ // possibleBanners is not guaranteed to be consistent
between
+ // requests, but that shouldn't matter.
+ for ( i = 0; i < this.possibleBanners.length; i ++ ) {
+ banner = this.possibleBanners[i];
+ blockEnd = blockStart + banner.allocation;
+
+ if ( ( random >= blockStart ) && ( random <
blockEnd ) ) {
+ mw.centralNotice.data.banner =
banner.name;
+ mw.centralNotice.data.campaign =
banner.campaignName;
+ return;
+ }
+
+ blockStart = blockEnd;
+ }
+
+ // We get here if there is less than full allocation
(including no
+ // allocation).
+ mw.centralNotice.data.banner = null;
+ }
};
} )( jQuery, mediaWiki );
\ No newline at end of file
diff --git a/special/SpecialBannerLoader.php b/special/SpecialBannerLoader.php
index 82cea9e..692a90b 100644
--- a/special/SpecialBannerLoader.php
+++ b/special/SpecialBannerLoader.php
@@ -42,11 +42,8 @@
$bucket = intval( $this->getSanitized( 'bucket',
ApiCentralNoticeAllocations::BUCKET_FILTER ) );
$device = $this->getSanitized( 'device',
ApiCentralNoticeAllocations::DEVICE_NAME_FILTER );
- $this->siteName = $request->getText( 'sitename' );
-
$required_values = array(
- $project, $language, $country, $anonymous, $bucket,
$device,
- $this->siteName,
+ $project, $language, $country, $anonymous, $bucket,
$device
);
foreach ( $required_values as $value ) {
if ( is_null( $value ) ) {
diff --git a/special/SpecialCentralNotice.php b/special/SpecialCentralNotice.php
index 3b8a2f7..893bacb 100644
--- a/special/SpecialCentralNotice.php
+++ b/special/SpecialCentralNotice.php
@@ -1,6 +1,9 @@
<?php
class CentralNotice extends SpecialPage {
+
+ // Note: These values are not arbitrary. Higher priority is indicated
by a
+ // higher value.
const LOW_PRIORITY = 0;
const NORMAL_PRIORITY = 1;
const HIGH_PRIORITY = 2;
--
To view, visit https://gerrit.wikimedia.org/r/173220
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I04e2d4bfdd43529a83d32998e20d4bd885b9033d
Gerrit-PatchSet: 15
Gerrit-Project: mediawiki/extensions/CentralNotice
Gerrit-Branch: master
Gerrit-Owner: AndyRussG <[email protected]>
Gerrit-Reviewer: AndyRussG <[email protected]>
Gerrit-Reviewer: Awight <[email protected]>
Gerrit-Reviewer: Ejegg <[email protected]>
Gerrit-Reviewer: Katie Horn <[email protected]>
Gerrit-Reviewer: Mwalker <[email protected]>
Gerrit-Reviewer: Ssmith <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits