Merge pull request #14773 from overleaf/ii-modify-design-system-update-split-test

[web] Modify design-system-update split test

GitOrigin-RevId: f28aeef5ba782006afd30fd2862d0ad129077f6c
This commit is contained in:
ilkin-overleaf 2023-09-15 11:47:20 +03:00 committed by Copybot
parent dc937f4bc8
commit c6289cc67f
11 changed files with 179 additions and 55 deletions

View file

@ -43,7 +43,23 @@ module.exports = HomeController = {
async home(req, res) { async home(req, res) {
if (Features.hasFeature('homepage') && homepageExists) { if (Features.hasFeature('homepage') && homepageExists) {
return res.render('external/home/v2') let designSystemUpdatesAssignment = { variant: 'default' }
try {
designSystemUpdatesAssignment =
await SplitTestHandler.promises.getAssignment(
req,
res,
'design-system-updates'
)
} catch (error) {
logger.error(
{ err: error },
'failed to get "design-system-updates" split test assignment'
)
}
return res.render('external/home/v2', {
designSystemUpdatesVariant: designSystemUpdatesAssignment.variant,
})
} else { } else {
return res.redirect('/login') return res.redirect('/login')
} }

View file

@ -1,6 +1,8 @@
const AnalyticsManager = require('../Analytics/AnalyticsManager') const AnalyticsManager = require('../Analytics/AnalyticsManager')
const SubscriptionEmailHandler = require('./SubscriptionEmailHandler') const SubscriptionEmailHandler = require('./SubscriptionEmailHandler')
const { ObjectID } = require('mongodb') const { ObjectID } = require('mongodb')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const logger = require('@overleaf/logger')
const INVOICE_SUBSCRIPTION_LIMIT = 10 const INVOICE_SUBSCRIPTION_LIMIT = 10
@ -99,11 +101,25 @@ async function _sendSubscriptionUpdatedEvent(userId, eventData) {
async function _sendSubscriptionCancelledEvent(userId, eventData) { async function _sendSubscriptionCancelledEvent(userId, eventData) {
const { planCode, quantity, state, isTrial, subscriptionId } = const { planCode, quantity, state, isTrial, subscriptionId } =
_getSubscriptionData(eventData) _getSubscriptionData(eventData)
let designSystemUpdatesAssignment = { variant: 'default' }
try {
designSystemUpdatesAssignment =
await SplitTestHandler.promises.getAssignmentForUser(
userId,
'design-system-updates'
)
} catch (error) {
logger.error(
{ err: error },
'failed to get "design-system-updates" split test assignment'
)
}
AnalyticsManager.recordEventForUser(userId, 'subscription-cancelled', { AnalyticsManager.recordEventForUser(userId, 'subscription-cancelled', {
plan_code: planCode, plan_code: planCode,
quantity, quantity,
is_trial: isTrial, is_trial: isTrial,
subscriptionId, subscriptionId,
'split-test-design-system-updates': designSystemUpdatesAssignment.variant,
}) })
AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state) AnalyticsManager.setUserPropertyForUser(userId, 'subscription-state', state)
AnalyticsManager.setUserPropertyForUser( AnalyticsManager.setUserPropertyForUser(

View file

@ -280,8 +280,28 @@ async function userSubscriptionPage(req, res) {
SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash( SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash(
personalSubscription?.plan personalSubscription?.plan
) )
let designSystemUpdatesAssignment = { variant: 'default' }
try {
designSystemUpdatesAssignment =
await SplitTestHandler.promises.getAssignment(
req,
res,
'design-system-updates'
)
} catch (error) {
logger.error(
{ err: error },
'failed to get "design-system-updates" split test assignment'
)
}
AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view') AnalyticsManager.recordEventForSession(
req.session,
'subscription-page-view',
{
'split-test-design-system-updates': designSystemUpdatesAssignment.variant,
}
)
const cancelButtonAssignment = await SplitTestHandler.promises.getAssignment( const cancelButtonAssignment = await SplitTestHandler.promises.getAssignment(
req, req,

View file

@ -7,6 +7,7 @@ const UserDeleter = require('./UserDeleter')
const UserGetter = require('./UserGetter') const UserGetter = require('./UserGetter')
const UserUpdater = require('./UserUpdater') const UserUpdater = require('./UserUpdater')
const Analytics = require('../Analytics/AnalyticsManager') const Analytics = require('../Analytics/AnalyticsManager')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const UserOnboardingEmailManager = require('./UserOnboardingEmailManager') const UserOnboardingEmailManager = require('./UserOnboardingEmailManager')
const UserPostRegistrationAnalyticsManager = require('./UserPostRegistrationAnalyticsManager') const UserPostRegistrationAnalyticsManager = require('./UserPostRegistrationAnalyticsManager')
const OError = require('@overleaf/o-error') const OError = require('@overleaf/o-error')
@ -36,9 +37,24 @@ async function _addAffiliation(user, affiliationOptions) {
} }
async function recordRegistrationEvent(user) { async function recordRegistrationEvent(user) {
let designSystemUpdatesAssignment = { variant: 'default' }
try {
designSystemUpdatesAssignment =
await SplitTestHandler.promises.getAssignmentForUser(
user._id,
'design-system-updates'
)
} catch (error) {
logger.error(
{ err: error },
'failed to get "design-system-updates" split test assignment'
)
}
try { try {
const segmentation = { const segmentation = {
'home-registration': 'default', 'home-registration': 'default',
'split-test-design-system-updates': designSystemUpdatesAssignment.variant,
} }
if (user.thirdPartyIdentifiers && user.thirdPartyIdentifiers.length > 0) { if (user.thirdPartyIdentifiers && user.thirdPartyIdentifiers.length > 0) {
segmentation.provider = user.thirdPartyIdentifiers[0].providerId segmentation.provider = user.thirdPartyIdentifiers[0].providerId

View file

@ -8,11 +8,13 @@ import { PendingPlanChange } from './pending-plan-change'
import { TrialEnding } from './trial-ending' import { TrialEnding } from './trial-ending'
import { PendingAdditionalLicenses } from './pending-additional-licenses' import { PendingAdditionalLicenses } from './pending-additional-licenses'
import { ContactSupportToChangeGroupPlan } from './contact-support-to-change-group-plan' import { ContactSupportToChangeGroupPlan } from './contact-support-to-change-group-plan'
import SubscriptionRemainder from './subscription-remainder'
import isInFreeTrial from '../../../../util/is-in-free-trial' import isInFreeTrial from '../../../../util/is-in-free-trial'
import { ChangePlanModal } from './change-plan/modals/change-plan-modal' import { ChangePlanModal } from './change-plan/modals/change-plan-modal'
import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan-modal' import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan-modal'
import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal' import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal' import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal'
import { isSplitTestEnabled } from '../../../../../../../../frontend/js/utils/splitTestUtils'
export function ActiveSubscription({ export function ActiveSubscription({
subscription, subscription,
@ -23,6 +25,10 @@ export function ActiveSubscription({
const { recurlyLoadError, setModalIdShown, showCancellation } = const { recurlyLoadError, setModalIdShown, showCancellation } =
useSubscriptionDashboardContext() useSubscriptionDashboardContext()
const isDesignSystemUpdatesEnabled = isSplitTestEnabled(
'design-system-updates'
)
if (showCancellation) return <CancelSubscription /> if (showCancellation) return <CancelSubscription />
return ( return (
@ -115,10 +121,25 @@ export function ActiveSubscription({
> >
{t('view_your_invoices')} {t('view_your_invoices')}
</a> </a>
{!recurlyLoadError && isDesignSystemUpdatesEnabled && (
<CancelSubscriptionButton className="btn btn-danger-ghost ms-1" />
)}
</p> </p>
{!recurlyLoadError && ( {!recurlyLoadError && (
<CancelSubscriptionButton subscription={subscription} /> <>
<br />
{!isDesignSystemUpdatesEnabled && (
<p>
<CancelSubscriptionButton className="btn btn-danger" />
</p>
)}
<p>
<i>
<SubscriptionRemainder subscription={subscription} />
</i>
</p>
</>
)} )}
<ChangePlanModal /> <ChangePlanModal />

View file

@ -1,67 +1,32 @@
import { useTranslation, Trans } from 'react-i18next' import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../../../../infrastructure/event-tracking' import * as eventTracking from '../../../../../../infrastructure/event-tracking'
import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context' import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
import { getSplitTestVariant } from '../../../../../../../../frontend/js/utils/splitTestUtils'
export function CancelSubscriptionButton({ export function CancelSubscriptionButton(
subscription, props: React.ComponentProps<'button'>
}: { ) {
subscription: RecurlySubscription
}) {
const { t } = useTranslation() const { t } = useTranslation()
const { recurlyLoadError, setShowCancellation } = const { recurlyLoadError, setShowCancellation } =
useSubscriptionDashboardContext() useSubscriptionDashboardContext()
const stillInATrial = const designSystemUpdatesVariant = getSplitTestVariant(
subscription.recurly.trialEndsAtFormatted && 'design-system-updates',
subscription.recurly.trial_ends_at && 'default'
new Date(subscription.recurly.trial_ends_at).getTime() > Date.now() )
function handleCancelSubscriptionClick() { function handleCancelSubscriptionClick() {
eventTracking.sendMB('subscription-page-cancel-button-click') eventTracking.sendMB('subscription-page-cancel-button-click', {
'split-test-design-system-updates': designSystemUpdatesVariant,
})
setShowCancellation(true) setShowCancellation(true)
} }
if (recurlyLoadError) return null if (recurlyLoadError) return null
return ( return (
<> <button onClick={handleCancelSubscriptionClick} {...props}>
<br /> {t('cancel_your_subscription')}
<p> </button>
<button
className="btn btn-danger"
onClick={handleCancelSubscriptionClick}
>
{t('cancel_your_subscription')}
</button>
</p>
<p>
<i>
{stillInATrial ? (
<Trans
i18nKey="subscription_will_remain_active_until_end_of_trial_period_x"
values={{
terminationDate: subscription.recurly.nextPaymentDueAt,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
) : (
<Trans
i18nKey="subscription_will_remain_active_until_end_of_billing_period_x"
values={{
terminationDate: subscription.recurly.nextPaymentDueAt,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
)}
</i>
</p>
</>
) )
} }

View file

@ -0,0 +1,39 @@
import { Trans } from 'react-i18next'
import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
type SubscriptionRemainderProps = {
subscription: RecurlySubscription
}
function SubscriptionRemainder({ subscription }: SubscriptionRemainderProps) {
const stillInATrial =
subscription.recurly.trialEndsAtFormatted &&
subscription.recurly.trial_ends_at &&
new Date(subscription.recurly.trial_ends_at).getTime() > Date.now()
return stillInATrial ? (
<Trans
i18nKey="subscription_will_remain_active_until_end_of_trial_period_x"
values={{
terminationDate: subscription.recurly.nextPaymentDueAt,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
) : (
<Trans
i18nKey="subscription_will_remain_active_until_end_of_billing_period_x"
values={{
terminationDate: subscription.recurly.nextPaymentDueAt,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
)
}
export default SubscriptionRemainder

View file

@ -17,6 +17,7 @@ import TosAgreementNotice from './tos-agreement-notice'
import SubmitButton from './submit-button' import SubmitButton from './submit-button'
import ThreeDSecure from './three-d-secure' import ThreeDSecure from './three-d-secure'
import getMeta from '../../../../../utils/meta' import getMeta from '../../../../../utils/meta'
import { getSplitTestVariant } from '../../../../../../../frontend/js/utils/splitTestUtils'
import { postJSON } from '../../../../../infrastructure/fetch-json' import { postJSON } from '../../../../../infrastructure/fetch-json'
import * as eventTracking from '../../../../../infrastructure/event-tracking' import * as eventTracking from '../../../../../infrastructure/event-tracking'
import classnames from 'classnames' import classnames from 'classnames'
@ -69,6 +70,10 @@ function CheckoutPanel() {
'#add-company-details-checkbox' '#add-company-details-checkbox'
)?.checked )?.checked
) )
const designSystemUpdatesVariant = getSplitTestVariant(
'design-system-updates',
'default'
)
const completeSubscription = useCallback( const completeSubscription = useCallback(
async ( async (
@ -140,6 +145,7 @@ function CheckoutPanel() {
plan_code: postData.subscriptionDetails.plan_code, plan_code: postData.subscriptionDetails.plan_code,
coupon_code: postData.subscriptionDetails.coupon_code, coupon_code: postData.subscriptionDetails.coupon_code,
isPaypal: postData.subscriptionDetails.isPaypal, isPaypal: postData.subscriptionDetails.isPaypal,
'split-test-design-system-updates': designSystemUpdatesVariant,
}) })
eventTracking.send( eventTracking.send(
'subscription-funnel', 'subscription-funnel',
@ -172,6 +178,7 @@ function CheckoutPanel() {
} }
}, },
[ [
designSystemUpdatesVariant,
ITMCampaign, ITMCampaign,
ITMContent, ITMContent,
ITMReferrer, ITMReferrer,

View file

@ -11,6 +11,7 @@ import {
import { currencies, CurrencyCode, CurrencySymbol } from '../data/currency' import { currencies, CurrencyCode, CurrencySymbol } from '../data/currency'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import getMeta from '../../../utils/meta' import getMeta from '../../../utils/meta'
import { getSplitTestVariant } from '../../../../../frontend/js/utils/splitTestUtils'
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '../../../infrastructure/event-tracking'
import { import {
PaymentContextValue, PaymentContextValue,
@ -38,6 +39,10 @@ function usePayment({ publicKey }: RecurlyOptions) {
'ol-recommendedCurrency' 'ol-recommendedCurrency'
) )
const planCode: string = getMeta('ol-planCode') const planCode: string = getMeta('ol-planCode')
const designSystemUpdatesVariant = getSplitTestVariant(
'design-system-updates',
'default'
)
const [planName, setPlanName] = useState(plan.name) const [planName, setPlanName] = useState(plan.name)
const [recurlyLoading, setRecurlyLoading] = useState(true) const [recurlyLoading, setRecurlyLoading] = useState(true)
@ -98,6 +103,7 @@ function usePayment({ publicKey }: RecurlyOptions) {
eventTracking.sendMB('payment-page-view', { eventTracking.sendMB('payment-page-view', {
plan: planCode, plan: planCode,
currency: currencyCode, currency: currencyCode,
'split-test-design-system-updates': designSystemUpdatesVariant,
}) })
eventTracking.send( eventTracking.send(
'subscription-funnel', 'subscription-funnel',
@ -142,6 +148,7 @@ function usePayment({ publicKey }: RecurlyOptions) {
setupPricing() setupPricing()
}, [ }, [
designSystemUpdatesVariant,
initialCountry, initialCountry,
initialCouponCode, initialCouponCode,
initiallySelectedCurrencyCode, initiallySelectedCurrencyCode,

View file

@ -4,6 +4,10 @@ export function isSplitTestEnabled(name: string) {
return getMeta('ol-splitTestVariants')?.[name] === 'enabled' return getMeta('ol-splitTestVariants')?.[name] === 'enabled'
} }
export function getSplitTestVariant(name: string, fallback?: string) {
return getMeta('ol-splitTestVariants')?.[name] || fallback
}
export function parseIntFromSplitTest(name: string, defaultValue: number) { export function parseIntFromSplitTest(name: string, defaultValue: number) {
const v = getMeta('ol-splitTestVariants')?.[name] const v = getMeta('ol-splitTestVariants')?.[name]
const n = parseInt(v, 10) const n = parseInt(v, 10)

View file

@ -34,6 +34,13 @@ describe('RecurlyEventHandler', function () {
recordEventForUser: sinon.stub(), recordEventForUser: sinon.stub(),
setUserPropertyForUser: sinon.stub(), setUserPropertyForUser: sinon.stub(),
}), }),
'../SplitTests/SplitTestHandler': (this.SplitTestHandler = {
promises: {
getAssignmentForUser: sinon.stub().resolves({
variant: 'default',
}),
},
}),
}, },
}) })
}) })
@ -159,12 +166,17 @@ describe('RecurlyEventHandler', function () {
) )
}) })
it('with canceled_subscription_notification', function () { it('with canceled_subscription_notification', async function () {
this.eventData.subscription.state = 'cancelled' this.eventData.subscription.state = 'cancelled'
this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'canceled_subscription_notification', 'canceled_subscription_notification',
this.eventData this.eventData
) )
sinon.assert.calledWith(
this.SplitTestHandler.promises.getAssignmentForUser,
this.userId,
'design-system-updates'
)
sinon.assert.calledWith( sinon.assert.calledWith(
this.AnalyticsManager.recordEventForUser, this.AnalyticsManager.recordEventForUser,
this.userId, this.userId,
@ -174,6 +186,7 @@ describe('RecurlyEventHandler', function () {
quantity: 1, quantity: 1,
is_trial: true, is_trial: true,
subscriptionId: this.eventData.subscription.uuid, subscriptionId: this.eventData.subscription.uuid,
'split-test-design-system-updates': 'default',
} }
) )
sinon.assert.calledWith( sinon.assert.calledWith(