From 150447cc71f0dcb1152cbf577dacc05d2763af2d Mon Sep 17 00:00:00 2001 From: Ian Ramos <5714212+IanRamosC@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:58:42 -0300 Subject: [PATCH] My Jetpack: add A/B experiment to welcome flow (#38664) * My Jetpack: add A/B test to Welcome flow * changelog * Fix project version * Sideload Tracks script after connection * Default experiment and control to redirect to user connection * My Jetpack: only reset notice after experiment loads * My Jetpack: don't change step if loading experiment * Prevent 'control' user to see evaluation step and code adjustments * Reshuffle conditions * Add try/catch to explats connection --------- Co-authored-by: robertsreberski --- .../welcome-flow/ConnectionStep.tsx | 46 +++++++++++++-- .../_inc/components/welcome-flow/index.tsx | 18 ++++-- .../context/value-store/valueStoreContext.tsx | 1 + .../my-jetpack/_inc/utils/side-load-tracks.ts | 58 +++++++++++++++++++ .../changelog/add-ab-experiment-welcome-flow | 5 ++ 5 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 projects/packages/my-jetpack/_inc/utils/side-load-tracks.ts create mode 100644 projects/packages/my-jetpack/changelog/add-ab-experiment-welcome-flow diff --git a/projects/packages/my-jetpack/_inc/components/welcome-flow/ConnectionStep.tsx b/projects/packages/my-jetpack/_inc/components/welcome-flow/ConnectionStep.tsx index fe727a0702d9a..358260fce50a4 100644 --- a/projects/packages/my-jetpack/_inc/components/welcome-flow/ConnectionStep.tsx +++ b/projects/packages/my-jetpack/_inc/components/welcome-flow/ConnectionStep.tsx @@ -1,13 +1,18 @@ import { Col, Button, Text, TermsOfService } from '@automattic/jetpack-components'; +import { initializeExPlat, loadExperimentAssignment } from '@automattic/jetpack-explat'; import { __ } from '@wordpress/i18n'; import { useCallback, useContext } from 'react'; import { NoticeContext } from '../../context/notices/noticeContext'; import { NOTICE_SITE_CONNECTED } from '../../context/notices/noticeTemplates'; import useAnalytics from '../../hooks/use-analytics'; +import sideloadTracks from '../../utils/side-load-tracks'; import styles from './style.module.scss'; +import { WelcomeFlowExperiment } from '.'; +import type { Dispatch, SetStateAction } from 'react'; type ConnectionStepProps = { onActivateSite: ( e?: Event ) => Promise< void >; + onUpdateWelcomeFlowExperiment: Dispatch< SetStateAction< WelcomeFlowExperiment > >; isActivating: boolean; }; @@ -16,23 +21,52 @@ type ConnectionStepProps = { * * @param {object} props - ConnectioStepProps * @param {Function} props.onActivateSite - Alias for handleRegisterSite + * @param {Function} props.onUpdateWelcomeFlowExperiment - Updating the welcomeFlowExperiment state * @param {boolean} props.isActivating - Alias for siteIsRegistering * @returns {object} The ConnectionStep component. */ -const ConnectionStep = ( { onActivateSite, isActivating }: ConnectionStepProps ) => { +const ConnectionStep = ( { + onActivateSite, + onUpdateWelcomeFlowExperiment, + isActivating, +}: ConnectionStepProps ) => { const { recordEvent } = useAnalytics(); const { setNotice, resetNotice } = useContext( NoticeContext ); const activationButtonLabel = __( 'Activate Jetpack in one click', 'jetpack-my-jetpack' ); - const onConnectSiteClick = useCallback( () => { + const onConnectSiteClick = useCallback( async () => { recordEvent( 'jetpack_myjetpack_welcome_banner_connect_site_click' ); - onActivateSite().then( () => { - recordEvent( 'jetpack_myjetpack_welcome_banner_connect_site_success' ); + onUpdateWelcomeFlowExperiment( state => ( { ...state, isLoading: true } ) ); + await onActivateSite(); + + recordEvent( 'jetpack_myjetpack_welcome_banner_connect_site_success' ); + + try { + await sideloadTracks(); + + initializeExPlat(); + + const { variationName } = await loadExperimentAssignment( + 'jetpack_my_jetpack_post_connection_flow_202408' + ); + + if ( variationName !== 'treatment' ) { + // For control or default, we redirect to the connection page as described in the experiment. + window.location.href = 'admin.php?page=my-jetpack#/connection'; + } + + onUpdateWelcomeFlowExperiment( state => ( { + ...state, + variation: variationName as WelcomeFlowExperiment[ 'variation' ], // casting to 'control' or 'treatment' + } ) ); + } finally { resetNotice(); setNotice( NOTICE_SITE_CONNECTED, resetNotice ); - } ); - }, [ onActivateSite, recordEvent, resetNotice, setNotice ] ); + + onUpdateWelcomeFlowExperiment( state => ( { ...state, isLoading: false } ) ); + } + }, [ onActivateSite, onUpdateWelcomeFlowExperiment, recordEvent, resetNotice, setNotice ] ); return ( <> diff --git a/projects/packages/my-jetpack/_inc/components/welcome-flow/index.tsx b/projects/packages/my-jetpack/_inc/components/welcome-flow/index.tsx index 23f683f3e3d08..94719828bcb2e 100644 --- a/projects/packages/my-jetpack/_inc/components/welcome-flow/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/welcome-flow/index.tsx @@ -15,6 +15,11 @@ import EvaluationStep, { EvaluationAreas } from './EvaluationStep'; import styles from './style.module.scss'; import type { FC, PropsWithChildren } from 'react'; +export type WelcomeFlowExperiment = { + isLoading: boolean; + variation: 'control' | 'treatment'; +}; + const WelcomeFlow: FC< PropsWithChildren > = ( { children } ) => { const { recordEvent } = useAnalytics(); const { dismissWelcomeBanner } = useWelcomeBanner(); @@ -30,12 +35,16 @@ const WelcomeFlow: FC< PropsWithChildren > = ( { children } ) => { } ); const [ isProcessingEvaluation, setIsProcessingEvaluation ] = useState( false ); const [ prevStep, setPrevStep ] = useState( '' ); + const [ welcomeFlowExperiment, setWelcomeFlowExperiment ] = useState< WelcomeFlowExperiment >( { + isLoading: false, + variation: 'control', + } ); const currentStep = useMemo( () => { - if ( ! siteIsRegistered ) { + if ( ! siteIsRegistered || welcomeFlowExperiment.isLoading ) { return 'connection'; } else if ( ! isProcessingEvaluation ) { - if ( ! isJetpackUserNew() ) { + if ( ! isJetpackUserNew() || welcomeFlowExperiment.variation !== 'treatment' ) { // If the user is not new, we don't show the evaluation step return null; } @@ -43,7 +52,7 @@ const WelcomeFlow: FC< PropsWithChildren > = ( { children } ) => { } return 'evaluation-processing'; - }, [ isProcessingEvaluation, siteIsRegistered ] ); + }, [ isProcessingEvaluation, siteIsRegistered, welcomeFlowExperiment ] ); useEffect( () => { if ( prevStep !== currentStep ) { @@ -113,7 +122,8 @@ const WelcomeFlow: FC< PropsWithChildren > = ( { children } ) => { { 'connection' === currentStep && ( ) } { 'evaluation' === currentStep && ( diff --git a/projects/packages/my-jetpack/_inc/context/value-store/valueStoreContext.tsx b/projects/packages/my-jetpack/_inc/context/value-store/valueStoreContext.tsx index d73d95f18f3c9..d8902c9f2b7dc 100644 --- a/projects/packages/my-jetpack/_inc/context/value-store/valueStoreContext.tsx +++ b/projects/packages/my-jetpack/_inc/context/value-store/valueStoreContext.tsx @@ -3,6 +3,7 @@ import type { Dispatch, SetStateAction } from 'react'; type ValueStoreType = { isWelcomeBannerVisible: boolean; + isLoadingWelcomeFlowExperiment?: boolean; recommendedModules: JetpackModule[] | null; recommendedModulesVisible: boolean; }; diff --git a/projects/packages/my-jetpack/_inc/utils/side-load-tracks.ts b/projects/packages/my-jetpack/_inc/utils/side-load-tracks.ts new file mode 100644 index 0000000000000..244f3d42a6564 --- /dev/null +++ b/projects/packages/my-jetpack/_inc/utils/side-load-tracks.ts @@ -0,0 +1,58 @@ +declare global { + interface Window { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _tkq: Array< Array< any > >; + } +} + +/** + * Function to get the current year and week number. + * + * @returns {string} The current year and week number. + */ +function getCurrentYearAndWeek() { + const date = new Date(); + const year = date.getFullYear(); + + const firstDayOfYear = new Date( year, 0, 1 ); + const daysSinceFirstDay = Math.floor( + ( date.getTime() - firstDayOfYear.getTime() ) / ( 24 * 60 * 60 * 1000 ) + ); + + // Calculate the current week number (assuming week starts on Sunday) + const weekNumber = Math.ceil( ( daysSinceFirstDay + firstDayOfYear.getDay() + 1 ) / 7 ); + const formattedWeekNumber = weekNumber.toString().padStart( 2, '0' ); + + return `${ year }${ formattedWeekNumber }`; +} + +/** + * Function to dynamically load a script into the document. + * It creates a new script element, sets its source to the provided URL, + * and appends it to the document's head. + * + * @param {string} src - The URL of the script to load. + * @returns {Promise} A promise that resolves once the script has loaded. + */ +function loadScript( src: string ): Promise< void > { + return new Promise( resolve => { + const script = document.createElement( 'script' ); + script.src = src; + script.onload = () => resolve(); + document.head.appendChild( script ); + } ); +} + +/** + * Function to sideload Tracks script. + * + * It initializes the _tkq array on the window object if it doesn't exist, + * and then loads the tracking script from the specified URL. Once the script has loaded, + * the provided callback function is called. + * + * @returns {Promise} A promise that resolves once the Tracks has been side loaded. + */ +export default function sideloadTracks(): Promise< void > { + window._tkq = window._tkq || []; + return loadScript( `//stats.wp.com/w.js?${ getCurrentYearAndWeek() }` ); +} diff --git a/projects/packages/my-jetpack/changelog/add-ab-experiment-welcome-flow b/projects/packages/my-jetpack/changelog/add-ab-experiment-welcome-flow new file mode 100644 index 0000000000000..9a9bdcf5fba96 --- /dev/null +++ b/projects/packages/my-jetpack/changelog/add-ab-experiment-welcome-flow @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Added a simple A/B experimento to the Welcome Flow on My Jetpack. + +