Skip to content

Commit 43c70f8

Browse files
bcb37Copilot
andauthored
remove enrollmentComplete experiments from pools unless already enrolled (#2670)
* remove enrollmentComplete experiments from pools unless already enrolled * Update backend/packages/Upgrade/test/unit/services/ExperimentAssignmentService.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * no multiple assignment per decision point --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent bd8122c commit 43c70f8

File tree

5 files changed

+366
-82
lines changed

5 files changed

+366
-82
lines changed

backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,10 @@ export class ExperimentAssignmentService {
388388
filteredExperiments,
389389
mergedIndividualAssignment,
390390
groupEnrollments,
391-
experimentUserDoc
391+
individualExclusions,
392+
groupExclusions,
393+
experimentUserDoc,
394+
previewUser
392395
);
393396

394397
// return empty if no experiments
@@ -636,50 +639,59 @@ export class ExperimentAssignmentService {
636639
experiments: Experiment[],
637640
individualEnrollments: IndividualEnrollment[],
638641
groupEnrollments: GroupEnrollment[],
639-
experimentUser: ExperimentUser
642+
individualExclusions: IndividualExclusion[],
643+
groupExclusions: GroupExclusion[],
644+
experimentUser: ExperimentUser,
645+
previewUser: PreviewUser | null
640646
): Experiment[] {
641647
// Create experiment pool
642648
const experimentPools = this.createExperimentPool(experiments);
643-
644-
// Filter pools which are not assigned
645-
const unassignedPools = experimentPools.filter((pool) => {
646-
return !pool.some((experiment) => {
647-
const hasIndividualEnrollment = individualEnrollments.some((enrollment) => {
648-
return enrollment.experimentId === experiment.id;
649-
});
650-
const hasGroupEnrollment = groupEnrollments.some((enrollment) => {
651-
return (
652-
enrollment.experimentId === experiment.id &&
653-
enrollment.groupId === experimentUser.workingGroup[experiment.group]
654-
);
655-
});
656-
return !!(hasIndividualEnrollment || hasGroupEnrollment);
657-
});
658-
});
659-
660-
// Select experiments inside the pools
661649
const random = seedrandom(experimentUser.id)();
662-
const newSelectedExperiments = unassignedPools.map((pool) => {
663-
return pool[Math.floor(random * pool.length)];
664-
});
665650

666-
// Create new filtered experiment list
667-
const priorSelectedExperiments = experimentPools.map((pool) => {
668-
return pool.filter((experiment) => {
669-
const individualEnrollment = individualEnrollments.some((enrollment) => {
670-
return enrollment.experimentId === experiment.id;
671-
});
672-
const groupEnrollment = groupEnrollments.some((enrollment) => {
673-
return (
674-
enrollment.experimentId === experiment.id &&
675-
enrollment.groupId === experimentUser.workingGroup[experiment.group]
676-
);
651+
// Choose one experiment from each pool
652+
const experimentsToAssign = experimentPools
653+
.map((pool) => {
654+
const assignedExperiments = pool.filter((experiment) => {
655+
const hasIndividualEnrollment = individualEnrollments.some((enrollment) => {
656+
return enrollment.experimentId === experiment.id;
657+
});
658+
const hasGroupEnrollment = groupEnrollments.some((enrollment) => {
659+
return (
660+
enrollment.experimentId === experiment.id &&
661+
enrollment.groupId === experimentUser.workingGroup[experiment.group]
662+
);
663+
});
664+
return hasIndividualEnrollment || hasGroupEnrollment;
677665
});
678-
return !!(individualEnrollment || groupEnrollment);
679-
});
680-
});
666+
// If there is an experiment already assigned to the user, choose that experiment
667+
if (assignedExperiments.length === 1) {
668+
return assignedExperiments[0];
669+
}
670+
// If there are multiple experiments assigned to the user, choose the ENROLLING experiment
671+
if (assignedExperiments.length > 1) {
672+
return assignedExperiments.find((exp) => exp.state === EXPERIMENT_STATE.ENROLLING);
673+
}
681674

682-
return priorSelectedExperiments.flat().concat(newSelectedExperiments);
675+
// Otherwise choose a random ENROLLING experiment from which the user is not excluded
676+
const filteredUnassignedPool = pool.filter((experiment) => {
677+
const hasIndividualExclusion = individualExclusions.some((exclusion) => {
678+
return exclusion.experimentId === experiment.id;
679+
});
680+
const hasGroupExclusion = groupExclusions.some((exclusion) => {
681+
return (
682+
exclusion.experimentId === experiment.id &&
683+
exclusion.groupId === experimentUser.workingGroup[experiment.group]
684+
);
685+
});
686+
const includable =
687+
experiment.state === EXPERIMENT_STATE.ENROLLING ||
688+
(previewUser && experiment.state === EXPERIMENT_STATE.PREVIEW);
689+
return !(hasIndividualExclusion || hasGroupExclusion || !includable);
690+
});
691+
return filteredUnassignedPool[Math.floor(random * filteredUnassignedPool.length)];
692+
})
693+
.filter((exp) => exp !== undefined);
694+
return experimentsToAssign;
683695
}
684696

685697
private mapDecisionPoints(

backend/packages/Upgrade/test/integration/ExperimentAssignment/RevertToCondition.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,21 +181,9 @@ export default async function testCase(): Promise<void> {
181181
);
182182
checkMarkExperimentPointForUser(markedExperimentPoint, experimentUsers[2].id, experimentName, experimentPoint);
183183

184-
// get all experiment condition for user 4
184+
// user 4 should not be assigned to any condition as he has not marked any experiment point
185185
experimentConditionAssignments = await getAllExperimentCondition(experimentUsers[3].id, new UpgradeLogger());
186-
checkExperimentAssignedIsNotDefault(experimentConditionAssignments, experimentName, experimentPoint);
187-
checkConditionAssigned(experimentConditionAssignments, experimentName, experimentPoint, experimentObject.revertTo);
188-
189-
// mark experiment point for user 4
190-
markedExperimentPoint = await markExperimentPoint(
191-
experimentUsers[3].id,
192-
experimentName,
193-
experimentPoint,
194-
condition,
195-
experimentId,
196-
new UpgradeLogger()
197-
);
198-
checkMarkExperimentPointForUser(markedExperimentPoint, experimentUsers[3].id, experimentName, experimentPoint);
186+
expect(experimentConditionAssignments).toHaveLength(0);
199187
}
200188

201189
function checkConditionAssigned(

backend/packages/Upgrade/test/integration/ExperimentAssignment/Scenario2.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -178,20 +178,9 @@ export default async function testCase(): Promise<void> {
178178
);
179179
checkMarkExperimentPointForUser(markedExperimentPoint, experimentUsers[2].id, experimentName, experimentPoint);
180180

181-
// get all experiment condition for user 4
181+
// user 4 should not be assigned to any condition as he has not marked any experiment point
182182
experimentConditionAssignments = await getAllExperimentCondition(experimentUsers[3].id, new UpgradeLogger());
183-
checkExperimentAssignedIsNotDefault(experimentConditionAssignments, experimentName, experimentPoint);
184-
185-
// mark experiment point for user 4
186-
markedExperimentPoint = await markExperimentPoint(
187-
experimentUsers[3].id,
188-
experimentName,
189-
experimentPoint,
190-
condition,
191-
experimentId,
192-
new UpgradeLogger()
193-
);
194-
checkMarkExperimentPointForUser(markedExperimentPoint, experimentUsers[3].id, experimentName, experimentPoint);
183+
expect(experimentConditionAssignments).toHaveLength(0);
195184

196185
await checkDeletedExperiment(experimentId, user);
197186
}

backend/packages/Upgrade/test/integration/ExperimentStats/WithinSubjectEnrollment.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@ import { Container } from 'typedi';
22
import { ExperimentService } from '../../../src/api/services/ExperimentService';
33
import { UserService } from '../../../src/api/services/UserService';
44
import { systemUser } from '../mockData/user/index';
5-
import {
6-
checkExperimentAssignedIsNull,
7-
getAllExperimentCondition,
8-
markExperimentPoint,
9-
updateExcludeIfReachedFlag,
10-
} from '../utils';
5+
import { checkExperimentAssignedIsNull, getAllExperimentCondition, markExperimentPoint } from '../utils';
116
import { experimentUsers } from '../mockData/experimentUsers/index';
127
import { AnalyticsService } from '../../../src/api/services/AnalyticsService';
138
import { withinSubjectExperiment } from '../mockData/experiment/index';
@@ -30,7 +25,6 @@ export default async function testCase(): Promise<void> {
3025
const user = await userService.upsertUser(systemUser as any, new UpgradeLogger());
3126
// experiment object
3227
const experimentObject = withinSubjectExperiment;
33-
experimentObject.partitions = updateExcludeIfReachedFlag(experimentObject.partitions);
3428

3529
// create experiment
3630
await experimentService.create(

0 commit comments

Comments
 (0)