mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
551e2bfb5c
commit
a65c5dde01
6 changed files with 275 additions and 196 deletions
|
@ -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()
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -165,6 +165,7 @@ const UserSchema = new Schema({
|
|||
},
|
||||
onboardingEmailSentAt: { type: Date },
|
||||
auditLog: [AuditLogEntrySchema],
|
||||
splitTests: Schema.Types.Mixed,
|
||||
})
|
||||
|
||||
exports.User = mongoose.model('User', UserSchema)
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue