diff --git a/DemoSwiftApp/Samples/SamplesForAPI.swift b/DemoSwiftApp/Samples/SamplesForAPI.swift index 2ecb50c1..3d318a92 100644 --- a/DemoSwiftApp/Samples/SamplesForAPI.swift +++ b/DemoSwiftApp/Samples/SamplesForAPI.swift @@ -444,7 +444,7 @@ class SamplesForAPI { userId: "USER_123", attributes: ["country": "us"] ) - let options: [OptimizelyDecideOption] = [.ignoreCmabCache, .ignoreUserProfileService] + let options: [OptimizelyDecideOption] = [.ignoreCmabCache] let decision = await user.decideAsync(key: FLAG_KEY, options: options) print("CMAB decision: \(decision)") } @@ -462,7 +462,7 @@ class SamplesForAPI { userId: "USER_123", attributes: ["country": "us"] ) - let options: [OptimizelyDecideOption] = [.ignoreCmabCache, .ignoreUserProfileService] + let options: [OptimizelyDecideOption] = [.ignoreCmabCache] user.decideAsync(key: FLAG_KEY, options: options, completion: { decision in print("CMAB decision: \(decision)") }) @@ -494,7 +494,7 @@ class SamplesForAPI { userId: "USER_123", attributes: ["country": "us"] ) - let options: [OptimizelyDecideOption] = [.ignoreCmabCache, .ignoreUserProfileService] + let options: [OptimizelyDecideOption] = [.ignoreCmabCache] let decision = await user.decideAsync(key: FLAG_KEY, options: options) print("CMAB decision: \(decision)") } @@ -512,7 +512,7 @@ class SamplesForAPI { userId: "USER_123", attributes: ["country": "us"] ) - let options: [OptimizelyDecideOption] = [.ignoreCmabCache, .ignoreUserProfileService] + let options: [OptimizelyDecideOption] = [.ignoreCmabCache] user.decideAsync(key: FLAG_KEY, options: options, completion: { decision in print("CMAB decision: \(decision)") }) diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index b4fa3ff2..7014e124 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -277,7 +277,12 @@ class DefaultDecisionService: OPTDecisionService { let info = LogMessage.userBucketedIntoVariationInExperiment(userId, experiment.key, variation.key) logger.i(info) reasons.addInfo(info) - userProfileTracker?.updateProfile(experiment: experiment, variation: variation) + + // CMAB decision shouldn't be in the UPS + if !experiment.isCmab { + userProfileTracker?.updateProfile(experiment: experiment, variation: variation) + } + } else { let info = LogMessage.userNotBucketedIntoVariation(userId) logger.i(info) diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests_Experiments.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests_Experiments.swift index 8217002a..1fda868e 100644 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests_Experiments.swift +++ b/Tests/OptimizelyTests-Common/DecisionServiceTests_Experiments.swift @@ -732,6 +732,38 @@ extension DecisionServiceTests_Experiments { XCTAssertNotNil(variation) XCTAssertEqual(variation?.key, kVariationKeyA) } + + func testCMABVariationDoesnotTrackByProfileTracker() { + self.config.project.typedAudiences = try! OTUtils.model(from: sampleTypedAudiencesData) + var cmabExperiment: Experiment = try! OTUtils.model(from: sampleExperimentData) + cmabExperiment.cmab = try! OTUtils.model(from: ["trafficAllocation": 10000, "attributeIds": ["10389729780"]]) + self.config.project.experiments = [cmabExperiment] + let mocCmabService = MockCmabService() + mocCmabService.variationId = "10389729780" // kVariationKeyA + let ups = DefaultUserProfileService() + self.decisionService = DefaultDecisionService(userProfileService: ups, cmabService: mocCmabService) + + let user = optimizely.createUserContext( + userId: kUserId, + attributes: kAttributesCountryMatch + ) + + let tracker = UserProfileTracker(userId: "user_1234", userProfileService: ups, logger: self.decisionService.logger) + tracker.loadUserProfile() + + let decision = self.decisionService.getVariation(config: config, + experiment: cmabExperiment, + user: user, + options: nil, + isAsync: true, + userProfileTracker: tracker) + + let variation = decision.result?.variation + XCTAssertNotNil(variation) + XCTAssertEqual(variation?.key, kVariationKeyA) + XCTAssertFalse(tracker.profileUpdated) + XCTAssertTrue(tracker.userProfile!.isEmpty) + } func testGetVariationWithCMABZeroTrafficAllocation() { // Test when traffic allocation is 0% @@ -834,6 +866,7 @@ extension DecisionServiceTests_Experiments { self.config.project.experiments = [cmabExperiment] let mocCmabService = MockCmabService() mocCmabService.variationId = "unknown_var_id" + mocCmabService.cmabUUID = "test_UUID_1234" self.decisionService = DefaultDecisionService(userProfileService: DefaultUserProfileService(), cmabService: mocCmabService) @@ -843,8 +876,10 @@ extension DecisionServiceTests_Experiments { ) let expectedReasons = DecisionReasons() + expectedReasons.addInfo(LogMessage.cmabFetchSuccess("unknown_var_id", "test_UUID_1234", _expKey: cmabExperiment.key)) expectedReasons.addInfo(LogMessage.userNotBucketedIntoVariation(user.userId)) + let decision = self.decisionService.getVariation(config: config, experiment: cmabExperiment, user: user, @@ -885,6 +920,7 @@ fileprivate struct MockError: Error { fileprivate class MockCmabService: DefaultCmabService { var error: Error? var variationId: String? + var cmabUUID: String? init() { super.init(cmabClient: DefaultCmabClient(), cmabCache: CmabCache(size: 10, timeoutInSecs: 10)) @@ -892,7 +928,7 @@ fileprivate class MockCmabService: DefaultCmabService { override func getDecision(config: ProjectConfig, userContext: OptimizelyUserContext, ruleId: String, options: [OptimizelyDecideOption]) -> Result { if let variationId = self.variationId { - let cmabUUID = UUID().uuidString + let cmabUUID = self.cmabUUID ?? UUID().uuidString return .success(CmabDecision(variationId: variationId, cmabUUID: cmabUUID)) } else { return .failure(self.error ?? MockError()) diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_CMAB.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_CMAB.swift index 84fa2401..eafb6fe9 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_CMAB.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_CMAB.swift @@ -132,7 +132,7 @@ class OptimizelyUserContextTests_Decide_CMAB: XCTestCase { let user = optimizely.createUserContext(userId: kUserId, attributes: ["gender": "f"]) // Test multiple decisions with decideAsync - user.decideAsync(keys: featureKeys, options: [.ignoreUserProfileService]) { decisions in + user.decideAsync(keys: featureKeys) { decisions in // Verify correct number of decisions were returned XCTAssertEqual(decisions.count, 2) @@ -175,7 +175,7 @@ class OptimizelyUserContextTests_Decide_CMAB: XCTestCase { wait(for: [expectation], timeout: 5) // Increased timeout for reliability } - func testDecideAsync_cmabWithUserProfileCahing() { + func testDecideAsync_cmabIgnoreUPSCacheing() { let expectation1 = XCTestExpectation(description: "First CMAB decision") let expectation2 = XCTestExpectation(description: "Second CMAB decision") @@ -192,17 +192,17 @@ class OptimizelyUserContextTests_Decide_CMAB: XCTestCase { attributes: ["gender": "f", "age": 25] ) - // First decision cache into user profile + user.decideAsync(key: "feature_1") { decision in XCTAssertEqual(decision.variationKey, "a") XCTAssertEqual(self.mockCmabService.decisionCallCount, 1) expectation1.fulfill() - // Second decision (should use cache) + // Second decision, ignore UPS, fetch decision again user.decideAsync(key: "feature_1") { decision in XCTAssertEqual(decision.variationKey, "a") - // Call count should still be 1 (cached) - XCTAssertEqual(self.mockCmabService.decisionCallCount, 1) + // Call count should be increased by 1 + XCTAssertEqual(self.mockCmabService.decisionCallCount, 2) expectation2.fulfill() } } @@ -228,17 +228,17 @@ class OptimizelyUserContextTests_Decide_CMAB: XCTestCase { userId: kUserId, attributes: ["gender": "f", "age": 25] ) - user.decideAsync(key: "feature_1", options: [.ignoreUserProfileService, .ignoreCmabCache]) { decision in + user.decideAsync(key: "feature_1", options: [.ignoreCmabCache]) { decision in XCTAssertEqual(decision.variationKey, "a") XCTAssertTrue(self.mockCmabService.ignoreCacheUsed) exp1.fulfill() } - user.decideAsync(key: "feature_1", options: [.ignoreUserProfileService, .resetCmabCache]) { decision in + user.decideAsync(key: "feature_1", options: [.resetCmabCache]) { decision in XCTAssertEqual(decision.variationKey, "a") XCTAssertTrue(self.mockCmabService.resetCacheCache) exp2.fulfill() } - user.decideAsync(key: "feature_1", options: [.ignoreUserProfileService, .invalidateUserCmabCache]) { decision in + user.decideAsync(key: "feature_1", options: [.invalidateUserCmabCache]) { decision in XCTAssertEqual(decision.variationKey, "a") XCTAssertTrue(self.mockCmabService.invalidateUserCmabCache) exp3.fulfill() @@ -263,7 +263,7 @@ class OptimizelyUserContextTests_Decide_CMAB: XCTestCase { attributes: ["gender": "f", "age": 25] ) - user.decideAsync(key: "feature_1", options: [.ignoreUserProfileService, .includeReasons]) { decision in + user.decideAsync(key: "feature_1", options: [.includeReasons]) { decision in XCTAssertTrue(decision.reasons.contains(LogMessage.cmabFetchFailed("exp_with_audience").reason)) expectation.fulfill() }