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:
Stephen Niedzielski 2018-10-25 17:10:06 -07:00 committed by Niedzielski
parent b73f5c7520
commit 672df850cb
2 changed files with 43 additions and 43 deletions

View file

@ -1,18 +1,18 @@
/* /*
* 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.
@ -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
}; };
} }

View file

@ -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 ) );