Skip to content

[FSSDK-11181] chore: add cmab uuid into impression event meta #603

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Jul 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b40adda
wip: cmab client done
muzahidul-opti Jun 18, 2025
9c9b9b0
Update test cases
muzahidul-opti Jun 18, 2025
d998e0b
wip: add test cases for cmab services
muzahidul-opti Jun 20, 2025
c427a46
Merge branch 'master' into muzahid/cmab-service
muzahidul-opti Jun 24, 2025
8e168f6
Added log for cmab decision
muzahidul-opti Jun 24, 2025
0c4d072
Update copyright date
muzahidul-opti Jun 24, 2025
f342ae2
CMAB decision implemented
muzahidul-opti Jun 26, 2025
ebbde3c
CmabService testcases added for sync getDecision function
muzahidul-opti Jun 27, 2025
92d3140
Add factory method for DefaultCmabService
muzahidul-opti Jun 27, 2025
c6507b0
DefaultDecision initializer updated
muzahidul-opti Jun 27, 2025
b51c228
Add operation type enum
muzahidul-opti Jun 27, 2025
e9d01ac
CMAB not supported in sync mode
muzahidul-opti Jun 27, 2025
e827b27
Reuse experiment bucketing logic
muzahidul-opti Jun 27, 2025
445006b
Add seperate method for group exlusion, add bucketToEntityId method
muzahidul-opti Jun 30, 2025
b9ce0f5
Update bucketing logic for cmab experiment
muzahidul-opti Jun 30, 2025
093e372
Add test cases for cmab experiement
muzahidul-opti Jun 30, 2025
f2cc7df
Add test cases for cmab decision options
muzahidul-opti Jul 2, 2025
f0c1d5b
Merge branch 'master' into muzahid/cmab-decision
muzahidul-opti Jul 2, 2025
384bfa8
Add test cases for bucketToEntity
muzahidul-opti Jul 3, 2025
87bcecf
Return feature dicision with nil variation for CMAB fetch error
muzahidul-opti Jul 4, 2025
d1067d6
Add code doc and fix linting issue
muzahidul-opti Jul 4, 2025
0d761c9
Update cmab entity id matching logic
muzahidul-opti Jul 4, 2025
bd2a47c
Add cmab uuid to metadata
muzahidul-opti Jul 7, 2025
eff199b
Add test for impression event
muzahidul-opti Jul 7, 2025
17335e1
Merge branch 'master' into muzahid/cmab-impression-event
muzahidul-opti Jul 14, 2025
4584a69
clean up
muzahidul-opti Jul 14, 2025
ee38dfb
pass cmabuuid to feature decision
muzahidul-opti Jul 14, 2025
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
2 changes: 2 additions & 0 deletions Sources/Data Model/DispatchEvents/BatchEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,15 @@ struct DecisionMetadata: Codable, Equatable {
let flagKey: String
let variationKey: String
let enabled: Bool
var cmabUUID: String?

enum CodingKeys: String, CodingKey {
case ruleType = "rule_type"
case ruleKey = "rule_key"
case flagKey = "flag_key"
case variationKey = "variation_key"
case enabled = "enabled"
case cmabUUID = "cmab_uuid"
}
}

Expand Down
3 changes: 2 additions & 1 deletion Sources/Implementation/DefaultDecisionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ class DefaultDecisionService: OPTDecisionService {
}

let flagExpDecision = getVariationForFeatureExperiments(config: config, featureFlag: featureFlag, user: user, userProfileTracker: userProfileTracker, isAsync: isAsync, options: options)

reasons.merge(flagExpDecision.reasons)

if let decision = flagExpDecision.result {
Expand Down Expand Up @@ -459,7 +460,7 @@ class DefaultDecisionService: OPTDecisionService {
let featureDecision = FeatureDecision(experiment: experiment, variation: nil, source: Constants.DecisionSource.featureTest.rawValue)
return DecisionResponse(result: featureDecision, reasons: reasons)
} else if let variation = result.variation {
let featureDecision = FeatureDecision(experiment: experiment, variation: variation, source: Constants.DecisionSource.featureTest.rawValue)
let featureDecision = FeatureDecision(experiment: experiment, variation: variation, source: Constants.DecisionSource.featureTest.rawValue, cmabUUID: result.cmabUUID)
return DecisionResponse(result: featureDecision, reasons: reasons)
}
}
Expand Down
5 changes: 3 additions & 2 deletions Sources/Implementation/Events/BatchEventBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ class BatchEventBuilder {
attributes: OptimizelyAttributes?,
flagKey: String,
ruleType: String,
enabled: Bool) -> Data? {
enabled: Bool,
cmabUUID: String?) -> Data? {

let metaData = DecisionMetadata(ruleType: ruleType, ruleKey: experiment?.key ?? "", flagKey: flagKey, variationKey: variation?.key ?? "", enabled: enabled)
let metaData = DecisionMetadata(ruleType: ruleType, ruleKey: experiment?.key ?? "", flagKey: flagKey, variationKey: variation?.key ?? "", enabled: enabled, cmabUUID: cmabUUID)

let decision = Decision(variationID: variation?.id ?? "",
campaignID: experiment?.layerId ?? "",
Expand Down
3 changes: 2 additions & 1 deletion Sources/Optimizely+Decide/OptimizelyClient+Decide.swift
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ extension OptimizelyClient {
attributes: attributes,
flagKey: feature.key,
ruleType: ruleType,
enabled: flagEnabled)
enabled: flagEnabled,
cmabUUID: flagDecision?.cmabUUID)
decisionEventDispatched = true
}
}
Expand Down
12 changes: 8 additions & 4 deletions Sources/Optimizely/OptimizelyClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ open class OptimizelyClient: NSObject {
attributes: attributes,
flagKey: "",
ruleType: Constants.DecisionSource.experiment.rawValue,
enabled: true)
enabled: true,
cmabUUID: nil)

return variation.key
}
Expand Down Expand Up @@ -452,7 +453,8 @@ open class OptimizelyClient: NSObject {
attributes: attributes,
flagKey: featureKey,
ruleType: source,
enabled: featureEnabled)
enabled: featureEnabled,
cmabUUID: pair?.cmabUUID)
}

sendDecisionNotification(userId: userId,
Expand Down Expand Up @@ -817,7 +819,8 @@ extension OptimizelyClient {
attributes: OptimizelyAttributes? = nil,
flagKey: String,
ruleType: String,
enabled: Bool) {
enabled: Bool,
cmabUUID: String?) {

// non-blocking (event data serialization takes time)
eventLock.async {
Expand All @@ -830,7 +833,8 @@ extension OptimizelyClient {
attributes: attributes,
flagKey: flagKey,
ruleType: ruleType,
enabled: enabled) else {
enabled: enabled,
cmabUUID: cmabUUID) else {
self.logger.e(OptimizelyError.eventBuildFailure(DispatchEvent.activateEventKey))
return
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/OptimizelyTests-APIs/OptimizelyClientTests_Others.swift
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ class OptimizelyClientTests_Others: XCTestCase {
// set invalid (infinity) to attribute values, which will cause JSONEncoder.encode exception
let attributes = ["testvar": Double.infinity]

optimizely.sendImpressionEvent(experiment: experiment, variation: variation, userId: kUserId, attributes: attributes, flagKey: "", ruleType: Constants.DecisionSource.rollout.rawValue, enabled: true)
optimizely.sendImpressionEvent(experiment: experiment, variation: variation, userId: kUserId, attributes: attributes, flagKey: "", ruleType: Constants.DecisionSource.rollout.rawValue, enabled: true, cmabUUID: nil)
XCTAssert(eventDispatcher.events.count == 0)
}

Expand Down Expand Up @@ -321,7 +321,7 @@ class OptimizelyClientTests_Others: XCTestCase {
// force condition for sdk-not-ready
optimizely.config = nil

optimizely.sendImpressionEvent(experiment: experiment, variation: variation, userId: kUserId, flagKey: experiment.key, ruleType: Constants.DecisionSource.rollout.rawValue, enabled: true)
optimizely.sendImpressionEvent(experiment: experiment, variation: variation, userId: kUserId, flagKey: experiment.key, ruleType: Constants.DecisionSource.rollout.rawValue, enabled: true, cmabUUID: nil)
XCTAssert(eventDispatcher.events.isEmpty, "event should not be sent out sdk is not configured properly")

optimizely.sendConversionEvent(eventKey: kEventKey, userId: kUserId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class BatchEventBuilderTests_Events: XCTestCase {
XCTAssertEqual(metaData["rule_key"] as! String, "ab_running_exp_audience_combo_exact_foo_or_true__and__42_or_4_2")
XCTAssertEqual(metaData["flag_key"] as! String, "")
XCTAssertEqual(metaData["variation_key"] as! String, "all_traffic_variation")
XCTAssertNil(metaData["cmab_uuid"])
XCTAssertTrue(metaData["enabled"] as! Bool)

let de = (snapshot["events"] as! Array<Dictionary<String, Any>>)[0]
Expand Down Expand Up @@ -212,7 +213,7 @@ class BatchEventBuilderTests_Events: XCTestCase {
let experiment = optimizely.config?.getExperiment(id: "10390977714")

optimizely.config?.project.sendFlagDecisions = true
let event = BatchEventBuilder.createImpressionEvent(config: optimizely.config!, experiment: experiment!, variation: nil, userId: userId, attributes: attributes, flagKey: experiment!.key, ruleType: Constants.DecisionSource.featureTest.rawValue, enabled: false)
let event = BatchEventBuilder.createImpressionEvent(config: optimizely.config!, experiment: experiment!, variation: nil, userId: userId, attributes: attributes, flagKey: experiment!.key, ruleType: Constants.DecisionSource.featureTest.rawValue, enabled: false, cmabUUID: "cmab_uuid_124")
XCTAssertNotNil(event)

let visitor = (getEventJSON(data: event!)!["visitors"] as! Array<Dictionary<String, Any>>)[0]
Expand All @@ -224,14 +225,15 @@ class BatchEventBuilderTests_Events: XCTestCase {
XCTAssertEqual(metaData["rule_key"] as! String, "ab_running_exp_audience_combo_exact_foo_or_true__and__42_or_4_2")
XCTAssertEqual(metaData["flag_key"] as! String, "ab_running_exp_audience_combo_exact_foo_or_true__and__42_or_4_2")
XCTAssertEqual(metaData["variation_key"] as! String, "")
XCTAssertEqual(metaData["cmab_uuid"] as! String, "cmab_uuid_124")
XCTAssertFalse(metaData["enabled"] as! Bool)
optimizely.config?.project.sendFlagDecisions = nil
}

func testCreateImpressionEventWithoutExperimentAndVariation() {

optimizely.config?.project.sendFlagDecisions = true
let event = BatchEventBuilder.createImpressionEvent(config: optimizely.config!, experiment: nil, variation: nil, userId: userId, attributes: [String: Any](), flagKey: "feature_1", ruleType: Constants.DecisionSource.rollout.rawValue, enabled: true)
let event = BatchEventBuilder.createImpressionEvent(config: optimizely.config!, experiment: nil, variation: nil, userId: userId, attributes: [String: Any](), flagKey: "feature_1", ruleType: Constants.DecisionSource.rollout.rawValue, enabled: true, cmabUUID: nil)
XCTAssertNotNil(event)

let visitor = (getEventJSON(data: event!)!["visitors"] as! Array<Dictionary<String, Any>>)[0]
Expand All @@ -243,6 +245,7 @@ class BatchEventBuilderTests_Events: XCTestCase {
XCTAssertEqual(metaData["rule_key"] as! String, "")
XCTAssertEqual(metaData["flag_key"] as! String, "feature_1")
XCTAssertEqual(metaData["variation_key"] as! String, "")
XCTAssertEqual(metaData["cmab_uuid"] as? String, nil)
XCTAssertTrue(metaData["enabled"] as! Bool)
optimizely.config?.project.sendFlagDecisions = nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,30 @@ class OptimizelyUserContextTests_Decide_CMAB: XCTestCase {
XCTAssertTrue(self.mockCmabService.decisionCalled, "CMAB decision service was not called")
XCTAssertEqual(self.mockCmabService.lastRuleId, "10390977673", "Expected CMAB rule id '10390977673' but got \(String(describing: self.mockCmabService.lastRuleId))")

// Verify impression event
self.optimizely.eventLock.sync {}

guard let event = self.getFirstEventJSON(client: self.optimizely) else {
XCTFail("No impression event found")
expectation.fulfill()
return
}

let visitor = (event["visitors"] as! Array<Dictionary<String, Any>>)[0]
let snapshot = (visitor["snapshots"] as! Array<Dictionary<String, Any>>)[0]
let decision = (snapshot["decisions"] as! Array<Dictionary<String, Any>>)[0]
let metaData = decision["metadata"] as! Dictionary<String, Any>

// Verify event metadata
XCTAssertEqual(metaData["rule_type"] as! String, Constants.DecisionSource.featureTest.rawValue)
XCTAssertEqual(metaData["rule_key"] as! String, "exp_with_audience")
XCTAssertEqual(metaData["flag_key"] as! String, "feature_1")
XCTAssertEqual(metaData["variation_key"] as! String, "a")
XCTAssertEqual(metaData["cmab_uuid"] as? String, "test-uuid")
XCTAssertTrue(metaData["enabled"] as! Bool)

expectation.fulfill()

}

wait(for: [expectation], timeout: 5) // Increased timeout for reliability
Expand Down Expand Up @@ -285,3 +308,31 @@ fileprivate class MockCmabService: DefaultCmabService {
return .failure(CmabClientError.fetchFailed("No variation set"))
}
}

extension OptimizelyUserContextTests_Decide_CMAB {

func getFirstEvent(dispatcher: MockEventDispatcher) -> EventForDispatch? {
optimizely.eventLock.sync{}
return dispatcher.events.first
}

func getFirstEventJSON(dispatcher: MockEventDispatcher) -> [String: Any]? {
guard let event = getFirstEvent(dispatcher: dispatcher) else { return nil }

let json = try! JSONSerialization.jsonObject(with: event.body, options: .allowFragments) as! [String: Any]
return json
}

func getFirstEventJSON(client: OptimizelyClient) -> [String: Any]? {
guard let event = getFirstEvent(dispatcher: client.eventDispatcher as! MockEventDispatcher) else { return nil }

let json = try! JSONSerialization.jsonObject(with: event.body, options: .allowFragments) as! [String: Any]
return json
}

func getEventJSON(data: Data) -> [String: Any]? {
let json = try! JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
return json
}

}
Loading