Files
overleaf-cep/services/web/app/src/Features/SplitTests/SplitTestHandler.js
Alexandre Bourdin 51546b29c4 Merge pull request #4346 from overleaf/ab-configurable-split-test-2
Configurable Split Tests

GitOrigin-RevId: e648a77848ddb8b8b55a95887f87cf7cdd300ee9
2021-07-28 12:36:27 +00:00

128 lines
3.3 KiB
JavaScript

const UserGetter = require('../User/UserGetter')
const UserUpdater = require('../User/UserUpdater')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const Settings = require('@overleaf/settings')
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) {
for (const variant of splitTest.variants) {
if (variant.id === 'default') {
throw new OError(
`Split test variant ID cannot be 'default' (reserved value), defined in split test ${JSON.stringify(
splitTest
)}`
)
}
}
const totalVariantsRolloutPercent = _.sumBy(
splitTest.variants,
'rolloutPercent'
)
if (splitTest.active) {
if (totalVariantsRolloutPercent > 100) {
for (const variant of splitTest.variants) {
variant.rolloutPercent =
(variant.rolloutPercent * 100) / totalVariantsRolloutPercent
}
}
if (totalVariantsRolloutPercent > 0) {
ACTIVE_SPLIT_TESTS.push(splitTest)
}
}
}
async function getTestSegmentation(userId, splitTestId) {
const splitTest = _.find(ACTIVE_SPLIT_TESTS, ['id', splitTestId])
if (splitTest) {
const alreadyAssignedVariant = await getAlreadyAssignedVariant(
userId,
splitTestId
)
if (alreadyAssignedVariant) {
return {
enabled: true,
variant: alreadyAssignedVariant,
}
} else {
const variant = await assignUserToVariant(userId, splitTest)
return {
enabled: true,
variant,
}
}
}
return {
enabled: false,
}
}
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')
.update(userId + splitTestId)
.digest('hex')
const hashPrefix = hash.substr(0, 8)
return Math.floor((parseInt(hashPrefix, 16) / 0xffffffff) * 100)
}
module.exports = {
/**
* @deprecated: use SplitTestV2Handler.getAssignment instead
*/
getTestSegmentation: callbackify(getTestSegmentation),
promises: {
/**
* @deprecated: use SplitTestV2Handler.promises.getAssignment instead
*/
getTestSegmentation,
},
}