Sticky header edit button A/B test bucketing

Adds behaviour for conditionally adding the edit button
to the sticky-header based on A/B test bucketing.

This behaviour depends on having the `$wgVectorStickyHeaderEdit` config
set to true for logged-in users:

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

as well as an AB test configured with the following buckets:

    $wgVectorWebABTestEnrollment = [
        'name' => 'vector.sticky_header_edit',
        'enabled' => true,
        'buckets' => [
            'unsampled' => [
                'samplingRate' => 0
            ],
            'stickyHeaderEditButtonControl' => [
                'samplingRate' => 0
            ],
            'stickyHeaderEditButtonTreatment' => [
                'samplingRate' => 1
            ]
        ]
    ];

With that config, this change hides the sticky header for all users
except those in the stickyHeaderEditButtonTreatment bucket.

Bug: T299959
Change-Id: If252956bc530d8ce54eeda61f42a93ffa48255cb
This commit is contained in:
Jan Drewniak 2022-04-13 23:58:05 -04:00
parent dafe3c8fd4
commit 42b808738a
2 changed files with 67 additions and 20 deletions

View file

@ -71,26 +71,65 @@ const main = () => {
allowedAction && allowedAction &&
'IntersectionObserver' in window; 'IntersectionObserver' in window;
const isExperimentEnabled = /**
!!ABTestConfig.enabled && * Initialize sticky header AB tests and determine whether to show the sticky header
ABTestConfig.name === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME && * based on which buckets the user is in.
!mw.user.isAnon() && *
isStickyHeaderAllowed; * @typedef {Object} InitStickyHeaderABTests
* @property {boolean} disableEditIcons - Should the sticky header have an edit icon
* @property {boolean} showStickyHeader - Should the sticky header be shown
* @return {InitStickyHeaderABTests}
*/
function initStickyHeaderABTests() {
// If necessary, initialize experiment and fire the A/B test enrollment hook. let show = isStickyHeaderAllowed,
const stickyHeaderExperiment = isExperimentEnabled && stickyHeaderExperiment,
initExperiment( Object.assign( {}, ABTestConfig, { token: mw.user.getId() } ) ); noEditIcons = true;
// Determine if user is eligible for sticky header AB test
if (
isStickyHeaderAllowed && // The sticky header can be shown on the page
ABTestConfig.enabled && // An AB test config is enabled
!mw.user.isAnon() && // The user is logged-in
( // One of the sticky-header AB tests is specified in the config
ABTestConfig.name === stickyHeader.STICKY_HEADER_EXPERIMENT_NAME ||
ABTestConfig.name === 'vector.sticky_header_edit'
)
) {
// If eligible, initialize the AB test
stickyHeaderExperiment = initExperiment(
Object.assign( {}, ABTestConfig, { token: mw.user.getId() } )
);
// If running initial AB test, only show sticky header to treatment group.
if ( stickyHeaderExperiment.name === stickyHeader.STICKY_HEADER_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 ( stickyHeaderExperiment.name === 'vector.sticky_header_edit' ) {
show = true;
if ( stickyHeaderExperiment.isInTreatmentBucket() ) {
noEditIcons = false;
}
}
}
return {
showStickyHeader: show,
disableEditIcons: noEditIcons
};
}
const { showStickyHeader, disableEditIcons } = initStickyHeaderABTests();
// Remove class if present on the html element so that scroll padding isn't undesirably // Remove class if present on the html element so that scroll padding isn't undesirably
// applied to users who don't experience the new treatment. // applied to users who don't experience the new treatment.
if ( stickyHeaderExperiment && !stickyHeaderExperiment.isInTreatmentBucket() ) { if ( !showStickyHeader ) {
document.documentElement.classList.remove( 'vector-sticky-header-enabled' ); document.documentElement.classList.remove( 'vector-sticky-header-enabled' );
} }
const isStickyHeaderEnabled = stickyHeaderExperiment ?
stickyHeaderExperiment.isInTreatmentBucket() :
isStickyHeaderAllowed;
// Table of contents // Table of contents
const tocElement = document.getElementById( TOC_ID ); const tocElement = document.getElementById( TOC_ID );
const tocElementLegacy = document.getElementById( TOC_ID_LEGACY ); const tocElementLegacy = document.getElementById( TOC_ID_LEGACY );
@ -99,25 +138,26 @@ const main = () => {
// Set up intersection observer for page title, used by sticky header // Set up intersection observer for page title, used by sticky header
const observer = scrollObserver.initScrollObserver( const observer = scrollObserver.initScrollObserver(
() => { () => {
if ( isStickyHeaderAllowed && isStickyHeaderEnabled ) { if ( isStickyHeaderAllowed && showStickyHeader ) {
stickyHeader.show(); stickyHeader.show();
} }
scrollObserver.fireScrollHook( 'down', PAGE_TITLE_SCROLL_HOOK ); scrollObserver.fireScrollHook( 'down', PAGE_TITLE_SCROLL_HOOK );
}, },
() => { () => {
if ( isStickyHeaderAllowed && isStickyHeaderEnabled ) { if ( isStickyHeaderAllowed && showStickyHeader ) {
stickyHeader.hide(); stickyHeader.hide();
} }
scrollObserver.fireScrollHook( 'up', PAGE_TITLE_SCROLL_HOOK ); scrollObserver.fireScrollHook( 'up', PAGE_TITLE_SCROLL_HOOK );
} }
); );
if ( isStickyHeaderAllowed && isStickyHeaderEnabled ) { if ( isStickyHeaderAllowed && showStickyHeader ) {
stickyHeader.initStickyHeader( { stickyHeader.initStickyHeader( {
header, header,
userMenu, userMenu,
observer, observer,
stickyIntersection stickyIntersection,
disableEditIcons
} ); } );
} else if ( stickyIntersection ) { } else if ( stickyIntersection ) {
observer.observe( stickyIntersection ); observer.observe( stickyIntersection );

View file

@ -212,6 +212,7 @@ function prepareEditIcons(
if ( !primaryEditSticky || !wikitextSticky || !protectedSticky ) { if ( !primaryEditSticky || !wikitextSticky || !protectedSticky ) {
return; return;
} }
if ( !primaryEdit ) { if ( !primaryEdit ) {
removeNode( protectedSticky ); removeNode( protectedSticky );
removeNode( wikitextSticky ); removeNode( wikitextSticky );
@ -343,13 +344,15 @@ function prepareUserMenu( userMenu ) {
* @param {Element} userMenuStickyContainer * @param {Element} userMenuStickyContainer
* @param {IntersectionObserver} stickyObserver * @param {IntersectionObserver} stickyObserver
* @param {Element} stickyIntersection * @param {Element} stickyIntersection
* @param {boolean} disableEditIcons
*/ */
function makeStickyHeaderFunctional( function makeStickyHeaderFunctional(
header, header,
userMenu, userMenu,
userMenuStickyContainer, userMenuStickyContainer,
stickyObserver, stickyObserver,
stickyIntersection stickyIntersection,
disableEditIcons
) { ) {
const const
userMenuStickyContainerInner = userMenuStickyContainer userMenuStickyContainerInner = userMenuStickyContainer
@ -370,7 +373,9 @@ function makeStickyHeaderFunctional(
const ceEdit = document.querySelector( '#ca-edit a' ); const ceEdit = document.querySelector( '#ca-edit a' );
const protectedEdit = document.querySelector( '#ca-viewsource a' ); const protectedEdit = document.querySelector( '#ca-viewsource a' );
const isProtected = !!protectedEdit; const isProtected = !!protectedEdit;
const primaryEdit = protectedEdit || ( veEdit || ceEdit ); // For sticky header edit A/B test, conditionally remove the edit icon by setting null.
// Otherwise, use either protected, ve, or source edit (in that order).
const primaryEdit = disableEditIcons ? null : protectedEdit || veEdit || ceEdit;
const secondaryEdit = veEdit ? ceEdit : null; const secondaryEdit = veEdit ? ceEdit : null;
const disableStickyHeader = () => { const disableStickyHeader = () => {
document.body.classList.remove( STICKY_HEADER_VISIBLE_CLASS ); document.body.classList.remove( STICKY_HEADER_VISIBLE_CLASS );
@ -434,6 +439,7 @@ function isAllowedAction( action ) {
* @property {Element} userMenu * @property {Element} userMenu
* @property {IntersectionObserver} observer * @property {IntersectionObserver} observer
* @property {Element} stickyIntersection * @property {Element} stickyIntersection
* @property {boolean} disableEditIcons
*/ */
/** /**
@ -449,7 +455,8 @@ function initStickyHeader( props ) {
props.userMenu, props.userMenu,
userMenuStickyContainer, userMenuStickyContainer,
props.observer, props.observer,
props.stickyIntersection props.stickyIntersection,
props.disableEditIcons
); );
setupSearchIfNeeded( props.header ); setupSearchIfNeeded( props.header );