Merge pull request #32244 from overleaf/ab-labs-in-feature-flags

[web] Merge Labs programme into the Feature flags system

GitOrigin-RevId: db75e07bf3272becc11ef1eeda3850098b3daa9d
This commit is contained in:
Alexandre Bourdin
2026-03-26 09:06:51 +00:00
committed by Copybot
parent 6f3e0963b8
commit 671df33da3
25 changed files with 883 additions and 360 deletions
@@ -499,21 +499,322 @@ describe('SplitTestHandler', function () {
expect(assignment.variant).to.equal('variant-1')
})
})
it('should assign to default if userCount is undefined', async function (ctx) {
describe('labs phase assignment (split test)', function () {
beforeEach(function (ctx) {
ctx.AnalyticsManager.getIdsFromSession.returns({
userId: 'abc123abc123',
})
ctx.cachedSplitTests.set(
'active-test',
makeSplitTest('active-test', { userLimit: 100, userCount: undefined })
'labs-experiment',
makeSplitTest('labs-experiment', { phase: 'labs' })
)
})
it('should assign to variant when user is in labs program and has opted into the experiment', async function (ctx) {
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {},
labsProgram: true,
labsExperiments: ['labs-experiment'],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'active-test'
'labs-experiment'
)
expect(assignment.variant).to.equal('variant-1')
})
it('should assign to default when user is in labs program but has not opted into the experiment', async function (ctx) {
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {},
labsProgram: true,
labsExperiments: ['some-other-experiment'],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment'
)
expect(assignment.variant).to.equal('default')
})
it('should assign to default when user is not in labs program', async function (ctx) {
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {},
labsProgram: false,
labsExperiments: [],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment'
)
expect(assignment.variant).to.equal('default')
})
it('should assign to default when user has no labsExperiments field', async function (ctx) {
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {},
labsProgram: true,
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment'
)
expect(assignment.variant).to.equal('default')
})
it('should assign to variant when under user limit', async function (ctx) {
ctx.cachedSplitTests.set(
'labs-experiment',
makeSplitTest('labs-experiment', {
phase: 'labs',
userLimit: 10,
userCount: 5,
})
)
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {},
labsProgram: true,
labsExperiments: ['labs-experiment'],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment'
)
expect(assignment.variant).to.equal('variant-1')
})
it('should assign enrolled user to variant even when user limit is reached', async function (ctx) {
ctx.cachedSplitTests.set(
'labs-experiment',
makeSplitTest('labs-experiment', {
phase: 'labs',
userLimit: 10,
userCount: 10,
})
)
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {},
labsProgram: true,
labsExperiments: ['labs-experiment'],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment'
)
expect(assignment.variant).to.equal('variant-1')
})
it('should allow already-assigned user even when limit is reached', async function (ctx) {
ctx.cachedSplitTests.set(
'labs-experiment',
makeSplitTest('labs-experiment', {
phase: 'labs',
userLimit: 10,
userCount: 10,
})
)
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {
'labs-experiment': [
{
variantName: 'variant-1',
versionNumber: 1,
assignedAt: new Date(),
phase: 'labs',
},
],
},
labsProgram: true,
labsExperiments: ['labs-experiment'],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment'
)
expect(assignment.variant).to.equal('variant-1')
})
})
describe('labs phase assignment (gradual rollout)', function () {
beforeEach(function (ctx) {
ctx.AnalyticsManager.getIdsFromSession.returns({
userId: 'abc123abc123',
})
ctx.cachedSplitTests.set(
'labs-experiment-gr',
makeSplitTest('labs-experiment-gr', {
phase: 'labs',
analyticsEnabled: false,
})
)
})
it('should assign to variant when user is in labs program and has opted in', async function (ctx) {
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {},
labsProgram: true,
labsExperiments: ['labs-experiment-gr'],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment-gr'
)
expect(assignment.variant).to.equal('variant-1')
})
it('should assign to default when user has not opted in', async function (ctx) {
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {},
labsProgram: true,
labsExperiments: [],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment-gr'
)
expect(assignment.variant).to.equal('default')
})
it('should assign to default when user is not in labs program', async function (ctx) {
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {},
labsProgram: false,
labsExperiments: [],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment-gr'
)
expect(assignment.variant).to.equal('default')
})
it('should assign to variant when under user limit', async function (ctx) {
ctx.cachedSplitTests.set(
'labs-experiment-gr',
makeSplitTest('labs-experiment-gr', {
phase: 'labs',
analyticsEnabled: false,
userLimit: 10,
userCount: 5,
})
)
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {},
labsProgram: true,
labsExperiments: ['labs-experiment-gr'],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment-gr'
)
expect(assignment.variant).to.equal('variant-1')
})
it('should assign enrolled user to variant even when user limit is reached', async function (ctx) {
ctx.cachedSplitTests.set(
'labs-experiment-gr',
makeSplitTest('labs-experiment-gr', {
phase: 'labs',
analyticsEnabled: false,
userLimit: 10,
userCount: 10,
})
)
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {},
labsProgram: true,
labsExperiments: ['labs-experiment-gr'],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment-gr'
)
expect(assignment.variant).to.equal('variant-1')
})
it('should allow already-assigned user even when limit is reached', async function (ctx) {
ctx.cachedSplitTests.set(
'labs-experiment-gr',
makeSplitTest('labs-experiment-gr', {
phase: 'labs',
analyticsEnabled: false,
userLimit: 10,
userCount: 10,
})
)
ctx.SplitTestUserGetter.promises.getUser.resolves({
_id: new ObjectId('abc123abc123abc123abc123'),
splitTests: {
'labs-experiment-gr': [
{
variantName: 'variant-1',
versionNumber: 1,
assignedAt: new Date(),
phase: 'labs',
},
],
},
labsProgram: true,
labsExperiments: ['labs-experiment-gr'],
})
const assignment = await ctx.SplitTestHandler.promises.getAssignment(
ctx.req,
ctx.res,
'labs-experiment-gr'
)
expect(assignment.variant).to.equal('variant-1')
})
})
})
@@ -325,4 +325,52 @@ describe('SplitTestSessionHandler', function () {
],
})
})
describe('clearCachedVariant', function () {
it('should remove all cached entries for a given split test name', function (ctx) {
const session = {
cachedSplitTestAssignments: {
'my-test-1': 'variant-1',
'my-test-2': 'variant-2',
'other-test-1': 'variant-1',
},
}
ctx.SplitTestSessionHandler.clearCachedVariant(session, 'my-test')
expect(session.cachedSplitTestAssignments).to.deep.equal({
'other-test-1': 'variant-1',
})
})
it('should handle split test names with hyphens correctly', function (ctx) {
const session = {
cachedSplitTestAssignments: {
'monthly-texlive-1': 'enabled',
'monthly-texlive-2': 'enabled',
'monthly-1': 'variant-1',
},
}
ctx.SplitTestSessionHandler.clearCachedVariant(session, 'monthly-texlive')
expect(session.cachedSplitTestAssignments).to.deep.equal({
'monthly-1': 'variant-1',
})
})
it('should do nothing when session has no cached assignments', function (ctx) {
const session = {}
ctx.SplitTestSessionHandler.clearCachedVariant(session, 'my-test')
expect(session.cachedSplitTestAssignments).to.be.undefined
})
it('should do nothing when there are no matching entries', function (ctx) {
const session = {
cachedSplitTestAssignments: {
'other-test-1': 'variant-1',
},
}
ctx.SplitTestSessionHandler.clearCachedVariant(session, 'my-test')
expect(session.cachedSplitTestAssignments).to.deep.equal({
'other-test-1': 'variant-1',
})
})
})
})