Sticky header AB test bucketing for 2 treatment buckets

For idwiki/viwiki, we wish to run the sticky header edit button AB
test so that treatment1 group sees the sticky header without edit
buttons, treatment2 groups sees the sticky header with edit buttons,
and the control/unsampled groups see no sticky header at all.

This patch overrides the configuration to make the sticky header
w/o edit buttons for treatment1, sticky header w/ edit buttons for
treatment2, and hides sticky header for everyone else. This depends
on a configuration with the treatment groups having "treatment1"
and "treatment2" as substrings in their bucket names.

The full configuration for idwiki/viwiki would be something like
the following:

```
$wgVectorStickyHeader = [
	"logged_in" =>  true,
	"logged_out" => false,
];

$wgVectorStickyHeaderEdit = [
		"logged_in" => true,
		"logged_out" => false,
];

$wgVectorWebABTestEnrollment = [
	"name" => "vector.sticky_header_edit",
	"enabled" => true,
	"buckets" => [
		"unsampled" => [
			"samplingRate" => 0
		],
                "noStickyHeaderControl" => [
                        "samplingRate" => 0.34
                ],
		"stickyHeaderNoEditButtonTreatment1" => [
			"samplingRate" => 0.33
		],
		"stickyHeaderEditButtonTreatment2" => [
			"samplingRate" => 0.33
		]
	],
];
```

Bug: T312573
Change-Id: I15c360fdf5393f5594602acc33b5b916e904016d
(cherry picked from commit 942cd5b0f6)
This commit is contained in:
Jan Drewniak 2022-07-07 15:06:14 -04:00 committed by Clare Ming
parent 8772b0065e
commit 4c52b69780
3 changed files with 61 additions and 40 deletions

View File

@ -3,6 +3,11 @@
const EXCLUDED_BUCKET = 'unsampled';
const TREATMENT_BUCKET_SUBSTRING = 'treatment';
const WEB_AB_TEST_ENROLLMENT_HOOK = 'mediawiki.web_AB_test_enrollment';
/**
* @typedef {Function} TreatmentBucketFunction
* @param {string} [a]
* @return {boolean}
*/
/**
* @typedef {Object} WebABTest
@ -10,7 +15,7 @@ const WEB_AB_TEST_ENROLLMENT_HOOK = 'mediawiki.web_AB_test_enrollment';
* @property {function(): string} getBucket
* @property {function(string): boolean} isInBucket
* @property {function(): boolean} isInSample
* @property {function(): boolean} isInTreatmentBucket
* @property {TreatmentBucketFunction} isInTreatmentBucket
*/
/**
@ -41,12 +46,15 @@ const WEB_AB_TEST_ENROLLMENT_HOOK = 'mediawiki.web_AB_test_enrollment';
* name: 'nameOfExperiment',
* buckets: {
* unsampled: {
* samplingRate: 0.5
* samplingRate: 0.25
* },
* control: {
* samplingRate: 0.25
* },
* treatment: {
* treatment1: {
* samplingRate: 0.25
* },
* treatment2: {
* samplingRate: 0.25
* }
* },
@ -147,17 +155,18 @@ module.exports = function webABTest( props ) {
/**
* Whether or not the subject has been bucketed in a treatment bucket as
* defined by the bucket name containing the case-insensitive `treatment`
* substring (e.g. 'treatment', 'sticky-header-treatment' and
* 'stickyHeaderTreatment' are all assumed to be treatment buckets).
* defined by the bucket name containing the case-insensitive 'treatment',
* 'treatment1', or 'treatment2' substring (e.g. 'treatment', 'treatment1',
* 'sticky-header-treatment1' and 'stickyHeaderTreatment2' are all assumed
* to be treatment buckets).
*
* @param {string|null} [treatmentBucketName] lowercase name of bucket.
* @return {boolean}
*/
function isInTreatmentBucket() {
function isInTreatmentBucket( treatmentBucketName = '' ) {
const bucket = getBucket();
// eslint-disable-next-line no-restricted-syntax
return bucket.toLowerCase().includes( TREATMENT_BUCKET_SUBSTRING );
return bucket.toLowerCase().includes( `${TREATMENT_BUCKET_SUBSTRING}${treatmentBucketName}` );
}
/**

View File

@ -8,6 +8,7 @@ const
initTableOfContents = require( './tableOfContents.js' ),
deferUntilFrame = require( './deferUntilFrame.js' ),
ABTestConfig = require( /** @type {string} */ ( './config.json' ) ).wgVectorWebABTestEnrollment || {},
stickyHeaderEditIconConfig = require( /** @type {string} */ ( './config.json' ) ).wgVectorStickyHeaderEdit || true,
TOC_ID = 'mw-panel-toc',
TOC_ID_LEGACY = 'toc',
BODY_CONTENT_ID = 'bodyContent',
@ -58,7 +59,7 @@ const getHeadingIntersectionHandler = ( changeActiveSection ) => {
function initStickyHeaderABTests( abConfig, isStickyHeaderFeatureAllowed, getEnabledExperiment ) {
let show = isStickyHeaderFeatureAllowed,
stickyHeaderExperiment,
noEditIcons = true;
noEditIcons = stickyHeaderEditIconConfig;
// One of the sticky header AB tests is specified in the config
const abTestName = abConfig.name,
@ -74,16 +75,21 @@ function initStickyHeaderABTests( abConfig, isStickyHeaderFeatureAllowed, getEna
// If eligible, initialize the AB test
stickyHeaderExperiment = getEnabledExperiment( abConfig );
// If running initial AB test, only show sticky header to treatment group.
if ( abTestName === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME ) {
// If running initial or edit AB test, show sticky header to treatment groups
// only. Unsampled and control buckets do not see sticky header.
if ( abTestName === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME ||
abTestName === stickyHeader.STICKY_HEADER_EDIT_EXPERIMENT_NAME
) {
show = stickyHeaderExperiment.isInTreatmentBucket();
}
// If running edit-button AB test, show sticky header to all buckets
// and show edit button for treatment group
// If running edit-button AB test, the edit buttons in sticky header are shown
// to second treatment group only.
if ( abTestName === stickyHeader.STICKY_HEADER_EDIT_EXPERIMENT_NAME ) {
show = true;
if ( stickyHeaderExperiment.isInTreatmentBucket() ) {
if ( stickyHeaderExperiment.isInTreatmentBucket( '1' ) ) {
noEditIcons = true;
}
if ( stickyHeaderExperiment.isInTreatmentBucket( '2' ) ) {
noEditIcons = false;
}
}

View File

@ -62,7 +62,7 @@ describe( 'main.js', () => {
isEnabled: true,
isUserInTreatmentBucket: false,
expectedResult: {
showStickyHeader: true,
showStickyHeader: false,
disableEditIcons: true
}
},
@ -78,7 +78,7 @@ describe( 'main.js', () => {
{
abConfig: STICKY_HEADER_AB,
isEnabled: false, // sticky header unavailable
isUserInTreatmentBucket: false, // not in treament bucket
isUserInTreatmentBucket: false, // not in treatment bucket
expectedResult: {
showStickyHeader: false,
disableEditIcons: true
@ -87,7 +87,7 @@ describe( 'main.js', () => {
{
abConfig: STICKY_HEADER_AB,
isEnabled: true, // sticky header available
isUserInTreatmentBucket: false, // not in treament bucket
isUserInTreatmentBucket: false, // not in treatment bucket
expectedResult: {
showStickyHeader: false,
disableEditIcons: true
@ -96,7 +96,7 @@ describe( 'main.js', () => {
{
abConfig: STICKY_HEADER_AB,
isEnabled: false, // sticky header is not available
isUserInTreatmentBucket: true, // but the user is in the treament bucket
isUserInTreatmentBucket: true, // but the user is in the treatment bucket
expectedResult: {
showStickyHeader: false,
disableEditIcons: true
@ -129,27 +129,33 @@ describe( 'main.js', () => {
disableEditIcons: false
}
}
].forEach( ( { abConfig, isEnabled, isUserInTreatmentBucket, expectedResult } ) => {
document.documentElement.classList.add( 'vector-sticky-header-enabled' );
const result = test.initStickyHeaderABTests(
].forEach(
( {
abConfig,
// isStickyHeaderFeatureAllowed
isEnabled,
( experiment ) => ( {
name: experiment.name,
isInBucket: () => true,
isInSample: () => true,
getBucket: () => 'bucket',
isInTreatmentBucket: () => {
return isUserInTreatmentBucket;
}
} )
);
expect( result ).toMatchObject( expectedResult );
// Check that there are no side effects
expect(
document.documentElement.classList.contains( 'vector-sticky-header-enabled' )
).toBe( true );
} );
isUserInTreatmentBucket,
expectedResult
} ) => {
document.documentElement.classList.add( 'vector-sticky-header-enabled' );
const result = test.initStickyHeaderABTests(
abConfig,
// isStickyHeaderFeatureAllowed
isEnabled,
( experiment ) => ( {
name: experiment.name,
isInBucket: () => true,
isInSample: () => true,
getBucket: () => 'bucket',
isInTreatmentBucket: () => {
return isUserInTreatmentBucket;
}
} )
);
expect( result ).toMatchObject( expectedResult );
// Check that there are no side effects
expect(
document.documentElement.classList.contains( 'vector-sticky-header-enabled' )
).toBe( true );
} );
} );
} );