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

Change subject: Add the mediawiki.experiments module
......................................................................


Add the mediawiki.experiments module

The module provides a generic bucketing function - it accepts an
experiment specification and a token that identifies a unique user - and
doesn't have any side effects, i.e. the bucket isn't persisted to
storage. It is therefore assumed that clients are responsible for either
storing the token or storing the bucket for the duration of an
experiment.

The module was extracted from the - admittedly, unused - module of the
same name in the MobileFrontend extension as it's intended to be used by
the Gather and QuickSurveys extensions.

Bug: T109010
Change-Id: Icf7f6fedf0c2deb5d5548c9e24456cc7a7c6a743
---
M maintenance/jsduck/categories.json
M resources/Resources.php
A resources/src/mediawiki/mediawiki.experiments.js
M tests/qunit/QUnitTestResources.php
A tests/qunit/suites/resources/mediawiki/mediawiki.experiments.test.js
5 files changed, 181 insertions(+), 1 deletion(-)

Approvals:
  Ori.livneh: Looks good to me, but someone else must approve
  Robmoen: Looks good to me, but someone else must approve
  Jdlrobson: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/maintenance/jsduck/categories.json 
b/maintenance/jsduck/categories.json
index d547b7b..ec2e51d 100644
--- a/maintenance/jsduck/categories.json
+++ b/maintenance/jsduck/categories.json
@@ -31,7 +31,8 @@
                                        "mw.user",
                                        "mw.util",
                                        "mw.plugin.*",
-                                       "mw.cookie"
+                                       "mw.cookie",
+                                       "mw.experiments"
                                ]
                        },
                        {
diff --git a/resources/Resources.php b/resources/Resources.php
index 8e7e368..28a27d7 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -1198,6 +1198,10 @@
                'styles' => 'resources/src/mediawiki.toolbar/toolbar.less',
                'position' => 'top',
        ),
+       'mediawiki.experiments' => array(
+               'scripts' => 'resources/src/mediawiki/mediawiki.experiments.js',
+               'targets' => array( 'desktop', 'mobile' ),
+       ),
 
        /* MediaWiki Action */
 
diff --git a/resources/src/mediawiki/mediawiki.experiments.js 
b/resources/src/mediawiki/mediawiki.experiments.js
new file mode 100644
index 0000000..930bfec
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.experiments.js
@@ -0,0 +1,110 @@
+/* jshint bitwise:false */
+( function ( mw, $ ) {
+
+       var CONTROL_BUCKET = 'control',
+               MAX_INT32_UNSIGNED = 4294967295;
+
+       /**
+        * An implementation of Jenkins' one-at-a-time hash.
+        *
+        * @see http://en.wikipedia.org/wiki/Jenkins_hash_function
+        *
+        * @param {String} string String to hash
+        * @return {Number} The hash as a 32-bit unsigned integer
+        * @ignore
+        *
+        * @author Ori Livneh <[email protected]>
+        * @see http://jsbin.com/kejewi/4/watch?js,console
+        */
+       function hashString( string ) {
+               var hash = 0,
+                       i = string.length;
+
+               while ( i-- ) {
+                       hash += string.charCodeAt( i );
+                       hash += ( hash << 10 );
+                       hash ^= ( hash >> 6 );
+               }
+               hash += ( hash << 3 );
+               hash ^= ( hash >> 11 );
+               hash += ( hash << 15 );
+
+               return hash >>> 0;
+       }
+
+       /**
+        * Provides an API for bucketing users in experiments.
+        *
+        * @class mw.experiments
+        * @singleton
+        */
+       mw.experiments = {
+
+               /**
+                * Gets the bucket for the experiment given the token.
+                *
+                * The name of the experiment and the token are hashed. The 
hash is converted
+                * to a number which is then used to get a bucket.
+                *
+                * Consider the following experiment specification:
+                *
+                * ```
+                * {
+                *   name: 'My first experiment',
+                *   enabled: true,
+                *   buckets: {
+                *     control: 0.5
+                *     A: 0.25,
+                *     B: 0.25
+                *   }
+                * }
+                * ```
+                *
+                * The experiment has three buckets: control, A, and B. The 
user has a 50%
+                * chance of being assigned to the control bucket, and a 25% 
chance of being
+                * assigned to either the A or B buckets. If the experiment 
were disabled,
+                * then the user would always be assigned to the control bucket.
+                *
+                * This function is based on the deprecated `mw.user.bucket` 
function.
+                *
+                * @param {Object} experiment
+                * @param {String} experiment.name The name of the experiment
+                * @param {Boolean} experiment.enabled Whether or not the 
experiment is
+                *  enabled. If the experiment is disabled, then the user is 
always assigned
+                *  to the control bucket
+                * @param {Object} experiment.buckets A map of bucket name to 
probability
+                *  that the user will be assigned to that bucket
+                * @param {String} token A token that uniquely identifies the 
user for the
+                *  duration of the experiment
+                * @returns {String} The bucket
+                */
+               getBucket: function ( experiment, token ) {
+                       var buckets = experiment.buckets,
+                               key,
+                               range = 0,
+                               hash,
+                               max,
+                               acc = 0;
+
+                       if ( !experiment.enabled || $.isEmptyObject( 
experiment.buckets ) ) {
+                               return CONTROL_BUCKET;
+                       }
+
+                       for ( key in buckets ) {
+                               range += buckets[key];
+                       }
+
+                       hash = hashString( experiment.name + ':' + token );
+                       max = ( hash / MAX_INT32_UNSIGNED ) * range;
+
+                       for ( key in buckets ) {
+                               acc += buckets[key];
+
+                               if ( max <= acc ) {
+                                       return key;
+                               }
+                       }
+               }
+       };
+
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/QUnitTestResources.php 
b/tests/qunit/QUnitTestResources.php
index 60b2802..f9ddcf2 100644
--- a/tests/qunit/QUnitTestResources.php
+++ b/tests/qunit/QUnitTestResources.php
@@ -88,6 +88,7 @@
                        
'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js',
                        
'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js',
                        
'tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js',
+                       
'tests/qunit/suites/resources/mediawiki/mediawiki.experiments.test.js',
                ),
                'dependencies' => array(
                        'jquery.accessKeyLabel',
@@ -127,6 +128,7 @@
                        'mediawiki.language',
                        'mediawiki.cldr',
                        'mediawiki.cookie',
+                       'mediawiki.experiments',
                        'test.mediawiki.qunit.testrunner',
                ),
        )
diff --git 
a/tests/qunit/suites/resources/mediawiki/mediawiki.experiments.test.js 
b/tests/qunit/suites/resources/mediawiki/mediawiki.experiments.test.js
new file mode 100644
index 0000000..774b205
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki/mediawiki.experiments.test.js
@@ -0,0 +1,63 @@
+( function ( mw ) {
+
+       var getBucket = mw.experiments.getBucket;
+
+       function createExperiment() {
+               return {
+                       name: 'experiment',
+                       enabled: true,
+                       buckets: {
+                               control: 0.25,
+                               A: 0.25,
+                               B: 0.25,
+                               C: 0.25
+                       }
+               };
+       }
+
+       QUnit.module( 'mediawiki.experiments' );
+
+       QUnit.test( 'getBucket( experiment, token )', 4, function ( assert ) {
+               var experiment = createExperiment(),
+                       token = '123457890';
+
+               assert.equal(
+                       getBucket( experiment, token ),
+                       getBucket( experiment, token ),
+                       'It returns the same bucket for the same 
experiment-token pair.'
+               );
+
+               // --------
+               experiment = createExperiment();
+               experiment.buckets = {
+                       A: 0.314159265359
+               };
+
+               assert.equal(
+                       'A',
+                       getBucket( experiment, token ),
+                       'It returns the bucket if only one is defined.'
+               );
+
+               // --------
+               experiment = createExperiment();
+               experiment.enabled = false;
+
+               assert.equal(
+                       'control',
+                       getBucket( experiment, token ),
+                       'It returns "control" if the experiment is disabled.'
+               );
+
+               // --------
+               experiment = createExperiment();
+               experiment.buckets = {};
+
+               assert.equal(
+                       'control',
+                       getBucket( experiment, token ),
+                       'It returns "control" if the experiment doesn\'t have 
any buckets.'
+               );
+       } );
+
+}( mediaWiki ) );

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Icf7f6fedf0c2deb5d5548c9e24456cc7a7c6a743
Gerrit-PatchSet: 8
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Phuedx <[email protected]>
Gerrit-Reviewer: Bmansurov <[email protected]>
Gerrit-Reviewer: Edokter <[email protected]>
Gerrit-Reviewer: Jack Phoenix <[email protected]>
Gerrit-Reviewer: Jdlrobson <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: Ori.livneh <[email protected]>
Gerrit-Reviewer: Phuedx <[email protected]>
Gerrit-Reviewer: Robmoen <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to