mirror of
https://gerrit.wikimedia.org/r/mediawiki/skins/MinervaNeue
synced 2024-12-18 00:30:40 +00:00
Hygiene: revise A/B test terminology
Improve the comments and APIs provided by AB.js: - Control becomes unsampled. - A becomes control. - B becomes treatment. This code does not appear to be in use presently, so it's a great time to change it. Change-Id: I31d619f889ee45102a4aed774a6ec41f0d95ba7d
This commit is contained in:
parent
b73f5c7520
commit
672df850cb
|
@ -1,22 +1,22 @@
|
||||||
/*
|
/*
|
||||||
* Bucketing wrapper for creating AB-tests.
|
* Bucketing wrapper for creating AB-tests.
|
||||||
*
|
*
|
||||||
* Given a test name, sampling rate, and session ID, provides a class that buckets users into
|
* Given a test name, sampling rate, and session ID, provides a class that buckets a user into
|
||||||
* predefined bucket ("control", "A", "B") and starts an AB-test.
|
* a predefined bucket ("unsampled", "control", or "treatment") and starts an AB-test.
|
||||||
*/
|
*/
|
||||||
( function ( M, mwExperiments ) {
|
( function ( M, mwExperiments ) {
|
||||||
var bucket = {
|
var bucket = {
|
||||||
CONTROL: 'control',
|
UNSAMPLED: 'unsampled', // Old treatment: not sampled and not instrumented.
|
||||||
A: 'A',
|
CONTROL: 'control', // Old treatment: sampled and instrumented.
|
||||||
B: 'B'
|
TREATMENT: 'treatment' // New treatment: sampled and instrumented.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buckets users based on params and exposes an `isEnabled` and `getBucket` method.
|
* Buckets users based on params and exposes an `isSampled` and `getBucket` method.
|
||||||
* @param {Object} config Configuration object for AB test.
|
* @param {Object} config Configuration object for AB test.
|
||||||
* @param {string} config.testName
|
* @param {string} config.testName
|
||||||
* @param {number} config.samplingRate Sampling rate for the AB-test.
|
* @param {number} config.samplingRate Sampling rate for the AB-test.
|
||||||
* @param {number} config.sessionId Session ID for user bucketing.
|
* @param {number} config.sessionId Session ID for user bucketing.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function AB( config ) {
|
function AB( config ) {
|
||||||
|
@ -28,9 +28,9 @@
|
||||||
name: testName,
|
name: testName,
|
||||||
enabled: !!samplingRate,
|
enabled: !!samplingRate,
|
||||||
buckets: {
|
buckets: {
|
||||||
control: 1 - samplingRate,
|
unsampled: 1 - samplingRate,
|
||||||
A: samplingRate / 2,
|
control: samplingRate / 2,
|
||||||
B: samplingRate / 2
|
treatment: samplingRate / 2
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -39,37 +39,37 @@
|
||||||
*
|
*
|
||||||
* A boolean instead of an enum is usually a code smell. However, the nature of A/B testing
|
* A boolean instead of an enum is usually a code smell. However, the nature of A/B testing
|
||||||
* is to compare an A group's performance to a B group's so a boolean seems natural, even
|
* is to compare an A group's performance to a B group's so a boolean seems natural, even
|
||||||
* in the long term, and preferable to showing bucketing encoding ("A", "B", "control") to
|
* in the long term, and preferable to showing bucketing encoding ("unsampled", "control",
|
||||||
* callers which is necessary if getBucket(). The downside is that now two functions exist
|
* "treatment") to callers which is necessary if getBucket(). The downside is that now two
|
||||||
* where one would suffice.
|
* functions exist where one would suffice.
|
||||||
*
|
*
|
||||||
* @return {string} AB-test bucket, bucket.CONTROL_BUCKET by default, bucket.A or bucket.B
|
* @return {string} AB-test bucket, `bucket.UNSAMPLED` by default, `bucket.CONTROL` or
|
||||||
* buckets otherwise.
|
* `bucket.TREATMENT` buckets otherwise.
|
||||||
*/
|
*/
|
||||||
function getBucket() {
|
function getBucket() {
|
||||||
return mwExperiments.getBucket( test, sessionId );
|
return mwExperiments.getBucket( test, sessionId );
|
||||||
}
|
}
|
||||||
|
|
||||||
function isA() {
|
function isControl() {
|
||||||
return getBucket() === bucket.A;
|
return getBucket() === bucket.CONTROL;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isB() {
|
function isTreatment() {
|
||||||
return getBucket() === bucket.B;
|
return getBucket() === bucket.TREATMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether or not a user is in the AB-test,
|
* Checks whether or not a user is in the AB-test,
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
function isEnabled() {
|
function isSampled() {
|
||||||
return getBucket() !== bucket.CONTROL; // I.e., `isA() || isB()`;
|
return getBucket() !== bucket.UNSAMPLED; // I.e., `isControl() || isTreatment()`
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isA: isA,
|
isControl: isControl,
|
||||||
isB: isB,
|
isTreatment: isTreatment,
|
||||||
isEnabled: isEnabled
|
isSampled: isSampled
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,9 @@
|
||||||
|
|
||||||
QUnit.test( 'Bucketing test', function ( assert ) {
|
QUnit.test( 'Bucketing test', function ( assert ) {
|
||||||
var userBuckets = {
|
var userBuckets = {
|
||||||
|
unsampled: 0,
|
||||||
control: 0,
|
control: 0,
|
||||||
A: 0,
|
treatment: 0
|
||||||
B: 0
|
|
||||||
},
|
},
|
||||||
maxUsers = 1000,
|
maxUsers = 1000,
|
||||||
bucketingTest,
|
bucketingTest,
|
||||||
|
@ -26,31 +26,31 @@
|
||||||
sessionId: mw.user.generateRandomSessionId()
|
sessionId: mw.user.generateRandomSessionId()
|
||||||
} );
|
} );
|
||||||
bucketingTest = new AB( config );
|
bucketingTest = new AB( config );
|
||||||
if ( bucketingTest.isA() ) {
|
if ( bucketingTest.isControl() ) {
|
||||||
++userBuckets.A;
|
|
||||||
} else if ( bucketingTest.isB() ) {
|
|
||||||
++userBuckets.B;
|
|
||||||
} else if ( !bucketingTest.isEnabled() ) {
|
|
||||||
++userBuckets.control;
|
++userBuckets.control;
|
||||||
|
} else if ( bucketingTest.isTreatment() ) {
|
||||||
|
++userBuckets.treatment;
|
||||||
|
} else if ( !bucketingTest.isSampled() ) {
|
||||||
|
++userBuckets.unsampled;
|
||||||
} else {
|
} else {
|
||||||
throw new Error( 'Unknown bucket!' );
|
throw new Error( 'Unknown bucket!' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
( userBuckets.control / maxUsers > 0.4 ) &&
|
( userBuckets.unsampled / maxUsers > 0.4 ) &&
|
||||||
( userBuckets.control / maxUsers < 0.6 ),
|
( userBuckets.unsampled / maxUsers < 0.6 ),
|
||||||
true, 'test control group is about 50% (' + userBuckets.control / 10 + '%)' );
|
true, 'test unsampled group is about 50% (' + userBuckets.unsampled / 10 + '%)' );
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
( userBuckets.A / maxUsers > 0.2 ) &&
|
( userBuckets.control / maxUsers > 0.2 ) &&
|
||||||
( userBuckets.A / maxUsers < 0.3 ),
|
( userBuckets.control / maxUsers < 0.3 ),
|
||||||
true, 'test group A is about 25% (' + userBuckets.A / 10 + '%)' );
|
true, 'test control group is about 25% (' + userBuckets.control / 10 + '%)' );
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
( userBuckets.B / maxUsers > 0.2 ) &&
|
( userBuckets.treatment / maxUsers > 0.2 ) &&
|
||||||
( userBuckets.B / maxUsers < 0.3 ),
|
( userBuckets.treatment / maxUsers < 0.3 ),
|
||||||
true, 'test group B is about 25% (' + userBuckets.B / 10 + '%)' );
|
true, 'test new treatment group is about 25% (' + userBuckets.treatment / 10 + '%)' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
}( mw.mobileFrontend ) );
|
}( mw.mobileFrontend ) );
|
||||||
|
|
Loading…
Reference in a new issue