From 7d827e9527c5273cd8e277d12eb1430289de803f Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 5 Feb 2025 19:29:51 +0600 Subject: [PATCH 01/15] init --- lib/core/decision_service/index.spec.ts | 577 ++++++++++++++++++++++++ lib/core/decision_service/index.ts | 6 +- lib/optimizely/index.ts | 5 +- vitest.config.mts | 2 +- 4 files changed, 583 insertions(+), 7 deletions(-) create mode 100644 lib/core/decision_service/index.spec.ts diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts new file mode 100644 index 000000000..147792643 --- /dev/null +++ b/lib/core/decision_service/index.spec.ts @@ -0,0 +1,577 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi } from 'vitest'; +import { DecisionService } from '.'; +import { getMockLogger } from '../../tests/mock/mock_logger'; + +type MockLogger = ReturnType; + +type MockUserProfileService = { + lookup: typeof vi.fn; + save: typeof vi.fn; +}; + +type DecisionServiceInstanceOpt = { + logger?: boolean; + userProfileService?: boolean; +} + +type DecisionServiceInstance = { + logger?: MockLogger; + userProfileService?: MockUserProfileService; + decisionService: DecisionService; +} + +const getDecisionService = (opt: DecisionServiceInstanceOpt): DecisionServiceInstance => { + const logger = opt.logger ? getMockLogger() : undefined; + const userProfileService = opt.userProfileService ? { + lookup: vi.fn(), + save: vi.fn(), + } : undefined; + + const decisionService = new DecisionService({ + logger, + userProfileService, + UNSTABLE_conditionEvaluators: {}, + }); + + return { + logger, + userProfileService, + decisionService, + }; +}; + +const testGetVariationWithoutUserProfileService = (decisonService: DecisionServiceInstance) => { + +} + +describe('DecisionService', () => { + describe('getVariation', function() { + it('should return the correct variation for the given experiment key and user ID for a running experiment', () => { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'tester' + }); + var fakeDecisionResponse = { + result: '111128', + reasons: [], + }; + experiment = configObj.experimentIdMap['111127']; + bucketerStub.returns(fakeDecisionResponse); // contains variation ID of the 'control' variation from `test_data` + assert.strictEqual( + 'control', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledOnce(bucketerStub); + }); + + // it('should return the whitelisted variation if the user is whitelisted', function() { + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'user2' + // }); + // experiment = configObj.experimentIdMap['122227']; + // assert.strictEqual( + // 'variationWithAudience', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.notCalled(bucketerStub); + // assert.strictEqual(1, mockLogger.debug.callCount); + // assert.strictEqual(1, mockLogger.info.callCount); + + // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user2']); + + // assert.deepEqual(mockLogger.info.args[0], [USER_FORCED_IN_VARIATION, 'user2', 'variationWithAudience']); + // }); + + // it('should return null if the user does not meet audience conditions', function() { + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'user3' + // }); + // experiment = configObj.experimentIdMap['122227']; + // assert.isNull( + // decisionServiceInstance.getVariation(configObj, experiment, user, { foo: 'bar' }).result + // ); + + // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user3']); + + // assert.deepEqual(mockLogger.debug.args[1], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])]); + + // assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE']); + + // assert.deepEqual(mockLogger.info.args[1], [USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences']); + // }); + + // it('should return null if the experiment is not running', function() { + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'user1' + // }); + // experiment = configObj.experimentIdMap['133337']; + // assert.isNull(decisionServiceInstance.getVariation(configObj, experiment, user).result); + // sinon.assert.notCalled(bucketerStub); + // assert.strictEqual(1, mockLogger.info.callCount); + + // assert.deepEqual(mockLogger.info.args[0], [EXPERIMENT_NOT_RUNNING, 'testExperimentNotRunning']); + // }); + + // describe('when attributes.$opt_experiment_bucket_map is supplied', function() { + // it('should respect the sticky bucketing information for attributes', function() { + // var fakeDecisionResponse = { + // result: '111128', + // reasons: [], + // }; + // experiment = configObj.experimentIdMap['111127']; + // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation from `test_data` + // var attributes = { + // $opt_experiment_bucket_map: { + // '111127': { + // variation_id: '111129', // ID of the 'variation' variation + // }, + // }, + // }; + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // attributes, + // }); + + // assert.strictEqual( + // 'variation', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.notCalled(bucketerStub); + // }); + // }); + + // describe('when a user profile service is provided', function() { + // var fakeDecisionResponse = { + // result: '111128', + // reasons: [], + // }; + // var userProfileServiceInstance = null; + // var userProfileLookupStub; + // var userProfileSaveStub; + // var fakeDecisionWhitelistedVariation = { + // result: null, + // reasons: [], + // } + // beforeEach(function() { + // userProfileServiceInstance = { + // lookup: function() {}, + // save: function() {}, + // }; + + // decisionServiceInstance = createDecisionService({ + // logger: mockLogger, + // userProfileService: userProfileServiceInstance, + // }); + // userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup'); + // userProfileSaveStub = sinon.stub(userProfileServiceInstance, 'save'); + // sinon.stub(decisionServiceInstance, 'getWhitelistedVariation').returns(fakeDecisionWhitelistedVariation); + // }); + + // afterEach(function() { + // userProfileServiceInstance.lookup.restore(); + // userProfileServiceInstance.save.restore(); + // decisionServiceInstance.getWhitelistedVariation.restore(); + // }); + + // it('should return the previously bucketed variation', function() { + // userProfileLookupStub.returns({ + // user_id: 'decision_service_user', + // experiment_bucket_map: { + // '111127': { + // variation_id: '111128', // ID of the 'control' variation + // }, + // }, + // }); + // experiment = configObj.experimentIdMap['111127']; + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // }); + + // assert.strictEqual( + // 'control', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + // sinon.assert.notCalled(bucketerStub); + + // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + // assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'control', 'testExperiment', 'decision_service_user']); + // }); + + // it('should bucket if there was no prevously bucketed variation', function() { + // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + // userProfileLookupStub.returns({ + // user_id: 'decision_service_user', + // experiment_bucket_map: {}, + // }); + // experiment = configObj.experimentIdMap['111127']; + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // }); + + // assert.strictEqual( + // 'control', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + // sinon.assert.calledOnce(bucketerStub); + // // make sure we save the decision + // sinon.assert.calledWith(userProfileSaveStub, { + // user_id: 'decision_service_user', + // experiment_bucket_map: { + // '111127': { + // variation_id: '111128', + // }, + // }, + // }); + // }); + + // it('should bucket if the user profile service returns null', function() { + // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + // userProfileLookupStub.returns(null); + // experiment = configObj.experimentIdMap['111127']; + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // }); + // assert.strictEqual( + // 'control', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + // sinon.assert.calledOnce(bucketerStub); + // // make sure we save the decision + // sinon.assert.calledWith(userProfileSaveStub, { + // user_id: 'decision_service_user', + // experiment_bucket_map: { + // '111127': { + // variation_id: '111128', + // }, + // }, + // }); + // }); + + // it('should re-bucket if the stored variation is no longer valid', function() { + // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + // userProfileLookupStub.returns({ + // user_id: 'decision_service_user', + // experiment_bucket_map: { + // '111127': { + // variation_id: 'not valid variation', + // }, + // }, + // }); + // experiment = configObj.experimentIdMap['111127']; + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // }); + // assert.strictEqual( + // 'control', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + // sinon.assert.calledOnce(bucketerStub); + // // assert.strictEqual( + // // buildLogMessageFromArgs(mockLogger.log.args[0]), + // // 'DECISION_SERVICE: User decision_service_user is not in the forced variation map.' + // // ); + // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + // sinon.assert.calledWith( + // mockLogger.info, + // SAVED_VARIATION_NOT_FOUND, + // 'decision_service_user', + // 'not valid variation', + // 'testExperiment' + // ); + + // // make sure we save the decision + // sinon.assert.calledWith(userProfileSaveStub, { + // user_id: 'decision_service_user', + // experiment_bucket_map: { + // '111127': { + // variation_id: '111128', + // }, + // }, + // }); + // }); + + // it('should store the bucketed variation for the user', function() { + // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + // userProfileLookupStub.returns({ + // user_id: 'decision_service_user', + // experiment_bucket_map: {}, // no decisions for user + // }); + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // }); + // experiment = configObj.experimentIdMap['111127']; + + // assert.strictEqual( + // 'control', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + // sinon.assert.calledOnce(bucketerStub); + + // sinon.assert.calledWith(userProfileServiceInstance.save, { + // user_id: 'decision_service_user', + // experiment_bucket_map: { + // '111127': { + // variation_id: '111128', + // }, + // }, + // }); + + + // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + // assert.deepEqual(mockLogger.info.lastCall.args, [SAVED_USER_VARIATION, 'decision_service_user']); + // }); + + // it('should log an error message if "lookup" throws an error', function() { + // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + // userProfileLookupStub.throws(new Error('I am an error')); + // experiment = configObj.experimentIdMap['111127']; + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // }); + + // assert.strictEqual( + // 'control', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + // sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing + + // assert.deepEqual(mockLogger.error.args[0], [USER_PROFILE_LOOKUP_ERROR, 'decision_service_user', 'I am an error']); + + // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + // }); + + // it('should log an error message if "save" throws an error', function() { + // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + // userProfileLookupStub.returns(null); + // userProfileSaveStub.throws(new Error('I am an error')); + // experiment = configObj.experimentIdMap['111127']; + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // }); + // assert.strictEqual( + // 'control', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + // sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing + + + // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + // assert.deepEqual(mockLogger.error.args[0], [USER_PROFILE_SAVE_ERROR, 'decision_service_user', 'I am an error']); + + // // make sure that we save the decision + // sinon.assert.calledWith(userProfileSaveStub, { + // user_id: 'decision_service_user', + // experiment_bucket_map: { + // '111127': { + // variation_id: '111128', + // }, + // }, + // }); + // }); + + // describe('when passing `attributes.$opt_experiment_bucket_map`', function() { + // it('should respect attributes over the userProfileService for the matching experiment id', function() { + // userProfileLookupStub.returns({ + // user_id: 'decision_service_user', + // experiment_bucket_map: { + // '111127': { + // variation_id: '111128', // ID of the 'control' variation + // }, + // }, + // }); + + // var attributes = { + // $opt_experiment_bucket_map: { + // '111127': { + // variation_id: '111129', // ID of the 'variation' variation + // }, + // }, + // }; + + // experiment = configObj.experimentIdMap['111127']; + + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // attributes, + // }); + + // assert.strictEqual( + // 'variation', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + // sinon.assert.notCalled(bucketerStub); + + // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + // assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'variation', 'testExperiment', 'decision_service_user']); + // }); + + // it('should ignore attributes for a different experiment id', function() { + // userProfileLookupStub.returns({ + // user_id: 'decision_service_user', + // experiment_bucket_map: { + // '111127': { + // // 'testExperiment' ID + // variation_id: '111128', // ID of the 'control' variation + // }, + // }, + // }); + + // experiment = configObj.experimentIdMap['111127']; + + // var attributes = { + // $opt_experiment_bucket_map: { + // '122227': { + // // other experiment ID + // variation_id: '122229', // ID of the 'variationWithAudience' variation + // }, + // }, + // }; + + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // attributes, + // }); + + // assert.strictEqual( + // 'control', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + // sinon.assert.notCalled(bucketerStub); + + // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + // assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'control', 'testExperiment', 'decision_service_user']); + // }); + + // it('should use attributes when the userProfileLookup variations for other experiments', function() { + // userProfileLookupStub.returns({ + // user_id: 'decision_service_user', + // experiment_bucket_map: { + // '122227': { + // // other experiment ID + // variation_id: '122229', // ID of the 'variationWithAudience' variation + // }, + // }, + // }); + + // experiment = configObj.experimentIdMap['111127']; + + // var attributes = { + // $opt_experiment_bucket_map: { + // '111127': { + // // 'testExperiment' ID + // variation_id: '111129', // ID of the 'variation' variation + // }, + // }, + // }; + + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // attributes, + // }); + + // assert.strictEqual( + // 'variation', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + // sinon.assert.notCalled(bucketerStub); + + // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + // assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'variation', 'testExperiment', 'decision_service_user']); + // }); + + // it('should use attributes when the userProfileLookup returns null', function() { + // userProfileLookupStub.returns(null); + + // experiment = configObj.experimentIdMap['111127']; + + // var attributes = { + // $opt_experiment_bucket_map: { + // '111127': { + // variation_id: '111129', // ID of the 'variation' variation + // }, + // }, + // }; + + // user = new OptimizelyUserContext({ + // shouldIdentifyUser: false, + // optimizely: {}, + // userId: 'decision_service_user', + // attributes, + // }); + + // assert.strictEqual( + // 'variation', + // decisionServiceInstance.getVariation(configObj, experiment, user).result + // ); + // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + // sinon.assert.notCalled(bucketerStub); + + // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + // assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'variation', 'testExperiment', 'decision_service_user']); + // }); + // }); + // }); + }); +}); diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts index 21a63b763..85b888da3 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -110,7 +110,7 @@ export interface DecisionObj { } interface DecisionServiceOptions { - userProfileService: UserProfileService | null; + userProfileService?: UserProfileService; logger?: LoggerFacade; UNSTABLE_conditionEvaluators: unknown; } @@ -143,13 +143,13 @@ export class DecisionService { private logger?: LoggerFacade; private audienceEvaluator: AudienceEvaluator; private forcedVariationMap: { [key: string]: { [id: string]: string } }; - private userProfileService: UserProfileService | null; + private userProfileService?: UserProfileService; constructor(options: DecisionServiceOptions) { this.logger = options.logger; this.audienceEvaluator = createAudienceEvaluator(options.UNSTABLE_conditionEvaluators, this.logger); this.forcedVariationMap = {}; - this.userProfileService = options.userProfileService || null; + this.userProfileService = options.userProfileService; } /** diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index 9ca0c6089..0d10cd5e0 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -60,7 +60,7 @@ import { NODE_CLIENT_ENGINE, CLIENT_VERSION, } from '../utils/enums'; -import { Fn } from '../utils/type'; +import { Fn, Maybe } from '../utils/type'; import { resolvablePromise } from '../utils/promise/resolvablePromise'; import { NOTIFICATION_TYPES, DecisionNotificationType, DECISION_NOTIFICATION_TYPES } from '../notification_center/type'; @@ -211,8 +211,7 @@ export default class Optimizely extends BaseService implements Client { this.odpManager = config.odpManager; - - let userProfileService: UserProfileService | null = null; + let userProfileService: Maybe = undefined; if (config.userProfileService) { try { if (userProfileServiceValidator.validate(config.userProfileService)) { diff --git a/vitest.config.mts b/vitest.config.mts index 584eeb60d..c35f62ec3 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -26,7 +26,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/*.spec.ts'], + include: ['**/decision_service/index.spec.ts'], typecheck: { enabled: true, tsconfig: 'tsconfig.spec.json', From e32b9e179b0ec46200260767839b8a496b6b5413 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Mon, 3 Mar 2025 19:58:01 +0600 Subject: [PATCH 02/15] up --- lib/core/decision_service/index.spec.ts | 196 ++++++++++++++++++------ lib/core/decision_service/index.ts | 20 +-- 2 files changed, 157 insertions(+), 59 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index 147792643..12d4fbb13 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -13,9 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, MockInstance, beforeEach } from 'vitest'; import { DecisionService } from '.'; import { getMockLogger } from '../../tests/mock/mock_logger'; +import OptimizelyUserContext from '../../optimizely_user_context'; +import { bucket } from '../bucketer'; +import { getTestProjectConfig, getTestProjectConfigWithFeatures } from '../../tests/test_data'; +import { createProjectConfig, ProjectConfig } from '../../project_config/project_config'; +import { Experiment } from '../../shared_types'; +import { CONTROL_ATTRIBUTES } from '../../utils/enums'; + +import { + USER_HAS_NO_FORCED_VARIATION, + VALID_BUCKETING_ID, + SAVED_USER_VARIATION, + SAVED_VARIATION_NOT_FOUND, +} from 'log_message'; + +import { + EXPERIMENT_NOT_RUNNING, + RETURNING_STORED_VARIATION, + USER_NOT_IN_EXPERIMENT, + USER_FORCED_IN_VARIATION, + EVALUATING_AUDIENCES_COMBINED, + AUDIENCE_EVALUATION_RESULT_COMBINED, + USER_IN_ROLLOUT, + USER_NOT_IN_ROLLOUT, + FEATURE_HAS_NO_EXPERIMENTS, + USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE, + USER_NOT_BUCKETED_INTO_TARGETING_RULE, + USER_BUCKETED_INTO_TARGETING_RULE, + NO_ROLLOUT_EXISTS, + USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, +} from '../decision_service/index'; type MockLogger = ReturnType; @@ -35,7 +65,7 @@ type DecisionServiceInstance = { decisionService: DecisionService; } -const getDecisionService = (opt: DecisionServiceInstanceOpt): DecisionServiceInstance => { +const getDecisionService = (opt: DecisionServiceInstanceOpt = {}): DecisionServiceInstance => { const logger = opt.logger ? getMockLogger() : undefined; const userProfileService = opt.userProfileService ? { lookup: vi.fn(), @@ -55,70 +85,144 @@ const getDecisionService = (opt: DecisionServiceInstanceOpt): DecisionServiceIns }; }; +const mockBucket: MockInstance = vi.hoisted(() => vi.fn()); + +vi.mock('../bucketer', () => ({ + bucket: mockBucket, +})); + const testGetVariationWithoutUserProfileService = (decisonService: DecisionServiceInstance) => { } +const cloneDeep = (d: any) => JSON.parse(JSON.stringify(d)); + +const testData = getTestProjectConfig(); +const testDataWithFeatures = getTestProjectConfigWithFeatures(); + +const verifyBucketCall = ( + call: number, + projectConfig: ProjectConfig, + experiment: Experiment, + user: OptimizelyUserContext, +) => { + const { + experimentId, + experimentKey, + userId, + trafficAllocationConfig, + experimentKeyMap, + experimentIdMap, + groupIdMap, + variationIdMap, + bucketingId, + } = mockBucket.mock.calls[call][0]; + expect(experimentId).toBe(experiment.id); + expect(experimentKey).toBe(experiment.key); + expect(userId).toBe(user.getUserId()); + expect(trafficAllocationConfig).toBe(experiment.trafficAllocation); + expect(experimentKeyMap).toBe(projectConfig.experimentKeyMap); + expect(experimentIdMap).toBe(projectConfig.experimentIdMap); + expect(groupIdMap).toBe(projectConfig.groupIdMap); + expect(variationIdMap).toBe(projectConfig.variationIdMap); + expect(bucketingId).toBe(user.getAttributes()[CONTROL_ATTRIBUTES.BUCKETING_ID] || user.getUserId()); +}; describe('DecisionService', () => { describe('getVariation', function() { - it('should return the correct variation for the given experiment key and user ID for a running experiment', () => { - user = new OptimizelyUserContext({ - shouldIdentifyUser: false, - optimizely: {}, + beforeEach(() => { + mockBucket.mockClear(); + }); + + it('should return the correct variation from bucketer for the given experiment key and user ID for a running experiment', () => { + const user = new OptimizelyUserContext({ + optimizely: {} as any, userId: 'tester' }); - var fakeDecisionResponse = { + + const config = createProjectConfig(cloneDeep(testData)); + + const fakeDecisionResponse = { result: '111128', reasons: [], }; - experiment = configObj.experimentIdMap['111127']; - bucketerStub.returns(fakeDecisionResponse); // contains variation ID of the 'control' variation from `test_data` - assert.strictEqual( - 'control', - decisionServiceInstance.getVariation(configObj, experiment, user).result - ); - sinon.assert.calledOnce(bucketerStub); + + const experiment = config.experimentIdMap['111127']; + + mockBucket.mockReturnValue(fakeDecisionResponse); // contains variation ID of the 'control' variation from `test_data` + + const { decisionService } = getDecisionService(); + + const variation = decisionService.getVariation(config, experiment, user); + + expect(variation.result).toBe('control'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); }); - // it('should return the whitelisted variation if the user is whitelisted', function() { - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'user2' - // }); - // experiment = configObj.experimentIdMap['122227']; - // assert.strictEqual( - // 'variationWithAudience', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.notCalled(bucketerStub); - // assert.strictEqual(1, mockLogger.debug.callCount); - // assert.strictEqual(1, mockLogger.info.callCount); + it('should return the whitelisted variation if the user is whitelisted', function() { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'user2' + }); - // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user2']); + const config = createProjectConfig(cloneDeep(testData)); - // assert.deepEqual(mockLogger.info.args[0], [USER_FORCED_IN_VARIATION, 'user2', 'variationWithAudience']); - // }); + const experiment = config.experimentIdMap['122227']; - // it('should return null if the user does not meet audience conditions', function() { - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'user3' - // }); - // experiment = configObj.experimentIdMap['122227']; - // assert.isNull( - // decisionServiceInstance.getVariation(configObj, experiment, user, { foo: 'bar' }).result - // ); + const { decisionService, logger } = getDecisionService({ logger: true }); + + const variation = decisionService.getVariation(config, experiment, user); - // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user3']); + expect(variation.result).toBe('variationWithAudience'); + expect(mockBucket).not.toHaveBeenCalled(); + expect(logger?.debug).toHaveBeenCalledTimes(1); + expect(logger?.info).toHaveBeenCalledTimes(1); - // assert.deepEqual(mockLogger.debug.args[1], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])]); + expect(logger?.debug).toHaveBeenNthCalledWith(1, USER_HAS_NO_FORCED_VARIATION, 'user2'); + expect(logger?.info).toHaveBeenNthCalledWith(1, USER_FORCED_IN_VARIATION, 'user2', 'variationWithAudience'); + }); - // assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE']); + it('should return null if the user does not meet audience conditions', () => { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'user2' + }); - // assert.deepEqual(mockLogger.info.args[1], [USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences']); - // }); + const config = createProjectConfig(cloneDeep(testData)); + + const experiment = config.experimentIdMap['122227']; + + const { decisionService, logger } = getDecisionService({ logger: true }); + + const variation = decisionService.getVariation(config, experiment, user); + + expect(variation.result).toBe('variationWithAudience'); + expect(mockBucket).not.toHaveBeenCalled(); + expect(logger?.debug).toHaveBeenCalledTimes(1); + expect(logger?.info).toHaveBeenCalledTimes(1); + + expect(logger?.debug).toHaveBeenNthCalledWith(1, USER_HAS_NO_FORCED_VARIATION, 'user2'); + expect(logger?.info).toHaveBeenNthCalledWith(1, USER_FORCED_IN_VARIATION, 'user2', 'variationWithAudience'); + + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user3' + }); + experiment = configObj.experimentIdMap['122227']; + assert.isNull( + decisionServiceInstance.getVariation(configObj, experiment, user, { foo: 'bar' }).result + ); + + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user3']); + + assert.deepEqual(mockLogger.debug.args[1], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])]); + + assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE']); + + assert.deepEqual(mockLogger.info.args[1], [USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences']); + }); // it('should return null if the experiment is not running', function() { // user = new OptimizelyUserContext({ diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts index 85b888da3..a5fa83196 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -170,11 +170,14 @@ export class DecisionService { ): DecisionResponse { const userId = user.getUserId(); const attributes = user.getAttributes(); + // by default, the bucketing ID should be the user ID const bucketingId = this.getBucketingId(userId, attributes); - const decideReasons: (string | number)[][] = []; const experimentKey = experiment.key; - if (!this.checkIfExperimentIsActive(configObj, experimentKey)) { + + const decideReasons: (string | number)[][] = []; + + if (!isActive(configObj, experimentKey)) { this.logger?.info(EXPERIMENT_NOT_RUNNING, experimentKey); decideReasons.push([EXPERIMENT_NOT_RUNNING, experimentKey]); return { @@ -182,6 +185,7 @@ export class DecisionService { reasons: decideReasons, }; } + const decisionForcedVariation = this.getForcedVariation(configObj, experimentKey, userId); decideReasons.push(...decisionForcedVariation.reasons); const forcedVariationKey = decisionForcedVariation.result; @@ -192,6 +196,7 @@ export class DecisionService { reasons: decideReasons, }; } + const decisionWhitelistedVariation = this.getWhitelistedVariation(experiment, userId); decideReasons.push(...decisionWhitelistedVariation.reasons); let variation = decisionWhitelistedVariation.result; @@ -202,7 +207,6 @@ export class DecisionService { }; } - // check for sticky bucketing if decide options do not include shouldIgnoreUPS if (!shouldIgnoreUPS) { variation = this.getStoredVariation(configObj, experiment, userId, userProfileTracker.userProfile); @@ -349,16 +353,6 @@ export class DecisionService { return { ...userProfile.experiment_bucket_map, ...attributeExperimentBucketMap as any }; } - /** - * Checks whether the experiment is running - * @param {ProjectConfig} configObj The parsed project configuration object - * @param {string} experimentKey Key of experiment being validated - * @return {boolean} True if experiment is running - */ - private checkIfExperimentIsActive(configObj: ProjectConfig, experimentKey: string): boolean { - return isActive(configObj, experimentKey); - } - /** * Checks if user is whitelisted into any variation and return that variation if so * @param {Experiment} experiment From 0de767e18043326af61f7e94e2befeb26a9e4abc Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Tue, 4 Mar 2025 00:34:33 +0600 Subject: [PATCH 03/15] up --- lib/core/decision_service/index.spec.ts | 29 +++++-------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index 12d4fbb13..677ae21fb 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -186,7 +186,7 @@ describe('DecisionService', () => { it('should return null if the user does not meet audience conditions', () => { const user = new OptimizelyUserContext({ optimizely: {} as any, - userId: 'user2' + userId: 'user3' }); const config = createProjectConfig(cloneDeep(testData)); @@ -197,31 +197,14 @@ describe('DecisionService', () => { const variation = decisionService.getVariation(config, experiment, user); - expect(variation.result).toBe('variationWithAudience'); - expect(mockBucket).not.toHaveBeenCalled(); - expect(logger?.debug).toHaveBeenCalledTimes(1); - expect(logger?.info).toHaveBeenCalledTimes(1); - - expect(logger?.debug).toHaveBeenNthCalledWith(1, USER_HAS_NO_FORCED_VARIATION, 'user2'); - expect(logger?.info).toHaveBeenNthCalledWith(1, USER_FORCED_IN_VARIATION, 'user2', 'variationWithAudience'); - - user = new OptimizelyUserContext({ - shouldIdentifyUser: false, - optimizely: {}, - userId: 'user3' - }); - experiment = configObj.experimentIdMap['122227']; - assert.isNull( - decisionServiceInstance.getVariation(configObj, experiment, user, { foo: 'bar' }).result - ); - - assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user3']); + expect(variation.result).toBe(null); - assert.deepEqual(mockLogger.debug.args[1], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])]); - assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE']); + expect(logger?.debug).toHaveBeenNthCalledWith(1, USER_HAS_NO_FORCED_VARIATION, 'user3'); + expect(logger?.debug).toHaveBeenNthCalledWith(2, EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])); - assert.deepEqual(mockLogger.info.args[1], [USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences']); + expect(logger?.info).toHaveBeenNthCalledWith(1, AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE'); + expect(logger?.info).toHaveBeenNthCalledWith(2, USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences'); }); // it('should return null if the experiment is not running', function() { From b64f752fefc353120086174ff5235cd756cfcc2b Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Tue, 4 Mar 2025 19:46:40 +0600 Subject: [PATCH 04/15] getVariation tests --- lib/core/decision_service/index.spec.ts | 968 +++++++++++++----------- 1 file changed, 514 insertions(+), 454 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index 677ae21fb..afaa4575e 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -47,11 +47,13 @@ import { USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, } from '../decision_service/index'; +import { BUCKETING_ID_NOT_STRING, USER_PROFILE_LOOKUP_ERROR, USER_PROFILE_SAVE_ERROR } from 'error_message'; + type MockLogger = ReturnType; type MockUserProfileService = { - lookup: typeof vi.fn; - save: typeof vi.fn; + lookup: ReturnType; + save: ReturnType; }; type DecisionServiceInstanceOpt = { @@ -198,7 +200,7 @@ describe('DecisionService', () => { const variation = decisionService.getVariation(config, experiment, user); expect(variation.result).toBe(null); - + expect(mockBucket).not.toHaveBeenCalled(); expect(logger?.debug).toHaveBeenNthCalledWith(1, USER_HAS_NO_FORCED_VARIATION, 'user3'); expect(logger?.debug).toHaveBeenNthCalledWith(2, EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])); @@ -207,458 +209,516 @@ describe('DecisionService', () => { expect(logger?.info).toHaveBeenNthCalledWith(2, USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences'); }); - // it('should return null if the experiment is not running', function() { - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'user1' - // }); - // experiment = configObj.experimentIdMap['133337']; - // assert.isNull(decisionServiceInstance.getVariation(configObj, experiment, user).result); - // sinon.assert.notCalled(bucketerStub); - // assert.strictEqual(1, mockLogger.info.callCount); - - // assert.deepEqual(mockLogger.info.args[0], [EXPERIMENT_NOT_RUNNING, 'testExperimentNotRunning']); - // }); - - // describe('when attributes.$opt_experiment_bucket_map is supplied', function() { - // it('should respect the sticky bucketing information for attributes', function() { - // var fakeDecisionResponse = { - // result: '111128', - // reasons: [], - // }; - // experiment = configObj.experimentIdMap['111127']; - // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation from `test_data` - // var attributes = { - // $opt_experiment_bucket_map: { - // '111127': { - // variation_id: '111129', // ID of the 'variation' variation - // }, - // }, - // }; - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // attributes, - // }); - - // assert.strictEqual( - // 'variation', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.notCalled(bucketerStub); - // }); - // }); - - // describe('when a user profile service is provided', function() { - // var fakeDecisionResponse = { - // result: '111128', - // reasons: [], - // }; - // var userProfileServiceInstance = null; - // var userProfileLookupStub; - // var userProfileSaveStub; - // var fakeDecisionWhitelistedVariation = { - // result: null, - // reasons: [], - // } - // beforeEach(function() { - // userProfileServiceInstance = { - // lookup: function() {}, - // save: function() {}, - // }; - - // decisionServiceInstance = createDecisionService({ - // logger: mockLogger, - // userProfileService: userProfileServiceInstance, - // }); - // userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup'); - // userProfileSaveStub = sinon.stub(userProfileServiceInstance, 'save'); - // sinon.stub(decisionServiceInstance, 'getWhitelistedVariation').returns(fakeDecisionWhitelistedVariation); - // }); - - // afterEach(function() { - // userProfileServiceInstance.lookup.restore(); - // userProfileServiceInstance.save.restore(); - // decisionServiceInstance.getWhitelistedVariation.restore(); - // }); - - // it('should return the previously bucketed variation', function() { - // userProfileLookupStub.returns({ - // user_id: 'decision_service_user', - // experiment_bucket_map: { - // '111127': { - // variation_id: '111128', // ID of the 'control' variation - // }, - // }, - // }); - // experiment = configObj.experimentIdMap['111127']; - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // }); - - // assert.strictEqual( - // 'control', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - // sinon.assert.notCalled(bucketerStub); - - // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); - - // assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'control', 'testExperiment', 'decision_service_user']); - // }); - - // it('should bucket if there was no prevously bucketed variation', function() { - // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation - // userProfileLookupStub.returns({ - // user_id: 'decision_service_user', - // experiment_bucket_map: {}, - // }); - // experiment = configObj.experimentIdMap['111127']; - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // }); - - // assert.strictEqual( - // 'control', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - // sinon.assert.calledOnce(bucketerStub); - // // make sure we save the decision - // sinon.assert.calledWith(userProfileSaveStub, { - // user_id: 'decision_service_user', - // experiment_bucket_map: { - // '111127': { - // variation_id: '111128', - // }, - // }, - // }); - // }); - - // it('should bucket if the user profile service returns null', function() { - // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation - // userProfileLookupStub.returns(null); - // experiment = configObj.experimentIdMap['111127']; - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // }); - // assert.strictEqual( - // 'control', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - // sinon.assert.calledOnce(bucketerStub); - // // make sure we save the decision - // sinon.assert.calledWith(userProfileSaveStub, { - // user_id: 'decision_service_user', - // experiment_bucket_map: { - // '111127': { - // variation_id: '111128', - // }, - // }, - // }); - // }); - - // it('should re-bucket if the stored variation is no longer valid', function() { - // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation - // userProfileLookupStub.returns({ - // user_id: 'decision_service_user', - // experiment_bucket_map: { - // '111127': { - // variation_id: 'not valid variation', - // }, - // }, - // }); - // experiment = configObj.experimentIdMap['111127']; - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // }); - // assert.strictEqual( - // 'control', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - // sinon.assert.calledOnce(bucketerStub); - // // assert.strictEqual( - // // buildLogMessageFromArgs(mockLogger.log.args[0]), - // // 'DECISION_SERVICE: User decision_service_user is not in the forced variation map.' - // // ); - // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); - - // sinon.assert.calledWith( - // mockLogger.info, - // SAVED_VARIATION_NOT_FOUND, - // 'decision_service_user', - // 'not valid variation', - // 'testExperiment' - // ); - - // // make sure we save the decision - // sinon.assert.calledWith(userProfileSaveStub, { - // user_id: 'decision_service_user', - // experiment_bucket_map: { - // '111127': { - // variation_id: '111128', - // }, - // }, - // }); - // }); - - // it('should store the bucketed variation for the user', function() { - // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation - // userProfileLookupStub.returns({ - // user_id: 'decision_service_user', - // experiment_bucket_map: {}, // no decisions for user - // }); - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // }); - // experiment = configObj.experimentIdMap['111127']; - - // assert.strictEqual( - // 'control', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - // sinon.assert.calledOnce(bucketerStub); - - // sinon.assert.calledWith(userProfileServiceInstance.save, { - // user_id: 'decision_service_user', - // experiment_bucket_map: { - // '111127': { - // variation_id: '111128', - // }, - // }, - // }); - - - // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); - - // assert.deepEqual(mockLogger.info.lastCall.args, [SAVED_USER_VARIATION, 'decision_service_user']); - // }); - - // it('should log an error message if "lookup" throws an error', function() { - // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation - // userProfileLookupStub.throws(new Error('I am an error')); - // experiment = configObj.experimentIdMap['111127']; - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // }); - - // assert.strictEqual( - // 'control', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - // sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing - - // assert.deepEqual(mockLogger.error.args[0], [USER_PROFILE_LOOKUP_ERROR, 'decision_service_user', 'I am an error']); - - // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); - // }); - - // it('should log an error message if "save" throws an error', function() { - // bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation - // userProfileLookupStub.returns(null); - // userProfileSaveStub.throws(new Error('I am an error')); - // experiment = configObj.experimentIdMap['111127']; - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // }); - // assert.strictEqual( - // 'control', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - // sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing - - - // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); - - // assert.deepEqual(mockLogger.error.args[0], [USER_PROFILE_SAVE_ERROR, 'decision_service_user', 'I am an error']); - - // // make sure that we save the decision - // sinon.assert.calledWith(userProfileSaveStub, { - // user_id: 'decision_service_user', - // experiment_bucket_map: { - // '111127': { - // variation_id: '111128', - // }, - // }, - // }); - // }); + it('should return null if the experiment is not running', function() { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'user1' + }); + + const config = createProjectConfig(cloneDeep(testData)); + + const experiment = config.experimentIdMap['133337']; + + const { decisionService, logger } = getDecisionService({ logger: true }); + + const variation = decisionService.getVariation(config, experiment, user); + + expect(variation.result).toBe(null); + expect(mockBucket).not.toHaveBeenCalled(); + expect(logger?.info).toHaveBeenCalledTimes(1); + expect(logger?.info).toHaveBeenNthCalledWith(1, EXPERIMENT_NOT_RUNNING, 'testExperimentNotRunning'); + }); + + it('should respect the sticky bucketing information for attributes when attributes.$opt_experiment_bucket_map is supplied', () => { + const fakeDecisionResponse = { + result: '111128', + reasons: [], + }; + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const attributes: any = { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + attributes, + }); + + mockBucket.mockReturnValue(fakeDecisionResponse); // contains variation ID of the 'control' variation from `test_data` + + const { decisionService } = getDecisionService(); + + const variation = decisionService.getVariation(config, experiment, user); + + expect(variation.result).toBe('variation'); + expect(mockBucket).not.toHaveBeenCalled(); + }); + + describe('when a user profile service is provided', function() { + // var fakeDecisionResponse = { + // result: '111128', + // reasons: [], + // }; + // var userProfileServiceInstance = null; + // var userProfileLookupStub; + // var userProfileSaveStub; + // var fakeDecisionWhitelistedVariation = { + // result: null, + // reasons: [], + // } + // beforeEach(function() { + // userProfileServiceInstance = { + // lookup: function() {}, + // save: function() {}, + // }; + + // decisionServiceInstance = createDecisionService({ + // logger: mockLogger, + // userProfileService: userProfileServiceInstance, + // }); + // userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup'); + // userProfileSaveStub = sinon.stub(userProfileServiceInstance, 'save'); + // sinon.stub(decisionServiceInstance, 'getWhitelistedVariation').returns(fakeDecisionWhitelistedVariation); + // }); + + // afterEach(function() { + // userProfileServiceInstance.lookup.restore(); + // userProfileServiceInstance.save.restore(); + // decisionServiceInstance.getWhitelistedVariation.restore(); + // }); + + beforeEach(() => { + mockBucket.mockClear(); + }); + + it('should return the previously bucketed variation', () => { + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }); + + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('variation'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + expect(mockBucket).not.toHaveBeenCalled(); + + expect(logger?.debug).toHaveBeenCalledTimes(1); + expect(logger?.info).toHaveBeenCalledTimes(1); + + expect(logger?.debug).toHaveBeenNthCalledWith(1, USER_HAS_NO_FORCED_VARIATION, 'decision_service_user'); + expect(logger?.info).toHaveBeenNthCalledWith(1, RETURNING_STORED_VARIATION, 'variation', 'testExperiment', 'decision_service_user'); + }); + + it('should bucket and save user profile if there was no prevously bucketed variation', function() { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: {}, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + it('should bucket if the user profile service returns null', function() { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue(null); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + it('should re-bucket if the stored variation is no longer valid', function() { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: 'not valid variation', + }, + }, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(logger?.debug).toHaveBeenCalledWith(USER_HAS_NO_FORCED_VARIATION, 'decision_service_user'); + expect(logger?.info).toHaveBeenCalledWith(SAVED_VARIATION_NOT_FOUND, 'decision_service_user', 'not valid variation', 'testExperiment'); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + it('should store the bucketed variation for the user', function() { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: {}, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + + it('should log an error message and bucket if "lookup" throws an error', () => { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockImplementation(() => { + throw new Error('I am an error'); + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(logger?.debug).toHaveBeenCalledWith(USER_HAS_NO_FORCED_VARIATION, 'decision_service_user'); + expect(logger?.error).toHaveBeenCalledWith(USER_PROFILE_LOOKUP_ERROR, 'decision_service_user', 'I am an error'); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + it('should log an error message if "save" throws an error', () => { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue(null); + userProfileService?.save.mockImplementation(() => { + throw new Error('I am an error'); + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + + expect(logger?.error).toHaveBeenCalledWith(USER_PROFILE_SAVE_ERROR, 'decision_service_user', 'I am an error'); + }); + // describe('when passing `attributes.$opt_experiment_bucket_map`', function() { - // it('should respect attributes over the userProfileService for the matching experiment id', function() { - // userProfileLookupStub.returns({ - // user_id: 'decision_service_user', - // experiment_bucket_map: { - // '111127': { - // variation_id: '111128', // ID of the 'control' variation - // }, - // }, - // }); - - // var attributes = { - // $opt_experiment_bucket_map: { - // '111127': { - // variation_id: '111129', // ID of the 'variation' variation - // }, - // }, - // }; - - // experiment = configObj.experimentIdMap['111127']; - - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // attributes, - // }); - - // assert.strictEqual( - // 'variation', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - // sinon.assert.notCalled(bucketerStub); - - // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); - - // assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'variation', 'testExperiment', 'decision_service_user']); - // }); - - // it('should ignore attributes for a different experiment id', function() { - // userProfileLookupStub.returns({ - // user_id: 'decision_service_user', - // experiment_bucket_map: { - // '111127': { - // // 'testExperiment' ID - // variation_id: '111128', // ID of the 'control' variation - // }, - // }, - // }); - - // experiment = configObj.experimentIdMap['111127']; - - // var attributes = { - // $opt_experiment_bucket_map: { - // '122227': { - // // other experiment ID - // variation_id: '122229', // ID of the 'variationWithAudience' variation - // }, - // }, - // }; - - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // attributes, - // }); - - // assert.strictEqual( - // 'control', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - // sinon.assert.notCalled(bucketerStub); - - // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); - - // assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'control', 'testExperiment', 'decision_service_user']); - // }); - - // it('should use attributes when the userProfileLookup variations for other experiments', function() { - // userProfileLookupStub.returns({ - // user_id: 'decision_service_user', - // experiment_bucket_map: { - // '122227': { - // // other experiment ID - // variation_id: '122229', // ID of the 'variationWithAudience' variation - // }, - // }, - // }); - - // experiment = configObj.experimentIdMap['111127']; - - // var attributes = { - // $opt_experiment_bucket_map: { - // '111127': { - // // 'testExperiment' ID - // variation_id: '111129', // ID of the 'variation' variation - // }, - // }, - // }; - - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // attributes, - // }); - - // assert.strictEqual( - // 'variation', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - // sinon.assert.notCalled(bucketerStub); - - // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); - - // assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'variation', 'testExperiment', 'decision_service_user']); - // }); - - // it('should use attributes when the userProfileLookup returns null', function() { - // userProfileLookupStub.returns(null); - - // experiment = configObj.experimentIdMap['111127']; - - // var attributes = { - // $opt_experiment_bucket_map: { - // '111127': { - // variation_id: '111129', // ID of the 'variation' variation - // }, - // }, - // }; - - // user = new OptimizelyUserContext({ - // shouldIdentifyUser: false, - // optimizely: {}, - // userId: 'decision_service_user', - // attributes, - // }); - - // assert.strictEqual( - // 'variation', - // decisionServiceInstance.getVariation(configObj, experiment, user).result - // ); - // sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - // sinon.assert.notCalled(bucketerStub); - - // assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); - - // assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'variation', 'testExperiment', 'decision_service_user']); - // }); - // }); - // }); + it('should respect $opt_experiment_bucket_map attribute over the userProfileService for the matching experiment id', function() { + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', // ID of the 'control' variation + }, + }, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const attributes: any = { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + attributes, + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('variation'); + }); + + it('should ignore attributes for a different experiment id', function() { + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', // ID of the 'control' variation + }, + }, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const attributes: any = { + $opt_experiment_bucket_map: { + '122227': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + attributes, + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + }); + + it('should use $ opt_experiment_bucket_map attribute when the userProfile contains variations for other experiments', function() { + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '122227': { + variation_id: '122229', // ID of the 'variationWithAudience' variation + }, + }, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const attributes: any = { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + attributes, + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('variation'); + }); + + it('should use attributes when the userProfileLookup returns null', function() { + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue(null); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const attributes: any = { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + attributes, + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('variation'); + }); + }); }); }); From f9a0307506fb037890e58b92b9d4f5c1869803ec Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 5 Mar 2025 01:09:43 +0600 Subject: [PATCH 05/15] save --- lib/core/decision_service/index.spec.ts | 39 +++--------------------- lib/core/decision_service/index.tests.js | 18 +++++------ lib/optimizely/index.tests.js | 2 +- 3 files changed, 15 insertions(+), 44 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index afaa4575e..cdfd6438f 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -263,38 +263,6 @@ describe('DecisionService', () => { }); describe('when a user profile service is provided', function() { - // var fakeDecisionResponse = { - // result: '111128', - // reasons: [], - // }; - // var userProfileServiceInstance = null; - // var userProfileLookupStub; - // var userProfileSaveStub; - // var fakeDecisionWhitelistedVariation = { - // result: null, - // reasons: [], - // } - // beforeEach(function() { - // userProfileServiceInstance = { - // lookup: function() {}, - // save: function() {}, - // }; - - // decisionServiceInstance = createDecisionService({ - // logger: mockLogger, - // userProfileService: userProfileServiceInstance, - // }); - // userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup'); - // userProfileSaveStub = sinon.stub(userProfileServiceInstance, 'save'); - // sinon.stub(decisionServiceInstance, 'getWhitelistedVariation').returns(fakeDecisionWhitelistedVariation); - // }); - - // afterEach(function() { - // userProfileServiceInstance.lookup.restore(); - // userProfileServiceInstance.save.restore(); - // decisionServiceInstance.getWhitelistedVariation.restore(); - // }); - beforeEach(() => { mockBucket.mockClear(); }); @@ -593,8 +561,6 @@ describe('DecisionService', () => { expect(logger?.error).toHaveBeenCalledWith(USER_PROFILE_SAVE_ERROR, 'decision_service_user', 'I am an error'); }); - - // describe('when passing `attributes.$opt_experiment_bucket_map`', function() { it('should respect $opt_experiment_bucket_map attribute over the userProfileService for the matching experiment id', function() { const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); @@ -721,4 +687,9 @@ describe('DecisionService', () => { }); }); }); + + const featureTestData + describe('getVariationForFeature', () => { + + }); }); diff --git a/lib/core/decision_service/index.tests.js b/lib/core/decision_service/index.tests.js index b723d118b..e6d34a4da 100644 --- a/lib/core/decision_service/index.tests.js +++ b/lib/core/decision_service/index.tests.js @@ -658,15 +658,15 @@ describe('lib/core/decision_service', function() { }); }); - describe('checkIfExperimentIsActive', function() { - it('should return true if experiment is running', function() { - assert.isTrue(decisionServiceInstance.checkIfExperimentIsActive(configObj, 'testExperiment')); - }); - - it('should return false when experiment is not running', function() { - assert.isFalse(decisionServiceInstance.checkIfExperimentIsActive(configObj, 'testExperimentNotRunning')); - }); - }); + // describe('checkIfExperimentIsActive', function() { + // it('should return true if experiment is running', function() { + // assert.isTrue(decisionServiceInstance.checkIfExperimentIsActive(configObj, 'testExperiment')); + // }); + + // it('should return false when experiment is not running', function() { + // assert.isFalse(decisionServiceInstance.checkIfExperimentIsActive(configObj, 'testExperimentNotRunning')); + // }); + // }); describe('checkIfUserIsInAudience', function() { var __audienceEvaluateSpy; diff --git a/lib/optimizely/index.tests.js b/lib/optimizely/index.tests.js index c4efb2c67..cd74a2d00 100644 --- a/lib/optimizely/index.tests.js +++ b/lib/optimizely/index.tests.js @@ -262,7 +262,7 @@ describe('lib/optimizely', function() { }); sinon.assert.calledWith(decisionService.createDecisionService, { - userProfileService: null, + userProfileService: undefined, logger: createdLogger, UNSTABLE_conditionEvaluators: undefined, }); From a15a0a0b3f7ba70cb515777ab74e5d1c829a794f Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 5 Mar 2025 18:00:52 +0600 Subject: [PATCH 06/15] cleanup --- lib/core/decision_service/index.spec.ts | 8 +- lib/core/decision_service/index.tests.js | 708 +---------------------- 2 files changed, 27 insertions(+), 689 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index cdfd6438f..182e1d1ba 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -93,9 +93,6 @@ vi.mock('../bucketer', () => ({ bucket: mockBucket, })); -const testGetVariationWithoutUserProfileService = (decisonService: DecisionServiceInstance) => { - -} const cloneDeep = (d: any) => JSON.parse(JSON.stringify(d)); const testData = getTestProjectConfig(); @@ -688,8 +685,7 @@ describe('DecisionService', () => { }); }); - const featureTestData - describe('getVariationForFeature', () => { + // describe('getVariationForFeature', () => { - }); + // }); }); diff --git a/lib/core/decision_service/index.tests.js b/lib/core/decision_service/index.tests.js index e6d34a4da..a4616483b 100644 --- a/lib/core/decision_service/index.tests.js +++ b/lib/core/decision_service/index.tests.js @@ -1269,214 +1269,12 @@ describe('lib/core/decision_service', function() { it('returns a decision with a variation in the experiment the feature is attached to', function() { var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; - var expectedDecision = { - experiment: { - forcedVariations: {}, - status: 'Running', - key: 'testing_my_feature', - id: '594098', - variations: [ - { - id: '594096', - variables: [ - { - id: '4792309476491264', - value: '2', - }, - { - id: '5073784453201920', - value: 'true', - }, - { - id: '5636734406623232', - value: 'Buy me NOW', - }, - { - id: '6199684360044544', - value: '20.25', - }, - { - id: '1547854156498475', - value: '{ "num_buttons": 1, "text": "first variation"}', - }, - ], - featureEnabled: true, - key: 'variation', - }, - { - id: '594097', - variables: [ - { - id: '4792309476491264', - value: '10', - }, - { - id: '5073784453201920', - value: 'false', - }, - { - id: '5636734406623232', - value: 'Buy me', - }, - { - id: '6199684360044544', - value: '50.55', - }, - { - id: '1547854156498475', - value: '{ "num_buttons": 2, "text": "second variation"}', - }, - ], - featureEnabled: true, - key: 'control', - }, - { - id: '594099', - variables: [ - { - id: '4792309476491264', - value: '40', - }, - { - id: '5073784453201920', - value: 'true', - }, - { - id: '5636734406623232', - value: 'Buy me Later', - }, - { - id: '6199684360044544', - value: '99.99', - }, - { - id: '1547854156498475', - value: '{ "num_buttons": 3, "text": "third variation"}', - }, - ], - featureEnabled: false, - key: 'variation2', - }, - ], - audienceIds: [], - trafficAllocation: [ - { endOfRange: 5000, entityId: '594096' }, - { endOfRange: 10000, entityId: '594097' }, - ], - layerId: '594093', - variationKeyMap: { - control: { - id: '594097', - variables: [ - { - id: '4792309476491264', - value: '10', - }, - { - id: '5073784453201920', - value: 'false', - }, - { - id: '5636734406623232', - value: 'Buy me', - }, - { - id: '6199684360044544', - value: '50.55', - }, - { - id: '1547854156498475', - value: '{ "num_buttons": 2, "text": "second variation"}', - }, - ], - featureEnabled: true, - key: 'control', - }, - variation: { - id: '594096', - variables: [ - { - id: '4792309476491264', - value: '2', - }, - { - id: '5073784453201920', - value: 'true', - }, - { - id: '5636734406623232', - value: 'Buy me NOW', - }, - { - id: '6199684360044544', - value: '20.25', - }, - { - id: '1547854156498475', - value: '{ "num_buttons": 1, "text": "first variation"}', - }, - ], - featureEnabled: true, - key: 'variation', - }, - variation2: { - id: '594099', - variables: [ - { - id: '4792309476491264', - value: '40', - }, - { - id: '5073784453201920', - value: 'true', - }, - { - id: '5636734406623232', - value: 'Buy me Later', - }, - { - id: '6199684360044544', - value: '99.99', - }, - { - id: '1547854156498475', - value: '{ "num_buttons": 3, "text": "third variation"}', - }, - ], - featureEnabled: false, - key: 'variation2', - }, - }, - }, - variation: { - id: '594096', - variables: [ - { - id: '4792309476491264', - value: '2', - }, - { - id: '5073784453201920', - value: 'true', - }, - { - id: '5636734406623232', - value: 'Buy me NOW', - }, - { - id: '6199684360044544', - value: '20.25', - }, - { - id: '1547854156498475', - value: '{ "num_buttons": 1, "text": "first variation"}', - }, - ], - featureEnabled: true, - key: 'variation', - }, + const expectedDecision = { + experiment: configObj.experimentIdMap['594098'], + variation: configObj.variationIdMap['594096'], decisionSource: DECISION_SOURCES.FEATURE_TEST, }; + assert.deepEqual(decision, expectedDecision); sinon.assert.calledWith( getVariationStub, @@ -1540,42 +1338,11 @@ describe('lib/core/decision_service', function() { it('returns a decision with a variation in an experiment in a group', function() { var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; var expectedDecision = { - experiment: { - forcedVariations: {}, - status: 'Running', - key: 'exp_with_group', - id: '595010', - variations: [ - { id: '595008', variables: [], key: 'var' }, - { id: '595009', variables: [], key: 'con' }, - ], - audienceIds: [], - trafficAllocation: [ - { endOfRange: 5000, entityId: '595008' }, - { endOfRange: 10000, entityId: '595009' }, - ], - layerId: '595005', - groupId: '595024', - variationKeyMap: { - con: { - id: '595009', - variables: [], - key: 'con', - }, - var: { - id: '595008', - variables: [], - key: 'var', - }, - }, - }, - variation: { - id: '595008', - variables: [], - key: 'var', - }, + experiment: configObj.experimentIdMap['595010'], + variation: configObj.variationIdMap['595008'], decisionSource: DECISION_SOURCES.FEATURE_TEST, - }; + }; + assert.deepEqual(decision, expectedDecision); }); }); @@ -1649,103 +1416,11 @@ describe('lib/core/decision_service', function() { }); var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; var expectedDecision = { - experiment: { - forcedVariations: {}, - status: 'Not started', - key: '594031', - id: '594031', - isRollout: true, - variations: [ - { - id: '594032', - variables: [ - { - id: '4919852825313280', - value: 'true', - }, - { - id: '5482802778734592', - value: '395', - }, - { - id: '6045752732155904', - value: '4.99', - }, - { - id: '6327227708866560', - value: 'Hello audience', - }, - { - id: "8765345281230956", - value: '{ "count": 2, "message": "Hello audience" }', - }, - ], - featureEnabled: true, - key: '594032', - }, - ], - variationKeyMap: { - 594032: { - id: '594032', - variables: [ - { - id: '4919852825313280', - value: 'true', - }, - { - id: '5482802778734592', - value: '395', - }, - { - id: '6045752732155904', - value: '4.99', - }, - { - id: '6327227708866560', - value: 'Hello audience', - }, - { - id: "8765345281230956", - value: '{ "count": 2, "message": "Hello audience" }', - }, - ], - featureEnabled: true, - key: '594032', - }, - }, - audienceIds: ['594017'], - trafficAllocation: [{ endOfRange: 5000, entityId: '594032' }], - layerId: '594030', - }, - variation: { - id: '594032', - variables: [ - { - id: '4919852825313280', - value: 'true', - }, - { - id: '5482802778734592', - value: '395', - }, - { - id: '6045752732155904', - value: '4.99', - }, - { - id: '6327227708866560', - value: 'Hello audience', - }, - { - id: "8765345281230956", - value: '{ "count": 2, "message": "Hello audience" }', - }, - ], - featureEnabled: true, - key: '594032', - }, + experiment: configObj.experimentIdMap['594031'], + variation: configObj.variationIdMap['594032'], decisionSource: DECISION_SOURCES.ROLLOUT, }; + assert.deepEqual(decision, expectedDecision); sinon.assert.calledWithExactly( mockLogger.debug, @@ -1784,103 +1459,11 @@ describe('lib/core/decision_service', function() { }); var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; var expectedDecision = { - experiment: { - forcedVariations: {}, - status: 'Not started', - key: '594037', - id: '594037', - isRollout: true, - variations: [ - { - id: '594038', - variables: [ - { - id: '4919852825313280', - value: 'false', - }, - { - id: '5482802778734592', - value: '400', - }, - { - id: '6045752732155904', - value: '14.99', - }, - { - id: '6327227708866560', - value: 'Hello', - }, - { - id: '8765345281230956', - value: '{ "count": 1, "message": "Hello" }', - }, - ], - featureEnabled: false, - key: '594038', - }, - ], - audienceIds: [], - trafficAllocation: [{ endOfRange: 0, entityId: '594038' }], - layerId: '594030', - variationKeyMap: { - 594038: { - id: '594038', - variables: [ - { - id: '4919852825313280', - value: 'false', - }, - { - id: '5482802778734592', - value: '400', - }, - { - id: '6045752732155904', - value: '14.99', - }, - { - id: '6327227708866560', - value: 'Hello', - }, - { - id: '8765345281230956', - value: '{ "count": 1, "message": "Hello" }', - }, - ], - featureEnabled: false, - key: '594038', - }, - }, - }, - variation: { - id: '594038', - variables: [ - { - id: '4919852825313280', - value: 'false', - }, - { - id: '5482802778734592', - value: '400', - }, - { - id: '6045752732155904', - value: '14.99', - }, - { - id: '6327227708866560', - value: 'Hello', - }, - { - id: '8765345281230956', - value: '{ "count": 1, "message": "Hello" }', - }, - ], - featureEnabled: false, - key: '594038', - }, + experiment: configObj.experimentIdMap['594037'], + variation: configObj.variationIdMap['594038'], decisionSource: DECISION_SOURCES.ROLLOUT, - }; + }; + assert.deepEqual(decision, expectedDecision); sinon.assert.calledWithExactly( mockLogger.debug, @@ -1964,103 +1547,11 @@ describe('lib/core/decision_service', function() { }); var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; var expectedDecision = { - experiment: { - forcedVariations: {}, - status: 'Not started', - key: '594037', - id: '594037', - isRollout: true, - variations: [ - { - id: '594038', - variables: [ - { - id: '4919852825313280', - value: 'false', - }, - { - id: '5482802778734592', - value: '400', - }, - { - id: '6045752732155904', - value: '14.99', - }, - { - id: '6327227708866560', - value: 'Hello', - }, - { - id: '8765345281230956', - value: '{ "count": 1, "message": "Hello" }', - }, - ], - featureEnabled: false, - key: '594038', - }, - ], - audienceIds: [], - trafficAllocation: [{ endOfRange: 0, entityId: '594038' }], - layerId: '594030', - variationKeyMap: { - 594038: { - id: '594038', - variables: [ - { - id: '4919852825313280', - value: 'false', - }, - { - id: '5482802778734592', - value: '400', - }, - { - id: '6045752732155904', - value: '14.99', - }, - { - id: '6327227708866560', - value: 'Hello', - }, - { - id: '8765345281230956', - value: '{ "count": 1, "message": "Hello" }', - }, - ], - featureEnabled: false, - key: '594038', - }, - }, - }, - variation: { - id: '594038', - variables: [ - { - id: '4919852825313280', - value: 'false', - }, - { - id: '5482802778734592', - value: '400', - }, - { - id: '6045752732155904', - value: '14.99', - }, - { - id: '6327227708866560', - value: 'Hello', - }, - { - id: '8765345281230956', - value: '{ "count": 1, "message": "Hello" }', - }, - ], - featureEnabled: false, - key: '594038', - }, + experiment: configObj.experimentIdMap['594037'], + variation: configObj.variationIdMap['594038'], decisionSource: DECISION_SOURCES.ROLLOUT, }; + assert.deepEqual(decision, expectedDecision); sinon.assert.calledWithExactly( mockLogger.debug, @@ -2111,72 +1602,11 @@ describe('lib/core/decision_service', function() { }); var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; var expectedDecision = { - experiment: { - trafficAllocation: [ - { - endOfRange: 10000, - entityId: '599057', - }, - ], - layerId: '599055', - forcedVariations: {}, - audienceIds: [], - isRollout: true, - variations: [ - { - key: '599057', - id: '599057', - featureEnabled: true, - variables: [ - { - id: '4937719889264640', - value: '200', - }, - { - id: '6345094772817920', - value: "i'm a rollout", - }, - ], - }, - ], - status: 'Not started', - key: '599056', - id: '599056', - variationKeyMap: { - 599057: { - key: '599057', - id: '599057', - featureEnabled: true, - variables: [ - { - id: '4937719889264640', - value: '200', - }, - { - id: '6345094772817920', - value: "i'm a rollout", - }, - ], - }, - }, - }, - variation: { - key: '599057', - id: '599057', - featureEnabled: true, - variables: [ - { - id: '4937719889264640', - value: '200', - }, - { - id: '6345094772817920', - value: "i'm a rollout", - }, - ], - }, + experiment: configObj.experimentIdMap['599056'], + variation: configObj.variationIdMap['599057'], decisionSource: DECISION_SOURCES.ROLLOUT, }; + assert.deepEqual(decision, expectedDecision); sinon.assert.calledWithExactly( mockLogger.debug, @@ -2324,29 +1754,7 @@ describe('lib/core/decision_service', function() { var expectedExperiment = projectConfig.getExperimentFromId(configObj, '594066'); var expectedDecision = { experiment: expectedExperiment, - variation: { - id: '594067', - key: '594067', - featureEnabled: true, - variables: [ - { - id: '5060590313668608', - value: '30.34' - }, - { - id: '5342065290379264', - value: 'Winter is coming definitely' - }, - { - id: '6186490220511232', - value: '500' - }, - { - id: '6467965197221888', - value: 'true' - }, - ], - }, + variation: configObj.variationIdMap['594067'], decisionSource: DECISION_SOURCES.ROLLOUT, } assert.deepEqual(decision, expectedDecision); @@ -2369,29 +1777,7 @@ describe('lib/core/decision_service', function() { var expectedExperiment = projectConfig.getExperimentFromId(configObj, '594066', mockLogger); var expectedDecision = { experiment: expectedExperiment, - variation: { - id: '594067', - key: '594067', - featureEnabled: true, - variables: [ - { - id: '5060590313668608', - value: '30.34' - }, - { - id: '5342065290379264', - value: 'Winter is coming definitely' - }, - { - id: '6186490220511232', - value: '500' - }, - { - id: '6467965197221888', - value: 'true' - }, - ], - }, + variation: configObj.variationIdMap['594067'], decisionSource: DECISION_SOURCES.ROLLOUT, } assert.deepEqual(decision, expectedDecision); @@ -2507,29 +1893,7 @@ describe('lib/core/decision_service', function() { var expectedExperiment = projectConfig.getExperimentFromId(configObj, '594066'); var expectedDecision = { experiment: expectedExperiment, - variation: { - id: '594067', - key: '594067', - featureEnabled: true, - variables: [ - { - id: '5060590313668608', - value: '30.34' - }, - { - id: '5342065290379264', - value: 'Winter is coming definitely' - }, - { - id: '6186490220511232', - value: '500' - }, - { - id: '6467965197221888', - value: 'true' - }, - ], - }, + variation: configObj.variationIdMap['594067'], decisionSource: DECISION_SOURCES.ROLLOUT, } assert.deepEqual(decision, expectedDecision); @@ -2552,29 +1916,7 @@ describe('lib/core/decision_service', function() { var expectedExperiment = projectConfig.getExperimentFromId(configObj, '594066', mockLogger); var expectedDecision = { experiment: expectedExperiment, - variation: { - id: '594067', - key: '594067', - featureEnabled: true, - variables: [ - { - id: '5060590313668608', - value: '30.34' - }, - { - id: '5342065290379264', - value: 'Winter is coming definitely' - }, - { - id: '6186490220511232', - value: '500' - }, - { - id: '6467965197221888', - value: 'true' - }, - ], - }, + variation: configObj.variationIdMap['594067'], decisionSource: DECISION_SOURCES.ROLLOUT, } assert.deepEqual(decision, expectedDecision); From 7e963682d00c277c32a81f74c43d7169a1eaf851 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Tue, 11 Mar 2025 22:58:25 +0600 Subject: [PATCH 07/15] feature test 1 --- lib/core/decision_service/index.spec.ts | 55 +++- lib/core/decision_service/index.ts | 2 +- lib/tests/decision_test_datafile.ts | 408 ++++++++++++++++++++++++ 3 files changed, 461 insertions(+), 4 deletions(-) create mode 100644 lib/tests/decision_test_datafile.ts diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index 182e1d1ba..7a993954e 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -21,7 +21,8 @@ import { bucket } from '../bucketer'; import { getTestProjectConfig, getTestProjectConfigWithFeatures } from '../../tests/test_data'; import { createProjectConfig, ProjectConfig } from '../../project_config/project_config'; import { Experiment } from '../../shared_types'; -import { CONTROL_ATTRIBUTES } from '../../utils/enums'; +import { CONTROL_ATTRIBUTES, DECISION_SOURCES } from '../../utils/enums'; +import { getDecisionTestDatafile } from '../../tests/decision_test_datafile'; import { USER_HAS_NO_FORCED_VARIATION, @@ -48,6 +49,7 @@ import { } from '../decision_service/index'; import { BUCKETING_ID_NOT_STRING, USER_PROFILE_LOOKUP_ERROR, USER_PROFILE_SAVE_ERROR } from 'error_message'; +import exp from 'constants'; type MockLogger = ReturnType; @@ -685,7 +687,54 @@ describe('DecisionService', () => { }); }); - // describe('getVariationForFeature', () => { + describe('getVariationForFeature', () => { + it('should return variation from the first experiment for which a variation is available', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + config, + experiment: any, + user, + shouldIgnoreUPS, + userProfileTracker + ) => { + if (experiment.key === 'exp_2') { + return { + result: 'variation_2', + reasons: [], + }; + } + return { + result: null, + reasons: [], + } + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 40, + }, + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); - // }); + expect(resolveVariationSpy).toHaveBeenCalledTimes(2); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + config, config.experimentKeyMap['exp_1'], user, false, expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + config, config.experimentKeyMap['exp_2'], user, false, expect.anything()); + }); + }); }); diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts index a5fa83196..c01678fa1 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -615,7 +615,7 @@ export class DecisionService { isProfileUpdated: false, userProfile: null, } - const shouldIgnoreUPS = options[OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE]; + const shouldIgnoreUPS = !!options[OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE]; if(!shouldIgnoreUPS) { userProfileTracker.userProfile = this.resolveExperimentBucketMap(userId, attributes); diff --git a/lib/tests/decision_test_datafile.ts b/lib/tests/decision_test_datafile.ts new file mode 100644 index 000000000..ddc1ce142 --- /dev/null +++ b/lib/tests/decision_test_datafile.ts @@ -0,0 +1,408 @@ +// flag id starts from 1000 +// experiment id starts from 2000 +// rollout experiment id starts from 3000 +// audience id starts from 4000 +// variation id starts from 5000 +// variable id starts from 6000 +// attribute id starts from 7000 + +const testDatafile = { + accountId: "24535200037", + projectId: "5088239376138240", + revision: "21", + attributes: [ + { + id: "7001", + key: "age" + } + ], + audiences: [ + { + name: "age_22", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "4001" + }, + { + name: "age_60", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "4002" + }, + { + name: "age_90", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "4003" + }, + { + id: "$opt_dummy_audience", + name: "Optimizely-Generated Audience for Backwards Compatibility", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]" + } + ], + version: "4", + events: [], + integrations: [], + anonymizeIP: true, + botFiltering: false, + typedAudiences: [ + { + name: "age_22", + conditions: [ + "and", + [ + "or", + [ + "or", + { + "match": "le", + name: "age", + "type": "custom_attribute", + value: 22 + } + ] + ] + ], + id: "4001" + }, + { + name: "age_60", + conditions: [ + "and", + [ + "or", + [ + "or", + { + "match": "le", + name: "age", + "type": "custom_attribute", + value: 60 + } + ] + ], + [ + "or", + [ + "or", + { + "match": "gt", + name: "age", + "type": "custom_attribute", + value: 22 + } + ] + ] + ], + id: "4002" + }, + { + name: "age_90", + conditions: [ + "and", + [ + "or", + [ + "or", + { + "match": "le", + name: "age", + "type": "custom_attribute", + value: 90 + } + ] + ], + [ + "or", + [ + "or", + { + "match": "gt", + name: "age", + "type": "custom_attribute", + value: 60 + } + ] + ] + ], + id: "4003" + }, + ], + variables: [], + environmentKey: "production", + sdkKey: "sdk_key", + featureFlags: [ + { + id: "1001", + key: "flag_1", + rolloutId: "rollout-371334-671741182375276", + experimentIds: [ + "2001", + "2002", + "2003" + ], + variables: [ + { + id: "6001", + key: "integer_variable", + "type": "integer", + "defaultValue": "0" + } + ] + } + ], + "rollouts": [ + { + id: "rollout-371334-671741182375276", + experiments: [ + { + id: "3001", + key: "delivery_1", + status: "Running", + layerId: "9300001480454", + variations: [ + { + id: "5004", + key: "variation_4", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "4" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5004", + endOfRange: 1500 + } + ], + forcedVariations: { + + }, + audienceIds: [ + "4001" + ], + audienceConditions: [ + "or", + "4001" + ] + }, + { + id: "3002", + key: "delivery_2", + status: "Running", + layerId: "9300001480455", + variations: [ + { + id: "5005", + key: "variation_5", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "5" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5005", + endOfRange: 4000 + } + ], + forcedVariations: { + + }, + audienceIds: [ + "4002" + ], + audienceConditions: [ + "or", + "4002" + ] + }, + { + id: "3003", + key: "delivery_3", + status: "Running", + layerId: "9300001495996", + variations: [ + { + id: "5006", + key: "variation_6", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "6" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5006", + endOfRange: 8000 + } + ], + forcedVariations: { + + }, + audienceIds: [ + "4003" + ], + audienceConditions: [ + "or", + "4003" + ] + }, + { + id: "default-rollout-371334-671741182375276", + key: "default-rollout-371334-671741182375276", + status: "Running", + layerId: "rollout-371334-671741182375276", + variations: [ + { + id: "5007", + key: "variation_7", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "7" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5007", + endOfRange: 10000 + } + ], + forcedVariations: { + + }, + audienceIds: [], + audienceConditions: [] + } + ] + } + ], + experiments: [ + { + id: "2001", + key: "exp_1", + status: "Running", + layerId: "9300001480444", + variations: [ + { + id: "5001", + key: "variation_1", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "1" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5001", + endOfRange: 5000 + }, + { + entityId: "5001", + endOfRange: 10000 + } + ], + forcedVariations: { + + }, + audienceIds: [ + "4001" + ], + audienceConditions: [ + "or", + "4001" + ] + }, + { + id: "2002", + key: "exp_2", + status: "Running", + layerId: "9300001480448", + variations: [ + { + id: "5002", + key: "variation_2", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "2" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5002", + endOfRange: 5000 + }, + { + entityId: "5002", + endOfRange: 10000 + } + ], + forcedVariations: { + + }, + audienceIds: [], + audienceConditions: [] + }, + { + id: "2003", + key: "exp_3", + status: "Running", + layerId: "9300001480451", + variations: [ + { + id: "5003", + key: "variation_3", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "3" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5003", + endOfRange: 5000 + }, + { + entityId: "5003", + endOfRange: 10000 + } + ], + forcedVariations: { + + }, + audienceIds: [], + audienceConditions: [] + } + ], + "groups": [] +} + +export const getDecisionTestDatafile = () => { + return JSON.parse(JSON.stringify(testDatafile)); +} From 83fa2b06f80f470c34a6270887d716083a585a65 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 12 Mar 2025 02:06:51 +0600 Subject: [PATCH 08/15] feature test 2 --- lib/core/decision_service/index.spec.ts | 118 +++++++++++++++++++++++- lib/tests/decision_test_datafile.ts | 4 +- 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index 7a993954e..208a31751 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -20,7 +20,7 @@ import OptimizelyUserContext from '../../optimizely_user_context'; import { bucket } from '../bucketer'; import { getTestProjectConfig, getTestProjectConfigWithFeatures } from '../../tests/test_data'; import { createProjectConfig, ProjectConfig } from '../../project_config/project_config'; -import { Experiment } from '../../shared_types'; +import { BucketerParams, Experiment } from '../../shared_types'; import { CONTROL_ATTRIBUTES, DECISION_SOURCES } from '../../utils/enums'; import { getDecisionTestDatafile } from '../../tests/decision_test_datafile'; @@ -736,5 +736,121 @@ describe('DecisionService', () => { expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, config, config.experimentKeyMap['exp_2'], user, false, expect.anything()); }); + + describe('when no variation is found for any experiment and a target delivery audience condition is satisfied', () => { + beforeEach(() => { + mockBucket.mockReset(); + }); + + it('should return variation from the target delivery for which audience condition is satisfied if the user is bucketed into it', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockReturnValue({ + result: null, + reasons: [], + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 55, // this should satisfy the audience condition for the targeted delivery with key delivery_2 + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey === 'delivery_2') { + return { + result: '5005', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentIdMap['3002'], + variation: config.variationIdMap['5005'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + expect(resolveVariationSpy).toHaveBeenCalledTimes(3); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + config, config.experimentKeyMap['exp_1'], user, false, expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + config, config.experimentKeyMap['exp_2'], user, false, expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(3, + config, config.experimentKeyMap['exp_3'], user, false, expect.anything()); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, config.experimentIdMap['3002'], user); + }); + + it('should skip to everyone else targeting rule if the user is not bucketed into the targeted delivery for which \ + audience condition is satisfied', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockReturnValue({ + result: null, + reasons: [], + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 55, // this should satisfy the audience condition for the targeted delivery with key delivery_2 + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey === 'default-rollout-key') { + return { + result: '5007', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentIdMap['default-rollout-id'], + variation: config.variationIdMap['5007'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + expect(resolveVariationSpy).toHaveBeenCalledTimes(3); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + config, config.experimentKeyMap['exp_1'], user, false, expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + config, config.experimentKeyMap['exp_2'], user, false, expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(3, + config, config.experimentKeyMap['exp_3'], user, false, expect.anything()); + + expect(mockBucket).toHaveBeenCalledTimes(2); + verifyBucketCall(0, config, config.experimentIdMap['3002'], user); + verifyBucketCall(1, config, config.experimentIdMap['default-rollout-id'], user); + }); + }); }); }); diff --git a/lib/tests/decision_test_datafile.ts b/lib/tests/decision_test_datafile.ts index ddc1ce142..7d5e47108 100644 --- a/lib/tests/decision_test_datafile.ts +++ b/lib/tests/decision_test_datafile.ts @@ -259,8 +259,8 @@ const testDatafile = { ] }, { - id: "default-rollout-371334-671741182375276", - key: "default-rollout-371334-671741182375276", + id: "default-rollout-id", + key: "default-rollout-key", status: "Running", layerId: "rollout-371334-671741182375276", variations: [ From 9f56e3083ed27bd1e01f96a8c95da06214c30bba Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 12 Mar 2025 16:05:31 +0600 Subject: [PATCH 09/15] feature test --- lib/core/decision_service/index.spec.ts | 202 +++++++++++++++++++++++- lib/core/decision_service/index.ts | 4 +- lib/tests/decision_test_datafile.ts | 24 --- 3 files changed, 197 insertions(+), 33 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index 208a31751..7ae6dcd82 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -20,7 +20,7 @@ import OptimizelyUserContext from '../../optimizely_user_context'; import { bucket } from '../bucketer'; import { getTestProjectConfig, getTestProjectConfigWithFeatures } from '../../tests/test_data'; import { createProjectConfig, ProjectConfig } from '../../project_config/project_config'; -import { BucketerParams, Experiment } from '../../shared_types'; +import { BucketerParams, Experiment, UserProfile } from '../../shared_types'; import { CONTROL_ATTRIBUTES, DECISION_SOURCES } from '../../utils/enums'; import { getDecisionTestDatafile } from '../../tests/decision_test_datafile'; @@ -688,6 +688,10 @@ describe('DecisionService', () => { }); describe('getVariationForFeature', () => { + beforeEach(() => { + mockBucket.mockReset(); + }); + it('should return variation from the first experiment for which a variation is available', () => { const { decisionService } = getDecisionService(); @@ -737,12 +741,95 @@ describe('DecisionService', () => { config, config.experimentKeyMap['exp_2'], user, false, expect.anything()); }); - describe('when no variation is found for any experiment and a target delivery audience condition is satisfied', () => { + it('should save the variation found for an experiment in the user profile without', () => { + const { decisionService, userProfileService } = getDecisionService({ userProfileService: true }); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + config, + experiment: any, + user, + shouldIgnoreUPS, + userProfileTracker: any, + ) => { + if (experiment.key === 'exp_2') { + const variation = 'variation_2'; + + userProfileTracker.userProfile[experiment.id] = { + variation_id: '5002', + }; + userProfileTracker.isProfileUpdated = true; + + return { + result: 'variation_2', + reasons: [], + }; + } + return { + result: null, + reasons: [], + } + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 40, + }, + }); + + userProfileService?.lookup.mockImplementation((userId: string) => { + if (userId === 'tester') { + return { + user_id: 'tester', + experiment_bucket_map: { + '2001': { + variation_id: '5001', + }, + }, + }; + } + return null; + }); + + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('tester'); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'tester', + experiment_bucket_map: { + '2001': { + variation_id: '5001', + }, + '2002': { + variation_id: '5002', + }, + }, + }); + }); + + describe('when no variation is found for any experiment and a targeted delivery \ + audience condition is satisfied', () => { beforeEach(() => { mockBucket.mockReset(); }); - it('should return variation from the target delivery for which audience condition is satisfied if the user is bucketed into it', () => { + it('should return variation from the target delivery for which audience condition \ + is satisfied if the user is bucketed into it', () => { const { decisionService } = getDecisionService(); const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') @@ -757,7 +844,7 @@ describe('DecisionService', () => { optimizely: {} as any, userId: 'tester', attributes: { - age: 55, // this should satisfy the audience condition for the targeted delivery with key delivery_2 + age: 55, // this should satisfy the audience condition for the targeted delivery with key delivery_2 and delivery_3 }, }); @@ -796,8 +883,8 @@ describe('DecisionService', () => { verifyBucketCall(0, config, config.experimentIdMap['3002'], user); }); - it('should skip to everyone else targeting rule if the user is not bucketed into the targeted delivery for which \ - audience condition is satisfied', () => { + it('should skip to everyone else targeting rule if the user is not bucketed \ + into the targeted delivery for which audience condition is satisfied', () => { const { decisionService } = getDecisionService(); const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') @@ -812,7 +899,7 @@ describe('DecisionService', () => { optimizely: {} as any, userId: 'tester', attributes: { - age: 55, // this should satisfy the audience condition for the targeted delivery with key delivery_2 + age: 55, // this should satisfy the audience condition for the targeted delivery with key delivery_2 and delivery_3 }, }); @@ -852,5 +939,106 @@ describe('DecisionService', () => { verifyBucketCall(1, config, config.experimentIdMap['default-rollout-id'], user); }); }); + + it('should return variation from the everyone else targeting rule if no variation \ + is found for any experiment or targeted delivery', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockReturnValue({ + result: null, + reasons: [], + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 100, // this should not satisfy any audience condition for any targeted delivery + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + console.log('bucket called for ' + ruleKey); + if (ruleKey === 'default-rollout-key') { + return { + result: '5007', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentIdMap['default-rollout-id'], + variation: config.variationIdMap['5007'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + expect(resolveVariationSpy).toHaveBeenCalledTimes(3); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + config, config.experimentKeyMap['exp_1'], user, false, expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + config, config.experimentKeyMap['exp_2'], user, false, expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(3, + config, config.experimentKeyMap['exp_3'], user, false, expect.anything()); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, config.experimentIdMap['default-rollout-id'], user); + }); + + it('should return null if no variation is found for any experiment, targeted delivery, or everyone else targeting rule', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockReturnValue({ + result: null, + reasons: [], + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + const rolloutId = config.featureKeyMap['flag_1'].rolloutId; + config.rolloutIdMap[rolloutId].experiments = []; // remove the experiments from the rollout + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 10, + }, + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + expect(resolveVariationSpy).toHaveBeenCalledTimes(3); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + config, config.experimentKeyMap['exp_1'], user, false, expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + config, config.experimentKeyMap['exp_2'], user, false, expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(3, + config, config.experimentKeyMap['exp_3'], user, false, expect.anything()); + + expect(mockBucket).toHaveBeenCalledTimes(0); + }); }); + + // describe('getVariationsForFeatureList', () => { + + // }); }); diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts index c01678fa1..ea31d1bbc 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -655,10 +655,10 @@ export class DecisionService { } if(!shouldIgnoreUPS) { - this.saveUserProfile(userId, userProfileTracker) + this.saveUserProfile(userId, userProfileTracker); } - return decisions + return decisions; } diff --git a/lib/tests/decision_test_datafile.ts b/lib/tests/decision_test_datafile.ts index 7d5e47108..3831eef87 100644 --- a/lib/tests/decision_test_datafile.ts +++ b/lib/tests/decision_test_datafile.ts @@ -78,18 +78,6 @@ const testDatafile = { value: 60 } ] - ], - [ - "or", - [ - "or", - { - "match": "gt", - name: "age", - "type": "custom_attribute", - value: 22 - } - ] ] ], id: "4002" @@ -109,18 +97,6 @@ const testDatafile = { value: 90 } ] - ], - [ - "or", - [ - "or", - { - "match": "gt", - name: "age", - "type": "custom_attribute", - value: 60 - } - ] ] ], id: "4003" From f8cd69c5f21d2342d06b7d89e586067a22976a03 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 12 Mar 2025 17:22:27 +0600 Subject: [PATCH 10/15] feature list test --- lib/core/decision_service/index.spec.ts | 162 +++++++++++++++++++++++- lib/tests/decision_test_datafile.ts | 72 ++++++++++- 2 files changed, 230 insertions(+), 4 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index 7ae6dcd82..ae7f1de45 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -1038,7 +1038,165 @@ describe('DecisionService', () => { }); }); - // describe('getVariationsForFeatureList', () => { + describe('getVariationsForFeatureList', () => { + beforeEach(() => { + mockBucket.mockReset(); + }); + + it('should return correct results for all features in the feature list', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + config, + experiment: any, + user, + shouldIgnoreUPS, + userProfileTracker + ) => { + if (experiment.key === 'exp_2') { + return { + result: 'variation_2', + reasons: [], + }; + } else if (experiment.key === 'exp_4') { + return { + result: 'variation_flag_2', + reasons: [], + }; + } + return { + result: null, + reasons: [], + } + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 40, + }, + }); + + const featureList = [ + config.featureKeyMap['flag_1'], + config.featureKeyMap['flag_2'], + ]; + + const variations = decisionService.getVariationsForFeatureList(config, featureList, user); + + expect(variations[0].result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(variations[1].result).toEqual({ + experiment: config.experimentKeyMap['exp_4'], + variation: config.variationIdMap['5100'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); - // }); + const variations2 = decisionService.getVariationsForFeatureList(config, featureList.reverse(), user); + + expect(variations2[0].result).toEqual({ + experiment: config.experimentKeyMap['exp_4'], + variation: config.variationIdMap['5100'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(variations2[1].result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + }); + + it('should batch user profile lookup and save', () => { + const { decisionService, userProfileService } = getDecisionService({ userProfileService: true }); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + config, + experiment: any, + user, + shouldIgnoreUPS, + userProfileTracker: any, + ) => { + if (experiment.key === 'exp_2') { + userProfileTracker.userProfile[experiment.id] = { + variation_id: '5002', + }; + userProfileTracker.isProfileUpdated = true; + + return { + result: 'variation_2', + reasons: [], + }; + } else if (experiment.key === 'exp_4') { + userProfileTracker.userProfile[experiment.id] = { + variation_id: '5100', + }; + userProfileTracker.isProfileUpdated = true; + + return { + result: 'variation_flag_2', + reasons: [], + }; + } + return { + result: null, + reasons: [], + } + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 40, + }, + }); + + const featureList = [ + config.featureKeyMap['flag_1'], + config.featureKeyMap['flag_2'], + ]; + + const variations = decisionService.getVariationsForFeatureList(config, featureList, user); + + expect(variations[0].result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(variations[1].result).toEqual({ + experiment: config.experimentKeyMap['exp_4'], + variation: config.variationIdMap['5100'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('tester'); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'tester', + experiment_bucket_map: { + '2002': { + variation_id: '5002', + }, + '2004': { + variation_id: '5100', + }, + }, + }); + }); + }); }); diff --git a/lib/tests/decision_test_datafile.ts b/lib/tests/decision_test_datafile.ts index 3831eef87..ed46f9265 100644 --- a/lib/tests/decision_test_datafile.ts +++ b/lib/tests/decision_test_datafile.ts @@ -123,6 +123,15 @@ const testDatafile = { "defaultValue": "0" } ] + }, + { + id: "1002", + key: "flag_2", + "rolloutId": "rollout-374517-931741182375293", + experimentIds: [ + "2004" + ], + "variables": [] } ], "rollouts": [ @@ -260,12 +269,42 @@ const testDatafile = { ], forcedVariations: { + }, + audienceIds: [], + audienceConditions: [] + }, + ] + }, + { + id: "rollout-374517-931741182375293", + experiments: [ + { + id: "default-rollout-374517-931741182375293", + key: "default-rollout-374517-931741182375293", + status: "Running", + layerId: "rollout-374517-931741182375293", + variations: [ + { + id: "1177722", + key: "off", + featureEnabled: false, + variables: [] + } + ], + trafficAllocation: [ + { + "entityId": "1177722", + "endOfRange": 10000 + } + ], + forcedVariations: { + }, audienceIds: [], audienceConditions: [] } ] - } + }, ], experiments: [ { @@ -371,12 +410,41 @@ const testDatafile = { ], forcedVariations: { + }, + audienceIds: [], + audienceConditions: [] + }, + { + id: "2004", + key: "exp_4", + status: "Running", + layerId: "9300001497754", + variations: [ + { + id: "5100", + key: "variation_flag_2", + featureEnabled: true, + variables: [] + } + ], + trafficAllocation: [ + { + entityId: "5100", + endOfRange: 5000 + }, + { + entityId: "5100", + endOfRange: 10000 + } + ], + forcedVariations: { + }, audienceIds: [], audienceConditions: [] } ], - "groups": [] + groups: [] } export const getDecisionTestDatafile = () => { From c42cb91c49e617739512e0e64c37137081a243af Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 12 Mar 2025 19:33:58 +0600 Subject: [PATCH 11/15] test forced variation --- lib/core/decision_service/index.spec.ts | 140 ++++++++++++++++++++++- lib/core/decision_service/index.tests.js | 10 -- 2 files changed, 137 insertions(+), 13 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index ae7f1de45..07f7f568f 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -208,6 +208,52 @@ describe('DecisionService', () => { expect(logger?.info).toHaveBeenNthCalledWith(2, USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences'); }); + it('should return the forced variation set using setForcedVariation \ + in presence of a whitelisted variation', function() { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'user2' + }); + + const config = createProjectConfig(cloneDeep(testData)); + + const experiment = config.experimentIdMap['122227']; + + const { decisionService } = getDecisionService(); + + const forcedVariation = 'controlWithAudience'; + + decisionService.setForcedVariation(config, experiment.key, user.getUserId(), forcedVariation); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe(forcedVariation); + + const whitelistedVariation = experiment.forcedVariations?.[user.getUserId()]; + expect(whitelistedVariation).toBeDefined(); + expect(whitelistedVariation).not.toEqual(forcedVariation); + }); + + it('should return the forced variation set using setForcedVariation \ + even if user does not satisfy audience condition', function() { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'user3', // no attributes are set, should not satisfy audience condition 11154 + }); + + const config = createProjectConfig(cloneDeep(testData)); + + const experiment = config.experimentIdMap['122227']; + + const { decisionService } = getDecisionService(); + + const forcedVariation = 'controlWithAudience'; + + decisionService.setForcedVariation(config, experiment.key, user.getUserId(), forcedVariation); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe(forcedVariation); + }); + it('should return null if the experiment is not running', function() { const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -741,7 +787,55 @@ describe('DecisionService', () => { config, config.experimentKeyMap['exp_2'], user, false, expect.anything()); }); - it('should save the variation found for an experiment in the user profile without', () => { + it('should return the variation forced for an experiment in the userContext if available', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + config, + experiment: any, + user, + shouldIgnoreUPS, + userProfileTracker + ) => { + if (experiment.key === 'exp_2') { + return { + result: 'variation_2', + reasons: [], + }; + } + return { + result: null, + reasons: [], + } + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 40, + }, + }); + + user.setForcedDecision( + { flagKey: 'flag_1', ruleKey: 'exp_2' }, + { variationKey: 'variation_5' } + ); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5005'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + }); + + it('should save the variation found for an experiment in the user profile', () => { const { decisionService, userProfileService } = getDecisionService({ userProfileService: true }); const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') @@ -866,7 +960,7 @@ describe('DecisionService', () => { const variation = decisionService.getVariationForFeature(config, feature, user); expect(variation.result).toEqual({ - experiment: config.experimentIdMap['3002'], + experiment: config.experimentKeyMap['delivery_2'], variation: config.variationIdMap['5005'], decisionSource: DECISION_SOURCES.ROLLOUT, }); @@ -940,6 +1034,47 @@ describe('DecisionService', () => { }); }); + it('should return the forced variation for targeted delivery rule when no variation \ + is found for any experiment and a there is a forced decision \ + for a targeted delivery in the userContext', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + config, + experiment: any, + user, + shouldIgnoreUPS, + userProfileTracker + ) => { + return { + result: null, + reasons: [], + } + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + }); + + user.setForcedDecision( + { flagKey: 'flag_1', ruleKey: 'delivery_2' }, + { variationKey: 'variation_1' } + ); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['delivery_2'], + variation: config.variationIdMap['5001'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + }); + it('should return variation from the everyone else targeting rule if no variation \ is found for any experiment or targeted delivery', () => { const { decisionService } = getDecisionService(); @@ -962,7 +1097,6 @@ describe('DecisionService', () => { mockBucket.mockImplementation((param: BucketerParams) => { const ruleKey = param.experimentKey; - console.log('bucket called for ' + ruleKey); if (ruleKey === 'default-rollout-key') { return { result: '5007', diff --git a/lib/core/decision_service/index.tests.js b/lib/core/decision_service/index.tests.js index a4616483b..ed98a982e 100644 --- a/lib/core/decision_service/index.tests.js +++ b/lib/core/decision_service/index.tests.js @@ -658,16 +658,6 @@ describe('lib/core/decision_service', function() { }); }); - // describe('checkIfExperimentIsActive', function() { - // it('should return true if experiment is running', function() { - // assert.isTrue(decisionServiceInstance.checkIfExperimentIsActive(configObj, 'testExperiment')); - // }); - - // it('should return false when experiment is not running', function() { - // assert.isFalse(decisionServiceInstance.checkIfExperimentIsActive(configObj, 'testExperimentNotRunning')); - // }); - // }); - describe('checkIfUserIsInAudience', function() { var __audienceEvaluateSpy; From 4a54701b3cb935917ef229e23abd8efe00be964e Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 12 Mar 2025 20:03:28 +0600 Subject: [PATCH 12/15] forced test --- lib/core/decision_service/index.spec.ts | 88 ++++++++++++++++++++++++- lib/core/decision_service/index.ts | 4 +- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index 07f7f568f..5df9f586e 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -105,6 +105,7 @@ const verifyBucketCall = ( projectConfig: ProjectConfig, experiment: Experiment, user: OptimizelyUserContext, + bucketIdFromAttribute = false, ) => { const { experimentId, @@ -125,7 +126,7 @@ const verifyBucketCall = ( expect(experimentIdMap).toBe(projectConfig.experimentIdMap); expect(groupIdMap).toBe(projectConfig.groupIdMap); expect(variationIdMap).toBe(projectConfig.variationIdMap); - expect(bucketingId).toBe(user.getAttributes()[CONTROL_ATTRIBUTES.BUCKETING_ID] || user.getUserId()); + expect(bucketingId).toBe(bucketIdFromAttribute ? user.getAttributes()[CONTROL_ATTRIBUTES.BUCKETING_ID] : user.getUserId()); }; describe('DecisionService', () => { @@ -161,6 +162,36 @@ describe('DecisionService', () => { verifyBucketCall(0, config, experiment, user); }); + it('should use $opt_bucketing_id attribute as bucketing id if provided', () => { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + $opt_bucketing_id: 'test_bucketing_id', + }, + }); + + const config = createProjectConfig(cloneDeep(testData)); + + const fakeDecisionResponse = { + result: '111128', + reasons: [], + }; + + const experiment = config.experimentIdMap['111127']; + + mockBucket.mockReturnValue(fakeDecisionResponse); // contains variation ID of the 'control' variation from `test_data` + + const { decisionService } = getDecisionService(); + + const variation = decisionService.getVariation(config, experiment, user); + + expect(variation.result).toBe('control'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user, true); + }); + it('should return the whitelisted variation if the user is whitelisted', function() { const user = new OptimizelyUserContext({ optimizely: {} as any, @@ -977,6 +1008,61 @@ describe('DecisionService', () => { verifyBucketCall(0, config, config.experimentIdMap['3002'], user); }); + it('should return variation from the target delivery and use $opt_bucketing_id attribute as bucketing id', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockReturnValue({ + result: null, + reasons: [], + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 55, // this should satisfy the audience condition for the targeted delivery with key delivery_2 and delivery_3 + $opt_bucketing_id: 'test_bucketing_id', + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey === 'delivery_2') { + return { + result: '5005', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['delivery_2'], + variation: config.variationIdMap['5005'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + expect(resolveVariationSpy).toHaveBeenCalledTimes(3); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + config, config.experimentKeyMap['exp_1'], user, false, expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + config, config.experimentKeyMap['exp_2'], user, false, expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(3, + config, config.experimentKeyMap['exp_3'], user, false, expect.anything()); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, config.experimentIdMap['3002'], user, true); + }); + it('should skip to everyone else targeting rule if the user is not bucketed \ into the targeted delivery for which audience condition is satisfied', () => { const { decisionService } = getDecisionService(); diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts index ea31d1bbc..4e748ac9d 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -1170,7 +1170,7 @@ export class DecisionService { } } - getVariationFromExperimentRule( + private getVariationFromExperimentRule( configObj: ProjectConfig, flagKey: string, rule: Experiment, @@ -1201,7 +1201,7 @@ export class DecisionService { }; } - getVariationFromDeliveryRule( + private getVariationFromDeliveryRule( configObj: ProjectConfig, flagKey: string, rules: Experiment[], From 41a114766436983634e3b213ddea7b11642c6e35 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Thu, 13 Mar 2025 15:52:32 +0600 Subject: [PATCH 13/15] forced variation test --- lib/core/decision_service/index.spec.ts | 318 ++++++++++++++++++++++++ lib/core/decision_service/index.ts | 2 +- 2 files changed, 319 insertions(+), 1 deletion(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index 5df9f586e..3868caf34 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -1419,4 +1419,322 @@ describe('DecisionService', () => { }); }); }); + + + describe('forced variation management', () => { + it('should return true for a valid forcedVariation in setForcedVariation', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation).toBe(true); + }); + + it('should return the same variation from getVariation as was set in setVariation', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + + const variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + expect(variation).toBe('control'); + }); + + it('should return null from getVariation if no forced variation was set for a valid experimentKey', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + expect(config.experimentKeyMap['testExperiment']).toBeDefined(); + const variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + + expect(variation).toBe(null); + }); + + it('should return null from getVariation for an invalid experimentKey', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + expect(config.experimentKeyMap['definitely_not_valid_exp_key']).not.toBeDefined(); + const variation = decisionService.getForcedVariation(config, 'definitely_not_valid_exp_key', 'user1').result; + + expect(variation).toBe(null); + }); + + it('should return null when a forced decision is set on another experiment key', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + decisionService.setForcedVariation(config, 'testExperiment', 'user1', 'control'); + var variation = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + expect(variation).toBe(null); + }); + + it('should not set forced variation for an invalid variation key and return false', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const wasSet = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'definitely_not_valid_variation_key' + ); + + expect(wasSet).toBe(false); + const variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + expect(variation).toBe(null); + }); + + it('should reset the forcedVariation if null is passed to setForcedVariation', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + + expect(didSetVariation).toBe(true); + + let variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + expect(variation).toBe('control'); + + const didSetVariationAgain = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + null + ); + + expect(didSetVariationAgain).toBe(true); + + variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + expect(variation).toBe(null); + }); + + it('should be able to add variations for multiple experiments for one user', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation1 = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation1).toBe(true); + + const didSetVariation2 = decisionService.setForcedVariation( + config, + 'testExperimentLaunched', + 'user1', + 'controlLaunched' + ); + expect(didSetVariation2).toBe(true); + + const variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + const variation2 = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + expect(variation).toBe('control'); + expect(variation2).toBe('controlLaunched'); + }); + + it('should be able to forced variation to same experiment for multiple users', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation1 = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation1).toBe(true); + + const didSetVariation2 = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user2', + 'variation' + ); + expect(didSetVariation2).toBe(true); + + const variationControl = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + const variationVariation = decisionService.getForcedVariation(config, 'testExperiment', 'user2').result; + + expect(variationControl).toBe('control'); + expect(variationVariation).toBe('variation'); + }); + + it('should be able to reset a variation for a user with multiple experiments', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + // Set the first time + const didSetVariation1 = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation1).toBe(true); + + const didSetVariation2 = decisionService.setForcedVariation( + config, + 'testExperimentLaunched', + 'user1', + 'controlLaunched' + ); + expect(didSetVariation2).toBe(true); + + let variation1 = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + let variation2 = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + + expect(variation1).toBe('control'); + expect(variation2).toBe('controlLaunched'); + + // Reset for one of the experiments + const didSetVariationAgain = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'variation' + ); + expect(didSetVariationAgain).toBe(true); + + variation1 = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + variation2 = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + + expect(variation1).toBe('variation'); + expect(variation2).toBe('controlLaunched'); + }); + + it('should be able to unset a variation for a user with multiple experiments', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + // Set the first time + const didSetVariation1 = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation1).toBe(true); + + const didSetVariation2 = decisionService.setForcedVariation( + config, + 'testExperimentLaunched', + 'user1', + 'controlLaunched' + ); + expect(didSetVariation2).toBe(true); + + let variation1 = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + let variation2 = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + + expect(variation1).toBe('control'); + expect(variation2).toBe('controlLaunched'); + + // Unset for one of the experiments + decisionService.setForcedVariation(config, 'testExperiment', 'user1', null); + + variation1 = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + variation2 = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + + expect(variation1).toBe(null); + expect(variation2).toBe('controlLaunched'); + }); + + it('should return false for an empty variation key', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation(config, 'testExperiment', 'user1', ''); + expect(didSetVariation).toBe(false); + }); + + it('should return null when a variation was previously set, and that variation no longer exists on the config object', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation).toBe(true); + + const newDatafile = cloneDeep(testData); + // Remove 'control' variation from variations, traffic allocation, and datafile forcedVariations. + newDatafile.experiments[0].variations = [ + { + key: 'variation', + id: '111129', + }, + ]; + newDatafile.experiments[0].trafficAllocation = [ + { + entityId: '111129', + endOfRange: 9000, + }, + ]; + newDatafile.experiments[0].forcedVariations = { + user1: 'variation', + user2: 'variation', + }; + // Now the only variation in testExperiment is 'variation' + const newConfigObj = createProjectConfig(newDatafile); + const forcedVar = decisionService.getForcedVariation(newConfigObj, 'testExperiment', 'user1').result; + expect(forcedVar).toBe(null); + }); + + it("should return null when a variation was previously set, and that variation's experiment no longer exists on the config object", function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation).toBe(true); + + const newConfigObj = createProjectConfig(cloneDeep(testDataWithFeatures)); + const forcedVar = decisionService.getForcedVariation(newConfigObj, 'testExperiment', 'user1').result; + expect(forcedVar).toBe(null); + }); + + it('should return false from setForcedVariation and not set for invalid experiment key', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation( + config, + 'definitelyNotAValidExperimentKey', + 'user1', + 'control' + ); + expect(didSetVariation).toBe(false); + + const variation = decisionService.getForcedVariation( + config, + 'definitelyNotAValidExperimentKey', + 'user1' + ).result; + expect(variation).toBe(null); + }); + }); }); diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts index 4e748ac9d..b7a25620c 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -962,7 +962,7 @@ export class DecisionService { * @param {string} experimentKey Key representing the experiment id * @throws If the user id is not valid or not in the forced variation map */ - removeForcedVariation(userId: string, experimentId: string, experimentKey: string): void { + private removeForcedVariation(userId: string, experimentId: string, experimentKey: string): void { if (!userId) { throw new OptimizelyError(INVALID_USER_ID); } From a85e75dcd4ddc9cd363ef6631cf74b6a27db39bf Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Thu, 13 Mar 2025 15:57:18 +0600 Subject: [PATCH 14/15] up --- lib/core/decision_service/index.spec.ts | 2 +- lib/tests/decision_test_datafile.ts | 2 +- vitest.config.mts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index 3868caf34..cbfbaf7be 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -1474,7 +1474,7 @@ describe('DecisionService', () => { const { decisionService } = getDecisionService(); decisionService.setForcedVariation(config, 'testExperiment', 'user1', 'control'); - var variation = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + const variation = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; expect(variation).toBe(null); }); diff --git a/lib/tests/decision_test_datafile.ts b/lib/tests/decision_test_datafile.ts index ed46f9265..537ea4f11 100644 --- a/lib/tests/decision_test_datafile.ts +++ b/lib/tests/decision_test_datafile.ts @@ -447,6 +447,6 @@ const testDatafile = { groups: [] } -export const getDecisionTestDatafile = () => { +export const getDecisionTestDatafile = (): any => { return JSON.parse(JSON.stringify(testDatafile)); } diff --git a/vitest.config.mts b/vitest.config.mts index c35f62ec3..584eeb60d 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -26,7 +26,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/decision_service/index.spec.ts'], + include: ['**/*.spec.ts'], typecheck: { enabled: true, tsconfig: 'tsconfig.spec.json', From e5093585e3c0c267f1e6d9ccff401105d9a26284 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Thu, 13 Mar 2025 23:18:04 +0600 Subject: [PATCH 15/15] copyright --- lib/core/decision_service/index.tests.js | 2 +- lib/core/decision_service/index.ts | 2 +- lib/optimizely/index.ts | 2 +- lib/tests/decision_test_datafile.ts | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/core/decision_service/index.tests.js b/lib/core/decision_service/index.tests.js index ed98a982e..8dd68aa88 100644 --- a/lib/core/decision_service/index.tests.js +++ b/lib/core/decision_service/index.tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2017-2022, 2024, Optimizely + * Copyright 2017-2022, 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts index b7a25620c..386606cc9 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2017-2022, 2024, Optimizely + * Copyright 2017-2022, 2024-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index 0d10cd5e0..bf8e6c717 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2020-2024, Optimizely + * Copyright 2020-2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/lib/tests/decision_test_datafile.ts b/lib/tests/decision_test_datafile.ts index 537ea4f11..84c72de90 100644 --- a/lib/tests/decision_test_datafile.ts +++ b/lib/tests/decision_test_datafile.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + // flag id starts from 1000 // experiment id starts from 2000 // rollout experiment id starts from 3000