Merge pull request #16307 from overleaf/ab-split-tests-never-throw

[web] Guarantee split test assignments can never throw and cleanup boilterplate error handling

GitOrigin-RevId: ab50abde6e0632c5a9625b5c3d3e98f3383cc56c
This commit is contained in:
Alexandre Bourdin 2024-01-18 15:25:01 +01:00 committed by Copybot
parent 29d41497ef
commit 3b9eb4e8aa
5 changed files with 149 additions and 317 deletions

View file

@ -542,78 +542,31 @@ const ProjectController = {
req, req,
res, res,
'project-share-modal-paywall', 'project-share-modal-paywall',
{}, cb
() => {
// do not fail editor load if assignment fails
cb()
}
) )
}, },
sharingModalNullTest(cb) { sharingModalNullTest(cb) {
// null test targeting logged in users, for front-end side // null test targeting logged in users, for front-end side
SplitTestHandler.getAssignment( SplitTestHandler.getAssignment(req, res, 'null-test-share-modal', cb)
req,
res,
'null-test-share-modal',
{},
() => {
// do not fail editor load if assignment fails
cb()
}
)
}, },
pdfjsAssignment(cb) { pdfjsAssignment(cb) {
SplitTestHandler.getAssignment( SplitTestHandler.getAssignment(req, res, 'pdfjs-40', cb)
req,
res,
'pdfjs-40',
{},
(error, assignment) => {
// do not fail editor load if assignment fails
if (error) {
cb(null, { variant: 'default' })
} else {
cb(null, assignment)
}
}
)
}, },
latexLogParserAssignment(cb) { latexLogParserAssignment(cb) {
SplitTestHandler.getAssignment( SplitTestHandler.getAssignment(req, res, 'latex-log-parser', cb)
req,
res,
'latex-log-parser',
(error, assignment) => {
// do not fail editor load if assignment fails
if (error) {
cb(null, { variant: 'default' })
} else {
cb(null, assignment)
}
}
)
}, },
trackPdfDownloadAssignment(cb) { trackPdfDownloadAssignment(cb) {
SplitTestHandler.getAssignment(req, res, 'track-pdf-download', () => { SplitTestHandler.getAssignment(req, res, 'track-pdf-download', cb)
// We'll pick up the assignment from the res.locals assignment.
cb()
})
}, },
pdfCachingModeAssignment(cb) { pdfCachingModeAssignment(cb) {
SplitTestHandler.getAssignment(req, res, 'pdf-caching-mode', () => { SplitTestHandler.getAssignment(req, res, 'pdf-caching-mode', cb)
// We'll pick up the assignment from the res.locals assignment.
cb()
})
}, },
pdfCachingPrefetchingAssignment(cb) { pdfCachingPrefetchingAssignment(cb) {
SplitTestHandler.getAssignment( SplitTestHandler.getAssignment(
req, req,
res, res,
'pdf-caching-prefetching', 'pdf-caching-prefetching',
() => { cb
// We'll pick up the assignment from the res.locals assignment.
cb()
}
) )
}, },
pdfCachingPrefetchLargeAssignment(cb) { pdfCachingPrefetchLargeAssignment(cb) {
@ -621,10 +574,7 @@ const ProjectController = {
req, req,
res, res,
'pdf-caching-prefetch-large', 'pdf-caching-prefetch-large',
() => { cb
// We'll pick up the assignment from the res.locals assignment.
cb()
}
) )
}, },
pdfCachingCachedUrlLookupAssignment(cb) { pdfCachingCachedUrlLookupAssignment(cb) {
@ -632,10 +582,7 @@ const ProjectController = {
req, req,
res, res,
'pdf-caching-cached-url-lookup', 'pdf-caching-cached-url-lookup',
() => { cb
// We'll pick up the assignment from the res.locals assignment.
cb()
}
) )
}, },
tableGeneratorPromotionAssignment(cb) { tableGeneratorPromotionAssignment(cb) {
@ -643,52 +590,17 @@ const ProjectController = {
req, req,
res, res,
'table-generator-promotion', 'table-generator-promotion',
() => { cb
// We'll pick up the assignment from the res.locals assignment.
cb()
}
) )
}, },
personalAccessTokenAssignment(cb) { personalAccessTokenAssignment(cb) {
SplitTestHandler.getAssignment( SplitTestHandler.getAssignment(req, res, 'personal-access-token', cb)
req,
res,
'personal-access-token',
(error, assignment) => {
// do not fail editor load if assignment fails
if (error) {
cb(null, { variant: 'default' })
} else {
cb(null, assignment)
}
}
)
}, },
idePageAssignment(cb) { idePageAssignment(cb) {
SplitTestHandler.getAssignment( SplitTestHandler.getAssignment(req, res, 'ide-page', cb)
req,
res,
'ide-page',
(error, assignment) => {
// do not fail editor load if assignment fails
if (error) {
cb(null, { variant: 'default' })
} else {
cb(null, assignment)
}
}
)
}, },
writefullIntegrationAssignment(cb) { writefullIntegrationAssignment(cb) {
SplitTestHandler.getAssignment( SplitTestHandler.getAssignment(req, res, 'writefull-integration', cb)
req,
res,
'writefull-integration',
() => {
// We'll pick up the assignment from the res.locals assignment.
cb()
}
)
}, },
projectTags(cb) { projectTags(cb) {
if (!userId) { if (!userId) {

View file

@ -326,19 +326,13 @@ async function projectListPage(req, res, next) {
) )
} }
} }
try {
// The assignment will be picked up via 'ol-splitTestVariants' in react. // The assignment will be picked up via 'ol-splitTestVariants' in react.
await SplitTestHandler.promises.getAssignment( await SplitTestHandler.promises.getAssignment(
req, req,
res, res,
'download-pdf-dashboard' 'download-pdf-dashboard'
) )
} catch (err) {
logger.error(
{ err },
'failed to get "download-pdf-dashboard" split test assignment'
)
}
const hasPaidAffiliation = userAffiliations.some( const hasPaidAffiliation = userAffiliations.some(
affiliation => affiliation.licence && affiliation.licence !== 'free' affiliation => affiliation.licence && affiliation.licence !== 'free'
@ -370,18 +364,11 @@ async function projectListPage(req, res, next) {
} }
} }
try { await SplitTestHandler.promises.getAssignment(
await SplitTestHandler.promises.getAssignment( req,
req, res,
res, 'writefull-integration'
'writefull-integration' )
)
} catch (err) {
logger.warn(
{ err },
'failed to get "writefull-integration" split test assignment'
)
}
let showInrGeoBanner, inrGeoBannerSplitTestName let showInrGeoBanner, inrGeoBannerSplitTestName
let inrGeoBannerVariant = 'default' let inrGeoBannerVariant = 'default'
@ -390,63 +377,35 @@ async function projectListPage(req, res, next) {
if (usersBestSubscription?.type === 'free') { if (usersBestSubscription?.type === 'free') {
const { currencyCode, countryCode } = const { currencyCode, countryCode } =
await GeoIpLookup.promises.getCurrencyCode(req.ip) await GeoIpLookup.promises.getCurrencyCode(req.ip)
let inrGeoPricingVariant = 'default' // Split test is kept active, but all users geolocated in India can
try { // now use the INR currency (See #13507)
// Split test is kept active, but all users geolocated in India can const { variant: inrGeoPricingVariant } =
// now use the INR currency (See #13507) await SplitTestHandler.promises.getAssignment(req, res, 'geo-pricing-inr')
const inrGeoPricingAssignment = const latamGeoPricingAssignment =
await SplitTestHandler.promises.getAssignment( await SplitTestHandler.promises.getAssignment(
req, req,
res, res,
'geo-pricing-inr' 'geo-pricing-latam'
)
inrGeoPricingVariant = inrGeoPricingAssignment.variant
} catch (error) {
logger.error(
{ err: error },
'Failed to get geo-pricing-inr split test assignment'
)
}
try {
const latamGeoPricingAssignment =
await SplitTestHandler.promises.getAssignment(
req,
res,
'geo-pricing-latam'
)
showLATAMBanner =
latamGeoPricingAssignment.variant === 'latam' &&
['BR', 'MX', 'CO', 'CL', 'PE'].includes(countryCode)
// LATAM Banner needs to know which currency to display
if (showLATAMBanner) {
recommendedCurrency = currencyCode
}
} catch (error) {
logger.error(
{ err: error },
'Failed to get geo-pricing-latam split test assignment'
) )
showLATAMBanner =
latamGeoPricingAssignment.variant === 'latam' &&
['BR', 'MX', 'CO', 'CL', 'PE'].includes(countryCode)
// LATAM Banner needs to know which currency to display
if (showLATAMBanner) {
recommendedCurrency = currencyCode
} }
if (countryCode === 'IN') { if (countryCode === 'IN') {
inrGeoBannerSplitTestName = inrGeoBannerSplitTestName =
inrGeoPricingVariant === 'inr' inrGeoPricingVariant === 'inr'
? 'geo-banners-inr-2' ? 'geo-banners-inr-2'
: 'geo-banners-inr-1' : 'geo-banners-inr-1'
try { const geoBannerAssignment = await SplitTestHandler.promises.getAssignment(
const geoBannerAssignment = req,
await SplitTestHandler.promises.getAssignment( res,
req, inrGeoBannerSplitTestName
res, )
inrGeoBannerSplitTestName showInrGeoBanner = true
) inrGeoBannerVariant = geoBannerAssignment.variant
showInrGeoBanner = true
inrGeoBannerVariant = geoBannerAssignment.variant
} catch (error) {
logger.error(
{ err: error },
`Failed to get INR geo banner lookup or assignment (${inrGeoBannerSplitTestName})`
)
}
} }
} }

View file

@ -13,6 +13,7 @@ const Features = require('../../infrastructure/Features')
const SplitTestUtils = require('./SplitTestUtils') const SplitTestUtils = require('./SplitTestUtils')
const Settings = require('@overleaf/settings') const Settings = require('@overleaf/settings')
const SessionManager = require('../Authentication/SessionManager') const SessionManager = require('../Authentication/SessionManager')
const logger = require('@overleaf/logger')
const DEFAULT_VARIANT = 'default' const DEFAULT_VARIANT = 'default'
const ALPHA_PHASE = 'alpha' const ALPHA_PHASE = 'alpha'
@ -54,37 +55,42 @@ async function getAssignment(req, res, splitTestName, { sync = false } = {}) {
const query = req.query || {} const query = req.query || {}
let assignment let assignment
if (!Features.hasFeature('saas')) { try {
assignment = _getNonSaasAssignment(splitTestName) if (!Features.hasFeature('saas')) {
} else { assignment = _getNonSaasAssignment(splitTestName)
await _loadSplitTestInfoInLocals(res.locals, splitTestName, req.session) } else {
await _loadSplitTestInfoInLocals(res.locals, splitTestName, req.session)
// Check the query string for an override, ignoring an invalid value // Check the query string for an override, ignoring an invalid value
const queryVariant = query[splitTestName] const queryVariant = query[splitTestName]
if (queryVariant) { if (queryVariant) {
const variants = await _getVariantNames(splitTestName) const variants = await _getVariantNames(splitTestName)
if (variants.includes(queryVariant)) { if (variants.includes(queryVariant)) {
assignment = { assignment = {
variant: queryVariant, variant: queryVariant,
analytics: { analytics: {
segmentation: {}, segmentation: {},
}, },
}
} }
} }
}
if (!assignment) { if (!assignment) {
const { userId, analyticsId } = AnalyticsManager.getIdsFromSession( const { userId, analyticsId } = AnalyticsManager.getIdsFromSession(
req.session req.session
) )
assignment = await _getAssignment(splitTestName, { assignment = await _getAssignment(splitTestName, {
analyticsId, analyticsId,
userId, userId,
session: req.session, session: req.session,
sync, sync,
}) })
_collectSessionStats(req.session) _collectSessionStats(req.session)
}
} }
} catch (error) {
logger.error({ err: error }, 'Failed to get split test assignment')
assignment = DEFAULT_ASSIGNMENT
} }
LocalsHelper.setSplitTestVariant( LocalsHelper.setSplitTestVariant(
@ -112,12 +118,17 @@ async function getAssignmentForUser(
splitTestName, splitTestName,
{ sync = false } = {} { sync = false } = {}
) { ) {
if (!Features.hasFeature('saas')) { try {
return _getNonSaasAssignment(splitTestName) if (!Features.hasFeature('saas')) {
} return _getNonSaasAssignment(splitTestName)
}
const analyticsId = await UserAnalyticsIdCache.get(userId) const analyticsId = await UserAnalyticsIdCache.get(userId)
return _getAssignment(splitTestName, { analyticsId, userId, sync }) return _getAssignment(splitTestName, { analyticsId, userId, sync })
} catch (error) {
logger.error({ err: error }, 'Failed to get split test assignment for user')
return DEFAULT_ASSIGNMENT
}
} }
/** /**
@ -136,16 +147,24 @@ async function getAssignmentForMongoUser(
splitTestName, splitTestName,
{ sync = false } = {} { sync = false } = {}
) { ) {
if (!Features.hasFeature('saas')) { try {
return _getNonSaasAssignment(splitTestName) if (!Features.hasFeature('saas')) {
} return _getNonSaasAssignment(splitTestName)
}
return _getAssignment(splitTestName, { return _getAssignment(splitTestName, {
analyticsId: await UserAnalyticsIdCache.get(user._id), analyticsId: await UserAnalyticsIdCache.get(user._id),
sync, sync,
user, user,
userId: user._id.toString(), userId: user._id.toString(),
}) })
} catch (error) {
logger.error(
{ err: error },
'Failed to get split test assignment for mongo user'
)
return DEFAULT_ASSIGNMENT
}
} }
/** /**

View file

@ -76,39 +76,23 @@ async function plansPage(req, res) {
geoPricingINRTestVariant === 'inr' geoPricingINRTestVariant === 'inr'
? 'geo-banners-inr-2' ? 'geo-banners-inr-2'
: 'geo-banners-inr-1' : 'geo-banners-inr-1'
try { const geoBannerAssignment = await SplitTestHandler.promises.getAssignment(
const geoBannerAssignment = await SplitTestHandler.promises.getAssignment( req,
req, res,
res, inrGeoBannerSplitTestName
inrGeoBannerSplitTestName )
) inrGeoBannerVariant = geoBannerAssignment.variant
inrGeoBannerVariant = geoBannerAssignment.variant if (inrGeoBannerVariant !== 'default') {
if (inrGeoBannerVariant !== 'default') { showInrGeoBanner = true
showInrGeoBanner = true
}
} catch (error) {
logger.error(
{ err: error },
`Failed to get INR geo banner lookup or assignment (${inrGeoBannerSplitTestName})`
)
} }
} }
// annual plans with the free trial option split test - nudge variant // annual plans with the free trial option split test - nudge variant
let annualTrialsAssignment = { variant: 'default' } const annualTrialsAssignment = await SplitTestHandler.promises.getAssignment(
req,
try { res,
annualTrialsAssignment = await SplitTestHandler.promises.getAssignment( 'annual-trials'
req, )
res,
'annual-trials'
)
} catch (error) {
logger.error(
{ err: error },
'failed to get "annualTrialsAssignment" split test assignment'
)
}
const websiteRedesignVariant = const websiteRedesignVariant =
res.locals.splitTestVariants?.['website-redesign'] res.locals.splitTestVariants?.['website-redesign']
@ -285,40 +269,20 @@ async function interstitialPaymentPage(req, res) {
geoPricingINRTestVariant === 'inr' geoPricingINRTestVariant === 'inr'
? 'geo-banners-inr-2' ? 'geo-banners-inr-2'
: 'geo-banners-inr-1' : 'geo-banners-inr-1'
try { const geoBannerAssignment = await SplitTestHandler.promises.getAssignment(
const geoBannerAssignment = req,
await SplitTestHandler.promises.getAssignment( res,
req, inrGeoBannerSplitTestName
res, )
inrGeoBannerSplitTestName inrGeoBannerVariant = geoBannerAssignment.variant
) if (inrGeoBannerVariant !== 'default') {
inrGeoBannerVariant = geoBannerAssignment.variant showInrGeoBanner = true
if (inrGeoBannerVariant !== 'default') {
showInrGeoBanner = true
}
} catch (error) {
logger.error(
{ err: error },
`Failed to get INR geo banner lookup or assignment (${inrGeoBannerSplitTestName})`
)
} }
} }
// annual plans with the free trial option split test - nudge variant // annual plans with the free trial option split test - nudge variant
let annualTrialsAssignment = { variant: 'default' } const annualTrialsAssignment =
await SplitTestHandler.promises.getAssignment(req, res, 'annual-trials')
try {
annualTrialsAssignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'annual-trials'
)
} catch (error) {
logger.error(
{ err: error },
'failed to get "annualTrialsAssignment" split test assignment'
)
}
const paywallPlansPageViewSegmentation = { const paywallPlansPageViewSegmentation = {
currency: recommendedCurrency, currency: recommendedCurrency,
@ -680,36 +644,21 @@ async function _getRecommendedCurrency(req, res) {
} }
const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(ip) const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(ip)
const countryCode = currencyLookup.countryCode const countryCode = currencyLookup.countryCode
let assignmentINR, assignmentLATAM
let recommendedCurrency = currencyLookup.currencyCode let recommendedCurrency = currencyLookup.currencyCode
// for #12703 // for #12703
try { // Split test is kept active, but all users geolocated in India can
// Split test is kept active, but all users geolocated in India can // now use the INR currency (See #13507)
// now use the INR currency (See #13507) const assignmentINR = await SplitTestHandler.promises.getAssignment(
assignmentINR = await SplitTestHandler.promises.getAssignment( req,
req, res,
res, 'geo-pricing-inr'
'geo-pricing-inr' )
)
} catch (error) {
logger.error(
{ err: error },
'Failed to get assignment for geo-pricing-inr test'
)
}
// for #13559 // for #13559
try { const assignmentLATAM = await SplitTestHandler.promises.getAssignment(
assignmentLATAM = await SplitTestHandler.promises.getAssignment( req,
req, res,
res, 'geo-pricing-latam'
'geo-pricing-latam' )
)
} catch (error) {
logger.error(
{ err: error },
'Failed to get assignment for geo-pricing-latam test'
)
}
if ( if (
['BRL', 'MXN', 'COP', 'CLP', 'PEN'].includes(recommendedCurrency) && ['BRL', 'MXN', 'COP', 'CLP', 'PEN'].includes(recommendedCurrency) &&
assignmentLATAM?.variant !== 'latam' assignmentLATAM?.variant !== 'latam'

View file

@ -74,23 +74,16 @@ async function settingsPage(req, res) {
// if not already enabled, use a split test to determine whether to offer personal access tokens // if not already enabled, use a split test to determine whether to offer personal access tokens
let optionalPersonalAccessToken = false let optionalPersonalAccessToken = false
if (!showPersonalAccessToken) { if (!showPersonalAccessToken) {
try { const { variant } = await SplitTestHandler.promises.getAssignment(
const { variant } = await SplitTestHandler.promises.getAssignment( req,
req, res,
res, 'personal-access-token'
'personal-access-token' )
) optionalPersonalAccessToken = variant === 'enabled' // `?personal-access-token=enabled`
optionalPersonalAccessToken = variant === 'enabled' // `?personal-access-token=enabled`
} catch (error) {
logger.error(
{ err: error },
'Failed to get personal-access-token split test assignment'
)
}
} }
// eslint-disable-next-line no-unused-vars -- getAssignment sets res.locals, which will pass to the splitTest context // getAssignment sets res.locals, which will pass to the splitTest context
const writefullSplitTest = await SplitTestHandler.promises.getAssignment( await SplitTestHandler.promises.getAssignment(
req, req,
res, res,
'writefull-integration' 'writefull-integration'