Skip to content

Commit b543eb2

Browse files
authored
Merge pull request #2994 from CarnegieLearningWeb/feature/rewards-summary-direct-query
Feature/rewards summary direct query
2 parents 172c88e + 22cad5b commit b543eb2

30 files changed

Lines changed: 1401 additions & 69 deletions

clientlibs/js/quickTest.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,17 @@ const groupsForSession = { classId: ['EPHEMERAL_USER_GROUP'] };
1919
const includeStoredUserGroups = true; // true to merge with stored user groups, false for session-only groups
2020
const alias = 'alias' + userId;
2121
const hostUrl = URL.LOCAL;
22-
const context = 'upgrade-internal';
23-
const site = 'asdf';
24-
const target = 'fssfs';
22+
const context = 'assign-prog';
23+
const site = 'fakesite';
24+
const target = 'faketarget';
2525
const status = MARKED_DECISION_POINT_STATUS.CONDITION_APPLIED;
2626
const featureFlagKey = 'TEST_FEATURE_FLAG';
2727

2828
// reward testing variables ----- //
29-
const experimentId = 'f9c3927c-b786-45f5-a96c-dd9262e3b4b6'; // needed for reward testing
29+
const experimentId = '1a43d51e-b286-40a2-9dd7-a01b67797276'; // needed for reward testing
3030
const rewardSite = site; // if using decision point for reward
3131
const rewardTarget = target; // if using decision point for reward
32-
const rewardValue = 'SUCCESS'; // or 'FAILURE' or use an UpgradeClient.BINARY_REWARD_VALUE enum
32+
const rewardValue = 'FAILURE'; // or 'FAILURE' or use an UpgradeClient.BINARY_REWARD_VALUE enum
3333
// ---------------------------- //
3434

3535
const options: UpGradeClientInterfaces.IConfigOptions = {
@@ -81,7 +81,7 @@ async function quickTest() {
8181
await doInit(client);
8282
await doGroupMembership(client);
8383
await doWorkingGroupMembership(client);
84-
await doAliases(client);
84+
// await doAliases(client);
8585
// await doAssign(client);
8686
// await doAssignIgnoreCache(client);
8787
// await doAssign(client);

packages/backend/rest-client-vscode/MoocletAPI.http

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@
2424
# 11. Repeat steps 8-10 as needed
2525

2626
############ env variables
27-
@host = http://localhost:8000
27+
@host = https://apps.qa-cli.net/mooclet-service
2828

2929
# Replace with your token, i.e.
30-
@token = Token 7439dc95718b525e8ae267604178575da15820e9
30+
@token = Token abc123
3131
# @token =
3232

3333
@apiEndpoint = /engine/api/v1
3434

3535
############ request variables (change as needed)
36-
@moocletId = 5
36+
@moocletId = 196
3737
@moocletName = newmooc4
3838
@policyId = 17
3939
@policyParametersId = 2
@@ -144,6 +144,11 @@ Content-type: application/json
144144
"policy": {{policyId}}
145145
}
146146

147+
########## query rewards by mooclet id:
148+
GET {{host}}{{apiEndpoint}}/value?mooclet={{moocletId}}&variable__name={{outcomeVariableName}}
149+
Authorization: {{token}}
150+
Content-type: application/json
151+
147152
##### EDITS:
148153

149154
########### create policyparameters

packages/backend/src/api/controllers/ExperimentController.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import { SegmentInputValidator } from './validators/SegmentInputValidator';
3838
import { ExperimentSegmentExclusion } from '../models/ExperimentSegmentExclusion';
3939
import { IdValidator } from './validators/ExperimentUserValidator';
4040
import { Segment } from '../models/Segment';
41+
import { MoocletRewardsService } from '../services/MoocletRewardsService';
42+
import { ExperimentRewardsSummary } from 'upgrade_types';
4143

4244
interface ExperimentPaginationInfo extends PaginationResponse {
4345
nodes: Experiment[];
@@ -656,6 +658,7 @@ export class ExperimentController {
656658
public experimentService: ExperimentService,
657659
public experimentAssignmentService: ExperimentAssignmentService,
658660
public moocletExperimentService: MoocletExperimentService,
661+
public moocletRewardService: MoocletRewardsService,
659662
public importExportService: ImportExportService
660663
) {}
661664

@@ -1933,4 +1936,18 @@ export class ExperimentController {
19331936

19341937
return lists;
19351938
}
1939+
1940+
/**
1941+
* Get Mooclet Rewards Feedback data
1942+
*/
1943+
@Get('/mooclet-rewards/:id')
1944+
public getMoocletRewards(
1945+
@Params({ validate: true }) { id }: IdValidator,
1946+
@Req() request: AppRequest
1947+
): Promise<ExperimentRewardsSummary> {
1948+
if (!env.mooclets?.enabled) {
1949+
throw new BadRequestError('Mooclet is not enabled in the environment');
1950+
}
1951+
return this.moocletRewardService.getRewardsSummaryForExperiment(id, request.logger);
1952+
}
19361953
}

packages/backend/src/api/services/MoocletDataService.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
MoocletVariableResponseDetails,
1717
MoocletValueRequestBody,
1818
MoocletValueResponseDetails,
19+
MoocletRewardCountRequestBody,
20+
MoocletPaginatedResponse,
1921
} from '../../types/Mooclet';
2022
import { UpgradeLogger } from '../../lib/logger/UpgradeLogger';
2123
import { MoocletError } from '../errors/MoocletError';
@@ -232,6 +234,28 @@ export class MoocletDataService {
232234
return response;
233235
}
234236

237+
public async getRewardsForExperiment(
238+
requestBody: MoocletRewardCountRequestBody,
239+
logger: UpgradeLogger,
240+
nextPageUrl?: string
241+
): Promise<MoocletPaginatedResponse<MoocletValueResponseDetails>> {
242+
// this endpoint serves a paginated response
243+
// if there are more results "pages" mooclet api sends the exact url to use for "next" page
244+
// else it is nul/undefined and we'll fetch from the beginning
245+
const url =
246+
nextPageUrl || `${this.apiUrl}/value?mooclet=${requestBody.moocletId}&variable__name=${requestBody.variableName}`;
247+
248+
const requestParams: MoocletProxyRequestParams = {
249+
method: 'GET',
250+
url,
251+
apiToken: this.apiToken,
252+
};
253+
254+
const response = await this.fetchExternalMoocletsData(requestParams, logger);
255+
256+
return response;
257+
}
258+
235259
public async postNewVariable(
236260
requestBody: MoocletVariableRequestBody,
237261
logger: UpgradeLogger

packages/backend/src/api/services/MoocletRewardsService.ts

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@ import { IndividualEnrollmentRepository } from '../repositories/IndividualEnroll
99
import { Service } from 'typedi';
1010
import { InjectRepository } from '../../typeorm-typedi-extensions';
1111
import { HttpError } from 'routing-controllers';
12-
import { MoocletValueRequestBody } from '../../types/Mooclet';
12+
import {
13+
MoocletPaginatedResponse,
14+
MoocletRewardCountRequestBody,
15+
MoocletValueRequestBody,
16+
MoocletValueResponseDetails,
17+
} from '../../types/Mooclet';
1318
import { RewardValidator } from '../controllers/validators/RewardValidator';
19+
import { ExperimentRewardsByCondition, ExperimentRewardsSummary } from 'upgrade_types';
20+
import { MoocletExperimentService } from './MoocletExperimentService';
1421

1522
export interface IRewardResponse {
1623
message: string;
@@ -25,7 +32,8 @@ export class MoocletRewardsService {
2532
private moocletExperimentRefRepository: MoocletExperimentRefRepository,
2633
@InjectRepository()
2734
private individualEnrollmentRepository: IndividualEnrollmentRepository,
28-
private moocletDataService: MoocletDataService
35+
private moocletDataService: MoocletDataService,
36+
private moocletExperimentService: MoocletExperimentService
2937
) {}
3038

3139
/**
@@ -204,6 +212,97 @@ export class MoocletRewardsService {
204212
return map.moocletVersionId;
205213
}
206214

215+
public async getRewardsSummaryForExperiment(
216+
experimentId: string,
217+
logger: UpgradeLogger
218+
): Promise<ExperimentRewardsSummary> {
219+
try {
220+
const moocletExperimentRef = await this.moocletExperimentService.getMoocletExperimentRefByUpgradeExperimentId(
221+
experimentId
222+
);
223+
const rewards: MoocletValueResponseDetails[] = [];
224+
logger.info({
225+
message: `Fetching Rewards data from mooclet server.`,
226+
experimentId,
227+
});
228+
let response = await this.fetchRewardsForExperiment(moocletExperimentRef, logger);
229+
if (Array.isArray(response.results)) {
230+
rewards.push(...response.results);
231+
}
232+
233+
while (response.next) {
234+
logger.info({
235+
message: `But wait there's more (Fetching more Rewards data from Mooclet server for experiment...)`,
236+
totalFound: response.count,
237+
totalFetched: response.results.length,
238+
next: response.next,
239+
});
240+
response = await this.fetchRewardsForExperiment(moocletExperimentRef, logger, response.next);
241+
if (Array.isArray(response.results)) {
242+
rewards.push(...response.results);
243+
}
244+
}
245+
246+
return this.createExperimentRewardsSummary(moocletExperimentRef, rewards, logger);
247+
} catch (error) {
248+
logger.error({ message: 'Error fetching rewards summary for experiment', experimentId, error });
249+
throw error;
250+
}
251+
}
252+
253+
public async fetchRewardsForExperiment(
254+
moocletExperimentRef: MoocletExperimentRef,
255+
logger: UpgradeLogger,
256+
nextPageUrl?: string
257+
): Promise<MoocletPaginatedResponse<MoocletValueResponseDetails>> {
258+
const requestBody: MoocletRewardCountRequestBody = {
259+
moocletId: moocletExperimentRef.moocletId,
260+
variableName: moocletExperimentRef.outcomeVariableName,
261+
};
262+
263+
return await this.moocletDataService.getRewardsForExperiment(requestBody, logger, nextPageUrl);
264+
}
265+
266+
public async createExperimentRewardsSummary(
267+
moocletExperimentRef: MoocletExperimentRef,
268+
rewardsData: MoocletValueResponseDetails[],
269+
logger: UpgradeLogger
270+
): Promise<ExperimentRewardsSummary> {
271+
const rewards: MoocletValueResponseDetails[] = rewardsData;
272+
273+
if (!rewardsData) {
274+
logger.warn({
275+
message: 'No rewards data returned from Mooclet API',
276+
experimentId: moocletExperimentRef.experimentId,
277+
});
278+
return [];
279+
}
280+
281+
const rewardsSummaries = moocletExperimentRef.versionConditionMaps.map(
282+
({ experimentCondition, moocletVersionId }) => {
283+
const versionRewards = rewards.filter((reward) => reward.version === moocletVersionId);
284+
const successes = versionRewards.filter((reward) => reward.value === 1.0).length;
285+
const failures = versionRewards.filter((reward) => reward.value === 0.0).length;
286+
const total = successes + failures;
287+
const percentSuccess = total > 0 ? (successes / total) * 100 : 0.0;
288+
const successRate = percentSuccess.toFixed(1) + '%';
289+
290+
const rewardsForCondition: ExperimentRewardsByCondition = {
291+
conditionCode: experimentCondition.conditionCode,
292+
successes,
293+
failures,
294+
total,
295+
successRate,
296+
order: experimentCondition.order,
297+
};
298+
return rewardsForCondition;
299+
}
300+
);
301+
302+
const orderedRewardsSummary = rewardsSummaries.sort((a, b) => a.order - b.order);
303+
return orderedRewardsSummary;
304+
}
305+
207306
/**
208307
* Throws a 409 data-conflict error for most unexpected cases
209308
*/

packages/backend/src/types/Mooclet.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ export interface MoocletRequestBody {
1212
policy: number;
1313
}
1414

15+
export interface MoocletPaginatedResponse<T> {
16+
count: number;
17+
next: string | null;
18+
previous: string | null;
19+
results: T[];
20+
}
21+
1522
export interface MoocletResponseDetails {
1623
id: number;
1724
name: string;
@@ -73,6 +80,11 @@ export interface MoocletValueRequestBody {
7380
policy?: number;
7481
}
7582

83+
export interface MoocletRewardCountRequestBody {
84+
moocletId: number;
85+
variableName: string;
86+
}
87+
7688
export interface MoocletValueResponseDetails {
7789
id: string;
7890
variable: string;

packages/backend/test/unit/controllers/ExperimentController.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { ExperimentAssignmentService } from '../../../src/api/services/Experimen
1111
import ExperimentAssignmentServiceMock from './mocks/ExperimentAssignmentServiceMock';
1212
import { MoocletExperimentService } from '../../../src/api/services/MoocletExperimentService';
1313
import MoocletExperimentServiceMock from './mocks/MoocletExperimentServiceMock';
14+
import { MoocletRewardsService } from '../../../src/api/services/MoocletRewardsService';
15+
import MoocletRewardsServiceMock from './mocks/MoocletRewardsServiceMock';
1416
import { ImportExportService } from '../../../src/api/services/ImportExportService';
1517
import ImportExportServiceMock from './mocks/ImportExportServiceMock';
1618
import { env } from './../../../src/env';
@@ -37,6 +39,7 @@ describe('Experiment Controller Testing', () => {
3739
Container.set(ExperimentService, new ExperimentServiceMock());
3840
Container.set(ExperimentAssignmentService, new ExperimentAssignmentServiceMock());
3941
Container.set(MoocletExperimentService, new MoocletExperimentServiceMock());
42+
Container.set(MoocletRewardsService, new MoocletRewardsServiceMock());
4043
Container.set(ImportExportService, new ImportExportServiceMock());
4144
});
4245

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Service } from 'typedi';
2+
3+
@Service()
4+
export default class MoocletRewardsServiceMock {
5+
public async getRewardsSummaryForExperiment(experimentId: string, logger: any): Promise<any> {
6+
return [
7+
{
8+
conditionCode: 'Control',
9+
successes: 10,
10+
failures: 5,
11+
total: 15,
12+
successRate: '66.7%',
13+
order: 0,
14+
},
15+
{
16+
conditionCode: 'Treatment',
17+
successes: 8,
18+
failures: 7,
19+
total: 15,
20+
successRate: '53.3%',
21+
order: 1,
22+
},
23+
];
24+
}
25+
}

0 commit comments

Comments
 (0)