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.setUserPropertyForUser( 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, }, }