Merge pull request #5881 from overleaf/ab-split-test-middleware

New global split test middleware for locals

GitOrigin-RevId: b530572f709572663fc3d051f544064bd8804f76
This commit is contained in:
Miguel Serrano 2021-12-02 11:13:50 +01:00 committed by Copybot
parent 993a6d0eb9
commit d5bf5b0614
6 changed files with 260 additions and 8 deletions

View file

@ -0,0 +1,10 @@
function setSplitTestVariant(locals, splitTestName, variant) {
if (!locals.splitTestVariants) {
locals.splitTestVariants = {}
}
locals.splitTestVariants[splitTestName] = variant
}
module.exports = {
setSplitTestVariant,
}

View file

@ -0,0 +1,54 @@
const SplitTestV2Handler = require('./SplitTestV2Handler')
const SplitTestCache = require('./SplitTestCache')
const LocalsHelper = require('./LocalsHelper')
const logger = require('@overleaf/logger')
function loadAssignmentsInLocals(splitTestNames) {
return async function (req, res, next) {
try {
if (!req.session.cachedSplitTestAssignments) {
req.session.cachedSplitTestAssignments = {}
}
for (const splitTestName of splitTestNames) {
const splitTest = await SplitTestCache.get(splitTestName)
if (splitTest) {
await _loadAssignmentInLocals(splitTest, req.session, res.locals)
}
}
} catch (error) {
logger.error(
{ err: error, splitTestNames },
'Failed to load split test assignments in express locals in middleware'
)
}
next()
}
}
async function _loadAssignmentInLocals(splitTest, session, locals) {
const currentVersion = splitTest.getCurrentVersion()
const cacheKey = `${splitTest.name}-${currentVersion.versionNumber}`
if (currentVersion.active) {
const cachedVariant = session.cachedSplitTestAssignments[cacheKey]
if (cachedVariant) {
LocalsHelper.setSplitTestVariant(locals, splitTest.name, cachedVariant)
} else {
const assignment = await SplitTestV2Handler.promises.getAssignmentForSession(
session,
splitTest.name
)
session.cachedSplitTestAssignments[cacheKey] = assignment.variant
LocalsHelper.setSplitTestVariant(
locals,
splitTest.name,
assignment.variant
)
}
} else {
delete session.cachedSplitTestAssignments[cacheKey]
}
}
module.exports = {
loadAssignmentsInLocals,
}

View file

@ -2,6 +2,7 @@ const UserGetter = require('../User/UserGetter')
const UserUpdater = require('../User/UserUpdater')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const UserAnalyticsIdCache = require('../Analytics/UserAnalyticsIdCache')
const LocalsHelper = require('./LocalsHelper')
const crypto = require('crypto')
const _ = require('lodash')
const { callbackify } = require('util')
@ -89,10 +90,11 @@ async function getAssignmentForSession(session, splitTestName, options) {
*/
async function assignInLocalsContext(res, userId, splitTestName, options) {
const assignment = await getAssignment(userId, splitTestName, options)
if (!res.locals.splitTestVariants) {
res.locals.splitTestVariants = {}
}
res.locals.splitTestVariants[splitTestName] = assignment.variant
LocalsHelper.setSplitTestVariant(
res.locals,
splitTestName,
assignment.variant
)
}
/**
@ -115,10 +117,11 @@ async function assignInLocalsContextForSession(
splitTestName,
options
)
if (!res.locals.splitTestVariants) {
res.locals.splitTestVariants = {}
}
res.locals.splitTestVariants[splitTestName] = assignment.variant
LocalsHelper.setSplitTestVariant(
res.locals,
splitTestName,
assignment.variant
)
}
async function _getAssignment(

View file

@ -51,6 +51,7 @@ const UserMembershipRouter = require('./Features/UserMembership/UserMembershipRo
const SystemMessageController = require('./Features/SystemMessages/SystemMessageController')
const AnalyticsRegistrationSourceMiddleware = require('./Features/Analytics/AnalyticsRegistrationSourceMiddleware')
const AnalyticsUTMTrackingMiddleware = require('./Features/Analytics/AnalyticsUTMTrackingMiddleware')
const SplitTestMiddleware = require('./Features/SplitTests/SplitTestMiddleware')
const { Joi, validate } = require('./infrastructure/Validation')
const {
renderUnsupportedBrowserPage,
@ -59,6 +60,7 @@ const {
const logger = require('@overleaf/logger')
const _ = require('underscore')
const { expressify } = require('./util/promises')
module.exports = { initialize }
@ -71,6 +73,10 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
webRouter.get('*', AnalyticsRegistrationSourceMiddleware.setInbound())
webRouter.get('*', AnalyticsUTMTrackingMiddleware.recordUTMTags())
webRouter.get(
'*',
expressify(SplitTestMiddleware.loadAssignmentsInLocals([]))
)
webRouter.get('/login', UserPagesController.loginPage)
AuthenticationController.addEndpointToLoginWhitelist('/login')

View file

@ -0,0 +1,178 @@
const SandboxedModule = require('sandboxed-module')
const path = require('path')
const modulePath = path.join(
__dirname,
'../../../../app/src/Features/SplitTests/SplitTestMiddleware'
)
const sinon = require('sinon')
const { assert } = require('chai')
const MockResponse = require('../helpers/MockResponse')
const MockRequest = require('../helpers/MockRequest')
describe('SplitTestMiddleware', function () {
beforeEach(function () {
this.SplitTestMiddleware = SandboxedModule.require(modulePath, {
requires: {
'./SplitTestV2Handler': (this.SplitTestV2Handler = {
promises: {
getAssignmentForSession: sinon.stub().resolves(),
},
}),
'./SplitTestCache': (this.SplitTestCache = {
get: sinon.stub().resolves(),
}),
},
})
this.req = new MockRequest()
this.req.session = {}
this.res = new MockResponse()
this.next = sinon.stub()
})
it('assign split test variant in locals', async function () {
this.SplitTestCache.get.withArgs('ui-overhaul').resolves({
name: 'ui-overhaul',
getCurrentVersion: () => ({
versionNumber: 1,
active: true,
}),
})
this.SplitTestV2Handler.promises.getAssignmentForSession
.withArgs(this.req.session, 'ui-overhaul')
.resolves({
variant: 'new',
})
const middleware = this.SplitTestMiddleware.loadAssignmentsInLocals([
'ui-overhaul',
])
await middleware(this.req, this.res, this.next)
assert.equal(this.res.locals.splitTestVariants['ui-overhaul'], 'new')
assert.deepEqual(this.req.session.cachedSplitTestAssignments, {
'ui-overhaul-1': 'new',
})
sinon.assert.calledOnce(this.next)
})
it('assign multiple split test variant in locals', async function () {
this.SplitTestCache.get
.withArgs('ui-overhaul')
.resolves({
name: 'ui-overhaul',
getCurrentVersion: () => ({
versionNumber: 1,
active: true,
}),
})
.withArgs('other-test')
.resolves({
name: 'other-test',
getCurrentVersion: () => ({
versionNumber: 1,
active: true,
}),
})
this.SplitTestV2Handler.promises.getAssignmentForSession
.withArgs(this.req.session, 'ui-overhaul')
.resolves({
variant: 'default',
})
this.SplitTestV2Handler.promises.getAssignmentForSession
.withArgs(this.req.session, 'other-test')
.resolves({
variant: 'foobar',
})
const middleware = this.SplitTestMiddleware.loadAssignmentsInLocals([
'ui-overhaul',
'other-test',
])
await middleware(this.req, this.res, this.next)
assert.equal(this.res.locals.splitTestVariants['ui-overhaul'], 'default')
assert.equal(this.res.locals.splitTestVariants['other-test'], 'foobar')
assert.deepEqual(this.req.session.cachedSplitTestAssignments, {
'ui-overhaul-1': 'default',
'other-test-1': 'foobar',
})
sinon.assert.calledOnce(this.next)
})
it('cached assignment in session is used', async function () {
this.req.session.cachedSplitTestAssignments = {
'ui-overhaul-1': 'cached-variant',
}
this.SplitTestCache.get.withArgs('ui-overhaul').resolves({
name: 'ui-overhaul',
getCurrentVersion: () => ({
versionNumber: 1,
active: true,
}),
})
const middleware = this.SplitTestMiddleware.loadAssignmentsInLocals([
'ui-overhaul',
])
await middleware(this.req, this.res, this.next)
sinon.assert.notCalled(
this.SplitTestV2Handler.promises.getAssignmentForSession
)
assert.equal(
this.res.locals.splitTestVariants['ui-overhaul'],
'cached-variant'
)
assert.deepEqual(this.req.session.cachedSplitTestAssignments, {
'ui-overhaul-1': 'cached-variant',
})
sinon.assert.calledOnce(this.next)
})
it('inactive split test is not assigned in locals', async function () {
this.SplitTestCache.get.withArgs('ui-overhaul').resolves({
name: 'ui-overhaul',
getCurrentVersion: () => ({
versionNumber: 1,
active: false,
}),
})
const middleware = this.SplitTestMiddleware.loadAssignmentsInLocals([
'ui-overhaul',
])
await middleware(this.req, this.res, this.next)
assert.equal(this.res.locals.splitTestVariants, undefined)
assert.deepEqual(this.req.session.cachedSplitTestAssignments, {})
sinon.assert.calledOnce(this.next)
})
it('not existing split test is not assigned in locals', async function () {
this.SplitTestCache.get.withArgs('not-found').resolves(undefined)
const middleware = this.SplitTestMiddleware.loadAssignmentsInLocals([
'not-found',
])
await middleware(this.req, this.res, this.next)
assert.equal(this.res.locals.splitTestVariants, undefined)
assert.deepEqual(this.req.session.cachedSplitTestAssignments, {})
sinon.assert.calledOnce(this.next)
})
it('next middleware is called even if there is an error', async function () {
this.SplitTestCache.get.throws('some error')
const middleware = this.SplitTestMiddleware.loadAssignmentsInLocals([
'some-test',
])
await middleware(this.req, this.res, this.next)
assert.equal(this.res.locals.splitTestVariants, undefined)
assert.deepEqual(this.req.session.cachedSplitTestAssignments, {})
sinon.assert.calledOnce(this.next)
})
})

View file

@ -27,6 +27,7 @@ class MockResponse {
this.redirected = false
this.returned = false
this.headers = {}
this.locals = {}
}
render(template, variables) {