From 0e7ddef84d861135480fbb0f4baa0a1950ab8980 Mon Sep 17 00:00:00 2001 From: doswalt Date: Tue, 7 Apr 2026 13:25:44 -0400 Subject: [PATCH 01/10] wip conditions priors --- .../rest-client-vscode/MoocletAPI.http | 13 +- .../api/services/MoocletExperimentService.ts | 47 +++++++- .../core/experiments/experiments.service.ts | 17 +++ .../mooclet-helper.service.spec.ts | 69 +---------- .../experiments/mooclet-helper.service.ts | 36 ++---- .../experiments/store/experiments.model.ts | 2 - ...edit-condition-priors-modal.component.html | 80 +++++++++++++ ...edit-condition-priors-modal.component.scss | 99 +++++++++++++++ .../edit-condition-priors-modal.component.ts | 113 ++++++++++++++++++ ...able-policy-parameters-form.component.html | 46 ------- ...urable-policy-parameters-form.component.ts | 2 - ...ent-conditions-section-card.component.html | 2 + ...iment-conditions-section-card.component.ts | 13 +- ...experiment-conditions-table.component.html | 46 +++++++ ...experiment-conditions-table.component.scss | 13 +- .../experiment-conditions-table.component.ts | 26 +++- .../shared/services/common-dialog.service.ts | 34 ++++++ .../projects/upgrade/src/assets/i18n/en.json | 10 ++ ...oocletTSConfigurablePolicyParametersDTO.ts | 7 +- packages/types/src/index.ts | 2 + 20 files changed, 523 insertions(+), 154 deletions(-) create mode 100644 packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.html create mode 100644 packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.scss create mode 100644 packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.ts diff --git a/packages/backend/rest-client-vscode/MoocletAPI.http b/packages/backend/rest-client-vscode/MoocletAPI.http index 3f2207cf3..e86f74abe 100644 --- a/packages/backend/rest-client-vscode/MoocletAPI.http +++ b/packages/backend/rest-client-vscode/MoocletAPI.http @@ -24,17 +24,17 @@ # 11. Repeat steps 8-10 as needed ############ env variables -@host = https://apps.qa-cli.net/mooclet-service +@host = http://localhost:8000/mooclet-service # Replace with your token, i.e. -@token = Token abc123 +@token = Token 0656e428497a93e7e8fa2f2009d1cf4786d467db # @token = @apiEndpoint = /engine/api/v1 ############ request variables (change as needed) -@moocletId = 196 -@moocletName = newmooc4 +@moocletId = 108 +@moocletName = @policyId = 17 @policyParametersId = 2 @version1Name = controlz @@ -50,6 +50,11 @@ GET {{host}}{{apiEndpoint}}/policy HTTP/1.1 Authorization: {{token}} Content-type: application/json +########### Get policy id by assignment algorithm name +GET {{host}}{{apiEndpoint}}/mooclet/{{moocletId}} HTTP/1.1 +Authorization: {{token}} +Content-type: application/json + ########### create mooclet POST {{host}}{{apiEndpoint}}/mooclet HTTP/1.1 Authorization: {{token}} diff --git a/packages/backend/src/api/services/MoocletExperimentService.ts b/packages/backend/src/api/services/MoocletExperimentService.ts index 0c1d55855..f984aaf8f 100644 --- a/packages/backend/src/api/services/MoocletExperimentService.ts +++ b/packages/backend/src/api/services/MoocletExperimentService.ts @@ -52,6 +52,7 @@ import { EXPERIMENT_STATE, MoocletPolicyParametersDTO, MoocletTSConfigurablePolicyParametersDTO, + Prior, SUPPORTED_MOOCLET_ALGORITHMS, } from 'upgrade_types'; import { ExperimentCondition } from '../models/ExperimentCondition'; @@ -394,6 +395,22 @@ export class MoocletExperimentService extends ExperimentService { updatedExperiment.moocletPolicyParameters = policyParameterResponse.parameters; + // Transform priors keys from Mooclet version IDs back to UpGrade condition codes, + // so the PUT response is consistent with the GET response from attachPolicyParamsToExperimentDTO. + const updatedTsParams = updatedExperiment.moocletPolicyParameters as MoocletTSConfigurablePolicyParametersDTO; + if (updatedTsParams?.priors) { + const transformedPriors: Record = {}; + for (const [versionId, priorData] of Object.entries(updatedTsParams.priors)) { + const map = currentMoocletExperimentRef.versionConditionMaps.find( + (m) => String(m.moocletVersionId) === versionId + ); + if (map?.experimentCondition?.conditionCode) { + transformedPriors[map.experimentCondition.conditionCode] = priorData; + } + } + updatedTsParams.priors = transformedPriors; + } + // --------- update versions ---------------------- if (!versionEdits) { @@ -522,6 +539,21 @@ export class MoocletExperimentService extends ExperimentService { currentMoocletExperimentRef: MoocletExperimentRef, logger: UpgradeLogger ): Promise { + const tsParams = newPolicyParameters as MoocletTSConfigurablePolicyParametersDTO; + if (tsParams.priors) { + // Translate conditionCode keys to Mooclet version IDs before sending to the Mooclet API + const translatedPriors: Record = {}; + for (const [conditionCode, priorData] of Object.entries(tsParams.priors)) { + const versionConditionMap = currentMoocletExperimentRef.versionConditionMaps?.find( + (map) => map.experimentCondition?.conditionCode === conditionCode + ); + if (versionConditionMap?.moocletVersionId) { + translatedPriors[String(versionConditionMap.moocletVersionId)] = priorData; + } + } + newPolicyParameters = { ...tsParams, priors: translatedPriors } as MoocletPolicyParametersDTO; + } + return this.moocletDataService.updatePolicyParameters( currentMoocletExperimentRef.policyParametersId, { @@ -1168,7 +1200,7 @@ export class MoocletExperimentService extends ExperimentService { logger ); - // Transform current_posteriors keys from Mooclet version IDs to UpGrade condition codes + // Transform current_posteriors and priors keys from Mooclet version IDs to UpGrade condition codes const tsConfigurableParams = policyParameters.parameters as MoocletTSConfigurablePolicyParametersDTO; if (tsConfigurableParams.current_posteriors) { const transformedPosteriors = {}; @@ -1188,6 +1220,19 @@ export class MoocletExperimentService extends ExperimentService { tsConfigurableParams.current_posteriors = transformedPosteriors; } + if (tsConfigurableParams.priors) { + const transformedPriors: Record = {}; + for (const [versionId, priorData] of Object.entries(tsConfigurableParams.priors)) { + const versionConditionMap = moocletExperimentRef.versionConditionMaps.find( + (map) => map.moocletVersionId === parseInt(versionId, 10) + ); + if (versionConditionMap?.experimentCondition?.conditionCode) { + transformedPriors[versionConditionMap.experimentCondition.conditionCode] = priorData; + } + } + tsConfigurableParams.priors = transformedPriors; + } + experiment.moocletPolicyParameters = policyParameters.parameters; return experiment; diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts index 612d6585b..6da2fb881 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts @@ -60,6 +60,7 @@ import { map, take, tap } from 'rxjs/operators'; import { LocalStorageService } from '../local-storage/local-storage.service'; import { ExperimentSegmentListRequest } from '../segments/store/segments.model'; import { ConditionWeightUpdate } from '../../features/dashboard/experiments/modals/edit-condition-weights-modal/edit-condition-weights-modal.component'; +import { MoocletTSConfigurablePolicyParametersDTO, Prior } from 'upgrade_types'; import { selectCurrentUserEmail } from '../auth/store/auth.selectors'; @Injectable() @@ -299,6 +300,22 @@ export class ExperimentService { ); } + updateExperimentConditionPriors(experiment: ExperimentVM, priors: Record): void { + const updatedExperiment: ExperimentVM = { + ...experiment, + moocletPolicyParameters: { + ...experiment.moocletPolicyParameters, + priors, + } as MoocletTSConfigurablePolicyParametersDTO, + }; + this.store$.dispatch( + experimentAction.actionUpsertExperiment({ + experiment: updatedExperiment, + actionType: UpsertExperimentType.UPDATE_EXPERIMENT, + }) + ); + } + setIsLoadingImportExperiment(isLoadingImportExperiment: boolean) { this.store$.dispatch(experimentAction.actionSetIsLoadingImportExperiment({ isLoadingImportExperiment })); } diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.spec.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.spec.ts index 976eddfe5..dacc5c746 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.spec.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.spec.ts @@ -62,7 +62,6 @@ describe('MoocletAlgorithmHelperService', () => { expect(result.batch_size).toBe(expected.batch_size); expect(result.uniform_threshold).toBe(expected.uniform_threshold); expect(result.tspostdiff_thresh).toBe(expected.tspostdiff_thresh); - expect(result.prior).toEqual(expected.prior); expect(result.max_rating).toBe(expected.max_rating); expect(result.min_rating).toBe(expected.min_rating); }); @@ -81,8 +80,6 @@ describe('MoocletAlgorithmHelperService', () => { batch_size: defaults.batch_size, uniform_threshold: defaults.uniform_threshold, tspostdiff_thresh: defaults.tspostdiff_thresh, - prior_success: defaults.prior.success, - prior_failure: defaults.prior.failure, }); }); @@ -91,7 +88,6 @@ describe('MoocletAlgorithmHelperService', () => { const defaults = new MoocletTSConfigurablePolicyParametersDTO(); expect(result.batch_size).toBe(defaults.batch_size); - expect(result.prior_success).toBe(defaults.prior.success); }); it('should extract editable fields from existing params', () => { @@ -99,7 +95,6 @@ describe('MoocletAlgorithmHelperService', () => { batch_size: 50, uniform_threshold: 100, tspostdiff_thresh: 5, - prior: { success: 10, failure: 10 }, }); const result = service.deriveEditableParametersForTSConfigurable(existingParams); @@ -108,23 +103,9 @@ describe('MoocletAlgorithmHelperService', () => { batch_size: 50, uniform_threshold: 100, tspostdiff_thresh: 5, - prior_success: 10, - prior_failure: 10, }); }); - it('should flatten nested prior object into prior_success and prior_failure', () => { - const existingParams = createMockTSConfigurableParams({ - prior: { success: 20, failure: 30 }, - }); - - const result = service.deriveEditableParametersForTSConfigurable(existingParams); - - expect(result.prior_success).toBe(20); - expect(result.prior_failure).toBe(30); - expect((result as any).prior).toBeUndefined(); - }); - it('should not include non-editable fields in result', () => { const existingParams = createMockTSConfigurableParams({ assignmentAlgorithm: ASSIGNMENT_ALGORITHM.MOOCLET_TS_CONFIGURABLE, @@ -160,8 +141,6 @@ describe('MoocletAlgorithmHelperService', () => { batch_size: 50, uniform_threshold: 100, tspostdiff_thresh: 5, - prior_success: 10, - prior_failure: 10, }; const result = service.buildTSConfigurablePolicyParametersDTO(editableParams); @@ -169,7 +148,6 @@ describe('MoocletAlgorithmHelperService', () => { expect(result.batch_size).toBe(50); expect(result.uniform_threshold).toBe(100); expect(result.tspostdiff_thresh).toBe(5); - expect(result.prior).toEqual({ success: 10, failure: 10 }); }); it('should set assignmentAlgorithm to MOOCLET_TS_CONFIGURABLE', () => { @@ -196,22 +174,6 @@ describe('MoocletAlgorithmHelperService', () => { expect(result.min_rating).toBe(defaults.min_rating); }); - it('should convert flat prior fields to nested prior object', () => { - const editableParams: EditableTSConfigurablePolicyParameters = { - batch_size: 30, - uniform_threshold: 50, - tspostdiff_thresh: 3, - prior_success: 15, - prior_failure: 20, - }; - - const result = service.buildTSConfigurablePolicyParametersDTO(editableParams); - - expect(result.prior).toBeDefined(); - expect(result.prior.success).toBe(15); - expect(result.prior.failure).toBe(20); - }); - it('should not include outcome_variable_name for any experiment name', () => { const editableParams = createMockEditableParams(); @@ -234,8 +196,6 @@ describe('MoocletAlgorithmHelperService', () => { expect(validators.batch_size).toBeDefined(); expect(validators.uniform_threshold).toBeDefined(); expect(validators.tspostdiff_thresh).toBeDefined(); - expect(validators.prior_success).toBeDefined(); - expect(validators.prior_failure).toBeDefined(); }); it('should include required validator for all fields', () => { @@ -252,9 +212,6 @@ describe('MoocletAlgorithmHelperService', () => { expect(validators.batch_size.length).toBe(4); expect(validators.uniform_threshold.length).toBe(4); - expect(validators.prior_success.length).toBe(4); - expect(validators.prior_failure.length).toBe(4); - expect(validators.tspostdiff_thresh.length).toBe(3); // Test that min validator works correctly for batch_size @@ -351,7 +308,6 @@ describe('MoocletAlgorithmHelperService', () => { batch_size: 30, uniform_threshold: 50, tspostdiff_thresh: 3, - prior: { success: 1, failure: 1 }, outcome_variable_name: 'test_outcome', max_rating: 5, min_rating: 1, @@ -368,7 +324,6 @@ describe('MoocletAlgorithmHelperService', () => { batch_size: 30, uniform_threshold: 50, tspostdiff_thresh: 3, - prior: { success: 1, failure: 1 }, outcome_variable_name: 'test', max_rating: 5, min_rating: 1, @@ -385,7 +340,6 @@ describe('MoocletAlgorithmHelperService', () => { batch_size: 30, uniform_threshold: 50, tspostdiff_thresh: 3, - prior: { success: 1, failure: 1 }, outcome_variable_name: '', // Empty string should not cause validation errors max_rating: 5, min_rating: 1, @@ -402,7 +356,6 @@ describe('MoocletAlgorithmHelperService', () => { batch_size: 30, uniform_threshold: 50, tspostdiff_thresh: 3, - prior: { success: 1, failure: 1 }, // outcome_variable_name property completely missing max_rating: 5, min_rating: 1, @@ -461,7 +414,6 @@ describe('formatTSConfigurablePolicyParamDetails (pure function)', () => { batch_size: 50, uniform_threshold: 100, tspostdiff_thresh: 5, - prior: { success: 10, failure: 10 }, }), }); @@ -469,22 +421,7 @@ describe('formatTSConfigurablePolicyParamDetails (pure function)', () => { expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe(5); - }); - - it('should include prior values in formatted output', () => { - environment.moocletToggle = true; - const experiment = createMockExperimentVM({ - assignmentAlgorithm: ASSIGNMENT_ALGORITHM.MOOCLET_TS_CONFIGURABLE, - moocletPolicyParameters: createMockTSConfigurableParams({ - prior: { success: 15, failure: 20 }, - }), - }); - - const result = formatTSConfigurablePolicyParamDetails(experiment); - - expect(result.some((param) => param.value === 15)).toBe(true); - expect(result.some((param) => param.value === 20)).toBe(true); + expect(result.length).toBe(3); }); it('should include all configurable parameters', () => { @@ -495,7 +432,6 @@ describe('formatTSConfigurablePolicyParamDetails (pure function)', () => { batch_size: 50, uniform_threshold: 100, tspostdiff_thresh: 5, - prior: { success: 10, failure: 10 }, }), }); @@ -505,7 +441,6 @@ describe('formatTSConfigurablePolicyParamDetails (pure function)', () => { expect(values).toContain(50); expect(values).toContain(100); expect(values).toContain(5); - expect(values).toContain(10); }); }); @@ -521,8 +456,6 @@ function createMockEditableParams( batch_size: defaults.batch_size, uniform_threshold: defaults.uniform_threshold, tspostdiff_thresh: defaults.tspostdiff_thresh, - prior_success: defaults.prior.success, - prior_failure: defaults.prior.failure, ...overrides, }; } diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.ts index 056cc1f5c..2fc848fbf 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.ts @@ -39,17 +39,6 @@ export function formatTSConfigurablePolicyParamDetails( const params = experiment.moocletPolicyParameters; const formattedParams: BullettedListKeyValueFormat[] = []; - if (params.prior) { - formattedParams.push({ - labelKey: TS_CONFIGURABLE_OVERVIEW_PARAM_LABELS.PRIOR_SUCCESS, - value: params.prior.success, - }); - formattedParams.push({ - labelKey: TS_CONFIGURABLE_OVERVIEW_PARAM_LABELS.PRIOR_FAILURE, - value: params.prior.failure, - }); - } - formattedParams.push({ labelKey: TS_CONFIGURABLE_OVERVIEW_PARAM_LABELS.BATCH_SIZE, value: params.batch_size, @@ -85,8 +74,6 @@ export interface EditableTSConfigurablePolicyParameters { batch_size: number; uniform_threshold: number; tspostdiff_thresh: number; - prior_success: number; - prior_failure: number; } /** @@ -174,8 +161,6 @@ export class MoocletExperimentHelperService { batch_size: source.batch_size, uniform_threshold: source.uniform_threshold, tspostdiff_thresh: source.tspostdiff_thresh, - prior_success: source.prior?.success, - prior_failure: source.prior?.failure, }; } @@ -193,10 +178,6 @@ export class MoocletExperimentHelperService { batch_size: editableParams.batch_size, uniform_threshold: editableParams.uniform_threshold, tspostdiff_thresh: editableParams.tspostdiff_thresh, - prior: { - success: editableParams.prior_success, - failure: editableParams.prior_failure, - }, // System-managed fields assignmentAlgorithm: ASSIGNMENT_ALGORITHM.MOOCLET_TS_CONFIGURABLE, max_rating: defaults.max_rating, @@ -225,15 +206,24 @@ export class MoocletExperimentHelperService { CommonFormHelpersService.integerValidator(), ], tspostdiff_thresh: [Validators.required, Validators.min(defaults.tspostdiff_thresh), Validators.max(1.0)], - prior_success: [ + }; + } + + /** + * Get field validators for per-condition prior success/failure inputs used in the priors editor. + */ + getPriorsFieldValidators(): Record { + const priorsDefault = 1; + return { + successes: [ Validators.required, - Validators.min(defaults.prior.success), + Validators.min(priorsDefault), Validators.max(DEFAULT_MAX_NUMBER_INPUT), CommonFormHelpersService.integerValidator(), ], - prior_failure: [ + failures: [ Validators.required, - Validators.min(defaults.prior.failure), + Validators.min(priorsDefault), Validators.max(DEFAULT_MAX_NUMBER_INPUT), CommonFormHelpersService.integerValidator(), ], diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts index a89cf858a..632169489 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts @@ -598,8 +598,6 @@ export const EXPERIMENT_OVERVIEW_LABELS = { export const TS_CONFIGURABLE_OVERVIEW_PARAM_LABELS = { BATCH_SIZE: 'home.new-experiment.design.ts-configurable-policy.batch-size.label.text', - PRIOR_SUCCESS: 'home.new-experiment.design.ts-configurable-policy.prior-success.label.text', - PRIOR_FAILURE: 'home.new-experiment.design.ts-configurable-policy.prior-failure.label.text', UNIFORM_THRESHOLD: 'home.new-experiment.design.ts-configurable-policy.uniform-threshold.label.text', TSPOSTDIFF_THRESH: 'home.new-experiment.design.ts-configurable-policy.tspostdiff-thresh.label.text', }; diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.html new file mode 100644 index 000000000..e046c3034 --- /dev/null +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.html @@ -0,0 +1,80 @@ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{ 'experiments.edit-condition-priors-modal.condition-header.text' | translate }} + + + {{ condition.conditionCode }} + + + {{ 'experiments.edit-condition-priors-modal.successes-header.text' | translate }} + + + + @if (getSuccessesControl(i).hasError('min')) { + {{ 'experiments.edit-condition-priors-modal.min-error.text' | translate }} + } @if (getSuccessesControl(i).hasError('max')) { + {{ 'experiments.edit-condition-priors-modal.max-error.text' | translate }} + } @if (getSuccessesControl(i).hasError('integer')) { + {{ 'experiments.edit-condition-priors-modal.integer-error.text' | translate }} + } + + + {{ 'experiments.edit-condition-priors-modal.failures-header.text' | translate }} + + + + @if (getFailuresControl(i).hasError('min')) { + {{ 'experiments.edit-condition-priors-modal.min-error.text' | translate }} + } @if (getFailuresControl(i).hasError('max')) { + {{ 'experiments.edit-condition-priors-modal.max-error.text' | translate }} + } @if (getFailuresControl(i).hasError('integer')) { + {{ 'experiments.edit-condition-priors-modal.integer-error.text' | translate }} + } + +
+ {{ 'experiments.edit-condition-priors-modal.no-data.text' | translate }} +
+
+
+
diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.scss b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.scss new file mode 100644 index 000000000..680f2dda8 --- /dev/null +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.scss @@ -0,0 +1,99 @@ +.priors-input { + width: 90px; + + .mat-mdc-form-field-subscript-wrapper { + display: none; + } +} + +.table-container { + position: relative; + overflow: auto; + width: 100%; + + ::ng-deep .no-data tbody:before { + display: block; + line-height: 8px; + content: '\200C'; + } + + .conditions-table { + input[type='number'] { + -webkit-appearance: textfield; + -moz-appearance: textfield; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + } + + ::ng-deep thead { + background-color: var(--zircon); + + tr.mat-mdc-header-row { + height: 48px; + border: 0; + + th { + padding-left: 0; + color: var(--darker-grey); + + &:first-child { + border-top-left-radius: 4px; + } + + &:last-child { + border-top-right-radius: 4px; + } + } + } + } + + ::ng-deep tbody { + tr.mat-mdc-row { + height: 56px; + + td { + min-width: 96px; + padding-left: 0; + color: var(--black-2); + } + } + + tr.mat-mdc-no-data-row { + height: 48px; + + td { + text-align: center; + border: 1.5px dashed var(--light-grey-2); + color: var(--dark-grey); + } + } + } + + .condition-column { + width: 50%; + padding-left: 32px; + } + + .priors-column { + width: 25%; + text-align: right; + padding-right: 16px; + + .priors-input { + width: 90px; + + input { + text-align: right; + } + + .mat-mdc-form-field-subscript-wrapper { + display: none; + } + } + } + } +} diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.ts new file mode 100644 index 000000000..5388224cc --- /dev/null +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.ts @@ -0,0 +1,113 @@ +import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, ReactiveFormsModule, FormArray, FormControl } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTableModule } from '@angular/material/table'; +import { TranslateModule } from '@ngx-translate/core'; +import { Observable, combineLatest, map, startWith } from 'rxjs'; +import { CommonModalComponent } from '@shared-component-lib'; +import { CommonFormHelpersService } from '../../../../../shared/services/common-form-helpers.service'; +import { CommonModalConfig } from '@shared-component-lib/common-modal/common-modal.types'; +import { MoocletExperimentHelperService } from '../../../../../core/experiments/mooclet-helper.service'; +import { Prior } from 'upgrade_types'; +import { SharedModule } from '../../../../../shared/shared.module'; + +export interface ConditionPriorUpdate { + conditionCode: string; + successes: number; + failures: number; +} + +@Component({ + selector: 'app-edit-condition-priors-modal', + imports: [ + CommonModalComponent, + MatTableModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + CommonModule, + ReactiveFormsModule, + TranslateModule, + SharedModule, + ], + templateUrl: './edit-condition-priors-modal.component.html', + styleUrl: './edit-condition-priors-modal.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EditConditionPriorsModalComponent implements OnInit { + isPrimaryButtonDisabled$: Observable; + priorsForm: FormGroup; + displayedColumns: string[] = ['condition', 'successes', 'failures']; + + conditions: ConditionPriorUpdate[] = []; + + constructor( + @Inject(MAT_DIALOG_DATA) + public config: CommonModalConfig<{ conditions: ConditionPriorUpdate[] }>, + public dialogRef: MatDialogRef, + private readonly formBuilder: FormBuilder, + private readonly moocletHelperService: MoocletExperimentHelperService + ) {} + + ngOnInit(): void { + this.conditions = this.config.params.conditions; + this.createPriorsForm(); + } + + createPriorsForm(): void { + const validators = this.moocletHelperService.getPriorsFieldValidators(); + + const conditionsFormArray = this.formBuilder.array( + this.conditions.map((condition) => + this.formBuilder.group({ + conditionCode: [condition.conditionCode], + successes: [condition.successes, validators.successes], + failures: [condition.failures, validators.failures], + }) + ) + ); + + this.priorsForm = this.formBuilder.group({ conditions: conditionsFormArray }); + + this.isPrimaryButtonDisabled$ = combineLatest([ + this.priorsForm.statusChanges.pipe(startWith(this.priorsForm.status)), + this.priorsForm.valueChanges.pipe(startWith(this.priorsForm.value)), + ]).pipe(map(([status]) => status === 'INVALID' || this.priorsForm.pristine)); + } + + get conditionsFormArray(): FormArray { + return this.priorsForm.get('conditions') as FormArray; + } + + getSuccessesControl(index: number): FormControl { + return this.conditionsFormArray.at(index).get('successes') as FormControl; + } + + getFailuresControl(index: number): FormControl { + return this.conditionsFormArray.at(index).get('failures') as FormControl; + } + + onPrimaryActionBtnClicked(): void { + if (this.priorsForm.valid) { + const result: Record = {}; + this.conditionsFormArray.controls.forEach((control) => { + const conditionCode = control.get('conditionCode')?.value; + result[conditionCode] = { + success: control.get('successes')?.value, + failure: control.get('failures')?.value, + }; + }); + this.dialogRef.close(result); + } else { + CommonFormHelpersService.triggerTouchedToDisplayErrors(this.priorsForm); + } + } + + closeModal(): void { + this.dialogRef.close(); + } +} diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/upsert-experiment-modal/ts-configurable-policy-parameters-form/ts-configurable-policy-parameters-form.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/upsert-experiment-modal/ts-configurable-policy-parameters-form/ts-configurable-policy-parameters-form.component.html index 29b68fe52..85acb7ff5 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/upsert-experiment-modal/ts-configurable-policy-parameters-form/ts-configurable-policy-parameters-form.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/upsert-experiment-modal/ts-configurable-policy-parameters-form/ts-configurable-policy-parameters-form.component.html @@ -10,52 +10,6 @@ >
- - {{ - 'home.new-experiment.design.ts-configurable-policy.prior-success.label.text' | translate - }} - - {{ - 'home.new-experiment.design.ts-configurable-policy.prior-success.hint.text' | translate - }} - @if (policyForm.get('prior_success')?.hasError('min')) { - {{ - 'home.new-experiment.design.ts-configurable-policy.prior-success.min-error.text' | translate - }} - } @if (policyForm.get('prior_success')?.hasError('max')) { - - {{ 'home.new-experiment.design.ts-configurable-policy.general.max-error.text' | translate }} - - } @if (policyForm.get('prior_success')?.hasError('integer')) { - - {{ 'home.new-experiment.design.ts-configurable-policy.general.integer-error.text' | translate }} - - } - - - - {{ - 'home.new-experiment.design.ts-configurable-policy.prior-failure.label.text' | translate - }} - - {{ - 'home.new-experiment.design.ts-configurable-policy.prior-failure.hint.text' | translate - }} - @if (policyForm.get('prior_failure')?.hasError('min')) { - {{ - 'home.new-experiment.design.ts-configurable-policy.prior-failure.min-error.text' | translate - }} - } @if (policyForm.get('prior_failure')?.hasError('max')) { - - {{ 'home.new-experiment.design.ts-configurable-policy.general.max-error.text' | translate }} - - } @if (policyForm.get('prior_failure')?.hasError('integer')) { - - {{ 'home.new-experiment.design.ts-configurable-policy.general.integer-error.text' | translate }} - - } - - {{ 'home.new-experiment.design.ts-configurable-policy.batch-size.label.text' | translate diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/upsert-experiment-modal/ts-configurable-policy-parameters-form/ts-configurable-policy-parameters-form.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/upsert-experiment-modal/ts-configurable-policy-parameters-form/ts-configurable-policy-parameters-form.component.ts index a6872789f..cc28d0b6c 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/upsert-experiment-modal/ts-configurable-policy-parameters-form/ts-configurable-policy-parameters-form.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/upsert-experiment-modal/ts-configurable-policy-parameters-form/ts-configurable-policy-parameters-form.component.ts @@ -80,8 +80,6 @@ export class TsConfigurablePolicyParametersFormComponent implements OnInit, OnDe batch_size: [params.batch_size, validators.batch_size], uniform_threshold: [params.uniform_threshold, validators.uniform_threshold], tspostdiff_thresh: [params.tspostdiff_thresh, validators.tspostdiff_thresh], - prior_success: [params.prior_success, validators.prior_success], - prior_failure: [params.prior_failure, validators.prior_failure], }); } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.html index ecd5671b2..01d8fecac 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.html @@ -35,8 +35,10 @@ [actionsDisabled]="vm.restriction.isDisabled" [actionsTooltip]="restrictionTooltip" [isMoocletExperiment]="isMoocletExperiment(vm.experiment)" + [priors]="vm.experiment.moocletPolicyParameters?.priors" (rowAction)="onRowAction($event, vm.experiment.id, appContext)" (editWeights)="onEditWeights($event, vm.experiment)" + (editPriors)="onEditPriors($event, vm.experiment)" > } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.ts index 60194d98d..fe15a18ff 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.ts @@ -22,6 +22,7 @@ import { UserPermission } from '../../../../../../../core/auth/store/auth.models import { AuthService } from '../../../../../../../core/auth/auth.service'; import { DialogService } from '../../../../../../../shared/services/common-dialog.service'; import { ConditionWeightUpdate } from '../../../../modals/edit-condition-weights-modal/edit-condition-weights-modal.component'; +import { Prior } from 'upgrade_types'; import { ConditionHelperService } from '../../../../../../../core/experiments/condition-helper.service'; import { selectConditionWeightsValid } from '../../../../../../../core/experiments/store/experiments.selectors'; import { Store } from '@ngrx/store'; @@ -128,9 +129,19 @@ export class ExperimentConditionsSectionCardComponent implements OnInit { .openEditConditionWeightsModal(conditions, experiment.weightingMethod) .subscribe((result: ConditionWeightUpdate[] | undefined) => { if (result) { - // Update the experiment with new condition weights this.experimentService.updateExperimentConditionWeights(experiment, result); } }); } + + onEditPriors(conditions: ExperimentCondition[], experiment: ExperimentVM): void { + const existingPriors = experiment.moocletPolicyParameters?.priors; + this.dialogService + .openEditConditionPriorsModal(conditions, existingPriors) + .subscribe((result: Record | undefined) => { + if (result) { + this.experimentService.updateExperimentConditionPriors(experiment, result); + } + }); + } } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.html index 061124d8c..3ec68a479 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.html @@ -71,6 +71,52 @@ + + + + {{ CONDITION_TRANSLATION_KEYS.PRIORS_SUCCESSES | translate }} + + + {{ getPriorSuccesses(condition) }} + + + + + + + {{ CONDITION_TRANSLATION_KEYS.PRIORS_FAILURES | translate }} + + + {{ getPriorFailures(condition) }} + + + + + + + @if (showActions && isMoocletExperiment) { +
+ +
+ } + + + + +
+ diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.scss b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.scss index 472e6f47c..fa1d29485 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.scss +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.scss @@ -73,7 +73,14 @@ text-align: right; } - .weight-edit-column { + .priors-column { + width: 10%; + min-width: 72px; + text-align: right; + } + + .weight-edit-column, + .priors-edit-column { width: 10%; text-align: left; @@ -92,6 +99,10 @@ } } + .priors-edit-column { + padding-left: 20px; + } + .description-column { width: 40%; } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts index c2107def0..e2941229b 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts @@ -13,6 +13,7 @@ import { EXPERIMENT_ROW_ACTION, } from '../../../../../../../../core/experiments/store/experiments.model'; import { SharedModule } from '../../../../../../../../shared/shared.module'; +import { Prior } from 'upgrade_types'; @Component({ selector: 'app-experiment-conditions-table', @@ -37,20 +38,35 @@ export class ExperimentConditionsTableComponent { @Input() actionsDisabled?: boolean = false; @Input() actionsTooltip?: string = ''; @Input() isMoocletExperiment = false; + @Input() priors?: Record; @Output() rowAction = new EventEmitter(); @Output() editWeights = new EventEmitter(); + @Output() editPriors = new EventEmitter(); - displayedColumns: string[] = ['condition', 'weight', 'weightEdit', 'description', 'actions']; - - // Make enum accessible in template + get displayedColumns(): string[] { + if (this.isMoocletExperiment) { + return ['condition', 'priorsSuccesses', 'priorsFailures', 'priorsEdit', 'description', 'actions']; + } + return ['condition', 'weight', 'weightEdit', 'description', 'actions']; + } CONDITION_TRANSLATION_KEYS = { CONDITION: 'experiments.details.conditions.condition.text', DESCRIPTION: 'experiments.details.conditions.description.text', WEIGHT: 'experiments.details.conditions.weight.text', + PRIORS_SUCCESSES: 'experiments.details.conditions.priors-successes.text', + PRIORS_FAILURES: 'experiments.details.conditions.priors-failures.text', ACTIONS: 'experiments.details.conditions.actions.text', }; + getPriorSuccesses(condition: ExperimentCondition): number { + return this.priors?.[condition.conditionCode]?.success ?? 1; + } + + getPriorFailures(condition: ExperimentCondition): number { + return this.priors?.[condition.conditionCode]?.failure ?? 1; + } + onEditButtonClick(condition: ExperimentCondition): void { this.rowAction.emit({ action: EXPERIMENT_ROW_ACTION.EDIT, condition }); } @@ -62,4 +78,8 @@ export class ExperimentConditionsTableComponent { onEditWeightsClick(): void { this.editWeights.emit(this.conditions); } + + onEditPriorsClick(): void { + this.editPriors.emit(this.conditions); + } } diff --git a/packages/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts b/packages/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts index 8784b655c..5b7d02ebc 100644 --- a/packages/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts +++ b/packages/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts @@ -61,6 +61,11 @@ import { ConditionWeightUpdate, EditConditionWeightsModalComponent, } from '../../features/dashboard/experiments/modals/edit-condition-weights-modal/edit-condition-weights-modal.component'; +import { + ConditionPriorUpdate, + EditConditionPriorsModalComponent, +} from '../../features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component'; +import { Prior } from 'upgrade_types'; import { EditPayloadModalComponent, EditPayloadModalParams, @@ -613,6 +618,35 @@ export class DialogService { return dialogRef.afterClosed(); } + openEditConditionPriorsModal( + conditions: ExperimentCondition[], + existingPriors?: Record + ): Observable> { + const conditionPriorUpdates: ConditionPriorUpdate[] = conditions.map((condition) => ({ + conditionCode: condition.conditionCode, + successes: existingPriors?.[condition.conditionCode]?.success ?? 1, + failures: existingPriors?.[condition.conditionCode]?.failure ?? 1, + })); + + const dialogRef = this.dialog.open(EditConditionPriorsModalComponent, { + panelClass: ['experiment-modal', 'modal-shadow'], + hasBackdrop: true, + autoFocus: false, + restoreFocus: false, + backdropClass: 'modal-backdrop', + width: ModalSize.STANDARD, + data: { + title: 'experiments.edit-condition-priors-modal.title.text', + primaryActionBtnLabel: 'Save', + primaryActionBtnColor: 'primary', + cancelBtnLabel: 'Cancel', + params: { conditions: conditionPriorUpdates }, + }, + }); + + return dialogRef.afterClosed(); + } + openAddListModal(appContext: string, segmentId: string, segmentType: SEGMENT_TYPE) { const commonModalConfig: CommonModalConfig = { title: segmentType === SEGMENT_TYPE.GLOBAL_EXCLUDE ? 'Add Exclude List' : 'Add List', diff --git a/packages/frontend/projects/upgrade/src/assets/i18n/en.json b/packages/frontend/projects/upgrade/src/assets/i18n/en.json index 2e38911fa..e65b1aaac 100644 --- a/packages/frontend/projects/upgrade/src/assets/i18n/en.json +++ b/packages/frontend/projects/upgrade/src/assets/i18n/en.json @@ -504,6 +504,8 @@ "experiments.details.conditions.weight-adaptive-tooltip.text": "Adaptive experiments do not have static weights. Initial weights can be configured via the adaptive algorithm parameters. See GitBook documentation.", "experiments.details.conditions.description.text": "Description", "experiments.details.conditions.actions.text": "Actions", + "experiments.details.conditions.priors-successes.text": "Prior Successes", + "experiments.details.conditions.priors-failures.text": "Prior Failures", "experiments.details.conditions.card.no-data-row.text": "No conditions defined. Simple experiments require conditions.", "experiments.upsert-condition-modal.condition-hint.text": "The name of the condition that will be used to identify the variant in your code.", "experiments.upsert-condition-modal.condition-warning.text": "Condition name must be unique within the experiment.", @@ -594,6 +596,14 @@ "experiments.edit-condition-weights-modal.equal-assignment-weights.description.text": "Equally distribute weight percentages across all conditions.", "experiments.edit-condition-weights-modal.custom-percentages.label.text": "Custom Percentages", "experiments.edit-condition-weights-modal.custom-percentages.description.text": "Define a custom weight percentage for each condition.", + "experiments.edit-condition-priors-modal.title.text": "Edit Condition Priors", + "experiments.edit-condition-priors-modal.condition-header.text": "Condition", + "experiments.edit-condition-priors-modal.successes-header.text": "Successes", + "experiments.edit-condition-priors-modal.failures-header.text": "Failures", + "experiments.edit-condition-priors-modal.min-error.text": "Value must be at least 1.", + "experiments.edit-condition-priors-modal.max-error.text": "Value exceeds the maximum allowed.", + "experiments.edit-condition-priors-modal.integer-error.text": "Value must be a whole number.", + "experiments.edit-condition-priors-modal.no-data.text": "No conditions available.", "feature-flags.global-name.text": "Name", "feature-flags.global-status.text": "Status", "feature-flags.global-updated-at.text": "Updated at", diff --git a/packages/types/src/Mooclet/MoocletTSConfigurablePolicyParametersDTO.ts b/packages/types/src/Mooclet/MoocletTSConfigurablePolicyParametersDTO.ts index 7a8915e48..9be04f454 100644 --- a/packages/types/src/Mooclet/MoocletTSConfigurablePolicyParametersDTO.ts +++ b/packages/types/src/Mooclet/MoocletTSConfigurablePolicyParametersDTO.ts @@ -25,10 +25,11 @@ export class CurrentPosteriors { } export class MoocletTSConfigurablePolicyParametersDTO extends MoocletPolicyParametersDTO { - @IsDefined() - @ValidateNested() + @IsOptional() + @IsObject() + @ValidateNested({ each: true }) @Type(() => Prior) - prior: Prior = new Prior(); + priors?: Record; @IsOptional() @IsObject() diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index d603299ee..c81fb464f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -91,6 +91,8 @@ export { ExperimentQueryComparator, } from './Experiment/interfaces'; export { + Prior, + CurrentPosteriors, MoocletPolicyParametersDTO, MoocletTSConfigurablePolicyParametersDTO, MOOCLET_POLICY_SCHEMA_MAP, From ff45a6c0273da6943a198e5adc5b3fd24f778200 Mon Sep 17 00:00:00 2001 From: doswalt Date: Fri, 10 Apr 2026 11:12:08 -0400 Subject: [PATCH 02/10] fix priors naming to prior --- .../api/services/MoocletExperimentService.ts | 34 +++++++++---------- .../core/experiments/experiments.service.ts | 4 +-- .../experiments/mooclet-helper.service.ts | 10 +++--- ...edit-condition-prior-modal.component.html} | 34 +++++++++---------- ...edit-condition-prior-modal.component.scss} | 6 ++-- .../edit-condition-prior-modal.component.ts} | 32 ++++++++--------- ...ent-conditions-section-card.component.html | 4 +-- ...iment-conditions-section-card.component.ts | 8 ++--- ...experiment-conditions-table.component.html | 30 ++++++++-------- ...experiment-conditions-table.component.scss | 6 ++-- .../experiment-conditions-table.component.ts | 20 +++++------ .../shared/services/common-dialog.service.ts | 16 ++++----- ...oocletTSConfigurablePolicyParametersDTO.ts | 2 +- 13 files changed, 103 insertions(+), 103 deletions(-) rename packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/{edit-condition-priors-modal/edit-condition-priors-modal.component.html => edit-condition-prior-modal/edit-condition-prior-modal.component.html} (63%) rename packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/{edit-condition-priors-modal/edit-condition-priors-modal.component.scss => edit-condition-prior-modal/edit-condition-prior-modal.component.scss} (96%) rename packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/{edit-condition-priors-modal/edit-condition-priors-modal.component.ts => edit-condition-prior-modal/edit-condition-prior-modal.component.ts} (80%) diff --git a/packages/backend/src/api/services/MoocletExperimentService.ts b/packages/backend/src/api/services/MoocletExperimentService.ts index f984aaf8f..ab644f4f4 100644 --- a/packages/backend/src/api/services/MoocletExperimentService.ts +++ b/packages/backend/src/api/services/MoocletExperimentService.ts @@ -395,20 +395,20 @@ export class MoocletExperimentService extends ExperimentService { updatedExperiment.moocletPolicyParameters = policyParameterResponse.parameters; - // Transform priors keys from Mooclet version IDs back to UpGrade condition codes, + // Transform prior keys from Mooclet version IDs back to UpGrade condition codes, // so the PUT response is consistent with the GET response from attachPolicyParamsToExperimentDTO. const updatedTsParams = updatedExperiment.moocletPolicyParameters as MoocletTSConfigurablePolicyParametersDTO; - if (updatedTsParams?.priors) { - const transformedPriors: Record = {}; - for (const [versionId, priorData] of Object.entries(updatedTsParams.priors)) { + if (updatedTsParams?.prior) { + const transformedPrior: Record = {}; + for (const [versionId, priorData] of Object.entries(updatedTsParams.prior)) { const map = currentMoocletExperimentRef.versionConditionMaps.find( (m) => String(m.moocletVersionId) === versionId ); if (map?.experimentCondition?.conditionCode) { - transformedPriors[map.experimentCondition.conditionCode] = priorData; + transformedPrior[map.experimentCondition.conditionCode] = priorData; } } - updatedTsParams.priors = transformedPriors; + updatedTsParams.prior = transformedPrior; } // --------- update versions ---------------------- @@ -540,18 +540,18 @@ export class MoocletExperimentService extends ExperimentService { logger: UpgradeLogger ): Promise { const tsParams = newPolicyParameters as MoocletTSConfigurablePolicyParametersDTO; - if (tsParams.priors) { + if (tsParams.prior) { // Translate conditionCode keys to Mooclet version IDs before sending to the Mooclet API - const translatedPriors: Record = {}; - for (const [conditionCode, priorData] of Object.entries(tsParams.priors)) { + const translatedprior: Record = {}; + for (const [conditionCode, priorData] of Object.entries(tsParams.prior)) { const versionConditionMap = currentMoocletExperimentRef.versionConditionMaps?.find( (map) => map.experimentCondition?.conditionCode === conditionCode ); if (versionConditionMap?.moocletVersionId) { - translatedPriors[String(versionConditionMap.moocletVersionId)] = priorData; + translatedprior[String(versionConditionMap.moocletVersionId)] = priorData; } } - newPolicyParameters = { ...tsParams, priors: translatedPriors } as MoocletPolicyParametersDTO; + newPolicyParameters = { ...tsParams, prior: translatedprior } as MoocletPolicyParametersDTO; } return this.moocletDataService.updatePolicyParameters( @@ -1200,7 +1200,7 @@ export class MoocletExperimentService extends ExperimentService { logger ); - // Transform current_posteriors and priors keys from Mooclet version IDs to UpGrade condition codes + // Transform current_posteriors and prior keys from Mooclet version IDs to UpGrade condition codes const tsConfigurableParams = policyParameters.parameters as MoocletTSConfigurablePolicyParametersDTO; if (tsConfigurableParams.current_posteriors) { const transformedPosteriors = {}; @@ -1220,17 +1220,17 @@ export class MoocletExperimentService extends ExperimentService { tsConfigurableParams.current_posteriors = transformedPosteriors; } - if (tsConfigurableParams.priors) { - const transformedPriors: Record = {}; - for (const [versionId, priorData] of Object.entries(tsConfigurableParams.priors)) { + if (tsConfigurableParams.prior) { + const transformedprior: Record = {}; + for (const [versionId, priorData] of Object.entries(tsConfigurableParams.prior)) { const versionConditionMap = moocletExperimentRef.versionConditionMaps.find( (map) => map.moocletVersionId === parseInt(versionId, 10) ); if (versionConditionMap?.experimentCondition?.conditionCode) { - transformedPriors[versionConditionMap.experimentCondition.conditionCode] = priorData; + transformedprior[versionConditionMap.experimentCondition.conditionCode] = priorData; } } - tsConfigurableParams.priors = transformedPriors; + tsConfigurableParams.prior = transformedprior; } experiment.moocletPolicyParameters = policyParameters.parameters; diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts index 6da2fb881..452d65d38 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts @@ -300,12 +300,12 @@ export class ExperimentService { ); } - updateExperimentConditionPriors(experiment: ExperimentVM, priors: Record): void { + updateExperimentConditionprior(experiment: ExperimentVM, prior: Record): void { const updatedExperiment: ExperimentVM = { ...experiment, moocletPolicyParameters: { ...experiment.moocletPolicyParameters, - priors, + prior, } as MoocletTSConfigurablePolicyParametersDTO, }; this.store$.dispatch( diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.ts index 2fc848fbf..904ef20c2 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/mooclet-helper.service.ts @@ -210,20 +210,20 @@ export class MoocletExperimentHelperService { } /** - * Get field validators for per-condition prior success/failure inputs used in the priors editor. + * Get field validators for per-condition prior success/failure inputs used in the prior editor. */ - getPriorsFieldValidators(): Record { - const priorsDefault = 1; + getpriorFieldValidators(): Record { + const priorDefault = 1; return { successes: [ Validators.required, - Validators.min(priorsDefault), + Validators.min(priorDefault), Validators.max(DEFAULT_MAX_NUMBER_INPUT), CommonFormHelpersService.integerValidator(), ], failures: [ Validators.required, - Validators.min(priorsDefault), + Validators.min(priorDefault), Validators.max(DEFAULT_MAX_NUMBER_INPUT), CommonFormHelpersService.integerValidator(), ], diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-prior-modal/edit-condition-prior-modal.component.html similarity index 63% rename from packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.html rename to packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-prior-modal/edit-condition-prior-modal.component.html index e046c3034..b95dff5b7 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-prior-modal/edit-condition-prior-modal.component.html @@ -6,7 +6,7 @@ [primaryActionBtnDisabled]="isPrimaryButtonDisabled$ | async" (primaryActionBtnClicked)="onPrimaryActionBtnClicked()" > - +
- @@ -47,18 +47,18 @@ - - @@ -71,7 +71,7 @@
- {{ 'experiments.edit-condition-priors-modal.condition-header.text' | translate }} + {{ 'experiments.edit-condition-prior-modal.condition-header.text' | translate }} @@ -28,18 +28,18 @@ - - {{ 'experiments.edit-condition-priors-modal.successes-header.text' | translate }} + + {{ 'experiments.edit-condition-prior-modal.successes-header.text' | translate }} - + + @if (getSuccessesControl(i).hasError('min')) { - {{ 'experiments.edit-condition-priors-modal.min-error.text' | translate }} + {{ 'experiments.edit-condition-prior-modal.min-error.text' | translate }} } @if (getSuccessesControl(i).hasError('max')) { - {{ 'experiments.edit-condition-priors-modal.max-error.text' | translate }} + {{ 'experiments.edit-condition-prior-modal.max-error.text' | translate }} } @if (getSuccessesControl(i).hasError('integer')) { - {{ 'experiments.edit-condition-priors-modal.integer-error.text' | translate }} + {{ 'experiments.edit-condition-prior-modal.integer-error.text' | translate }} } - {{ 'experiments.edit-condition-priors-modal.failures-header.text' | translate }} + + {{ 'experiments.edit-condition-prior-modal.failures-header.text' | translate }} - + + @if (getFailuresControl(i).hasError('min')) { - {{ 'experiments.edit-condition-priors-modal.min-error.text' | translate }} + {{ 'experiments.edit-condition-prior-modal.min-error.text' | translate }} } @if (getFailuresControl(i).hasError('max')) { - {{ 'experiments.edit-condition-priors-modal.max-error.text' | translate }} + {{ 'experiments.edit-condition-prior-modal.max-error.text' | translate }} } @if (getFailuresControl(i).hasError('integer')) { - {{ 'experiments.edit-condition-priors-modal.integer-error.text' | translate }} + {{ 'experiments.edit-condition-prior-modal.integer-error.text' | translate }} }
- {{ 'experiments.edit-condition-priors-modal.no-data.text' | translate }} + {{ 'experiments.edit-condition-prior-modal.no-data.text' | translate }}
diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.scss b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-prior-modal/edit-condition-prior-modal.component.scss similarity index 96% rename from packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.scss rename to packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-prior-modal/edit-condition-prior-modal.component.scss index 680f2dda8..8205cb82e 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.scss +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-prior-modal/edit-condition-prior-modal.component.scss @@ -1,4 +1,4 @@ -.priors-input { +.prior-input { width: 90px; .mat-mdc-form-field-subscript-wrapper { @@ -78,12 +78,12 @@ padding-left: 32px; } - .priors-column { + .prior-column { width: 25%; text-align: right; padding-right: 16px; - .priors-input { + .prior-input { width: 90px; input { diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-prior-modal/edit-condition-prior-modal.component.ts similarity index 80% rename from packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.ts rename to packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-prior-modal/edit-condition-prior-modal.component.ts index 5388224cc..02ddf5935 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/modals/edit-condition-prior-modal/edit-condition-prior-modal.component.ts @@ -22,7 +22,7 @@ export interface ConditionPriorUpdate { } @Component({ - selector: 'app-edit-condition-priors-modal', + selector: 'app-edit-condition-prior-modal', imports: [ CommonModalComponent, MatTableModule, @@ -34,13 +34,13 @@ export interface ConditionPriorUpdate { TranslateModule, SharedModule, ], - templateUrl: './edit-condition-priors-modal.component.html', - styleUrl: './edit-condition-priors-modal.component.scss', + templateUrl: './edit-condition-prior-modal.component.html', + styleUrl: './edit-condition-prior-modal.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class EditConditionPriorsModalComponent implements OnInit { +export class EditConditionpriorModalComponent implements OnInit { isPrimaryButtonDisabled$: Observable; - priorsForm: FormGroup; + priorForm: FormGroup; displayedColumns: string[] = ['condition', 'successes', 'failures']; conditions: ConditionPriorUpdate[] = []; @@ -48,18 +48,18 @@ export class EditConditionPriorsModalComponent implements OnInit { constructor( @Inject(MAT_DIALOG_DATA) public config: CommonModalConfig<{ conditions: ConditionPriorUpdate[] }>, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef, private readonly formBuilder: FormBuilder, private readonly moocletHelperService: MoocletExperimentHelperService ) {} ngOnInit(): void { this.conditions = this.config.params.conditions; - this.createPriorsForm(); + this.createpriorForm(); } - createPriorsForm(): void { - const validators = this.moocletHelperService.getPriorsFieldValidators(); + createpriorForm(): void { + const validators = this.moocletHelperService.getpriorFieldValidators(); const conditionsFormArray = this.formBuilder.array( this.conditions.map((condition) => @@ -71,16 +71,16 @@ export class EditConditionPriorsModalComponent implements OnInit { ) ); - this.priorsForm = this.formBuilder.group({ conditions: conditionsFormArray }); + this.priorForm = this.formBuilder.group({ conditions: conditionsFormArray }); this.isPrimaryButtonDisabled$ = combineLatest([ - this.priorsForm.statusChanges.pipe(startWith(this.priorsForm.status)), - this.priorsForm.valueChanges.pipe(startWith(this.priorsForm.value)), - ]).pipe(map(([status]) => status === 'INVALID' || this.priorsForm.pristine)); + this.priorForm.statusChanges.pipe(startWith(this.priorForm.status)), + this.priorForm.valueChanges.pipe(startWith(this.priorForm.value)), + ]).pipe(map(([status]) => status === 'INVALID' || this.priorForm.pristine)); } get conditionsFormArray(): FormArray { - return this.priorsForm.get('conditions') as FormArray; + return this.priorForm.get('conditions') as FormArray; } getSuccessesControl(index: number): FormControl { @@ -92,7 +92,7 @@ export class EditConditionPriorsModalComponent implements OnInit { } onPrimaryActionBtnClicked(): void { - if (this.priorsForm.valid) { + if (this.priorForm.valid) { const result: Record = {}; this.conditionsFormArray.controls.forEach((control) => { const conditionCode = control.get('conditionCode')?.value; @@ -103,7 +103,7 @@ export class EditConditionPriorsModalComponent implements OnInit { }); this.dialogRef.close(result); } else { - CommonFormHelpersService.triggerTouchedToDisplayErrors(this.priorsForm); + CommonFormHelpersService.triggerTouchedToDisplayErrors(this.priorForm); } } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.html index 01d8fecac..cbf5e5829 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.html @@ -35,10 +35,10 @@ [actionsDisabled]="vm.restriction.isDisabled" [actionsTooltip]="restrictionTooltip" [isMoocletExperiment]="isMoocletExperiment(vm.experiment)" - [priors]="vm.experiment.moocletPolicyParameters?.priors" + [prior]="vm.experiment.moocletPolicyParameters?.prior" (rowAction)="onRowAction($event, vm.experiment.id, appContext)" (editWeights)="onEditWeights($event, vm.experiment)" - (editPriors)="onEditPriors($event, vm.experiment)" + (editprior)="onEditprior($event, vm.experiment)" > } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.ts index fe15a18ff..d4d298c2b 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-section-card.component.ts @@ -134,13 +134,13 @@ export class ExperimentConditionsSectionCardComponent implements OnInit { }); } - onEditPriors(conditions: ExperimentCondition[], experiment: ExperimentVM): void { - const existingPriors = experiment.moocletPolicyParameters?.priors; + onEditprior(conditions: ExperimentCondition[], experiment: ExperimentVM): void { + const existingprior = experiment.moocletPolicyParameters?.prior; this.dialogService - .openEditConditionPriorsModal(conditions, existingPriors) + .openEditConditionpriorModal(conditions, existingprior) .subscribe((result: Record | undefined) => { if (result) { - this.experimentService.updateExperimentConditionPriors(experiment, result); + this.experimentService.updateExperimentConditionprior(experiment, result); } }); } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.html index 3ec68a479..e3ca6b532 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.html @@ -71,29 +71,29 @@ - - - - {{ CONDITION_TRANSLATION_KEYS.PRIORS_SUCCESSES | translate }} + + + + {{ CONDITION_TRANSLATION_KEYS.prior_SUCCESSES | translate }} - - {{ getPriorSuccesses(condition) }} + + {{ getprioruccesses(condition) }} - - - - {{ CONDITION_TRANSLATION_KEYS.PRIORS_FAILURES | translate }} + + + + {{ CONDITION_TRANSLATION_KEYS.prior_FAILURES | translate }} - + {{ getPriorFailures(condition) }} - - - + + + @if (showActions && isMoocletExperiment) {
edit diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.scss b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.scss index fa1d29485..d03a8501b 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.scss +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.scss @@ -73,14 +73,14 @@ text-align: right; } - .priors-column { + .prior-column { width: 10%; min-width: 72px; text-align: right; } .weight-edit-column, - .priors-edit-column { + .prior-edit-column { width: 10%; text-align: left; @@ -99,7 +99,7 @@ } } - .priors-edit-column { + .prior-edit-column { padding-left: 20px; } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts index e2941229b..aa5d003ba 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts @@ -38,14 +38,14 @@ export class ExperimentConditionsTableComponent { @Input() actionsDisabled?: boolean = false; @Input() actionsTooltip?: string = ''; @Input() isMoocletExperiment = false; - @Input() priors?: Record; + @Input() prior?: Record; @Output() rowAction = new EventEmitter(); @Output() editWeights = new EventEmitter(); - @Output() editPriors = new EventEmitter(); + @Output() editprior = new EventEmitter(); get displayedColumns(): string[] { if (this.isMoocletExperiment) { - return ['condition', 'priorsSuccesses', 'priorsFailures', 'priorsEdit', 'description', 'actions']; + return ['condition', 'priorSuccesses', 'priorFailures', 'priorEdit', 'description', 'actions']; } return ['condition', 'weight', 'weightEdit', 'description', 'actions']; } @@ -54,17 +54,17 @@ export class ExperimentConditionsTableComponent { CONDITION: 'experiments.details.conditions.condition.text', DESCRIPTION: 'experiments.details.conditions.description.text', WEIGHT: 'experiments.details.conditions.weight.text', - PRIORS_SUCCESSES: 'experiments.details.conditions.priors-successes.text', - PRIORS_FAILURES: 'experiments.details.conditions.priors-failures.text', + prior_SUCCESSES: 'experiments.details.conditions.prior-successes.text', + prior_FAILURES: 'experiments.details.conditions.prior-failures.text', ACTIONS: 'experiments.details.conditions.actions.text', }; - getPriorSuccesses(condition: ExperimentCondition): number { - return this.priors?.[condition.conditionCode]?.success ?? 1; + getprioruccesses(condition: ExperimentCondition): number { + return this.prior?.[condition.conditionCode]?.success ?? 1; } getPriorFailures(condition: ExperimentCondition): number { - return this.priors?.[condition.conditionCode]?.failure ?? 1; + return this.prior?.[condition.conditionCode]?.failure ?? 1; } onEditButtonClick(condition: ExperimentCondition): void { @@ -79,7 +79,7 @@ export class ExperimentConditionsTableComponent { this.editWeights.emit(this.conditions); } - onEditPriorsClick(): void { - this.editPriors.emit(this.conditions); + onEditpriorClick(): void { + this.editprior.emit(this.conditions); } } diff --git a/packages/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts b/packages/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts index 5b7d02ebc..3cbc07f41 100644 --- a/packages/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts +++ b/packages/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts @@ -63,8 +63,8 @@ import { } from '../../features/dashboard/experiments/modals/edit-condition-weights-modal/edit-condition-weights-modal.component'; import { ConditionPriorUpdate, - EditConditionPriorsModalComponent, -} from '../../features/dashboard/experiments/modals/edit-condition-priors-modal/edit-condition-priors-modal.component'; + EditConditionpriorModalComponent, +} from '../../features/dashboard/experiments/modals/edit-condition-prior-modal/edit-condition-prior-modal.component'; import { Prior } from 'upgrade_types'; import { EditPayloadModalComponent, @@ -618,17 +618,17 @@ export class DialogService { return dialogRef.afterClosed(); } - openEditConditionPriorsModal( + openEditConditionpriorModal( conditions: ExperimentCondition[], - existingPriors?: Record + existingprior?: Record ): Observable> { const conditionPriorUpdates: ConditionPriorUpdate[] = conditions.map((condition) => ({ conditionCode: condition.conditionCode, - successes: existingPriors?.[condition.conditionCode]?.success ?? 1, - failures: existingPriors?.[condition.conditionCode]?.failure ?? 1, + successes: existingprior?.[condition.conditionCode]?.success ?? 1, + failures: existingprior?.[condition.conditionCode]?.failure ?? 1, })); - const dialogRef = this.dialog.open(EditConditionPriorsModalComponent, { + const dialogRef = this.dialog.open(EditConditionpriorModalComponent, { panelClass: ['experiment-modal', 'modal-shadow'], hasBackdrop: true, autoFocus: false, @@ -636,7 +636,7 @@ export class DialogService { backdropClass: 'modal-backdrop', width: ModalSize.STANDARD, data: { - title: 'experiments.edit-condition-priors-modal.title.text', + title: 'experiments.edit-condition-prior-modal.title.text', primaryActionBtnLabel: 'Save', primaryActionBtnColor: 'primary', cancelBtnLabel: 'Cancel', diff --git a/packages/types/src/Mooclet/MoocletTSConfigurablePolicyParametersDTO.ts b/packages/types/src/Mooclet/MoocletTSConfigurablePolicyParametersDTO.ts index 9be04f454..ef6f229bc 100644 --- a/packages/types/src/Mooclet/MoocletTSConfigurablePolicyParametersDTO.ts +++ b/packages/types/src/Mooclet/MoocletTSConfigurablePolicyParametersDTO.ts @@ -29,7 +29,7 @@ export class MoocletTSConfigurablePolicyParametersDTO extends MoocletPolicyParam @IsObject() @ValidateNested({ each: true }) @Type(() => Prior) - priors?: Record; + prior?: Record; @IsOptional() @IsObject() From 6b339a575617bbb1091b4afbee58cbd6c8179151 Mon Sep 17 00:00:00 2001 From: doswalt Date: Wed, 15 Apr 2026 10:39:45 -0400 Subject: [PATCH 03/10] wip wip wip --- .../api/services/MoocletExperimentService.ts | 7 +- .../src/api/services/MoocletRewardsService.ts | 133 +++++++++++++++++- packages/backend/src/api/services/estimate.ts | 86 +++++++++++ ...nt-condition-expandable-row.component.html | 10 +- ...nt-condition-expandable-row.component.scss | 4 + ...igurable-reward-count-table.component.html | 34 +++-- ...igurable-reward-count-table.component.scss | 2 +- ...nfigurable-reward-count-table.component.ts | 17 ++- .../projects/upgrade/src/assets/i18n/en.json | 24 ++-- packages/types/src/Mooclet/index.ts | 6 +- 10 files changed, 286 insertions(+), 37 deletions(-) create mode 100644 packages/backend/src/api/services/estimate.ts diff --git a/packages/backend/src/api/services/MoocletExperimentService.ts b/packages/backend/src/api/services/MoocletExperimentService.ts index ab644f4f4..7624b082e 100644 --- a/packages/backend/src/api/services/MoocletExperimentService.ts +++ b/packages/backend/src/api/services/MoocletExperimentService.ts @@ -1077,10 +1077,9 @@ export class MoocletExperimentService extends ExperimentService { moocletExperimentRef.variableId = moocletVariableResponse?.id; moocletExperimentRef.policyId = newMoocletRequest.policy; - moocletExperimentRef.outcomeVariableName = - upgradeExperiment.assignmentAlgorithm === ASSIGNMENT_ALGORITHM.MOOCLET_TS_CONFIGURABLE - ? (moocletPolicyParameters as MoocletTSConfigurablePolicyParametersDTO).outcome_variable_name - : undefined; + moocletExperimentRef.outcomeVariableName = ( + moocletPolicyParameters as MoocletTSConfigurablePolicyParametersDTO + ).outcome_variable_name; } catch (err) { await this.orchestrateDeleteMoocletResources(moocletExperimentRef, logger); throw err; diff --git a/packages/backend/src/api/services/MoocletRewardsService.ts b/packages/backend/src/api/services/MoocletRewardsService.ts index 592683b54..8193a7a94 100644 --- a/packages/backend/src/api/services/MoocletRewardsService.ts +++ b/packages/backend/src/api/services/MoocletRewardsService.ts @@ -1,5 +1,11 @@ import { UpgradeLogger } from '../../lib/logger/UpgradeLogger'; -import { EXPERIMENT_STATE, SERVER_ERROR, BinaryRewardValueMap } from 'upgrade_types'; +import { + EXPERIMENT_STATE, + SERVER_ERROR, + BinaryRewardValueMap, + MoocletTSConfigurablePolicyParametersDTO, + Prior, +} from 'upgrade_types'; import { RequestedExperimentUser } from '../controllers/validators/ExperimentUserValidator'; import { MoocletExperimentRef } from '../models/MoocletExperimentRef'; import { MoocletDataService } from './MoocletDataService'; @@ -243,7 +249,24 @@ export class MoocletRewardsService { } } - return this.createExperimentRewardsSummary(moocletExperimentRef, rewards, logger); + let tsConfigurableParams: MoocletTSConfigurablePolicyParametersDTO | undefined; + if (moocletExperimentRef.policyParametersId) { + try { + const policyParametersResponse = await this.moocletDataService.getPolicyParameters( + moocletExperimentRef.policyParametersId, + logger + ); + tsConfigurableParams = policyParametersResponse.parameters as MoocletTSConfigurablePolicyParametersDTO; + } catch (policyError) { + logger.warn({ + message: 'Could not fetch policy parameters for Thompson sampling estimate', + experimentId, + error: policyError, + }); + } + } + + return this.createExperimentRewardsSummary(moocletExperimentRef, rewards, logger, tsConfigurableParams); } catch (error) { logger.error({ message: 'Error fetching rewards summary for experiment', experimentId, error }); throw error; @@ -266,7 +289,8 @@ export class MoocletRewardsService { public async createExperimentRewardsSummary( moocletExperimentRef: MoocletExperimentRef, rewardsData: MoocletValueResponseDetails[], - logger: UpgradeLogger + logger: UpgradeLogger, + policyParameters?: MoocletTSConfigurablePolicyParametersDTO ): Promise { const rewards: MoocletValueResponseDetails[] = rewardsData; @@ -278,8 +302,17 @@ export class MoocletRewardsService { return []; } + const conditionCodes = moocletExperimentRef.versionConditionMaps.map( + ({ experimentCondition }) => experimentCondition.conditionCode + ); + const DEFAULT_PRIOR: Prior = { success: 1, failure: 1 }; + const estimatedWeightMap = policyParameters + ? this.computeThompsonWeightsMap(conditionCodes, policyParameters) + : null; + const rewardsSummaries = moocletExperimentRef.versionConditionMaps.map( ({ experimentCondition, moocletVersionId }) => { + const conditionCode = experimentCondition.conditionCode; const versionRewards = rewards.filter((reward) => reward.version === moocletVersionId); const successes = versionRewards.filter((reward) => reward.value === 1.0).length; const failures = versionRewards.filter((reward) => reward.value === 0.0).length; @@ -287,13 +320,20 @@ export class MoocletRewardsService { const percentSuccess = total > 0 ? (successes / total) * 100 : 0.0; const successRate = percentSuccess.toFixed(1) + '%'; + const conditionPrior: Prior = policyParameters?.prior?.[conditionCode] ?? DEFAULT_PRIOR; + const conditionPosteriors = policyParameters?.current_posteriors?.[conditionCode]; + const rewardsForCondition: ExperimentRewardsByCondition = { - conditionCode: experimentCondition.conditionCode, + conditionCode, successes, failures, - total, successRate, order: experimentCondition.order, + estimatedWeight: estimatedWeightMap?.get(conditionCode), + priorSuccess: conditionPrior.success, + priorFailure: conditionPrior.failure, + posteriorSuccesses: conditionPosteriors?.successes ?? 0, + posteriorFailures: conditionPosteriors?.failures ?? 0, }; return rewardsForCondition; } @@ -303,6 +343,89 @@ export class MoocletRewardsService { return orderedRewardsSummary; } + /** + * Computes Thompson Sampling estimated weight percentages per condition. + * Combines per-condition priors with current_posteriors as Beta distribution inputs. + * Returns a Map of conditionCode → integer estimated weight (all values sum to 100). + */ + private computeThompsonWeightsMap( + conditionCodes: string[], + params: MoocletTSConfigurablePolicyParametersDTO + ): Map { + const DEFAULT_PRIOR: Prior = { success: 1, failure: 1 }; + + const arms = conditionCodes.map((conditionCode) => { + const prior: Prior = params.prior?.[conditionCode] ?? DEFAULT_PRIOR; + const posteriors = params.current_posteriors?.[conditionCode]; + return { + conditionCode, + alpha: prior.success + (posteriors?.successes ?? 0), + beta: prior.failure + (posteriors?.failures ?? 0), + }; + }); + + const iterations = 10_000; + const wins = new Array(arms.length).fill(0); + for (let i = 0; i < iterations; i++) { + let maxSample = -1; + let maxIdx = 0; + for (let j = 0; j < arms.length; j++) { + const sample = this.randBeta(arms[j].alpha, arms[j].beta); + if (sample > maxSample) { + maxSample = sample; + maxIdx = j; + } + } + wins[maxIdx]++; + } + + // Largest Remainder Method: normalize to integer percentages summing to exactly 100 + const raw = arms.map((_, i) => (wins[i] / iterations) * 100); + const floored = raw.map(Math.floor); + const remainder = 100 - floored.reduce((a, b) => a + b, 0); + const indices = raw + .map((v, i) => ({ diff: v - floored[i], i })) + .sort((a, b) => b.diff - a.diff) + .map(({ i }) => i); + for (let k = 0; k < remainder; k++) floored[indices[k]]++; + + return new Map(arms.map((arm, i) => [arm.conditionCode, floored[i]])); + } + + // --- Beta distribution sampling (Marsaglia-Tsang / Box-Muller) --- + + private randNormal(): number { + const u = Math.random() || Number.EPSILON; // guard against log(0) + return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * Math.random()); + } + + private randGamma(shape: number): number { + if (shape < 1) { + return this.randGamma(1 + shape) * Math.pow(Math.random(), 1 / shape); + } + const d = shape - 1 / 3; + const c = 1 / Math.sqrt(9 * d); + let x: number; + let v: number; + let u: number; + do { + do { + x = this.randNormal(); + v = 1 + c * x; + } while (v <= 0); + v = v * v * v; + u = Math.random(); + // eslint-disable-next-line no-constant-condition + } while (!(u < 1 - 0.0331 * x * x * x * x) && !(Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v)))); + return d * v; + } + + private randBeta(alpha: number, beta: number): number { + const g1 = this.randGamma(alpha); + const g2 = this.randGamma(beta); + return g1 / (g1 + g2); + } + /** * Throws a 409 data-conflict error for most unexpected cases */ diff --git a/packages/backend/src/api/services/estimate.ts b/packages/backend/src/api/services/estimate.ts new file mode 100644 index 000000000..8ec7f63a3 --- /dev/null +++ b/packages/backend/src/api/services/estimate.ts @@ -0,0 +1,86 @@ +// Pure TS Beta distribution sampler (no dependencies) + +function randNormal(): number { + return Math.sqrt(-2 * Math.log(Math.random())) * Math.cos(2 * Math.PI * Math.random()); +} + +function randGamma(shape: number): number { + if (shape < 1) { + return randGamma(1 + shape) * Math.pow(Math.random(), 1 / shape); + } + const d = shape - 1 / 3; + const c = 1 / Math.sqrt(9 * d); + // eslint-disable-next-line no-constant-condition + while (true) { + let v: number; + let x: number; + do { + x = randNormal(); + v = 1 + c * x; + } while (v <= 0); + v = v * v * v; + const u = Math.random(); + if (u < 1 - 0.0331 * x * x * x * x) return d * v; + if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) return d * v; + } +} + +function randBeta(alpha: number, beta: number): number { + const g1 = randGamma(alpha); + const g2 = randGamma(beta); + return g1 / (g1 + g2); +} + +// --- + +interface ArmPosterior { + versionId: string; + successes: number; + failures: number; +} + +interface ArmPrior { + priorSuccess: number; + priorFailure: number; +} + +interface VersionWeight { + versionId: string; + estimatedCurrentWeight: string; +} + +export function estimateThompsonWeights(arms: ArmPosterior[], prior: ArmPrior, iterations = 10_000): VersionWeight[] { + const wins = new Array(arms.length).fill(0); + + for (let i = 0; i < iterations; i++) { + let maxSample = -1; + let maxIdx = 0; + for (let j = 0; j < arms.length; j++) { + const alpha = arms[j].successes + prior.priorSuccess; + const beta = arms[j].failures + prior.priorFailure; + const sample = randBeta(alpha, beta); + if (sample > maxSample) { + maxSample = sample; + maxIdx = j; + } + } + wins[maxIdx]++; + } + + const raw = arms.map((arm, i) => (wins[i] / iterations) * 100); + + // Normalize so rounded values sum exactly to 100 + const floored = raw.map(Math.floor); + const remainder = 100 - floored.reduce((a, b) => a + b, 0); + const diffs = raw.map((v, i) => v - floored[i]); + const indices = diffs + .map((d, i) => ({ d, i })) + .sort((a, b) => b.d - a.d) + .map(({ i }) => i); + for (let k = 0; k < remainder; k++) floored[indices[k]]++; + + return arms.map((arm, i) => ({ + versionId: arm.versionId, + estimatedCurrentWeight: floored[i].toFixed(1), + })); +} diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-enrollment-data-section-card/enrollment-condition-table/enrollment-condition-expandable-row/enrollment-condition-expandable-row.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-enrollment-data-section-card/enrollment-condition-table/enrollment-condition-expandable-row/enrollment-condition-expandable-row.component.html index 96f5e3763..4fe23e04e 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-enrollment-data-section-card/enrollment-condition-table/enrollment-condition-expandable-row/enrollment-condition-expandable-row.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-enrollment-data-section-card/enrollment-condition-table/enrollment-condition-expandable-row/enrollment-condition-expandable-row.component.html @@ -4,7 +4,10 @@ @for (key of displayedColumns; track key) { - - - - {{ 'experiments.details.posteriors.total.text' | translate }} - - - {{ row.total }} - - - @@ -58,6 +48,30 @@ + + + + {{ 'experiments.details.posteriors.estimated-weight.text' | translate }} + + + {{ row.estimatedWeight != null ? '~' + row.estimatedWeight + '%' : '—' }} + + + diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.scss b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.scss index 74e799edc..bb3df3f6a 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.scss +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.scss @@ -70,8 +70,8 @@ .successes-column, .failures-column, - .total-column, .success-rate-column, + .estimated-weight-column, .spacer-column { width: auto; text-align: right; diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts index 579ace171..dbca6d45c 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts @@ -2,13 +2,14 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatTableModule } from '@angular/material/table'; import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; -import { ExperimentRewardsSummary } from 'upgrade_types'; +import { ExperimentRewardsByCondition, ExperimentRewardsSummary } from 'upgrade_types'; @Component({ selector: 'app-ts-configurable-reward-count-table', standalone: true, - imports: [CommonModule, MatTableModule, MatProgressBarModule, TranslateModule], + imports: [CommonModule, MatTableModule, MatProgressBarModule, MatTooltipModule, TranslateModule], templateUrl: './ts-configurable-reward-count-table.component.html', styleUrl: './ts-configurable-reward-count-table.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -17,5 +18,15 @@ export class TSConfigurableRewardCountTableComponent { @Input() dataSource: ExperimentRewardsSummary = []; @Input() isLoading = false; - displayedColumns = ['conditionCode', 'successes', 'failures', 'total', 'successRate', 'spacer']; + displayedColumns = ['conditionCode', 'successes', 'failures', 'successRate', 'estimatedWeight', 'spacer']; + + getEstimatedWeightTooltip(row: ExperimentRewardsByCondition): string { + const ps = row.posteriorSuccesses ?? 0; + const pf = row.posteriorFailures ?? 0; + const priorS = row.priorSuccess ?? 1; + const priorF = row.priorFailure ?? 1; + const alpha = ps + priorS; + const beta = pf + priorF; + return `successes: ${ps} + (${priorS}) = ${alpha}\nfailures: ${pf} + (${priorF}) = ${beta}\nbeta(${alpha}, ${beta})`; + } } diff --git a/packages/frontend/projects/upgrade/src/assets/i18n/en.json b/packages/frontend/projects/upgrade/src/assets/i18n/en.json index e65b1aaac..43ca49d4a 100644 --- a/packages/frontend/projects/upgrade/src/assets/i18n/en.json +++ b/packages/frontend/projects/upgrade/src/assets/i18n/en.json @@ -504,8 +504,9 @@ "experiments.details.conditions.weight-adaptive-tooltip.text": "Adaptive experiments do not have static weights. Initial weights can be configured via the adaptive algorithm parameters. See GitBook documentation.", "experiments.details.conditions.description.text": "Description", "experiments.details.conditions.actions.text": "Actions", - "experiments.details.conditions.priors-successes.text": "Prior Successes", - "experiments.details.conditions.priors-failures.text": "Prior Failures", + "experiments.details.conditions.prior-successes.text": "Prior Successes", + "experiments.details.conditions.prior-failures.text": "Prior Failures", + "experiments.details.enrollment.estimated-weight.text": "Est. Weight", "experiments.details.conditions.card.no-data-row.text": "No conditions defined. Simple experiments require conditions.", "experiments.upsert-condition-modal.condition-hint.text": "The name of the condition that will be used to identify the variant in your code.", "experiments.upsert-condition-modal.condition-warning.text": "Condition name must be unique within the experiment.", @@ -576,7 +577,8 @@ "experiments.details.posteriors.successes.text": "Successes", "experiments.details.posteriors.failures.text": "Failures", "experiments.details.posteriors.successRate.text": "Success Rate", - "experiments.details.posteriors.total.text": "Total Rewards", + "experiments.details.posteriors.estimated-weight.text": "Est. Weight", + "experiments.details.posteriors.estimated-weight-header-tooltip.text": "Estimated chance of condition \"winning\" a draw when Thompson sampling is applied. Calculated by the percentage of times in a Monte Carlo of 10,000 draws of a beta(success_total, failure_total) distribution that a condition \"wins\" by drawing highest. Total = posterior + (prior).", "experiments.details.posteriors.percentOfTotalRewards.text": "% of All Rewards Received", "experiments.details.posteriors.no-data-row.text": "Experiment has not begun to collect feedback.", "experiments.upsert-include-list-modal.name-hint.text": "The name for this include list.", @@ -596,14 +598,14 @@ "experiments.edit-condition-weights-modal.equal-assignment-weights.description.text": "Equally distribute weight percentages across all conditions.", "experiments.edit-condition-weights-modal.custom-percentages.label.text": "Custom Percentages", "experiments.edit-condition-weights-modal.custom-percentages.description.text": "Define a custom weight percentage for each condition.", - "experiments.edit-condition-priors-modal.title.text": "Edit Condition Priors", - "experiments.edit-condition-priors-modal.condition-header.text": "Condition", - "experiments.edit-condition-priors-modal.successes-header.text": "Successes", - "experiments.edit-condition-priors-modal.failures-header.text": "Failures", - "experiments.edit-condition-priors-modal.min-error.text": "Value must be at least 1.", - "experiments.edit-condition-priors-modal.max-error.text": "Value exceeds the maximum allowed.", - "experiments.edit-condition-priors-modal.integer-error.text": "Value must be a whole number.", - "experiments.edit-condition-priors-modal.no-data.text": "No conditions available.", + "experiments.edit-condition-prior-modal.title.text": "Edit Condition prior", + "experiments.edit-condition-prior-modal.condition-header.text": "Condition", + "experiments.edit-condition-prior-modal.successes-header.text": "Successes", + "experiments.edit-condition-prior-modal.failures-header.text": "Failures", + "experiments.edit-condition-prior-modal.min-error.text": "Value must be at least 1.", + "experiments.edit-condition-prior-modal.max-error.text": "Value exceeds the maximum allowed.", + "experiments.edit-condition-prior-modal.integer-error.text": "Value must be a whole number.", + "experiments.edit-condition-prior-modal.no-data.text": "No conditions available.", "feature-flags.global-name.text": "Name", "feature-flags.global-status.text": "Status", "feature-flags.global-updated-at.text": "Updated at", diff --git a/packages/types/src/Mooclet/index.ts b/packages/types/src/Mooclet/index.ts index 982ef063d..ea41ff776 100644 --- a/packages/types/src/Mooclet/index.ts +++ b/packages/types/src/Mooclet/index.ts @@ -27,9 +27,13 @@ interface ExperimentRewardsByCondition { conditionCode: string; successes: number; failures: number; - total: number; successRate: string; order: number; + estimatedWeight?: number; + priorSuccess?: number; + priorFailure?: number; + posteriorSuccesses?: number; + posteriorFailures?: number; } type ExperimentRewardsSummary = Array; From 6494e7707e4a71070055d43987d62b0cc5d77bb8 Mon Sep 17 00:00:00 2001 From: doswalt Date: Fri, 17 Apr 2026 14:34:38 -0400 Subject: [PATCH 04/10] style updates --- .../enrollment-condition-expandable-row.component.html | 5 ++--- .../ts-configurable-reward-count-table.component.html | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-enrollment-data-section-card/enrollment-condition-table/enrollment-condition-expandable-row/enrollment-condition-expandable-row.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-enrollment-data-section-card/enrollment-condition-table/enrollment-condition-expandable-row/enrollment-condition-expandable-row.component.html index 4fe23e04e..d09cc9b7c 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-enrollment-data-section-card/enrollment-condition-table/enrollment-condition-expandable-row/enrollment-condition-expandable-row.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-enrollment-data-section-card/enrollment-condition-table/enrollment-condition-expandable-row/enrollment-condition-expandable-row.component.html @@ -5,8 +5,7 @@ - {{ key.includes('Icon') ? '' : columnHeaders[key] }} + {{ key.includes('Icon') || (key === 'weight' && isMoocletExperiment(experiment)) ? '' : columnHeaders[key] }} - {{ row.estimatedWeight != null ? '~' + row.estimatedWeight + '%' : '—' }} + {{ row.estimatedWeight != null ? '≈' + row.estimatedWeight + '%' : '—' }} From e46557e77a3f1fbfba01cfdf90eee071dfad05d8 Mon Sep 17 00:00:00 2001 From: doswalt Date: Fri, 24 Apr 2026 16:53:10 -0400 Subject: [PATCH 05/10] style the table --- .../src/api/services/MoocletRewardsService.ts | 23 ++++---- ...igurable-reward-count-table.component.html | 52 +++++++++++++------ ...igurable-reward-count-table.component.scss | 30 ++++++----- ...nfigurable-reward-count-table.component.ts | 13 ++++- .../projects/upgrade/src/assets/i18n/en.json | 5 +- 5 files changed, 80 insertions(+), 43 deletions(-) diff --git a/packages/backend/src/api/services/MoocletRewardsService.ts b/packages/backend/src/api/services/MoocletRewardsService.ts index 8193a7a94..e835b18c9 100644 --- a/packages/backend/src/api/services/MoocletRewardsService.ts +++ b/packages/backend/src/api/services/MoocletRewardsService.ts @@ -302,17 +302,21 @@ export class MoocletRewardsService { return []; } - const conditionCodes = moocletExperimentRef.versionConditionMaps.map( - ({ experimentCondition }) => experimentCondition.conditionCode + const versionConditionPairs = moocletExperimentRef.versionConditionMaps.map( + ({ experimentCondition, moocletVersionId }) => ({ + conditionCode: experimentCondition.conditionCode, + moocletVersionId, + }) ); const DEFAULT_PRIOR: Prior = { success: 1, failure: 1 }; const estimatedWeightMap = policyParameters - ? this.computeThompsonWeightsMap(conditionCodes, policyParameters) + ? this.computeThompsonWeightsMap(versionConditionPairs, policyParameters) : null; const rewardsSummaries = moocletExperimentRef.versionConditionMaps.map( ({ experimentCondition, moocletVersionId }) => { const conditionCode = experimentCondition.conditionCode; + const versionIdKey = String(moocletVersionId); const versionRewards = rewards.filter((reward) => reward.version === moocletVersionId); const successes = versionRewards.filter((reward) => reward.value === 1.0).length; const failures = versionRewards.filter((reward) => reward.value === 0.0).length; @@ -320,8 +324,8 @@ export class MoocletRewardsService { const percentSuccess = total > 0 ? (successes / total) * 100 : 0.0; const successRate = percentSuccess.toFixed(1) + '%'; - const conditionPrior: Prior = policyParameters?.prior?.[conditionCode] ?? DEFAULT_PRIOR; - const conditionPosteriors = policyParameters?.current_posteriors?.[conditionCode]; + const conditionPrior: Prior = policyParameters?.prior?.[versionIdKey] ?? DEFAULT_PRIOR; + const conditionPosteriors = policyParameters?.current_posteriors?.[versionIdKey]; const rewardsForCondition: ExperimentRewardsByCondition = { conditionCode, @@ -349,14 +353,15 @@ export class MoocletRewardsService { * Returns a Map of conditionCode → integer estimated weight (all values sum to 100). */ private computeThompsonWeightsMap( - conditionCodes: string[], + versionConditionPairs: { conditionCode: string; moocletVersionId: number }[], params: MoocletTSConfigurablePolicyParametersDTO ): Map { const DEFAULT_PRIOR: Prior = { success: 1, failure: 1 }; - const arms = conditionCodes.map((conditionCode) => { - const prior: Prior = params.prior?.[conditionCode] ?? DEFAULT_PRIOR; - const posteriors = params.current_posteriors?.[conditionCode]; + const arms = versionConditionPairs.map(({ conditionCode, moocletVersionId }) => { + const versionIdKey = String(moocletVersionId); + const prior: Prior = params.prior?.[versionIdKey] ?? DEFAULT_PRIOR; + const posteriors = params.current_posteriors?.[versionIdKey]; return { conditionCode, alpha: prior.success + (posteriors?.successes ?? 0), diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.html index 32edd8f84..2263e5921 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.html @@ -20,31 +20,57 @@ - + {{ 'experiments.details.posteriors.successes.text' | translate }} - + {{ row.successes }} + + + + {{ 'experiments.details.posteriors.prior.text' | translate }} + + ({{ row.priorSuccess ?? 1 }}) + + + + + + {{ 'experiments.details.posteriors.posterior.text' | translate }} + + + {{ row.successes + (row.priorSuccess ?? 1) }} + + + - + {{ 'experiments.details.posteriors.failures.text' | translate }} - + {{ row.failures }} - - - - {{ 'experiments.details.posteriors.successRate.text' | translate }} + + + + {{ 'experiments.details.posteriors.prior.text' | translate }} + + ({{ row.priorFailure ?? 1 }}) + + + + + + {{ 'experiments.details.posteriors.posterior.text' | translate }} - - {{ row.successRate }} + + {{ row.failures + (row.priorFailure ?? 1) }} @@ -72,12 +98,6 @@ - - - - - - diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.scss b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.scss index bb3df3f6a..25fb221df 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.scss +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.scss @@ -63,31 +63,33 @@ } .condition-column { - width: 350px; text-align: left; padding-left: 32px; + width: 8%; } - .successes-column, - .failures-column, - .success-rate-column, - .estimated-weight-column, - .spacer-column { - width: auto; + .prior-column, + .posterior-column { + width: 6%; + } + + .count-column { + width: 20%; + } + + .count-column, + .prior-column, + .posterior-column, + .estimated-weight-column { text-align: right; - padding-right: 16px; } &.no-data .condition-column { width: auto; } - .successes-column { - width: 64px; - } - - .spacer-column { - min-width: 64px; + .prior-column { + color: var(--darker-grey); } } } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts index dbca6d45c..f71318083 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts @@ -18,7 +18,16 @@ export class TSConfigurableRewardCountTableComponent { @Input() dataSource: ExperimentRewardsSummary = []; @Input() isLoading = false; - displayedColumns = ['conditionCode', 'successes', 'failures', 'successRate', 'estimatedWeight', 'spacer']; + displayedColumns = [ + 'conditionCode', + 'successes', + 'successPrior', + 'successPosterior', + 'failures', + 'failurePrior', + 'failurePosterior', + 'estimatedWeight', + ]; getEstimatedWeightTooltip(row: ExperimentRewardsByCondition): string { const ps = row.posteriorSuccesses ?? 0; @@ -27,6 +36,6 @@ export class TSConfigurableRewardCountTableComponent { const priorF = row.priorFailure ?? 1; const alpha = ps + priorS; const beta = pf + priorF; - return `successes: ${ps} + (${priorS}) = ${alpha}\nfailures: ${pf} + (${priorF}) = ${beta}\nbeta(${alpha}, ${beta})`; + return `α: posteriors(${ps}) + prior(${priorS}) = ${alpha}\nβ: posteriors(${pf}) + prior(${priorF}) = ${beta}\nbeta(${alpha}, ${beta})`; } } diff --git a/packages/frontend/projects/upgrade/src/assets/i18n/en.json b/packages/frontend/projects/upgrade/src/assets/i18n/en.json index 43ca49d4a..e0509d8fa 100644 --- a/packages/frontend/projects/upgrade/src/assets/i18n/en.json +++ b/packages/frontend/projects/upgrade/src/assets/i18n/en.json @@ -575,10 +575,11 @@ "experiments.details.export-metrics-data.menu-item.text": "Export Metrics Data", "experiments.details.posteriors.condition.text": "Condition", "experiments.details.posteriors.successes.text": "Successes", + "experiments.details.posteriors.prior.text": "Prior", + "experiments.details.posteriors.posterior.text": "Posterior", "experiments.details.posteriors.failures.text": "Failures", - "experiments.details.posteriors.successRate.text": "Success Rate", "experiments.details.posteriors.estimated-weight.text": "Est. Weight", - "experiments.details.posteriors.estimated-weight-header-tooltip.text": "Estimated chance of condition \"winning\" a draw when Thompson sampling is applied. Calculated by the percentage of times in a Monte Carlo of 10,000 draws of a beta(success_total, failure_total) distribution that a condition \"wins\" by drawing highest. Total = posterior + (prior).", + "experiments.details.posteriors.estimated-weight-header-tooltip.text": "Estimated probability that this condition wins the next Thompson sampling draw. A random sample is drawn from each condition's posterior Beta distribution — Beta(successes, failures) — and the condition with the highest sample wins. Estimate is percentage of wins after 10,000 draws.", "experiments.details.posteriors.percentOfTotalRewards.text": "% of All Rewards Received", "experiments.details.posteriors.no-data-row.text": "Experiment has not begun to collect feedback.", "experiments.upsert-include-list-modal.name-hint.text": "The name for this include list.", From 7541123b23cd9be6201528f6d2635c92f360f7d9 Mon Sep 17 00:00:00 2001 From: doswalt Date: Fri, 24 Apr 2026 16:58:28 -0400 Subject: [PATCH 06/10] fix calculation error --- packages/backend/src/api/services/MoocletRewardsService.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/api/services/MoocletRewardsService.ts b/packages/backend/src/api/services/MoocletRewardsService.ts index e835b18c9..f8af8efe4 100644 --- a/packages/backend/src/api/services/MoocletRewardsService.ts +++ b/packages/backend/src/api/services/MoocletRewardsService.ts @@ -360,12 +360,11 @@ export class MoocletRewardsService { const arms = versionConditionPairs.map(({ conditionCode, moocletVersionId }) => { const versionIdKey = String(moocletVersionId); - const prior: Prior = params.prior?.[versionIdKey] ?? DEFAULT_PRIOR; const posteriors = params.current_posteriors?.[versionIdKey]; return { conditionCode, - alpha: prior.success + (posteriors?.successes ?? 0), - beta: prior.failure + (posteriors?.failures ?? 0), + alpha: posteriors?.successes ?? DEFAULT_PRIOR.success, + beta: posteriors?.failures ?? DEFAULT_PRIOR.failure, }; }); From a9c19edd836f896dea608f087a37b311fed2bd59 Mon Sep 17 00:00:00 2001 From: doswalt Date: Fri, 24 Apr 2026 17:02:18 -0400 Subject: [PATCH 07/10] remove moocletapi changes --- packages/backend/rest-client-vscode/MoocletAPI.http | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/backend/rest-client-vscode/MoocletAPI.http b/packages/backend/rest-client-vscode/MoocletAPI.http index e86f74abe..3f2207cf3 100644 --- a/packages/backend/rest-client-vscode/MoocletAPI.http +++ b/packages/backend/rest-client-vscode/MoocletAPI.http @@ -24,17 +24,17 @@ # 11. Repeat steps 8-10 as needed ############ env variables -@host = http://localhost:8000/mooclet-service +@host = https://apps.qa-cli.net/mooclet-service # Replace with your token, i.e. -@token = Token 0656e428497a93e7e8fa2f2009d1cf4786d467db +@token = Token abc123 # @token = @apiEndpoint = /engine/api/v1 ############ request variables (change as needed) -@moocletId = 108 -@moocletName = +@moocletId = 196 +@moocletName = newmooc4 @policyId = 17 @policyParametersId = 2 @version1Name = controlz @@ -50,11 +50,6 @@ GET {{host}}{{apiEndpoint}}/policy HTTP/1.1 Authorization: {{token}} Content-type: application/json -########### Get policy id by assignment algorithm name -GET {{host}}{{apiEndpoint}}/mooclet/{{moocletId}} HTTP/1.1 -Authorization: {{token}} -Content-type: application/json - ########### create mooclet POST {{host}}{{apiEndpoint}}/mooclet HTTP/1.1 Authorization: {{token}} From 7a7d1a4812e462f276111dab6906cf7f3c6a6fb8 Mon Sep 17 00:00:00 2001 From: doswalt Date: Mon, 27 Apr 2026 12:04:31 -0400 Subject: [PATCH 08/10] fix unit tests remove total --- .../services/MoocletRewardsService.test.ts | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/packages/backend/test/unit/services/MoocletRewardsService.test.ts b/packages/backend/test/unit/services/MoocletRewardsService.test.ts index 2f5b5ec5a..472dbbfa0 100644 --- a/packages/backend/test/unit/services/MoocletRewardsService.test.ts +++ b/packages/backend/test/unit/services/MoocletRewardsService.test.ts @@ -595,17 +595,25 @@ describe('MoocletRewardsService', () => { conditionCode: 'Control', successes: 2, failures: 1, - total: 3, successRate: '66.7%', order: 0, + estimatedWeight: undefined, + priorSuccess: 1, + priorFailure: 1, + posteriorSuccesses: 0, + posteriorFailures: 0, }); expect(result[1]).toEqual({ conditionCode: 'Treatment', successes: 1, failures: 1, - total: 2, successRate: '50.0%', order: 1, + estimatedWeight: undefined, + priorSuccess: 1, + priorFailure: 1, + posteriorSuccesses: 0, + posteriorFailures: 0, }); }); @@ -638,9 +646,13 @@ describe('MoocletRewardsService', () => { conditionCode: 'Control', successes: 0, failures: 0, - total: 0, successRate: '0.0%', order: 0, + estimatedWeight: undefined, + priorSuccess: 1, + priorFailure: 1, + posteriorSuccesses: 0, + posteriorFailures: 0, }); }); @@ -698,18 +710,26 @@ describe('MoocletRewardsService', () => { conditionCode: 'Control', successes: 2, failures: 1, - total: 3, successRate: '66.7%', order: 0, + estimatedWeight: undefined, + priorSuccess: 1, + priorFailure: 1, + posteriorSuccesses: 0, + posteriorFailures: 0, }); // Treatment (version 200): page1 has id3 (success), page2 has id5 (failure) + id6 (success) => 2 successes, 1 failure expect(result[1]).toEqual({ conditionCode: 'Treatment', successes: 2, failures: 1, - total: 3, successRate: '66.7%', order: 1, + estimatedWeight: undefined, + priorSuccess: 1, + priorFailure: 1, + posteriorSuccesses: 0, + posteriorFailures: 0, }); }); @@ -817,9 +837,9 @@ describe('MoocletRewardsService', () => { const result = await service.getRewardsSummaryForExperiment('experiment-123', mockLogger); expect(mockMoocletDataService.getRewardsForExperiment).toHaveBeenCalledTimes(3); - expect(result[0].total).toBe(3); expect(result[0].successes).toBe(2); expect(result[0].failures).toBe(1); + expect(result[0].successes + result[0].failures).toBe(3); }); it('should log error and re-throw when mooclet service fails', async () => { @@ -985,17 +1005,25 @@ describe('MoocletRewardsService', () => { conditionCode: 'Control', successes: 2, failures: 1, - total: 3, successRate: '66.7%', order: 0, + estimatedWeight: undefined, + priorSuccess: 1, + priorFailure: 1, + posteriorSuccesses: 0, + posteriorFailures: 0, }); expect(result[1]).toEqual({ conditionCode: 'Treatment', successes: 1, failures: 3, - total: 4, successRate: '25.0%', order: 1, + estimatedWeight: undefined, + priorSuccess: 1, + priorFailure: 1, + posteriorSuccesses: 0, + posteriorFailures: 0, }); }); @@ -1115,8 +1143,8 @@ describe('MoocletRewardsService', () => { ); // Version 999 should not be counted - expect(result[0].total).toBe(2); // Only version 100 - expect(result[1].total).toBe(2); // Only version 200 + expect(result[0].successes + result[0].failures).toBe(2); // Only version 100 + expect(result[1].successes + result[1].failures).toBe(2); // Only version 200 }); it('should handle condition with no rewards', async () => { @@ -1153,9 +1181,13 @@ describe('MoocletRewardsService', () => { conditionCode: 'Treatment', successes: 0, failures: 0, - total: 0, successRate: '0.0%', order: 1, + estimatedWeight: undefined, + priorSuccess: 1, + priorFailure: 1, + posteriorSuccesses: 0, + posteriorFailures: 0, }); }); }); From 10a937bf248a5880e71567db852857e84f516fc7 Mon Sep 17 00:00:00 2001 From: doswalt Date: Tue, 28 Apr 2026 15:54:59 -0400 Subject: [PATCH 09/10] fix bad find-replace typos, refactor this and that --- .../api/services/MoocletExperimentService.ts | 96 ++++++++++--------- packages/backend/src/api/services/estimate.ts | 86 ----------------- .../experiment-conditions-table.component.ts | 12 +-- ...nfigurable-reward-count-table.component.ts | 10 +- 4 files changed, 59 insertions(+), 145 deletions(-) delete mode 100644 packages/backend/src/api/services/estimate.ts diff --git a/packages/backend/src/api/services/MoocletExperimentService.ts b/packages/backend/src/api/services/MoocletExperimentService.ts index 14286d596..88170787e 100644 --- a/packages/backend/src/api/services/MoocletExperimentService.ts +++ b/packages/backend/src/api/services/MoocletExperimentService.ts @@ -52,7 +52,6 @@ import { EXPERIMENT_STATE, MoocletPolicyParametersDTO, MoocletTSConfigurablePolicyParametersDTO, - Prior, SUPPORTED_MOOCLET_ALGORITHMS, } from 'upgrade_types'; import { ExperimentCondition } from '../models/ExperimentCondition'; @@ -399,16 +398,10 @@ export class MoocletExperimentService extends ExperimentService { // so the PUT response is consistent with the GET response from attachPolicyParamsToExperimentDTO. const updatedTsParams = updatedExperiment.moocletPolicyParameters as MoocletTSConfigurablePolicyParametersDTO; if (updatedTsParams?.prior) { - const transformedPrior: Record = {}; - for (const [versionId, priorData] of Object.entries(updatedTsParams.prior)) { - const map = currentMoocletExperimentRef.versionConditionMaps.find( - (m) => String(m.moocletVersionId) === versionId - ); - if (map?.experimentCondition?.conditionCode) { - transformedPrior[map.experimentCondition.conditionCode] = priorData; - } - } - updatedTsParams.prior = transformedPrior; + updatedTsParams.prior = this.translateVersionIdsToConditionCodes( + updatedTsParams.prior, + currentMoocletExperimentRef.versionConditionMaps + ); } // --------- update versions ---------------------- @@ -542,16 +535,13 @@ export class MoocletExperimentService extends ExperimentService { const tsParams = newPolicyParameters as MoocletTSConfigurablePolicyParametersDTO; if (tsParams.prior) { // Translate conditionCode keys to Mooclet version IDs before sending to the Mooclet API - const translatedprior: Record = {}; - for (const [conditionCode, priorData] of Object.entries(tsParams.prior)) { - const versionConditionMap = currentMoocletExperimentRef.versionConditionMaps?.find( - (map) => map.experimentCondition?.conditionCode === conditionCode - ); - if (versionConditionMap?.moocletVersionId) { - translatedprior[String(versionConditionMap.moocletVersionId)] = priorData; - } - } - newPolicyParameters = { ...tsParams, prior: translatedprior } as MoocletPolicyParametersDTO; + newPolicyParameters = { + ...tsParams, + prior: this.translateConditionCodesToVersionIds( + tsParams.prior, + currentMoocletExperimentRef.versionConditionMaps + ), + } as MoocletPolicyParametersDTO; } return this.moocletDataService.updatePolicyParameters( @@ -1202,34 +1192,17 @@ export class MoocletExperimentService extends ExperimentService { // Transform current_posteriors and prior keys from Mooclet version IDs to UpGrade condition codes const tsConfigurableParams = policyParameters.parameters as MoocletTSConfigurablePolicyParametersDTO; if (tsConfigurableParams.current_posteriors) { - const transformedPosteriors = {}; - for (const [versionId, posteriorData] of Object.entries(tsConfigurableParams.current_posteriors)) { - const versionConditionMap = moocletExperimentRef.versionConditionMaps.find( - (map) => map.moocletVersionId === parseInt(versionId, 10) - ); - - if (versionConditionMap?.experimentCondition?.conditionCode) { - transformedPosteriors[versionConditionMap.experimentCondition.conditionCode] = posteriorData; - } else { - logger.warn({ - message: `No condition mapping found for Mooclet version ${versionId} in experiment ${experiment.id}`, - }); - } - } - tsConfigurableParams.current_posteriors = transformedPosteriors; + tsConfigurableParams.current_posteriors = this.translateVersionIdsToConditionCodes( + tsConfigurableParams.current_posteriors, + moocletExperimentRef.versionConditionMaps + ); } if (tsConfigurableParams.prior) { - const transformedprior: Record = {}; - for (const [versionId, priorData] of Object.entries(tsConfigurableParams.prior)) { - const versionConditionMap = moocletExperimentRef.versionConditionMaps.find( - (map) => map.moocletVersionId === parseInt(versionId, 10) - ); - if (versionConditionMap?.experimentCondition?.conditionCode) { - transformedprior[versionConditionMap.experimentCondition.conditionCode] = priorData; - } - } - tsConfigurableParams.prior = transformedprior; + tsConfigurableParams.prior = this.translateVersionIdsToConditionCodes( + tsConfigurableParams.prior, + moocletExperimentRef.versionConditionMaps + ); } experiment.moocletPolicyParameters = policyParameters.parameters; @@ -1394,6 +1367,37 @@ export class MoocletExperimentService extends ExperimentService { return SUPPORTED_MOOCLET_ALGORITHMS.includes(assignmentAlgorithm); } + private translateConditionCodesToVersionIds( + record: Record, + versionConditionMaps: MoocletVersionConditionMap[] + ): Record { + const result: Record = {}; + for (const [conditionCode, value] of Object.entries(record)) { + const map = versionConditionMaps?.find((m) => m.experimentCondition?.conditionCode === conditionCode); + if (map?.moocletVersionId) { + result[String(map.moocletVersionId)] = value; + } + } + return result; + } + + private translateVersionIdsToConditionCodes( + record: Record, + versionConditionMaps: MoocletVersionConditionMap[] + ): Record { + const result: Record = {}; + for (const [versionId, value] of Object.entries(record)) { + const map = versionConditionMaps?.find((m) => String(m.moocletVersionId) === versionId); + if (!map?.experimentCondition?.conditionCode) { + throw new MoocletError( + `Reward feedback summary could not be processed: no condition mapping found for Mooclet version ${versionId}` + ); + } + result[map.experimentCondition.conditionCode] = value; + } + return result; + } + /** * Generate a unique outcome variable name based on experiment name and timestamp. * Returns a string in this format: diff --git a/packages/backend/src/api/services/estimate.ts b/packages/backend/src/api/services/estimate.ts deleted file mode 100644 index 8ec7f63a3..000000000 --- a/packages/backend/src/api/services/estimate.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Pure TS Beta distribution sampler (no dependencies) - -function randNormal(): number { - return Math.sqrt(-2 * Math.log(Math.random())) * Math.cos(2 * Math.PI * Math.random()); -} - -function randGamma(shape: number): number { - if (shape < 1) { - return randGamma(1 + shape) * Math.pow(Math.random(), 1 / shape); - } - const d = shape - 1 / 3; - const c = 1 / Math.sqrt(9 * d); - // eslint-disable-next-line no-constant-condition - while (true) { - let v: number; - let x: number; - do { - x = randNormal(); - v = 1 + c * x; - } while (v <= 0); - v = v * v * v; - const u = Math.random(); - if (u < 1 - 0.0331 * x * x * x * x) return d * v; - if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) return d * v; - } -} - -function randBeta(alpha: number, beta: number): number { - const g1 = randGamma(alpha); - const g2 = randGamma(beta); - return g1 / (g1 + g2); -} - -// --- - -interface ArmPosterior { - versionId: string; - successes: number; - failures: number; -} - -interface ArmPrior { - priorSuccess: number; - priorFailure: number; -} - -interface VersionWeight { - versionId: string; - estimatedCurrentWeight: string; -} - -export function estimateThompsonWeights(arms: ArmPosterior[], prior: ArmPrior, iterations = 10_000): VersionWeight[] { - const wins = new Array(arms.length).fill(0); - - for (let i = 0; i < iterations; i++) { - let maxSample = -1; - let maxIdx = 0; - for (let j = 0; j < arms.length; j++) { - const alpha = arms[j].successes + prior.priorSuccess; - const beta = arms[j].failures + prior.priorFailure; - const sample = randBeta(alpha, beta); - if (sample > maxSample) { - maxSample = sample; - maxIdx = j; - } - } - wins[maxIdx]++; - } - - const raw = arms.map((arm, i) => (wins[i] / iterations) * 100); - - // Normalize so rounded values sum exactly to 100 - const floored = raw.map(Math.floor); - const remainder = 100 - floored.reduce((a, b) => a + b, 0); - const diffs = raw.map((v, i) => v - floored[i]); - const indices = diffs - .map((d, i) => ({ d, i })) - .sort((a, b) => b.d - a.d) - .map(({ i }) => i); - for (let k = 0; k < remainder; k++) floored[indices[k]]++; - - return arms.map((arm, i) => ({ - versionId: arm.versionId, - estimatedCurrentWeight: floored[i].toFixed(1), - })); -} diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts index aa5d003ba..c606bdfcc 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-conditions-section-card/experiment-conditions-table/experiment-conditions-table.component.ts @@ -41,7 +41,7 @@ export class ExperimentConditionsTableComponent { @Input() prior?: Record; @Output() rowAction = new EventEmitter(); @Output() editWeights = new EventEmitter(); - @Output() editprior = new EventEmitter(); + @Output() editPrior = new EventEmitter(); get displayedColumns(): string[] { if (this.isMoocletExperiment) { @@ -54,12 +54,12 @@ export class ExperimentConditionsTableComponent { CONDITION: 'experiments.details.conditions.condition.text', DESCRIPTION: 'experiments.details.conditions.description.text', WEIGHT: 'experiments.details.conditions.weight.text', - prior_SUCCESSES: 'experiments.details.conditions.prior-successes.text', - prior_FAILURES: 'experiments.details.conditions.prior-failures.text', + PRIOR_SUCCESSES: 'experiments.details.conditions.prior-successes.text', + PRIOR_FAILURES: 'experiments.details.conditions.prior-failures.text', ACTIONS: 'experiments.details.conditions.actions.text', }; - getprioruccesses(condition: ExperimentCondition): number { + getPriorSuccesses(condition: ExperimentCondition): number { return this.prior?.[condition.conditionCode]?.success ?? 1; } @@ -79,7 +79,7 @@ export class ExperimentConditionsTableComponent { this.editWeights.emit(this.conditions); } - onEditpriorClick(): void { - this.editprior.emit(this.conditions); + onEditPriorClick(): void { + this.editPrior.emit(this.conditions); } } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts index f71318083..ccad7317b 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-reward-feedback-section-card/ts-configurable-reward-count-table/ts-configurable-reward-count-table.component.ts @@ -30,12 +30,8 @@ export class TSConfigurableRewardCountTableComponent { ]; getEstimatedWeightTooltip(row: ExperimentRewardsByCondition): string { - const ps = row.posteriorSuccesses ?? 0; - const pf = row.posteriorFailures ?? 0; - const priorS = row.priorSuccess ?? 1; - const priorF = row.priorFailure ?? 1; - const alpha = ps + priorS; - const beta = pf + priorF; - return `α: posteriors(${ps}) + prior(${priorS}) = ${alpha}\nβ: posteriors(${pf}) + prior(${priorF}) = ${beta}\nbeta(${alpha}, ${beta})`; + const alpha = row.posteriorSuccesses ?? 0; + const beta = row.posteriorFailures ?? 0; + return `α: posteriors(${alpha})\nβ: posteriors(${beta})\nbeta(${alpha}, ${beta})`; } } From d3997bff89b4abb4183103522d7fdd6cf90798bf Mon Sep 17 00:00:00 2001 From: doswalt Date: Wed, 29 Apr 2026 10:47:06 -0400 Subject: [PATCH 10/10] correct tests --- .../services/MoocletExperimentService.test.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/backend/test/unit/services/MoocletExperimentService.test.ts b/packages/backend/test/unit/services/MoocletExperimentService.test.ts index 0ff960f04..89df0f1eb 100644 --- a/packages/backend/test/unit/services/MoocletExperimentService.test.ts +++ b/packages/backend/test/unit/services/MoocletExperimentService.test.ts @@ -1404,7 +1404,10 @@ describe('#MoocletExperimentService', () => { const mockPolicyParams = { parameters: { assignmentAlgorithm: ASSIGNMENT_ALGORITHM.MOOCLET_TS_CONFIGURABLE, - prior: { success: 1, failure: 1 }, + prior: { + '1': { success: 1, failure: 1 }, + '2': { success: 1, failure: 1 }, + }, }, }; @@ -1667,7 +1670,16 @@ describe('#MoocletExperimentService', () => { mockMoocletExperimentRef.id = 'ref-123'; mockMoocletExperimentRef.policyParametersId = 1; mockMoocletExperimentRef.variableId = 2; - mockMoocletExperimentRef.versionConditionMaps = []; + mockMoocletExperimentRef.versionConditionMaps = [ + { + moocletVersionId: 10, + experimentCondition: { conditionCode: 'control' } as any, + } as MoocletVersionConditionMap, + { + moocletVersionId: 20, + experimentCondition: { conditionCode: 'treatment' } as any, + } as MoocletVersionConditionMap, + ]; const mockExperiment = { id: 'exp-123', @@ -1713,7 +1725,13 @@ describe('#MoocletExperimentService', () => { updateSpy.mockResolvedValue(updatedExperiment); jest.spyOn(moocletExperimentService as any, 'doRevertablePolicyParameterChange').mockResolvedValue({ - parameters: mockExperiment.moocletPolicyParameters, + parameters: { + ...mockExperiment.moocletPolicyParameters, + prior: { + '10': { success: 1, failure: 1 }, + '20': { success: 1, failure: 1 }, + }, + }, }); const result = await (moocletExperimentService as any).handleEditMoocletTransaction(manager, params);