i13n: Extract experiments module

... from the statsvInstrumentation module so that the bucketing logic
can be shared with other instrumentation modules.

Change-Id: I5732fa539a3911939fa85fa88c102fa8dcfa5613
This commit is contained in:
Sam Smith 2017-06-14 11:32:29 +01:00 committed by jdlrobson
parent f2fbef6ec7
commit 6159af3151
7 changed files with 134 additions and 29 deletions

Binary file not shown.

Binary file not shown.

55
src/experiments.js Normal file
View file

@ -0,0 +1,55 @@
/**
* @module experiments
*/
/**
* @interface Experiments
*
* @global
*/
/**
* Creates a helper wrapper for the MediaWiki-provided
* `mw.experiments#getBucket` bucketing function.
*
* @param {mw.experiments} mwExperiments The `mw.experiments` singleton instance
* @return {Experiments}
*/
module.exports = function createExperiments( mwExperiments ) {
return {
/**
* Gets whether something is true given a name and a token.
*
* @example
* const experiments = require( './src/experiments' )( mw.experiments );
* const isFooEnabled = experiments.weightedBoolean(
* 'foo',
* 10 / 100, // 10% of all unique tokens should have foo enabled.
* token
* );
*
* @function
* @name Experiments#weightedBoolean
* @param {String} name The name of the thing. Since this is used as the
* name of the underlying experiment it should be unique to reduce the
* likelihood of collisions with other enabled experiments
* @param {Number} trueWeight A number between 0 and 1, representing the
* probability of the thing being true
* @param {String} token A token associated with the user for the duration
* of the experiment
* @return {Boolean}
*/
weightedBoolean: function ( name, trueWeight, token ) {
return mwExperiments.getBucket( {
enabled: true,
name: name,
buckets: {
'true': trueWeight,
'false': 1 - trueWeight
}
}, token ) === 'true';
}
};
};

View file

@ -17,6 +17,7 @@ var mw = mediaWiki,
createIsEnabled = require( './isEnabled' ), createIsEnabled = require( './isEnabled' ),
title = require( './title' ), title = require( './title' ),
renderer = require( './renderer' ), renderer = require( './renderer' ),
createExperiments = require( './experiments' ),
statsvInstrumentation = require( './statsvInstrumentation' ), statsvInstrumentation = require( './statsvInstrumentation' ),
changeListeners = require( './changeListeners' ), changeListeners = require( './changeListeners' ),
@ -116,6 +117,7 @@ mw.requestIdleCallback( function () {
gateway = createGateway( mw.config ), gateway = createGateway( mw.config ),
userSettings, userSettings,
settingsDialog, settingsDialog,
experiments,
statsvTracker, statsvTracker,
isEnabled, isEnabled,
schema, schema,
@ -123,7 +125,8 @@ mw.requestIdleCallback( function () {
userSettings = createUserSettings( mw.storage ); userSettings = createUserSettings( mw.storage );
settingsDialog = createSettingsDialogRenderer(); settingsDialog = createSettingsDialogRenderer();
statsvTracker = getStatsvTracker( mw.user, mw.config, mw.experiments ); experiments = createExperiments( mw.experiments );
statsvTracker = getStatsvTracker( mw.user, mw.config, experiments );
isEnabled = createIsEnabled( mw.user, userSettings, mw.config, mw.experiments ); isEnabled = createIsEnabled( mw.user, userSettings, mw.config, mw.experiments );

View file

@ -4,26 +4,22 @@
/** /**
* Gets whether Graphite logging (via [the statsv HTTP endpoint][0]) is enabled * Gets whether Graphite logging (via [the statsv HTTP endpoint][0]) is enabled
* for duration of the browser session. The sampling rate is controlled by * for the duration of the user's session. The bucketing rate is controlled by
* `wgPopupsStatsvSamplingRate`. * `wgPopupsStatsvSamplingRate`.
* *
* [0]: https://wikitech.wikimedia.org/wiki/Graphite#statsv * [0]: https://wikitech.wikimedia.org/wiki/Graphite#statsv
* *
* @param {mw.user} user The `mw.user` singleton instance * @param {mw.user} user The `mw.user` singleton instance
* @param {mw.Map} config The `mw.config` singleton instance * @param {mw.Map} config The `mw.config` singleton instance
* @param {mw.experiments} experiments The `mw.experiments` singleton instance * @param {Experiments} experiments
* @returns {Boolean} * @returns {Boolean}
*/ */
exports.isEnabled = function isEnabled( user, config, experiments ) { exports.isEnabled = function isEnabled( user, config, experiments ) {
var samplingRate = config.get( 'wgPopupsStatsvSamplingRate', 0 ), var bucketingRate = config.get( 'wgPopupsStatsvSamplingRate', 0 );
bucket = experiments.getBucket( {
name: 'ext.Popups.statsv',
enabled: true,
buckets: {
control: 1 - samplingRate,
A: samplingRate
}
}, user.sessionId() );
return bucket === 'A'; return experiments.weightedBoolean(
'ext.Popups.statsv',
bucketingRate,
user.sessionId()
);
}; };

View file

@ -0,0 +1,39 @@
var createExperiments = require( '../../src/experiments' );
QUnit.module( 'ext.popups/experiments#weightedBoolean' );
QUnit.test( 'it should call mw.experiments#getBucket', function ( assert ) {
var getBucketStub = this.sandbox.stub(),
stubMWExperiments = {
getBucket: getBucketStub
},
experiments = createExperiments( stubMWExperiments );
experiments.weightedBoolean( 'foo', 0.2, 'barbaz' );
assert.ok( getBucketStub.calledOnce );
assert.deepEqual(
getBucketStub.getCall( 0 ).args,
[
{
enabled: true,
name: 'foo',
buckets: {
'true': 0.2,
'false': 0.8 // 1 - 0.2
}
},
'barbaz'
]
);
// ---
getBucketStub.returns( 'true' );
assert.ok(
experiments.weightedBoolean( 'foo', 0.2, 'barbaz' ),
'It should return true if the bucket is "true".'
);
} );

View file

@ -1,27 +1,39 @@
var stubs = require( './stubs' ), var stubs = require( './stubs' ),
statsv = require( '../../src/statsvInstrumentation' ); statsv = require( '../../src/statsvInstrumentation' );
QUnit.module( 'ext.popups/statsvInstrumentation', { QUnit.module( 'ext.popups/statsvInstrumentation' );
beforeEach: function () {
this.user = stubs.createStubUser();
this.config = stubs.createStubMap();
}
} );
QUnit.test( 'isEnabled', function ( assert ) { QUnit.test( '#isEnabled', function ( assert ) {
var experiments = stubs.createStubExperiments( true ); var user = stubs.createStubUser(),
config = stubs.createStubMap(),
weightedBooleanStub = this.sandbox.stub(),
experiments = {
weightedBoolean: weightedBooleanStub
};
assert.expect( 2 ); config.set( 'wgPopupsStatsvSamplingRate', 0.3141 );
assert.ok( statsv.isEnabled( user, config, experiments );
statsv.isEnabled( this.user, this.config, experiments ),
'Logging is enabled when the user is in the sample.' assert.ok( weightedBooleanStub.calledOnce );
assert.deepEqual(
weightedBooleanStub.getCall( 0 ).args,
[
'ext.Popups.statsv',
config.get( 'wgPopupsStatsvSamplingRate' ),
user.sessionId()
]
); );
experiments = stubs.createStubExperiments( false ); // ---
assert.notOk( config.delete( 'wgPopupsStatsvSamplingRate' );
statsv.isEnabled( this.user, this.config, experiments ),
'Logging is disabled when the user is not in the sample.' statsv.isEnabled( user, config, experiments );
assert.deepEqual(
weightedBooleanStub.getCall( 1 ).args[ 1 ],
0,
'The bucketing rate should be 0 by default.'
); );
} ); } );