Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions DemoSwiftApp/Samples/SamplesForAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
Expand All @@ -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)")
})
Expand Down Expand Up @@ -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)")
}
Expand All @@ -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)")
})
Expand Down
7 changes: 6 additions & 1 deletion Sources/Implementation/DefaultDecisionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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%
Expand Down Expand Up @@ -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)

Expand All @@ -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,
Expand Down Expand Up @@ -885,14 +920,15 @@ 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))
}

override func getDecision(config: ProjectConfig, userContext: OptimizelyUserContext, ruleId: String, options: [OptimizelyDecideOption]) -> Result<CmabDecision, any Error> {
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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")

Expand All @@ -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()
}
}
Expand All @@ -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()
Expand All @@ -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()
}
Expand Down
Loading