mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #17906 from overleaf/ab-split-test-assignments-optim-pt2
[web] Store anonymous users split test assignments in new format in session GitOrigin-RevId: a5f71f7dcad7e7b11fc6a391bd5182525b3bdf03
This commit is contained in:
parent
e4e054e8bb
commit
bfe75c7d31
5 changed files with 166 additions and 172 deletions
|
@ -12,9 +12,7 @@ class SplitTestCache extends CacheLoader {
|
||||||
async load() {
|
async load() {
|
||||||
Metrics.inc('split_test_get_split_test_from_mongo', 1, {})
|
Metrics.inc('split_test_get_split_test_from_mongo', 1, {})
|
||||||
const splitTests = await SplitTestManager.getRuntimeTests()
|
const splitTests = await SplitTestManager.getRuntimeTests()
|
||||||
return new Map(
|
return new Map(splitTests.map(splitTest => [splitTest.name, splitTest]))
|
||||||
splitTests.map(splitTest => [splitTest.name, splitTest.toObject()])
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(value) {
|
serialize(value) {
|
||||||
|
|
|
@ -492,7 +492,10 @@ function _getNonSaasAssignment(splitTestName) {
|
||||||
|
|
||||||
async function _getSplitTest(name) {
|
async function _getSplitTest(name) {
|
||||||
const splitTests = await SplitTestCache.get('')
|
const splitTests = await SplitTestCache.get('')
|
||||||
return splitTests?.get(name)
|
const splitTest = splitTests?.get(name)
|
||||||
|
if (splitTest && !splitTest.archived) {
|
||||||
|
return splitTest
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -60,9 +60,7 @@ async function getSplitTests({ name, phase, type, active, archived }) {
|
||||||
|
|
||||||
async function getRuntimeTests() {
|
async function getRuntimeTests() {
|
||||||
try {
|
try {
|
||||||
return await SplitTest.find({
|
return SplitTest.find({}).lean().exec()
|
||||||
archived: { $ne: true },
|
|
||||||
}).exec()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw OError.tag(error, 'Failed to get active split tests list')
|
throw OError.tag(error, 'Failed to get active split tests list')
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,63 +9,68 @@ const SplitTestUtils = require('./SplitTestUtils')
|
||||||
const SplitTestUserGetter = require('./SplitTestUserGetter')
|
const SplitTestUserGetter = require('./SplitTestUserGetter')
|
||||||
|
|
||||||
const CACHE_TOMBSTONE_SPLIT_TEST_NOT_ACTIVE_FOR_USER = null
|
const CACHE_TOMBSTONE_SPLIT_TEST_NOT_ACTIVE_FOR_USER = null
|
||||||
|
const TOKEN_SEP = ';'
|
||||||
|
// this is safe to use as a separator adjacent to a base64 string because Mongo object IDs
|
||||||
|
// do not generate any padding when converted (24 hex digits = 12 bytes => multiple of 6),
|
||||||
|
// thus do not contain any trailing `=`
|
||||||
|
const KEY_VALUE_SEP = '='
|
||||||
|
const ID_VERSION_SEP = '_'
|
||||||
|
const VARIANT_DATE_SEP = ':'
|
||||||
|
|
||||||
async function getAssignments(session) {
|
async function getAssignments(session) {
|
||||||
if (!session.splitTests && !session.sta) {
|
await _convertAnonymousAssignmentsIfNeeded(session)
|
||||||
|
|
||||||
|
if (!session.sta) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// await _convertAnonymousAssignmentsIfNeeded(session)
|
const assignments = {}
|
||||||
const assignments = _.clone(session.splitTests || {})
|
const tokens = session.sta.split(TOKEN_SEP)
|
||||||
if (session.sta) {
|
const splitTests = Array.from((await SplitTestCache.get('')).values())
|
||||||
const tokens = session.sta.split(';')
|
for (const token of tokens) {
|
||||||
const splitTests = Array.from((await SplitTestCache.get('')).values())
|
try {
|
||||||
for (const token of tokens) {
|
if (!token.length) {
|
||||||
try {
|
continue
|
||||||
if (!token.length) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const [splitTestNameVersion, info] = token.split('=')
|
|
||||||
const [splitTestId64, versionStr] = splitTestNameVersion.split('_')
|
|
||||||
|
|
||||||
const splitTest = splitTests.find(
|
|
||||||
test =>
|
|
||||||
test._id.toString() ===
|
|
||||||
new ObjectId(Buffer.from(splitTestId64, 'base64')).toString()
|
|
||||||
)
|
|
||||||
if (!splitTest) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitTestName = splitTest.name
|
|
||||||
const versionNumber = parseInt(versionStr)
|
|
||||||
const [variantChar, timestampStr36] = info.split(':')
|
|
||||||
const assignedAt = new Date(parseInt(timestampStr36, 36) * 1000)
|
|
||||||
let variantName
|
|
||||||
if (variantChar === 'd') {
|
|
||||||
variantName = 'default'
|
|
||||||
} else {
|
|
||||||
const variantIndex = parseInt(variantChar)
|
|
||||||
variantName =
|
|
||||||
SplitTestUtils.getCurrentVersion(splitTest).variants[variantIndex]
|
|
||||||
.name
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!assignments[splitTestName]) {
|
|
||||||
assignments[splitTestName] = []
|
|
||||||
}
|
|
||||||
assignments[splitTestName].push({
|
|
||||||
versionNumber,
|
|
||||||
variantName,
|
|
||||||
phase: 'release', // anonymous users can only be exposed to tests in release phase
|
|
||||||
assignedAt,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
{ err: error, token },
|
|
||||||
'Failed to resolve anonymous split test assignment from session'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
const [splitTestNameVersion, info] = token.split(KEY_VALUE_SEP)
|
||||||
|
const [splitTestId64, versionStr] =
|
||||||
|
splitTestNameVersion.split(ID_VERSION_SEP)
|
||||||
|
|
||||||
|
const splitTest = splitTests.find(
|
||||||
|
test => splitTestId64 === _convertIdToBase64(test._id)
|
||||||
|
)
|
||||||
|
if (!splitTest) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitTestName = splitTest.name
|
||||||
|
const versionNumber = parseInt(versionStr)
|
||||||
|
const [variantChar, timestampStr36] = info.split(VARIANT_DATE_SEP)
|
||||||
|
const assignedAt = new Date(parseInt(timestampStr36, 36) * 1000)
|
||||||
|
let variantName
|
||||||
|
if (variantChar === 'd') {
|
||||||
|
variantName = 'default'
|
||||||
|
} else {
|
||||||
|
const variantIndex = parseInt(variantChar)
|
||||||
|
variantName =
|
||||||
|
SplitTestUtils.getCurrentVersion(splitTest).variants[variantIndex]
|
||||||
|
.name
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!assignments[splitTestName]) {
|
||||||
|
assignments[splitTestName] = []
|
||||||
|
}
|
||||||
|
assignments[splitTestName].push({
|
||||||
|
versionNumber,
|
||||||
|
variantName,
|
||||||
|
phase: 'release', // anonymous users can only be exposed to tests in release phase
|
||||||
|
assignedAt,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ err: error, token },
|
||||||
|
'Failed to resolve cached anonymous split test assignments from session'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,36 +78,23 @@ async function getAssignments(session) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function appendAssignment(session, assignment) {
|
async function appendAssignment(session, assignment) {
|
||||||
// await _convertAnonymousAssignmentsIfNeeded(session)
|
await _convertAnonymousAssignmentsIfNeeded(session)
|
||||||
|
|
||||||
if (!session.splitTests) {
|
|
||||||
session.splitTests = {}
|
|
||||||
}
|
|
||||||
if (!session.splitTests[assignment.splitTestName]) {
|
|
||||||
session.splitTests[assignment.splitTestName] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const assignments = await getAssignments(session)
|
|
||||||
if (
|
if (
|
||||||
!_.find(assignments[assignment.splitTestName], {
|
!_hasExistingAssignment(
|
||||||
variantName: assignment.variantName,
|
session,
|
||||||
versionNumber: assignment.versionNumber,
|
assignment.splitTestId,
|
||||||
})
|
assignment.versionNumber
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
// if (!session.sta) {
|
if (!session.sta) {
|
||||||
// session.sta = ''
|
session.sta = ''
|
||||||
// }
|
}
|
||||||
// const splitTests = await SplitTestCache.get('')
|
const splitTests = await SplitTestCache.get('')
|
||||||
// const splitTest = splitTests.get(assignment.splitTestName)
|
const splitTest = splitTests.get(assignment.splitTestName)
|
||||||
// const assignmentString = _buildAssignmentString(splitTest, assignment)
|
const assignmentString = _buildAssignmentString(splitTest, assignment)
|
||||||
// const separator = session.sta.length > 0 ? ';' : ''
|
const separator = session.sta.length > 0 ? TOKEN_SEP : ''
|
||||||
// session.sta += `${separator}${assignmentString}`
|
session.sta += `${separator}${assignmentString}`
|
||||||
session.splitTests[assignment.splitTestName].push({
|
|
||||||
variantName: assignment.variantName,
|
|
||||||
versionNumber: assignment.versionNumber,
|
|
||||||
phase: assignment.phase,
|
|
||||||
assignedAt: assignment.assignedAt,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,62 +174,63 @@ function collectSessionStats(session) {
|
||||||
JSON.stringify(session.cachedSplitTestAssignments).length
|
JSON.stringify(session.cachedSplitTestAssignments).length
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (session.splitTests) {
|
if (session.sta) {
|
||||||
Metrics.summary(
|
Metrics.summary(
|
||||||
'split_test_session_storage_count',
|
'split_test_session_storage_count',
|
||||||
(session.sta || '').split(';').length +
|
(session.sta || '').split(';').length
|
||||||
Object.keys(session.splitTests).length
|
|
||||||
)
|
)
|
||||||
Metrics.summary(
|
Metrics.summary(
|
||||||
'split_test_session_storage_size',
|
'split_test_session_storage_size',
|
||||||
(session.sta || '').length + JSON.stringify(session.splitTests).length
|
(session.sta || '').length
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// async function _convertAnonymousAssignmentsIfNeeded(session) {
|
async function _convertAnonymousAssignmentsIfNeeded(session) {
|
||||||
// if (typeof session.splitTests === 'object') {
|
if (session.splitTests) {
|
||||||
// const sessionAssignments = session.splitTests
|
const splitTests = await SplitTestCache.get('')
|
||||||
// const splitTests = await SplitTestCache.get('')
|
if (!session.sta) {
|
||||||
// session.splitTests = ''
|
session.sta = ''
|
||||||
// for (const [splitTestName, assignments] of Object.entries(
|
}
|
||||||
// sessionAssignments
|
for (const [splitTestName, assignments] of Object.entries(
|
||||||
// )) {
|
session.splitTests
|
||||||
// const splitTest = splitTests.get(splitTestName)
|
)) {
|
||||||
// for (const assignment of assignments) {
|
const splitTest = splitTests.get(splitTestName)
|
||||||
// const assignmentString = _buildAssignmentString(splitTest, assignment)
|
for (const assignment of assignments) {
|
||||||
// const separator = session.splitTests.length > 0 ? ';' : ''
|
const assignmentString = _buildAssignmentString(splitTest, assignment)
|
||||||
// session.splitTests += `${separator}${assignmentString}`
|
const separator = session.sta.length > 0 ? TOKEN_SEP : ''
|
||||||
// }
|
session.sta += `${separator}${assignmentString}`
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
delete session.splitTests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// function _hasExistingAssignment(session, splitTest, versionNumber) {
|
function _hasExistingAssignment(session, splitTest, versionNumber) {
|
||||||
// if (!session.sta) {
|
if (!session.sta) {
|
||||||
// return false
|
return false
|
||||||
// }
|
}
|
||||||
// const index = session.sta.indexOf(
|
const index = session.sta.indexOf(
|
||||||
// `${_convertIdToBase64(splitTest._id)}_${versionNumber}=`
|
`${_convertIdToBase64(splitTest._id)}${ID_VERSION_SEP}${versionNumber}=`
|
||||||
// )
|
)
|
||||||
// return index >= 0
|
return index >= 0
|
||||||
// }
|
}
|
||||||
|
|
||||||
// function _buildAssignmentString(splitTest, assignment) {
|
function _buildAssignmentString(splitTest, assignment) {
|
||||||
// const { versionNumber, variantName, assignedAt } = assignment
|
const { versionNumber, variantName, assignedAt } = assignment
|
||||||
// const variants = SplitTestUtils.getCurrentVersion(splitTest).variants
|
const variants = SplitTestUtils.getCurrentVersion(splitTest).variants
|
||||||
// const splitTestId = _convertIdToBase64(splitTest._id)
|
const splitTestId = _convertIdToBase64(splitTest._id)
|
||||||
// const variantChar =
|
const variantChar =
|
||||||
// variantName === 'default'
|
variantName === 'default'
|
||||||
// ? 'd'
|
? 'd'
|
||||||
// : _.findIndex(variants, { name: variantName })
|
: _.findIndex(variants, { name: variantName })
|
||||||
// const timestamp = Math.floor(assignedAt.getTime() / 1000).toString(36)
|
const timestamp = Math.floor(assignedAt.getTime() / 1000).toString(36)
|
||||||
// return `${splitTestId}_${versionNumber}=${variantChar}:${timestamp}`
|
return `${splitTestId}${ID_VERSION_SEP}${versionNumber}${KEY_VALUE_SEP}${variantChar}${VARIANT_DATE_SEP}${timestamp}`
|
||||||
// }
|
}
|
||||||
|
|
||||||
// function _convertIdToBase64(id) {
|
function _convertIdToBase64(id) {
|
||||||
// return new ObjectId(id).toString('base64')
|
return new ObjectId(id).toString('base64')
|
||||||
// }
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getAssignments: callbackify(getAssignments),
|
getAssignments: callbackify(getAssignments),
|
||||||
|
|
|
@ -15,6 +15,45 @@ describe('SplitTestSessionHandler', function () {
|
||||||
}
|
}
|
||||||
this.SplitTestUserGetter = {}
|
this.SplitTestUserGetter = {}
|
||||||
this.Metrics = {}
|
this.Metrics = {}
|
||||||
|
|
||||||
|
this.SplitTestCache.get = sinon.stub().resolves(
|
||||||
|
new Map(
|
||||||
|
Object.entries({
|
||||||
|
'anon-test-1': {
|
||||||
|
_id: '661f92a4669764bb03f73e37',
|
||||||
|
name: 'anon-test-1',
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
versionNumber: 1,
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
name: 'enabled',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'anon-test-2': {
|
||||||
|
_id: '661f92a9d68ea711d6bf2df4',
|
||||||
|
name: 'anon-test-2',
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
versionNumber: 1,
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
name: 'v-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'v-2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
this.SplitTestSessionHandler = SandboxedModule.require(MODULE_PATH, {
|
this.SplitTestSessionHandler = SandboxedModule.require(MODULE_PATH, {
|
||||||
requires: {
|
requires: {
|
||||||
'./SplitTestCache': this.SplitTestCache,
|
'./SplitTestCache': this.SplitTestCache,
|
||||||
|
@ -152,43 +191,6 @@ describe('SplitTestSessionHandler', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should merge assignments from both splitTests and sta fields', async function () {
|
it('should merge assignments from both splitTests and sta fields', async function () {
|
||||||
this.SplitTestCache.get = sinon.stub().resolves(
|
|
||||||
new Map(
|
|
||||||
Object.entries({
|
|
||||||
'anon-test-1': {
|
|
||||||
_id: '661f92a4669764bb03f73e37',
|
|
||||||
name: 'anon-test-1',
|
|
||||||
versions: [
|
|
||||||
{
|
|
||||||
versionNumber: 1,
|
|
||||||
variants: [
|
|
||||||
{
|
|
||||||
name: 'enabled',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'anon-test-2': {
|
|
||||||
_id: '661f92a9d68ea711d6bf2df4',
|
|
||||||
name: 'anon-test-2',
|
|
||||||
versions: [
|
|
||||||
{
|
|
||||||
versionNumber: 1,
|
|
||||||
variants: [
|
|
||||||
{
|
|
||||||
name: 'v-1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'v-2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
const session = {
|
const session = {
|
||||||
splitTests: {
|
splitTests: {
|
||||||
'anon-test-1': [
|
'anon-test-1': [
|
||||||
|
@ -223,18 +225,18 @@ describe('SplitTestSessionHandler', function () {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'anon-test-2': [
|
'anon-test-2': [
|
||||||
{
|
|
||||||
variantName: 'default',
|
|
||||||
versionNumber: 1,
|
|
||||||
phase: 'release',
|
|
||||||
assignedAt: new Date(1712307600000),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
variantName: 'v-2',
|
variantName: 'v-2',
|
||||||
versionNumber: 2,
|
versionNumber: 2,
|
||||||
phase: 'release',
|
phase: 'release',
|
||||||
assignedAt: new Date(1712858400000),
|
assignedAt: new Date(1712858400000),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
variantName: 'default',
|
||||||
|
versionNumber: 1,
|
||||||
|
phase: 'release',
|
||||||
|
assignedAt: new Date(1712307600000),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue