Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add assign mooclet proxy to assignment service #2169

Merged
merged 4 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import { UserStratificationFactorRepository } from '../repositories/UserStratifi
import { UserStratificationFactor } from '../models/UserStratificationFactor';
import { RequestedExperimentUser } from '../controllers/validators/ExperimentUserValidator';
import { In } from 'typeorm';
import { env } from '../../env';
import { MoocletExperimentService } from './MoocletExperimentService';
@Service()
export class ExperimentAssignmentService {
constructor(
Expand Down Expand Up @@ -110,7 +112,8 @@ export class ExperimentAssignmentService {
public settingService: SettingService,
public segmentService: SegmentService,
public experimentService: ExperimentService,
public cacheService: CacheService
public cacheService: CacheService,
public moocletExperimentService: MoocletExperimentService
) {}
public async markExperimentPoint(
userDoc: RequestedExperimentUser,
Expand Down Expand Up @@ -527,7 +530,7 @@ export class ExperimentAssignmentService {
if (experiment.assignmentAlgorithm === ASSIGNMENT_ALGORITHM.STRATIFIED_RANDOM_SAMPLING) {
enrollmentCountPerCondition = await this.getEnrollmentCountPerCondition(experiment, userId);
}
return this.assignExperiment(
return await this.assignExperiment(
experimentUser,
experiment,
individualEnrollment,
Expand Down Expand Up @@ -838,7 +841,7 @@ export class ExperimentAssignmentService {
`,
});
});

// log error if there are group experiments which are not previosly assigned and they are
// to be excluded from assignment due to working group and group data is not properly set
if (experimentToExclude.length > 0) {
Expand Down Expand Up @@ -1453,7 +1456,7 @@ export class ExperimentAssignmentService {
};
await this.individualEnrollmentRepository.save(individualEnrollmentDocument);
} else {
const conditionAssigned = this.assignExperiment(
const conditionAssigned = await this.assignExperiment(
user,
experiment,
individualEnrollment,
Expand All @@ -1477,15 +1480,16 @@ export class ExperimentAssignmentService {
}
}

private assignExperiment(
private async assignExperiment(
user: ExperimentUser,
experiment: Experiment,
individualEnrollment: IndividualEnrollment | undefined,
groupEnrollment: GroupEnrollment | undefined,
individualExclusion: IndividualExclusion | undefined,
groupExclusion: GroupExclusion | undefined,
enrollmentCount?: { conditionId: string; userCount: number }[]
): ExperimentCondition | void {
enrollmentCount?: { conditionId: string; userCount: number }[],
logger: UpgradeLogger = new UpgradeLogger('ExperimentAssignmentService: assignExperiment')
): Promise<ExperimentCondition | void> {
const userId = user.id;
const individualEnrollmentCondition = experiment.conditions.find(
(condition) => condition.id === individualEnrollment?.condition?.id
Expand Down Expand Up @@ -1537,7 +1541,7 @@ export class ExperimentAssignmentService {
? undefined
: groupEnrollmentCondition
? groupEnrollmentCondition
: this.assignRandom(experiment, user);
: this.getNewExperimentConditionAssignment(experiment, user, logger);
} else if (experiment.consistencyRule === CONSISTENCY_RULE.GROUP) {
return groupExclusion
? undefined
Expand All @@ -1547,7 +1551,7 @@ export class ExperimentAssignmentService {
? undefined
: individualEnrollmentCondition
? individualEnrollmentCondition
: this.assignRandom(experiment, user);
: this.getNewExperimentConditionAssignment(experiment, user, logger);
} else {
return (
(experiment.assignmentUnit === ASSIGNMENT_UNIT.INDIVIDUAL
Expand All @@ -1559,6 +1563,36 @@ export class ExperimentAssignmentService {
return;
}

private async getNewExperimentConditionAssignment(
experiment: Experiment,
user: ExperimentUser,
logger: UpgradeLogger,
enrollmentCount?: { conditionId: string; userCount: number }[]
): Promise<ExperimentCondition> {
const isMoocletExperiment = this.moocletExperimentService.isMoocletExperiment(experiment.assignmentAlgorithm);

if (isMoocletExperiment && !env.mooclets.enabled) {
logger.error({
message: 'Mooclet experiment algorithm is indicated but mooclets are not enabled',
experiment,
user,
});
return undefined;
}

if (isMoocletExperiment && env.mooclets.enabled) {
return this.getConditionFromMoocletProxy(experiment, user);
} else {
return this.assignRandom(experiment, user, enrollmentCount);
}
}

private async getConditionFromMoocletProxy(experiment: Experiment, user: ExperimentUser) {
const userId = user.id;

return await this.moocletExperimentService.getConditionFromMoocletProxy(experiment, userId);
}

private assignRandom(
experiment: Experiment,
user: ExperimentUser,
Expand Down
22 changes: 10 additions & 12 deletions backend/packages/Upgrade/src/api/services/MoocletDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,21 +199,19 @@ export class MoocletDataService {
return response;
}

/* not yet implemented */

// public async getVersionForNewLearner(moocletId: number, userId: string) {
// const endpoint = `/mooclet/${moocletId}/run?learner=${userId}`;
public async getVersionForNewLearner(moocletId: number, userId: string): Promise<MoocletVersionResponseDetails> {
const endpoint = `/mooclet/${moocletId}/run?learner=${userId}`;

// const requestParams: MoocletProxyRequestParams = {
// method: 'GET',
// url: this.apiUrl + endpoint,
// apiToken: this.apiToken,
// };
const requestParams: MoocletProxyRequestParams = {
method: 'GET',
url: this.apiUrl + endpoint,
apiToken: this.apiToken,
};

// const response = await this.fetchExternalMoocletsData(requestParams);
const response = await this.fetchExternalMoocletsData(requestParams);

// return response;
// }
return response;
}

/**
* Generic Requests to Mooclets API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ import { ConditionValidator, ExperimentDTO } from '../DTO/ExperimentDTO';
import { UserDTO } from '../DTO/UserDTO';
import { Experiment } from '../models/Experiment';
import { UpgradeLogger } from '../../lib/logger/UpgradeLogger';
import { ASSIGNMENT_ALGORITHM, MoocletPolicyParametersDTO } from 'upgrade_types';
import { ASSIGNMENT_ALGORITHM, MOOCLET_POLICY_SCHEMA_MAP, MoocletPolicyParametersDTO } from 'upgrade_types';
import { ExperimentCondition } from '../models/ExperimentCondition';

export interface SyncCreateParams {
experimentDTO: ExperimentDTO;
Expand Down Expand Up @@ -501,49 +502,55 @@ export class MoocletExperimentService extends ExperimentService {
): Promise<MoocletExperimentRef | undefined> {
const moocletExperimentRef = await this.moocletExperimentRefRepository.findOne({
where: { experimentId: upgradeExperimentId },
relations: ['versionConditionMaps'],
relations: ['versionConditionMaps', 'versionConditionMaps.experimentCondition'],
});
return moocletExperimentRef;
}

// NOTE: will do mooclet condition assignment changes in follow-up PR

// public async getConditionFromMoocletProxy(moocletExperimentRef: MoocletExperimentRef, userId: string): Promise<ExperimentCondition> {
// const versionResponse = await this.moocletDataService.getVersionForNewLearner(moocletExperimentRef.moocletId, userId);
// const experimentCondition = this.mapMoocletVersionToUpgradeCondition(versionResponse, moocletExperimentRef);
// return experimentCondition;
// }

// private mapMoocletVersionToUpgradeCondition(
// versionResponse: MoocletVersionResponseDetails,
// moocletExperimentRef: MoocletExperimentRef
// ): ExperimentCondition {
// // Find the corresponding versionConditionMap
// const versionConditionMap = moocletExperimentRef.versionConditionMaps.find(
// (map) => map.moocletVersionId === versionResponse.id
// );

// if (!versionConditionMap) {
// const error = {
// message: 'Version ID not found in version condition maps',
// version: versionResponse,
// versionConditionMaps: moocletExperimentRef.versionConditionMaps,
// };
// throw new Error(JSON.stringify(error));
// }

// // Get the experiment condition from the versionConditionMap
// const experimentCondition = versionConditionMap.experimentCondition;

// if (!experimentCondition) {
// const error = {
// message: 'Experiment condition not found in version condition map',
// version: versionResponse,
// versionConditionMap,
// };
// throw new Error(JSON.stringify(error));
// }

// return experimentCondition;
// }
public async getConditionFromMoocletProxy(experiment: Experiment, userId: string): Promise<ExperimentCondition> {
const moocletExperimentRef = await this.getMoocletExperimentRefByUpgradeExperimentId(experiment.id);
const versionResponse = await this.moocletDataService.getVersionForNewLearner(
moocletExperimentRef.moocletId,
userId
);
const experimentCondition = this.mapMoocletVersionToUpgradeCondition(versionResponse, moocletExperimentRef);
return experimentCondition;
}

private mapMoocletVersionToUpgradeCondition(
versionResponse: MoocletVersionResponseDetails,
moocletExperimentRef: MoocletExperimentRef
): ExperimentCondition {
// Find the corresponding versionConditionMap
const versionConditionMap = moocletExperimentRef.versionConditionMaps.find(
(map) => map.moocletVersionId === versionResponse.id
);

if (!versionConditionMap) {
const error = {
message: 'Version ID not found in version condition maps',
version: versionResponse,
versionConditionMaps: moocletExperimentRef.versionConditionMaps,
};
throw new Error(JSON.stringify(error));
}

// Get the experiment condition from the versionConditionMap
const experimentCondition = versionConditionMap.experimentCondition;

if (!experimentCondition) {
const error = {
message: 'Experiment condition not found in version condition map',
version: versionResponse,
versionConditionMap,
};
throw new Error(JSON.stringify(error));
}

return experimentCondition;
}

public isMoocletExperiment(assignmentAlgorithm: ASSIGNMENT_ALGORITHM): boolean {
return Object.keys(MOOCLET_POLICY_SCHEMA_MAP).includes(assignmentAlgorithm);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { MARKED_DECISION_POINT_STATUS } from 'upgrade_types';
import { CacheService } from '../../../src/api/services/CacheService';
import { UserStratificationFactorRepository } from '../../../src/api/repositories/UserStratificationRepository';
import { configureLogger } from '../../utils/logger';
import { MoocletExperimentService } from '../../../src/api/services/MoocletExperimentService';

describe('Experiment Assignment Service Test', () => {
let sandbox;
Expand All @@ -58,6 +59,7 @@ describe('Experiment Assignment Service Test', () => {
const segmentServiceMock = sinon.createStubInstance(SegmentService);
const experimentServiceMock = sinon.createStubInstance(ExperimentService);
const cacheServiceMock = sinon.createStubInstance(CacheService);
const moocletExperimentService = sinon.createStubInstance(MoocletExperimentService);
experimentServiceMock.formatingConditionPayload.restore();
experimentServiceMock.formatingPayload.restore();

Expand Down Expand Up @@ -89,7 +91,8 @@ describe('Experiment Assignment Service Test', () => {
settingServiceMock,
segmentServiceMock,
experimentServiceMock,
cacheServiceMock
cacheServiceMock,
moocletExperimentService
);
testedModule.cacheService.wrap.resolves([]);
testedModule.segmentService.getSegmentByIds.withArgs(['77777777-7777-7777-7777-777777777777']).resolves([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ describe('#MoocletDataService', () => {

describe('#postNewPolicyParameters', () => {
const mockPolicyParameters: MoocletTSConfigurablePolicyParametersDTO = {
assignmentAlgorithm: ASSIGNMENT_ALGORITHM.MOOCLET_TS_CONFIGURABLE,
prior: {
failure: 1,
success: 1,
Expand Down
Loading