diff --git a/includes/MinervaHooks.php b/includes/MinervaHooks.php index 0a52ca4..43a254c 100644 --- a/includes/MinervaHooks.php +++ b/includes/MinervaHooks.php @@ -73,7 +73,9 @@ class MinervaHooks { $testModule = [ 'dependencies' => [ 'mobile.startup', - 'skins.minerva.notifications.badge' + 'skins.minerva.notifications.badge', + 'mediawiki.user', + 'mediawiki.experiments' ], 'localBasePath' => dirname( __DIR__ ), 'remoteSkinPath' => 'MinervaNeue', @@ -82,9 +84,11 @@ class MinervaHooks { // additional scaffolding (minus initialisation scripts) 'resources/skins.minerva.scripts/utils.js', 'resources/skins.minerva.scripts/DownloadIcon.js', + 'resources/skins.minerva.scripts/AB.js', // test files 'tests/qunit/skins.minerva.scripts/test_DownloadIcon.js', 'tests/qunit/skins.minerva.scripts/test_utils.js', + 'tests/qunit/skins.minerva.scripts/test_AB.js', 'tests/qunit/skins.minerva.notifications.badge/test_NotificationBadge.js' ], ]; diff --git a/resources/skins.minerva.scripts/AB.js b/resources/skins.minerva.scripts/AB.js new file mode 100644 index 0000000..701a763 --- /dev/null +++ b/resources/skins.minerva.scripts/AB.js @@ -0,0 +1,78 @@ +/* + * Bucketing wrapper for creating AB-tests. + * + * Given a test name, sampling rate, and session ID, provides a class + * that buckets users into predefined bucket ("control", "A", "B") and + * starts an AB-test. + */ + +( function ( mw, M, mwExperiments ) { + + /** + * Buckets users based on params and exposes an `isEnabled` and `getBucket` method. + * + * @param {string} testName name of the AB-test. + * @param {number} samplingRate sampling rate for the AB-test. + * @param {number} sessionId session ID for user bucketing. + * @constructor + */ + function AB( testName, samplingRate, sessionId ) { + + var CONTROL_BUCKET = 'control', + test = { + name: testName, + enabled: !!samplingRate, + buckets: { + control: 1 - samplingRate, + A: samplingRate / 2, + B: samplingRate / 2 + } + }; + /** + * Starts the AB-test and enters the user into the Reading Depth test. + */ + function startABTest() { + // See: https://gerrit.wikimedia.org/r/#/c/mediawiki/extensions/WikimediaEvents/+/437686/ + mw.track( 'wikimedia.event.ReadingDepthSchema.enable' ); + } + + /** + * Gets the users AB-test bucket + * + * @return {string} AB-test bucket, CONTROL_BUCKET by default, "A" or "B" buckets otherwise. + */ + function getBucket() { + return mwExperiments.getBucket( test, sessionId ); + } + + /** + * Checks whether or not a user is in the AB-test, + * + * @return {boolean} + */ + function isEnabled() { + return getBucket() !== CONTROL_BUCKET; + } + + /** + * Initiates the AB-test. + * + * return {void} + */ + function init() { + if ( isEnabled() ) { + startABTest(); + } + } + + init(); + + return { + getBucket: getBucket, + isEnabled: isEnabled + }; + } + + M.define( 'skins.minerva.scripts/AB', AB ); + +}( mw, mw.mobileFrontend, mw.experiments ) ); diff --git a/skin.json b/skin.json index cfac3ea..9ee3814 100644 --- a/skin.json +++ b/skin.json @@ -328,7 +328,10 @@ "mobile.issues", "mobile.search.api", "mobile.search", - "mobile.references" + "mobile.references", + "mediawiki.user", + "mediawiki.storage", + "mediawiki.experiments" ], "messages": [ "edithelp", @@ -355,6 +358,7 @@ "resources/skins.minerva.scripts/search.js", "resources/skins.minerva.scripts/references.js", "resources/skins.minerva.scripts/utils.js", + "resources/skins.minerva.scripts/AB.js", "resources/skins.minerva.scripts/cleanuptemplates.js" ] }, diff --git a/tests/qunit/skins.minerva.scripts/test_AB.js b/tests/qunit/skins.minerva.scripts/test_AB.js new file mode 100644 index 0000000..755b72b --- /dev/null +++ b/tests/qunit/skins.minerva.scripts/test_AB.js @@ -0,0 +1,40 @@ +( function ( M ) { + + var AB = M.require( 'skins.minerva.scripts/AB' ), + aBName = 'WME.MinervaABTest', + samplingRate = 0.5; + + QUnit.module( 'Minerva AB-test' ); + + QUnit.test( 'Bucketing test', function ( assert ) { + var userBuckets = { + control: 0, + A: 0, + B: 0 + }, + maxUsers = 1000, + bucketingTest, + i; + + for ( i = 0; i < maxUsers; i++ ) { + bucketingTest = new AB( aBName, samplingRate, mw.user.generateRandomSessionId() ); + userBuckets[ bucketingTest.getBucket() ] += 1; + } + + assert.strictEqual( + ( userBuckets.control / maxUsers > 0.4 ) && + ( userBuckets.control / maxUsers < 0.6 ), + true, 'test control group is about 50% (' + userBuckets.control / 10 + '%)' ); + + assert.strictEqual( + ( userBuckets.A / maxUsers > 0.2 ) && + ( userBuckets.A / maxUsers < 0.3 ), + true, 'test group A is about 25% (' + userBuckets.A / 10 + '%)' ); + + assert.strictEqual( + ( userBuckets.B / maxUsers > 0.2 ) && + ( userBuckets.B / maxUsers < 0.3 ), + true, 'test group B is about 25% (' + userBuckets.B / 10 + '%)' ); + } ); + +}( mw.mobileFrontend ) );