Merge pull request #4068 from overleaf/ab-split-test-user-properties

Store assigned split tests as user properties

GitOrigin-RevId: 1cc09d4d8f19badb73e87c46064bdeac131dd307
This commit is contained in:
Alexandre Bourdin 2021-05-26 14:37:15 +02:00 committed by Copybot
parent 551e2bfb5c
commit a65c5dde01
6 changed files with 275 additions and 196 deletions

View file

@ -37,7 +37,7 @@ const BrandVariationsHandler = require('../BrandVariations/BrandVariationsHandle
const UserController = require('../User/UserController')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const Modules = require('../../infrastructure/Modules')
const { getTestSegmentation } = require('../SplitTests/SplitTestHandler')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const { getNewLogsUIVariantForUser } = require('../Helpers/NewLogsUI')
const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
@ -802,22 +802,10 @@ const ProjectController = {
}
}
const trackPdfDownload =
Settings.enablePdfCaching &&
(user.alphaProgram ||
(user.betaProgram &&
getTestSegmentation(userId, 'track_pdf_download').variant ===
'enabled'))
const enablePdfCaching =
Settings.enablePdfCaching &&
shouldDisplayFeature(
'enable_pdf_caching',
user.alphaProgram ||
(user.betaProgram &&
getTestSegmentation(userId, 'enable_pdf_caching')
.variant === 'enabled')
)
let trackPdfDownload = false
let enablePdfCaching = false
const render = () => {
res.render('project/editor', {
title: project.name,
priority_title: true,
@ -900,6 +888,52 @@ const ProjectController = {
})
timer.done()
}
Promise.all([
async () => {
if (Settings.enablePdfCaching) {
if (user.alphaProgram) {
trackPdfDownload = true
} else if (user.betaProgram) {
const testSegmentation = await SplitTestHandler.promises.getTestSegmentation(
userId,
'track_pdf_download'
)
trackPdfDownload =
testSegmentation.enabled &&
testSegmentation.variant === 'enabled'
}
}
},
async () => {
if (Settings.enablePdfCaching) {
if (user.alphaProgram) {
enablePdfCaching = shouldDisplayFeature(
'enable_pdf_caching',
true
)
} else if (user.betaProgram) {
const testSegmentation = await SplitTestHandler.promises.getTestSegmentation(
userId,
'enable_pdf_caching'
)
trackPdfDownload = shouldDisplayFeature(
'enable_pdf_caching',
testSegmentation.enabled &&
testSegmentation.variant === 'enabled'
)
}
}
},
])
.then(() => {
render()
})
.catch(error => {
logger.error({ err: error }, 'Failed to get test segmentation')
render()
})
}
)
}
)

View file

@ -71,7 +71,7 @@ async function createBasicProject(ownerId, projectName) {
async function createExampleProject(ownerId, projectName) {
const project = await _createBlankProject(ownerId, projectName)
const testSegmentation = SplitTestHandler.getTestSegmentation(
const testSegmentation = await SplitTestHandler.promises.getTestSegmentation(
ownerId,
EXAMPLE_PROJECT_SPLITTEST_ID
)

View file

@ -1,7 +1,23 @@
const UserGetter = require('../User/UserGetter')
const UserUpdater = require('../User/UserUpdater')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const Settings = require('settings-sharelatex')
const _ = require('lodash')
const crypto = require('crypto')
const OError = require('@overleaf/o-error')
const { callbackify } = require('util')
const duplicateSplitTest = _.findKey(
_.groupBy(Settings.splitTests, 'id'),
group => {
return group.length > 1
}
)
if (duplicateSplitTest) {
throw new OError(
`Split test IDs must be unique: ${duplicateSplitTest} is defined at least twice`
)
}
const ACTIVE_SPLIT_TESTS = []
for (const splitTest of Settings.splitTests) {
@ -31,23 +47,24 @@ for (const splitTest of Settings.splitTests) {
}
}
function getTestSegmentation(userId, splitTestId) {
async function getTestSegmentation(userId, splitTestId) {
const splitTest = _.find(ACTIVE_SPLIT_TESTS, ['id', splitTestId])
if (splitTest) {
let userIdAsPercentile = _getPercentile(userId, splitTestId)
for (const variant of splitTest.variants) {
if (userIdAsPercentile < variant.rolloutPercent) {
const alreadyAssignedVariant = await getAlreadyAssignedVariant(
userId,
splitTestId
)
if (alreadyAssignedVariant) {
return {
enabled: true,
variant: variant.id,
variant: alreadyAssignedVariant,
}
} else {
userIdAsPercentile -= variant.rolloutPercent
}
}
const variant = await assignUserToVariant(userId, splitTest)
return {
enabled: true,
variant: 'default',
variant,
}
}
}
return {
@ -55,6 +72,38 @@ function getTestSegmentation(userId, splitTestId) {
}
}
async function getAlreadyAssignedVariant(userId, splitTestId) {
const user = await UserGetter.promises.getUser(userId, { splitTests: 1 })
if (user && user.splitTests) {
return user.splitTests[splitTestId]
}
return undefined
}
async function assignUserToVariant(userId, splitTest) {
let userIdAsPercentile = await _getPercentile(userId, splitTest.id)
let selectedVariant = 'default'
for (const variant of splitTest.variants) {
if (userIdAsPercentile < variant.rolloutPercent) {
selectedVariant = variant.id
break
} else {
userIdAsPercentile -= variant.rolloutPercent
}
}
await UserUpdater.promises.updateUser(userId, {
$set: {
[`splitTests.${splitTest.id}`]: selectedVariant,
},
})
AnalyticsManager.setUserProperty(
userId,
`split-test-${splitTest.id}`,
selectedVariant
)
return selectedVariant
}
function _getPercentile(userId, splitTestId) {
const hash = crypto
.createHash('md5')
@ -65,5 +114,8 @@ function _getPercentile(userId, splitTestId) {
}
module.exports = {
getTestSegmentation: callbackify(getTestSegmentation),
promises: {
getTestSegmentation,
},
}

View file

@ -91,14 +91,11 @@ async function paymentPage(req, res) {
}
}
function userSubscriptionPage(req, res, next) {
async function userSubscriptionPage(req, res) {
const user = AuthenticationController.getSessionUser(req)
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
user,
function (error, results) {
if (error) {
return next(error)
}
const results = await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
user
)
const {
personalSubscription,
memberGroupSubscriptions,
@ -108,12 +105,9 @@ function userSubscriptionPage(req, res, next) {
managedPublishers,
v1SubscriptionStatus,
} = results
LimitationsManager.userHasV1OrV2Subscription(
user,
function (err, hasSubscription) {
if (error) {
return next(error)
}
const hasSubscription = await LimitationsManager.promises.userHasV1OrV2Subscription(
user
)
const fromPlansPage = req.query.hasSubscription
const plans = SubscriptionViewModelBuilder.buildPlansList(
personalSubscription ? personalSubscription.plan : undefined
@ -132,7 +126,8 @@ function userSubscriptionPage(req, res, next) {
) {
AnalyticsManager.recordEvent(user._id, 'subscription-page-view')
} else {
const testSegmentation = SplitTestHandler.getTestSegmentation(
try {
const testSegmentation = await SplitTestHandler.promises.getTestSegmentation(
user._id,
SUBSCRIPTION_PAGE_SPLIT_TEST
)
@ -146,6 +141,13 @@ function userSubscriptionPage(req, res, next) {
} else {
AnalyticsManager.recordEvent(user._id, 'subscription-page-view')
}
} catch (error) {
logger.error(
{ err: error },
`Failed to get segmentation for user '${user._id}' and split test '${SUBSCRIPTION_PAGE_SPLIT_TEST}'`
)
AnalyticsManager.recordEvent(user._id, 'subscription-page-view')
}
}
const data = {
@ -164,10 +166,6 @@ function userSubscriptionPage(req, res, next) {
v1SubscriptionStatus,
}
res.render('subscriptions/dashboard', data)
}
)
}
)
}
function createSubscription(req, res, next) {
@ -481,7 +479,7 @@ async function refreshUserFeatures(req, res) {
module.exports = {
plansPage: expressify(plansPage),
paymentPage: expressify(paymentPage),
userSubscriptionPage,
userSubscriptionPage: expressify(userSubscriptionPage),
createSubscription,
successfulSubscription,
cancelSubscription,

View file

@ -165,6 +165,7 @@ const UserSchema = new Schema({
},
onboardingEmailSentAt: { type: Date },
auditLog: [AuditLogEntrySchema],
splitTests: Schema.Types.Mixed,
})
exports.User = mongoose.model('User', UserSchema)

View file

@ -300,9 +300,7 @@ describe('SubscriptionController', function () {
describe('userSubscriptionPage', function () {
beforeEach(function (done) {
this.SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(
1,
null,
this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
{
personalSubscription: (this.personalSubscription = {
'personal-subscription': 'mock',
@ -315,11 +313,7 @@ describe('SubscriptionController', function () {
this.SubscriptionViewModelBuilder.buildPlansList.returns(
(this.plans = { plans: 'mock' })
)
this.LimitationsManager.userHasV1OrV2Subscription.callsArgWith(
1,
null,
false
)
this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(false)
this.res.render = (view, data) => {
this.data = data
expect(view).to.equal('subscriptions/dashboard')