diff --git a/backend/packages/Upgrade/src/api/repositories/MonitoredDecisionPointLogRepository.ts b/backend/packages/Upgrade/src/api/repositories/MonitoredDecisionPointLogRepository.ts index 18a72b8a9..945f99ea1 100644 --- a/backend/packages/Upgrade/src/api/repositories/MonitoredDecisionPointLogRepository.ts +++ b/backend/packages/Upgrade/src/api/repositories/MonitoredDecisionPointLogRepository.ts @@ -4,6 +4,13 @@ import { EntityRepository } from '../../typeorm-typedi-extensions'; import repositoryError from './utils/repositoryError'; import { UpgradeLogger } from 'src/lib/logger/UpgradeLogger'; +export interface MonitoredDecisionPointLogDataCount { + userId: string; + site: string; + target: string; + count: number; +} + @EntityRepository(MonitoredDecisionPointLog) export class MonitoredDecisionPointLogRepository extends Repository { public async getAllMonitoredDecisionPointLog( @@ -11,7 +18,7 @@ export class MonitoredDecisionPointLogRepository extends Repository { + ): Promise { const result = await this.createQueryBuilder('mdpLog') .select(['mdp.userId as userId', 'mdp.site as site', 'mdp.target as target']) .addSelect('COUNT(*) as count') @@ -24,7 +31,7 @@ export class MonitoredDecisionPointLogRepository extends Repository { const errorMsgString = repositoryError( 'monitoredDecisionPointLogRepository', - 'getAllmonitoredDecisionPointLog', + 'getAllMonitoredDecisionPointLog', {}, errorMsg ); diff --git a/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts b/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts index 5323966ff..f9692c347 100644 --- a/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts +++ b/backend/packages/Upgrade/src/api/services/ExperimentAssignmentService.ts @@ -52,7 +52,7 @@ import { ILogInput, ENROLLMENT_CODE } from 'upgrade_types'; import { StateTimeLogsRepository } from '../repositories/StateTimeLogsRepository'; import { UpgradeLogger } from '../../lib/logger/UpgradeLogger'; import { SegmentService } from './SegmentService'; -import { MonitoredDecisionPointLogRepository } from '../repositories/MonitoredDecisionPointLogRepository'; +import { MonitoredDecisionPointLogDataCount, MonitoredDecisionPointLogRepository } from '../repositories/MonitoredDecisionPointLogRepository'; import seedrandom from 'seedrandom'; import { globalExcludeSegment } from '../../../src/init/seed/globalExcludeSegment'; import { GroupEnrollment } from '../models/GroupEnrollment'; @@ -70,6 +70,14 @@ import { RequestedExperimentUser } from '../controllers/validators/ExperimentUse import { In } from 'typeorm'; import { env } from '../../env'; import { MoocletExperimentService } from './MoocletExperimentService'; + +export interface FactorialConditionResult { + factorialCondition: Omit; + payloads: string[]; + assignedFactor: Record; + conditionPayload: any | null; +} + @Service() export class ExperimentAssignmentService { constructor( @@ -114,7 +122,7 @@ export class ExperimentAssignmentService { public experimentService: ExperimentService, public cacheService: CacheService, public moocletExperimentService: MoocletExperimentService - ) {} + ) { } public async markExperimentPoint( userDoc: RequestedExperimentUser, site: string, @@ -188,15 +196,14 @@ export class ExperimentAssignmentService { } // ============= check if user or group is excluded - const [userExcluded, groupExcluded] = await this.checkUserOrGroupIsGloballyExcluded(userDoc); + const [isUserExcluded, isGroupExcluded] = await this.checkUserOrGroupIsGloballyExcluded(userDoc); - if (userExcluded || groupExcluded.length > 0) { + if (isUserExcluded || isGroupExcluded) { // no experiments if the user or group is excluded from the experiment experiments = []; } - const globalFilteredExperiments: Experiment[] = [...experiments]; - const experimentIds = globalFilteredExperiments.map((experiment) => experiment.id); + const experimentIds = experiments.map((experiment) => experiment.id); // no experiment if (experimentIds.length === 0) { @@ -204,7 +211,7 @@ export class ExperimentAssignmentService { } // experiment level inclusion and exclusion - const [, exclusionReason] = await this.experimentLevelExclusionInclusion(globalFilteredExperiments, userDoc); + const [, exclusionReason] = await this.experimentLevelExclusionInclusion(experiments, userDoc); let monitoredDocument: MonitoredDecisionPoint = await this.monitoredDecisionPointRepository.findOne({ where: { site: site, @@ -275,7 +282,7 @@ export class ExperimentAssignmentService { this.groupEnrollmentRepository.findOne({ where: { groupId: workingGroup[experiment.group], experiment: { id: experiment.id } }, })) || - Promise.resolve(undefined), + Promise.resolve(undefined), // query group exclusion (experiment.assignmentUnit === ASSIGNMENT_UNIT.GROUP && workingGroup && @@ -283,7 +290,7 @@ export class ExperimentAssignmentService { this.groupExclusionRepository.findOne({ where: { groupId: workingGroup[experiment.group], experiment: { id: experiment.id } }, })) || - Promise.resolve(undefined), + Promise.resolve(undefined), ]); } catch (error) { const err: any = error; @@ -301,8 +308,8 @@ export class ExperimentAssignmentService { groupExclusion: groupExclusions, }, { - user: userExcluded, - group: groupExcluded, + isUserExcluded: isUserExcluded, + isGroupExcluded: isGroupExcluded, }, exclusionReason, status, @@ -339,95 +346,254 @@ export class ExperimentAssignmentService { }; } + private async getExperimentsForUser(previewUser: PreviewUser, context: string): Promise { + const experiments = previewUser + ? await this.experimentRepository.getValidExperimentsWithPreview(context) + : await this.experimentService.getCachedValidExperiments(context); + // adding conditionPayloads at the root level instead of inside conditions + return experiments.map((exp) => this.experimentService.formattingConditionPayload(exp)); + } + + private async filterAndProcessGroupExperiments( + experiments: Experiment[], + experimentUser: ExperimentUser, + logger: UpgradeLogger + ): Promise { + // Filter Experiments that has assignment type as GROUP: + const groupExperiments = experiments.filter(({ assignmentUnit }) => assignmentUnit === ASSIGNMENT_UNIT.GROUP); + + if (groupExperiments.length > 0) { + // get invalid group/workingGroup experiments which dont have any enrolments yet: + const invalidGroupExperiments = await this.getInvalidGroupNotEnrolledExperiments(groupExperiments, experimentUser, logger); + const invalidGroupExperimentIds = invalidGroupExperiments.map((exp) => exp.id); + // filter out valid group experiments that doesn't have invalid group/workingGroup which are not enrolled yet + return experiments.filter(({ id }) => !invalidGroupExperimentIds.includes(id)); + } + return experiments; + } + + private async getInvalidGroupNotEnrolledExperiments( + groupExperiments: Experiment[], + experimentUser: ExperimentUser, + logger: UpgradeLogger + ): Promise { + // if empty group/workingGroup data: + const isGroupWorkingGroupMissing = + !experimentUser.group || + !experimentUser.workingGroup || + Object.keys(experimentUser.group).length === 0 || + Object.keys(experimentUser.workingGroup).length === 0; + // if group/workingGroup data present, check for invalid group/workingGroup: + const invalidExperiments = isGroupWorkingGroupMissing + ? groupExperiments + : this.experimentsWithInvalidGroupAndWorkingGroup(experimentUser, groupExperiments); + + // return invalid group experiments which are not enrolled yet + return this.groupExperimentWithoutEnrollments(invalidExperiments, experimentUser, logger); + } + + private async getAssignmentsAndExclusionsForUser(experimentUser: ExperimentUser, experimentIds: string[]): Promise { + const allGroupIds: string[] = (experimentUser.workingGroup && Object.values(experimentUser.workingGroup)) || []; + const [individualEnrollments, groupEnrollments, individualExclusions, groupExclusions] = await Promise.all([ + experimentIds.length > 0 + ? this.individualEnrollmentRepository.findEnrollments(experimentUser.id, experimentIds) + : Promise.resolve([] as IndividualEnrollment[]), + allGroupIds.length > 0 && experimentIds.length > 0 + ? this.groupEnrollmentRepository.findEnrollments(allGroupIds, experimentIds) + : Promise.resolve([] as GroupEnrollment[]), + experimentIds.length > 0 + ? this.individualExclusionRepository.findExcluded(experimentUser.id, experimentIds) + : Promise.resolve([] as IndividualExclusion[]), + allGroupIds.length > 0 && experimentIds.length > 0 + ? this.groupExclusionRepository.findExcluded(allGroupIds, experimentIds) + : Promise.resolve([] as GroupExclusion[]), + ]); + return [individualEnrollments, groupEnrollments, individualExclusions, groupExclusions]; + } + + private processExperimentPools( + experiments: Experiment[], + individualEnrollments: IndividualEnrollment[], + groupEnrollments: GroupEnrollment[], + experimentUser: ExperimentUser + ): Experiment[] { + // Create experiment pool + const experimentPools = this.createExperimentPool(experiments); + + // Filter pools which are not assigned + const unassignedPools = experimentPools.filter((pool) => { + return !pool.some((experiment) => { + const hasIndividualEnrollment = individualEnrollments.some((enrollment) => { + return enrollment.experiment.id === experiment.id; + }); + const hasGroupEnrollment = groupEnrollments.some((enrollment) => { + return ( + enrollment.experiment.id === experiment.id && + enrollment.groupId === experimentUser.workingGroup[experiment.group] + ); + }); + return !!(hasIndividualEnrollment || hasGroupEnrollment); + }); + }); + + // Select experiments inside the pools + const random = seedrandom(experimentUser.id)(); + const newSelectedExperiments = unassignedPools.map((pool) => { + return pool[Math.floor(random * pool.length)]; + }); + + // Create new filtered experiment list + const priorSelectedExperiments = experimentPools.map((pool) => { + return pool.filter((experiment) => { + const individualEnrollment = individualEnrollments.find((enrollment) => { + return enrollment.experiment.id === experiment.id; + }); + const groupEnrollment = groupEnrollments.find((enrollment) => { + return ( + enrollment.experiment.id === experiment.id && + enrollment.groupId === experimentUser.workingGroup[experiment.group] + ); + }); + return !!(individualEnrollment || groupEnrollment); + }); + }); + + return priorSelectedExperiments.flat().concat(newSelectedExperiments); + } + + private mapDecisionPoints( + experiment: Experiment, + conditionAssigned: ExperimentCondition | undefined, + userId: string, + conditionPayloads: ConditionPayloadDTO[], + type: EXPERIMENT_TYPE, + factors: FactorDTO[], + monitoredLogCounts: { site: string; target: string; count: number }[], + logger: UpgradeLogger + ): IExperimentAssignmentv5[] { + return experiment.partitions.map((decisionPoint) => { + const { target, site } = decisionPoint; + + // Determine payload and factorial object + const { payloadFound, factorialObject } = this.getPayloadAndFactorialObject( + conditionAssigned, + type, + conditionPayloads, + decisionPoint, + factors + ); + + // Log preview state information + if (experiment.state === EXPERIMENT_STATE.PREVIEW) { + logger.info({ + message: `getAllExperimentConditions: experiment: ${experiment.name}, user: ${userId}, condition: ${conditionAssigned ? conditionAssigned.conditionCode : null + }`, + }); + } + + const assignedFactors = factorialObject?.assignedFactor || null; + const factorialCondition = factorialObject?.factorialCondition || null; + const assignedConditionToReturn = + factorialCondition || + conditionAssigned || { + conditionCode: null, + }; + + if (experiment.assignmentUnit === ASSIGNMENT_UNIT.WITHIN_SUBJECTS) { + const count = monitoredLogCounts.find((log) => log.site === site && log.target === target)?.count || 0; + return withInSubjectType(experiment, conditionPayloads, site, target, factors, userId, count); + } else { + const experimentId = experiment.id; + return { + target, + site, + assignedCondition: [ + { + ...assignedConditionToReturn, + payload: payloadFound?.payload, + experimentId, + }, + ], + assignedFactor: assignedFactors ? [assignedFactors] : null, + experimentType: experiment.type, + }; + } + }); + } + + private getPayloadAndFactorialObject( + conditionAssigned: ExperimentCondition | undefined, + type: EXPERIMENT_TYPE, + conditionPayloads: ConditionPayloadDTO[], + decisionPoint: DecisionPoint, + factors: FactorDTO[] + ): { payloadFound: ConditionPayloadDTO | undefined; factorialObject: any } { + let payloadFound: ConditionPayloadDTO | undefined; + let factorialObject: any; + + if (conditionAssigned) { + if (type === EXPERIMENT_TYPE.FACTORIAL) { + payloadFound = conditionPayloads.find((x) => x.parentCondition.id === conditionAssigned.id); + factorialObject = this.getFactorialCondition(conditionAssigned, payloadFound, factors); + } else { + payloadFound = conditionPayloads.find( + (x) => + x.parentCondition.id === conditionAssigned.id && + x.decisionPoint.site === decisionPoint.site && + x.decisionPoint.target === decisionPoint.target + ); + } + } + + return { payloadFound, factorialObject }; + } + public async getAllExperimentConditions( experimentUserDoc: RequestedExperimentUser, context: string, logger: UpgradeLogger ): Promise { - logger.info({ message: `getAllExperimentConditions: User: ${experimentUserDoc?.requestedUserId}` }); - const userId = experimentUserDoc?.id; + logger.info({ message: `getAllExperimentConditions: User: ${experimentUserDoc.requestedUserId}` }); + const userId = experimentUserDoc.id; const previewUser = await this.previewUserService.findOne(userId, logger); - const experimentUser: ExperimentUser = experimentUserDoc as ExperimentUser; - // query all experiment and sub experiment - // check if user or group is excluded - let experiments: Experiment[] = []; + /** Below are the detailed steps for the assignment process: + * 1. Fetch experiments based on user type & moving conditionPayloads at the root level + * 2. Check if user or group is globally excluded + * 3. Filter out valid group experiments that doesn't have invalid group/workingGroup which are not enrolled yet + * 4. Process assignments and exclusions + * 5. Process experiment pools and filtered experiments + * 6. Assign condition from the remaining experiment + */ - if (previewUser) { - experiments = await this.experimentRepository.getValidExperimentsWithPreview(context); - } else { - experiments = await this.experimentService.getCachedValidExperiments(context); - } - experiments = experiments.map((exp) => this.experimentService.formatingConditionPayload(exp)); + // 1. Fetch experiments based on user type & moving conditionPayloads at the root level + const experiments: Experiment[] = await this.getExperimentsForUser(previewUser, context); - const [userExcluded, groupExcluded] = await this.checkUserOrGroupIsGloballyExcluded(experimentUser); + // 2. Check if user or group is globally excluded + const experimentUser: ExperimentUser = experimentUserDoc; - if (userExcluded || groupExcluded.length > 0) { - // return null if the user or group is excluded from the experiment + // return empty assignments if the user or group is excluded from the experiment + const [isUserExcluded, isGroupExcluded] = await this.checkUserOrGroupIsGloballyExcluded(experimentUser); + + // return empty assignments if the user or group is excluded from the experiment + if (isUserExcluded || isGroupExcluded) { return []; } - // Experiment has assignment type as GROUP_ASSIGNMENT - const groupExperiments = experiments.filter(({ assignmentUnit }) => assignmentUnit === ASSIGNMENT_UNIT.GROUP); - // check for group and working group - if (groupExperiments.length > 0) { - // if empty group/workingGroup data: - let invalidGroupExperiment: Experiment[] = []; - let experimentWithInvalidGroupOrWorkingGroup: Experiment[] = []; - const isGroupWorkingGroupMissing = - !experimentUser.group || - !experimentUser.workingGroup || - (experimentUser.group && Object.keys(experimentUser.group).length === 0) || - (experimentUser.workingGroup && Object.keys(experimentUser.workingGroup).length === 0); - - if (!isGroupWorkingGroupMissing) { - // if group/workingGroup data present, check for invalid group/workingGroup: - experimentWithInvalidGroupOrWorkingGroup = this.experimentsWithInvalidGroupAndWorkingGroup( - experimentUser, - groupExperiments - ); - } - - invalidGroupExperiment = await this.groupExperimentWithoutEnrollments( - isGroupWorkingGroupMissing ? groupExperiments : experimentWithInvalidGroupOrWorkingGroup, - experimentUser, - logger - ); - const invalidGroupExperimentIds = invalidGroupExperiment.map((experiment) => experiment.id); - experiments = experiments.filter(({ id }) => !invalidGroupExperimentIds.includes(id)); - } + // 3. Filter out valid group experiments that doesn't have invalid group/workingGroup which are not enrolled yet + const validExperiments = await this.filterAndProcessGroupExperiments(experiments, experimentUser, logger); - // try catch block for experiment assignment error + // 4. Process assignments and exclusions try { - // return if no experiment - if (experiments.length === 0) { + // return empty assignments if there are no valid experiments + if (validExperiments.length === 0) { return []; } - const globalFilteredExperiments: Experiment[] = [...experiments]; - const experimentIds = globalFilteredExperiments.map((experiment) => experiment.id); - - // return if no experiment - if (experimentIds.length === 0) { - return []; - } + const experimentIds = validExperiments.map((experiment) => experiment.id); - // ============ query assignment/exclusion for user - const allGroupIds: string[] = (experimentUser.workingGroup && Object.values(experimentUser.workingGroup)) || []; - const [individualEnrollments, groupEnrollments, individualExclusions, groupExclusions] = await Promise.all([ - experimentIds.length > 0 - ? this.individualEnrollmentRepository.findEnrollments(experimentUser.id, experimentIds) - : Promise.resolve([] as IndividualEnrollment[]), - allGroupIds.length > 0 && experimentIds.length > 0 - ? this.groupEnrollmentRepository.findEnrollments(allGroupIds, experimentIds) - : Promise.resolve([] as GroupEnrollment[]), - experimentIds.length > 0 - ? this.individualExclusionRepository.findExcluded(experimentUser.id, experimentIds) - : Promise.resolve([] as IndividualExclusion[]), - allGroupIds.length > 0 && experimentIds.length > 0 - ? this.groupExclusionRepository.findExcluded(allGroupIds, experimentIds) - : Promise.resolve([] as GroupExclusion[]), - ]); + // Query assignments and exclusions for the user + const [individualEnrollments, groupEnrollments, individualExclusions, groupExclusions] = await this.getAssignmentsAndExclusionsForUser(experimentUser, experimentIds); let mergedIndividualAssignment = individualEnrollments; // add assignments for individual assignments if preview user @@ -442,66 +608,26 @@ export class ExperimentAssignmentService { mergedIndividualAssignment = [...previewAssignment, ...mergedIndividualAssignment]; } - // experiment level inclusion and exclusion + // Check for experiment level inclusion and exclusion and return valid inclusion experiments let [filteredExperiments] = await this.experimentLevelExclusionInclusion( - globalFilteredExperiments, + validExperiments, experimentUser ); - // Create experiment pool - const experimentPools = this.createExperimentPool(filteredExperiments); - // console.log( - // 'experimentPools', - // experimentPools.map((exp) => exp.map(({ id }) => id)) - // ); - - // filter pools which are not assigned - const unassignedPools = experimentPools.filter((pool) => { - return !pool.some((experiment) => { - const individualEnrollment = mergedIndividualAssignment.find((enrollment) => { - return enrollment.experiment.id === experiment.id; - }); - const groupEnrollment = groupEnrollments.find((enrollment) => { - return ( - enrollment.experiment.id === experiment.id && - enrollment.groupId === experimentUser.workingGroup[experiment.group] - ); - }); - return individualEnrollment || groupEnrollment ? true : false; - }); - }); - - // Assign experiments inside the pools - const random = seedrandom(userId)(); - filteredExperiments = unassignedPools.map((pool) => { - return pool[Math.floor(random * pool.length)]; - }); - // console.log('Assigned pools', filteredExperiments); - - // Create new filtered experiment - const alreadyAssignedExperiment = experimentPools.map((pool) => { - return pool.filter((experiment) => { - const individualEnrollment = mergedIndividualAssignment.find((enrollment) => { - return enrollment.experiment.id === experiment.id; - }); - const groupEnrollment = groupEnrollments.find((enrollment) => { - return ( - enrollment.experiment.id === experiment.id && - enrollment.groupId === experimentUser.workingGroup[experiment.group] - ); - }); - return individualEnrollment || groupEnrollment ? true : false; - }); - }); - - filteredExperiments = alreadyAssignedExperiment.flat().concat(filteredExperiments); + // 5. Process experiment pools on filtered experiments + filteredExperiments = this.processExperimentPools( + filteredExperiments, + mergedIndividualAssignment, + groupEnrollments, + experimentUser + ); - // return if no experiment + // return empty if no experiments if (filteredExperiments.length === 0) { return []; } - // assign remaining experiment + // 6. Assign condition from the remaining experiment (Use the updated filtered experiments for further processing) const experimentAssignment = await Promise.all( filteredExperiments.map(async (experiment) => { const individualEnrollment = mergedIndividualAssignment.find((assignment) => { @@ -541,7 +667,7 @@ export class ExperimentAssignmentService { ); }) ); - let monitoredLogCounts = []; + let monitoredLogCounts: MonitoredDecisionPointLogDataCount[] = []; if (filteredExperiments.some((e) => e.assignmentUnit === ASSIGNMENT_UNIT.WITHIN_SUBJECTS)) { const allWithinSubjectsSites = []; const allWithinSubjectsTargets = []; @@ -561,69 +687,26 @@ export class ExperimentAssignmentService { return filteredExperiments.reduce((accumulator, experiment, index) => { const assignment = experimentAssignment[index]; - // const { state, name, id } = experiment; - const { state, name, conditionPayloads, type, id, factors } = - this.experimentService.formatingPayload(experiment); - const decisionPoints = experiment.partitions.map((decisionPoint) => { - const { target, site } = decisionPoint; - const conditionAssigned = assignment; - let factorialObject; - - let payloadFound: ConditionPayloadDTO; - if (conditionAssigned) { - if (type === EXPERIMENT_TYPE.FACTORIAL) { - // returns factorial alias condition or assigned condition - payloadFound = conditionPayloads.find((x) => x.parentCondition.id === conditionAssigned.id); - factorialObject = this.getFactorialCondition(conditionAssigned, payloadFound, factors); - } else { - // checking alias condition for simple experiment - payloadFound = conditionPayloads.find( - (x) => - x.parentCondition.id === conditionAssigned.id && - x.decisionPoint.site === decisionPoint.site && - x.decisionPoint.target === decisionPoint.target - ); - } - } - - // adding info based on experiment state - if (state === EXPERIMENT_STATE.PREVIEW) { - // TODO add enrollment code here - logger.info({ - message: `getAllExperimentConditions: experiment: ${name}, user: ${userId}, condition: ${ - conditionAssigned ? conditionAssigned.conditionCode : null - }`, - }); - } + const { conditionPayloads, type, factors } = + this.experimentService.formattingPayload(experiment); - const assignedFactors = factorialObject ? factorialObject['assignedFactor'] : null; - const factorialCondition = factorialObject ? factorialObject['factorialCondition'] : null; - const assignedConditionToReturn = factorialCondition || - conditionAssigned || { - conditionCode: null, - }; - - if (experiment.assignmentUnit === ASSIGNMENT_UNIT.WITHIN_SUBJECTS) { - const count = monitoredLogCounts.find((log) => log.site === site && log.target === target)?.count || 0; - return withInSubjectType(experiment, conditionPayloads, site, target, factors, userId, count); - } else { - return { - target, - site, - assignedCondition: [ - { - ...assignedConditionToReturn, - payload: payloadFound?.payload, - experimentId: id, - }, - ], - assignedFactor: assignedFactors ? [assignedFactors] : null, - experimentType: experiment.type, - }; - } - }); - return assignment ? [...accumulator, ...decisionPoints] : accumulator; + if (assignment) { + const decisionPoints = this.mapDecisionPoints( + experiment, + assignment, + userId, + conditionPayloads, + type, + factors, + monitoredLogCounts, + logger + ); + return [...accumulator, ...decisionPoints]; + } else { + return accumulator; + } }, []); + } catch (err) { const error = err as ErrorWithType; error.details = 'Error in assignment'; @@ -678,7 +761,7 @@ export class ExperimentAssignmentService { return pool; } - public async checkUserOrGroupIsGloballyExcluded(experimentUser: ExperimentUser): Promise<[string, string[]]> { + public async checkUserOrGroupIsGloballyExcluded(experimentUser: ExperimentUser): Promise<[boolean, boolean]> { let userGroup = []; userGroup = Object.keys(experimentUser.workingGroup || {}).map((type: string) => { return `${type}_${experimentUser.workingGroup[type]}`; @@ -713,7 +796,7 @@ export class ExperimentAssignmentService { const userExcluded = excludedUsers.find((userId) => userId === experimentUser.id); const groupExcluded = userGroup.length > 0 ? excludedGroups.filter((group) => userGroup.includes(group)) : []; - return [userExcluded, groupExcluded]; + return [userExcluded !== undefined, groupExcluded.length > 0]; } // When browser will be sending the blob data @@ -805,9 +888,9 @@ export class ExperimentAssignmentService { // fetch individual assignment for group experiments const individualEnrollments = await (experiments.length > 0 ? this.individualEnrollmentRepository.findEnrollments( - experimentUser.id, - experiments.map(({ id }) => id) - ) + experimentUser.id, + experiments.map(({ id }) => id) + ) : Promise.resolve([])); // check assignments for group experiment @@ -1236,7 +1319,7 @@ export class ExperimentAssignmentService { groupEnrollment: GroupEnrollment; groupExclusion: GroupExclusion; }, - globallyExcluded: { user: string; group: string[] }, + globallyExcluded: { isUserExcluded: boolean; isGroupExcluded: boolean }, experimentLevelExcluded: { experiment: Experiment; reason: string; matchedGroup: boolean }[], status: MARKED_DECISION_POINT_STATUS, condition: string @@ -1266,11 +1349,11 @@ export class ExperimentAssignmentService { experiment, exclusionCode: EXCLUSION_CODE.PARTICIPANT_ON_EXCLUSION_LIST, }; - if (globallyExcluded.user || globallyExcluded.group.length) { + if (globallyExcluded.isUserExcluded || globallyExcluded.isGroupExcluded) { // store Individual exclusion document for the Group Exclusion as well: const promiseArray = []; promiseArray.push(this.individualExclusionRepository.saveRawJson([excludeUserDoc])); - if (globallyExcluded.group.length) { + if (globallyExcluded.isGroupExcluded) { // store Group exclusion document: const excludeGroupDoc: Pick = { groupId: user?.workingGroup?.[experiment.group], @@ -1414,15 +1497,15 @@ export class ExperimentAssignmentService { if (!individualEnrollment && !individualExclusion && conditionAssigned && !invalidGroup && !noGroupSpecified) { const individualEnrollmentDocument: Omit = - { - id: uuid(), - experiment, - partition: decisionPoint as DecisionPoint, - user, - condition: conditionAssigned, - groupId: user?.workingGroup?.[experiment.group], - enrollmentCode: groupEnrollment ? ENROLLMENT_CODE.GROUP_LOGIC : ENROLLMENT_CODE.ALGORITHMIC, - }; + { + id: uuid(), + experiment, + partition: decisionPoint as DecisionPoint, + user, + condition: conditionAssigned, + groupId: user?.workingGroup?.[experiment.group], + enrollmentCode: groupEnrollment ? ENROLLMENT_CODE.GROUP_LOGIC : ENROLLMENT_CODE.ALGORITHMIC, + }; promiseArray.push(this.individualEnrollmentRepository.save(individualEnrollmentDocument)); } @@ -1466,14 +1549,14 @@ export class ExperimentAssignmentService { ); if (!individualEnrollment && !individualExclusion && conditionAssigned) { const individualEnrollmentDocument: Omit = - { - id: uuid(), - experiment, - partition: decisionPoint as DecisionPoint, - user, - condition: conditionAssigned, - enrollmentCode: ENROLLMENT_CODE.ALGORITHMIC, - }; + { + id: uuid(), + experiment, + partition: decisionPoint as DecisionPoint, + user, + condition: conditionAssigned, + enrollmentCode: ENROLLMENT_CODE.ALGORITHMIC, + }; await this.individualEnrollmentRepository.save(individualEnrollmentDocument); } } @@ -1503,18 +1586,18 @@ export class ExperimentAssignmentService { return individualExclusion ? undefined : individualEnrollmentCondition - ? individualEnrollmentCondition - : groupExclusion - ? undefined - : groupEnrollmentCondition; + ? individualEnrollmentCondition + : groupExclusion + ? undefined + : groupEnrollmentCondition; } else if (experiment.consistencyRule === CONSISTENCY_RULE.GROUP) { return groupExclusion ? undefined : groupEnrollmentCondition - ? groupEnrollmentCondition - : individualExclusion - ? undefined - : individualEnrollmentCondition; + ? groupEnrollmentCondition + : individualExclusion + ? undefined + : individualEnrollmentCondition; } else { return experiment.assignmentUnit === ASSIGNMENT_UNIT.INDIVIDUAL ? individualEnrollmentCondition @@ -1536,22 +1619,22 @@ export class ExperimentAssignmentService { return individualExclusion ? undefined : individualEnrollmentCondition - ? individualEnrollmentCondition - : groupExclusion - ? undefined - : groupEnrollmentCondition - ? groupEnrollmentCondition - : this.getNewExperimentConditionAssignment(experiment, user, logger); + ? individualEnrollmentCondition + : groupExclusion + ? undefined + : groupEnrollmentCondition + ? groupEnrollmentCondition + : this.getNewExperimentConditionAssignment(experiment, user, logger); } else if (experiment.consistencyRule === CONSISTENCY_RULE.GROUP) { return groupExclusion ? undefined : groupEnrollmentCondition - ? groupEnrollmentCondition - : individualExclusion - ? undefined - : individualEnrollmentCondition - ? individualEnrollmentCondition - : this.getNewExperimentConditionAssignment(experiment, user, logger); + ? groupEnrollmentCondition + : individualExclusion + ? undefined + : individualEnrollmentCondition + ? individualEnrollmentCondition + : this.getNewExperimentConditionAssignment(experiment, user, logger); } else { return ( (experiment.assignmentUnit === ASSIGNMENT_UNIT.INDIVIDUAL @@ -1600,7 +1683,7 @@ export class ExperimentAssignmentService { ): ExperimentCondition { const randomSeed = experiment.assignmentUnit === ASSIGNMENT_UNIT.INDIVIDUAL || - experiment.assignmentUnit === ASSIGNMENT_UNIT.WITHIN_SUBJECTS + experiment.assignmentUnit === ASSIGNMENT_UNIT.WITHIN_SUBJECTS ? `${experiment.id}_${user.id}` : `${experiment.id}_${user.workingGroup?.[experiment.group]}`; @@ -1926,7 +2009,7 @@ export class ExperimentAssignmentService { conditionAssigned: ExperimentCondition, conditionPayloads: ConditionPayloadDTO, factors: FactorDTO[] - ): object { + ): FactorialConditionResult { const levelsForCondition: string[] = []; const payloads: string[] = []; let factorialCondition; @@ -1982,7 +2065,12 @@ export class ExperimentAssignmentService { delete factorialCondition.levelCombinationElements; delete factorialCondition.conditionPayloads; - const objectToReturn = {}; + const objectToReturn: FactorialConditionResult = { + factorialCondition, + payloads, + assignedFactor, + conditionPayload: factorialConditionPayload, + }; objectToReturn['factorialCondition'] = factorialCondition; objectToReturn['payloads'] = payloads; objectToReturn['assignedFactor'] = assignedFactor; diff --git a/backend/packages/Upgrade/src/api/services/ExperimentService.ts b/backend/packages/Upgrade/src/api/services/ExperimentService.ts index 38fec7b8d..e1474032a 100644 --- a/backend/packages/Upgrade/src/api/services/ExperimentService.ts +++ b/backend/packages/Upgrade/src/api/services/ExperimentService.ts @@ -135,7 +135,7 @@ export class ExperimentService { } const experiments = await this.experimentRepository.findAllExperiments(); return experiments.map((experiment) => { - return this.reducedConditionPayload(this.formatingPayload(this.formatingConditionPayload(experiment))); + return this.reducedConditionPayload(this.formattingPayload(this.formattingConditionPayload(experiment))); }); } @@ -213,14 +213,14 @@ export class ExperimentService { } const experiments = await queryBuilderToReturn.getMany(); return experiments.map((experiment) => { - return this.reducedConditionPayload(this.formatingPayload(this.formatingConditionPayload(experiment))); + return this.reducedConditionPayload(this.formattingPayload(this.formattingConditionPayload(experiment))); }); } public async getSingleExperiment(id: string, logger?: UpgradeLogger): Promise { const experiment = await this.findOne(id, logger); if (experiment) { - return this.reducedConditionPayload(this.formatingPayload(experiment)); + return this.reducedConditionPayload(this.formattingPayload(experiment)); } else { return undefined; } @@ -233,7 +233,7 @@ export class ExperimentService { const experiment = await this.experimentRepository.findOneExperiment(id); if (experiment) { - return this.formatingConditionPayload(experiment); + return this.formattingConditionPayload(experiment); } else { return undefined; } @@ -254,7 +254,7 @@ export class ExperimentService { }; } - public async getCachedValidExperiments(context: string) { + public async getCachedValidExperiments(context: string): Promise { const cacheKey = CACHE_PREFIX.EXPERIMENT_KEY_PREFIX + context; return this.cacheService .wrap(cacheKey, this.experimentRepository.getValidExperiments.bind(this.experimentRepository, context)) @@ -609,7 +609,7 @@ export class ExperimentService { { experimentName: experiment.name }, user ); - return this.reducedConditionPayload(this.formatingPayload(this.formatingConditionPayload(experiment))); + return this.reducedConditionPayload(this.formattingPayload(this.formattingConditionPayload(experiment))); }); return formattedExperiments; @@ -1096,7 +1096,7 @@ export class ExperimentService { conditionPayloads: conditionPayloadDocToReturn as any, queries: (queryDocToReturn as any) || [], }; - const updatedExperiment = this.formatingPayload(newExperiment); + const updatedExperiment = this.formattingPayload(newExperiment); // removing unwanted params for diff const oldExperimentClone: Experiment = JSON.parse(JSON.stringify(oldExperiment)); @@ -1549,7 +1549,7 @@ export class ExperimentService { experimentName: createdExperiment.name, }; await this.experimentAuditLogRepository.saveRawJson(LOG_TYPE.EXPERIMENT_CREATED, createAuditLogData, user); - return this.reducedConditionPayload(this.formatingPayload(createdExperiment)); + return this.reducedConditionPayload(this.formattingPayload(createdExperiment)); } public async validateExperiments( @@ -1866,7 +1866,7 @@ export class ExperimentService { return createdExperiments; } - public formatingConditionPayload(experiment: Experiment): Experiment { + public formattingConditionPayload(experiment: Experiment): Experiment { if (experiment.type === EXPERIMENT_TYPE.FACTORIAL) { const conditionPayload: ConditionPayload[] = []; experiment.conditions.forEach((condition) => { @@ -1907,7 +1907,7 @@ export class ExperimentService { return { ...experiment, conditionPayloads: updatedCP }; } - public formatingPayload(experiment: Experiment): any { + public formattingPayload(experiment: Experiment): any { const updatedConditionPayloads = experiment.conditionPayloads.map((conditionPayload) => { const { payloadType, payloadValue, ...rest } = conditionPayload; return { diff --git a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts index 361211397..ed2039dda 100644 --- a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts +++ b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts @@ -81,11 +81,11 @@ export class FeatureFlagService { logger.info({ message: `getKeys: User: ${experimentUserDoc?.requestedUserId}` }); const filteredFeatureFlags = await this.featureFlagRepository.getFlagsFromContext(context); - const [userExcluded, groupExcluded] = await this.experimentAssignmentService.checkUserOrGroupIsGloballyExcluded( + const [isUserExcluded, isGroupExcluded] = await this.experimentAssignmentService.checkUserOrGroupIsGloballyExcluded( experimentUserDoc ); - if (userExcluded || groupExcluded.length > 0) { + if (isUserExcluded || isGroupExcluded) { return []; }