Skip to content

Commit 093e372

Browse files
Add test cases for cmab experiement
1 parent b9ce0f5 commit 093e372

File tree

7 files changed

+1123
-38
lines changed

7 files changed

+1123
-38
lines changed

OptimizelySwiftSDK.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2014,6 +2014,10 @@
20142014
98137C572A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98137C562A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift */; };
20152015
982C071F2D8C82AE0068B1FF /* HoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982C071E2D8C82AE0068B1FF /* HoldoutTests.swift */; };
20162016
982C07202D8C82AE0068B1FF /* HoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982C071E2D8C82AE0068B1FF /* HoldoutTests.swift */; };
2017+
9841590F2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9841590E2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift */; };
2018+
984159102E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9841590E2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift */; };
2019+
984159122E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984159112E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift */; };
2020+
984159132E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984159112E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift */; };
20172021
984E2FDC2B27199B001F477A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */; };
20182022
984E2FDD2B27199C001F477A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */; };
20192023
984E2FDE2B27199D001F477A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */; };
@@ -2569,6 +2573,8 @@
25692573
98137C542A41E86F004896EB /* OptimizelyClientTests_Init_Async_Await.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Async_Await.swift; sourceTree = "<group>"; };
25702574
98137C562A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_ODP_Aync_Await.swift; sourceTree = "<group>"; };
25712575
982C071E2D8C82AE0068B1FF /* HoldoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutTests.swift; sourceTree = "<group>"; };
2576+
9841590E2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_Async.swift; sourceTree = "<group>"; };
2577+
984159112E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_CMAB.swift; sourceTree = "<group>"; };
25722578
984FE5102CC8AA88004F6F41 /* UserProfileTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileTracker.swift; sourceTree = "<group>"; };
25732579
987F11D92AF3F56F0083D3F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
25742580
989428B22DBFA431008BA1C8 /* MockBucketer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBucketer.swift; sourceTree = "<group>"; };
@@ -3126,6 +3132,8 @@
31263132
6E75198122C5211100B2B157 /* OptimizelyErrorTests.swift */,
31273133
6EC6DD6824AE94820017D296 /* OptimizelyUserContextTests.swift */,
31283134
6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */,
3135+
9841590E2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift */,
3136+
984159112E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift */,
31293137
98D5AE832DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift */,
31303138
6E7E9B362523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift */,
31313139
98AC985D2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift */,
@@ -5005,6 +5013,7 @@
50055013
6E7518C122C520D400B2B157 /* Variable.swift in Sources */,
50065014
6E75170F22C520D400B2B157 /* OptimizelyClient.swift in Sources */,
50075015
6EC6DD4C24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */,
5016+
984159132E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift in Sources */,
50085017
8464087D28130D3200CCF97D /* Integration.swift in Sources */,
50095018
6E7517E922C520D400B2B157 /* DefaultDecisionService.swift in Sources */,
50105019
989428C02DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift in Sources */,
@@ -5025,6 +5034,7 @@
50255034
980CC9012D833F0D00E07D24 /* Holdout.swift in Sources */,
50265035
6EF8DE1724BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */,
50275036
8428D3D12807337400D0FB0C /* LruCacheTests.swift in Sources */,
5037+
984159102E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift in Sources */,
50285038
84861816286D0B8900B7F41B /* VuidManagerTests.swift in Sources */,
50295039
6E7517C522C520D400B2B157 /* DefaultDatafileHandler.swift in Sources */,
50305040
6E75190922C520D500B2B157 /* Attribute.swift in Sources */,
@@ -5297,6 +5307,7 @@
52975307
84861800286CF33700B7F41B /* OdpEvent.swift in Sources */,
52985308
6EC6DD4524ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */,
52995309
6E7517CB22C520D400B2B157 /* DefaultBucketer.swift in Sources */,
5310+
984159122E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift in Sources */,
53005311
845945C1287758A000D13E11 /* OdpConfig.swift in Sources */,
53015312
8464087528130D3200CCF97D /* Integration.swift in Sources */,
53025313
989428C12DBFDCE5008BA1C8 /* DecisionListenerTest_Holdouts.swift in Sources */,
@@ -5317,6 +5328,7 @@
53175328
6E75175D22C520D400B2B157 /* AtomicProperty.swift in Sources */,
53185329
6E7516D922C520D400B2B157 /* OPTUserProfileService.swift in Sources */,
53195330
6E7516E522C520D400B2B157 /* OPTEventDispatcher.swift in Sources */,
5331+
9841590F2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift in Sources */,
53205332
98AC98462DB7B762001405DD /* BucketTests_HoldoutToVariation.swift in Sources */,
53215333
6E7E9B552523F8C6009E4426 /* OptimizelyUserContextTests_Decide_Legacy.swift in Sources */,
53225334
6E652305278E688B00954EA1 /* LruCache.swift in Sources */,

Sources/Optimizely+Decide/OptimizelyClient+Decide.swift

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616

1717
import Foundation
1818

19-
typealias DecideCompletion = (OptimizelyDecision) -> Void
20-
typealias DecideForKeysCompletion = ([String: OptimizelyDecision]) -> Void
21-
2219
extension OptimizelyClient {
2320

2421
/// Create a context of the user for which decision APIs will be called.
@@ -117,14 +114,33 @@ extension OptimizelyClient {
117114
}
118115

119116
func decideAsync(user: OptimizelyUserContext,
120-
keys: [String],
121-
options: [OptimizelyDecideOption]? = nil, completion: @escaping DecideForKeysCompletion) {
117+
keys: [String],
118+
options: [OptimizelyDecideOption]? = nil,
119+
completion: @escaping DecideForKeysCompletion) {
122120
decisionQueue.async {
123121
let decisions = self.decide(user: user, keys: keys, options: options, opType: .async, ignoreDefaultOptions: false)
124122
completion(decisions)
125123
}
126124
}
127125

126+
func decide(user: OptimizelyUserContext,
127+
keys: [String],
128+
options: [OptimizelyDecideOption]? = nil,
129+
ignoreDefaultOptions: Bool) -> [String: OptimizelyDecision] {
130+
return self.decide(user: user, keys: keys, options: options, opType: .sync, ignoreDefaultOptions: ignoreDefaultOptions)
131+
}
132+
133+
func decideAsync(user: OptimizelyUserContext,
134+
keys: [String],
135+
options: [OptimizelyDecideOption]? = nil,
136+
ignoreDefaultOptions: Bool,
137+
completion: @escaping DecideForKeysCompletion) {
138+
decisionQueue.async {
139+
let decisions = self.decide(user: user, keys: keys, options: options, opType: .async, ignoreDefaultOptions: ignoreDefaultOptions)
140+
completion(decisions)
141+
}
142+
}
143+
128144
private func decide(user: OptimizelyUserContext,
129145
keys: [String],
130146
options: [OptimizelyDecideOption]? = nil,
@@ -205,13 +221,32 @@ extension OptimizelyClient {
205221
return decisionMap
206222
}
207223

208-
func decide(user: OptimizelyUserContext,
209-
keys: [String],
210-
options: [OptimizelyDecideOption]? = nil,
211-
ignoreDefaultOptions: Bool) -> [String: OptimizelyDecision] {
212-
return self.decide(user: user, keys: keys, options: options, opType: .sync, ignoreDefaultOptions: ignoreDefaultOptions)
224+
func decideAll(user: OptimizelyUserContext,
225+
options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] {
226+
guard let config = self.config else {
227+
logger.e(OptimizelyError.sdkNotReady)
228+
return [:]
229+
}
230+
231+
return decide(user: user, keys: config.featureFlagKeys, options: options)
213232
}
214233

234+
func decideAllAsync(user: OptimizelyUserContext,
235+
options: [OptimizelyDecideOption]? = nil,
236+
completion: @escaping DecideForKeysCompletion) {
237+
238+
decisionQueue.async {
239+
guard let config = self.config else {
240+
self.logger.e(OptimizelyError.sdkNotReady)
241+
completion([:])
242+
return
243+
}
244+
245+
let decision = self.decide(user: user, keys: config.featureFlagKeys, options: options, opType: .async, ignoreDefaultOptions: false)
246+
completion(decision)
247+
}
248+
}
249+
215250
private func createOptimizelyDecision(flagKey: String,
216251
user: OptimizelyUserContext,
217252
flagDecision: FeatureDecision?,
@@ -286,32 +321,6 @@ extension OptimizelyClient {
286321
reasons: reasonsToReport)
287322
}
288323

289-
func decideAll(user: OptimizelyUserContext,
290-
options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] {
291-
guard let config = self.config else {
292-
logger.e(OptimizelyError.sdkNotReady)
293-
return [:]
294-
}
295-
296-
return decide(user: user, keys: config.featureFlagKeys, options: options)
297-
}
298-
299-
func decideAllAsync(user: OptimizelyUserContext,
300-
options: [OptimizelyDecideOption]? = nil,
301-
completion: @escaping DecideForKeysCompletion) {
302-
303-
decisionQueue.async {
304-
guard let config = self.config else {
305-
self.logger.e(OptimizelyError.sdkNotReady)
306-
completion([:])
307-
return
308-
}
309-
310-
let decision = self.decide(user: user, keys: config.featureFlagKeys, options: options, opType: .async, ignoreDefaultOptions: false)
311-
completion(decision)
312-
}
313-
}
314-
315324
}
316325

317326
// MARK: - Utils

Sources/Optimizely+Decide/OptimizelyUserContext.swift

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
import Foundation
1818

19+
public typealias DecideCompletion = (OptimizelyDecision) -> Void
20+
public typealias DecideForKeysCompletion = ([String: OptimizelyDecision]) -> Void
21+
1922
/// An object for user contexts that the SDK will use to make decisions for.
2023
public class OptimizelyUserContext {
2124
weak var optimizely: OptimizelyClient?
@@ -121,6 +124,42 @@ public class OptimizelyUserContext {
121124
return optimizely.decide(user: clone, key: key, options: options)
122125
}
123126

127+
/// Asynchronously makes a feature decision for a given feature key.
128+
///
129+
/// - Parameters:
130+
/// - key: The feature key to make a decision for
131+
/// - options: Optional array of decision options that will be used for this decision only
132+
/// - completion: A callback that receives the resulting OptimizelyDecision
133+
///
134+
/// - Note: If the SDK is not ready, this method will immediately return an error decision through the completion handler.
135+
public func decideAsync(key: String,
136+
options: [OptimizelyDecideOption]? = nil,
137+
completion: @escaping DecideCompletion) {
138+
139+
guard let optimizely = self.optimizely, let clone = self.clone else {
140+
let decision = OptimizelyDecision.errorDecision(key: key, user: self, error: .sdkNotReady)
141+
completion(decision)
142+
return
143+
}
144+
optimizely.decideAsync(user: clone, key: key, options: options, completion: completion)
145+
146+
}
147+
148+
/// Returns a decision result asynchronously for a given flag key
149+
/// - Parameters:
150+
/// - key: A flag key for which a decision will be made
151+
/// - options: An array of options for decision-making
152+
/// - Returns: A decision result
153+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
154+
public func decideAsync(key: String,
155+
options: [OptimizelyDecideOption]? = nil) async -> OptimizelyDecision {
156+
return await withCheckedContinuation { continuation in
157+
decideAsync(key: key, options: options) { decision in
158+
continuation.resume(returning: decision)
159+
}
160+
}
161+
}
162+
124163
/// Returns a key-map of decision results for multiple flag keys and a user context.
125164
///
126165
/// - If the SDK finds an error (__flagKeyInvalid__, etc) for a key, the response will include a decision for the key showing `reasons` for the error (regardless of __includeReasons__ in options).
@@ -141,6 +180,43 @@ public class OptimizelyUserContext {
141180
return optimizely.decide(user: clone, keys: keys, options: options)
142181
}
143182

183+
/// Asynchronously decides variations for multiple feature flags.
184+
///
185+
/// - Parameters:
186+
/// - keys: An array of feature flag keys.
187+
/// - options: An array of options for decision-making.
188+
/// - completion: A callback that receives a dictionary mapping each feature flag key to its corresponding decision result.
189+
///
190+
/// - Note: If the SDK is not ready, this method will immediately return an empty dictionary through the completion handler.
191+
192+
public func decideAsync(keys: [String],
193+
options: [OptimizelyDecideOption]? = nil,
194+
completion: @escaping DecideForKeysCompletion) {
195+
196+
guard let optimizely = self.optimizely, let clone = self.clone else {
197+
logger.e(OptimizelyError.sdkNotReady)
198+
completion([:])
199+
return
200+
}
201+
202+
optimizely.decideAsync(user: clone, keys: keys, options: options, completion: completion)
203+
}
204+
205+
/// Returns decisions for multiple flag keys asynchronously
206+
/// - Parameters:
207+
/// - keys: An array of flag keys for which decisions will be made
208+
/// - options: An array of options for decision-making
209+
/// - Returns: A dictionary of all decision results, mapped by flag keys
210+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
211+
public func decideAsync(keys: [String],
212+
options: [OptimizelyDecideOption]? = nil) async -> [String: OptimizelyDecision] {
213+
return await withCheckedContinuation { continuation in
214+
decideAsync(keys: keys, options: options) { decisions in
215+
continuation.resume(returning: decisions)
216+
}
217+
}
218+
}
219+
144220
/// Returns a key-map of decision results for all active flag keys.
145221
///
146222
/// - Parameters:
@@ -155,6 +231,37 @@ public class OptimizelyUserContext {
155231
return optimizely.decideAll(user: clone, options: options)
156232
}
157233

234+
/// Asynchronously makes a decision for all features and experiments for this user.
235+
///
236+
/// - Parameters:
237+
/// - options: An array of decision options. If not provided, the default options will be used.
238+
/// - completion: A closure that will be called with the decision results for all keys.
239+
/// The closure takes a dictionary of feature/experiment keys to their corresponding decision results.
240+
///
241+
/// - Note: The completion handler will be called on a background queue. If you need to update the UI, dispatch to the main queue within the completion handler.
242+
243+
public func decideAllAsync(options: [OptimizelyDecideOption]? = nil, completion: @escaping DecideForKeysCompletion) {
244+
guard let optimizely = self.optimizely, let clone = self.clone else {
245+
logger.e(OptimizelyError.sdkNotReady)
246+
completion([:])
247+
return
248+
}
249+
250+
optimizely.decideAllAsync(user: clone, options: options, completion: completion)
251+
}
252+
253+
/// Returns decisions for all active flag keys asynchronously
254+
/// - Parameter options: An array of options for decision-making
255+
/// - Returns: A dictionary of all decision results, mapped by flag keys
256+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
257+
public func decideAllAsync(options: [OptimizelyDecideOption]? = nil) async -> [String: OptimizelyDecision] {
258+
return await withCheckedContinuation { continuation in
259+
decideAllAsync(options: options) { decisions in
260+
continuation.resume(returning: decisions)
261+
}
262+
}
263+
}
264+
158265
/// Tracks an event.
159266
///
160267
/// - Parameters:

Sources/Utils/LogMessage.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ enum LogMessage {
5858
case userNotBucketedIntoAnyExperimentInGroup(_ userId: String, _ group: String)
5959
case userBucketedIntoInvalidExperiment(_ id: String)
6060
case userNotInExperiment(_ userId: String, _ expKey: String)
61+
case userNotInCmabExperiment(_ userId: String, _ expKey: String)
6162
case userReceivedDefaultVariableValue(_ userId: String, _ feature: String, _ variable: String)
6263
case userReceivedAllDefaultVariableValues(_ userId: String, _ feature: String)
6364
case featureNotEnabledReturnDefaultVariableValue(_ userId: String, _ feature: String, _ variable: String)
@@ -129,6 +130,7 @@ extension LogMessage: CustomStringConvertible {
129130
case .userNotBucketedIntoAnyExperimentInGroup(let userId, let group): message = "User (\(userId)) is not in any experiment of group (\(group))."
130131
case .userBucketedIntoInvalidExperiment(let id): message = "Bucketed into an invalid experiment id (\(id))"
131132
case .userNotInExperiment(let userId, let expKey): message = "User (\(userId)) does not meet conditions to be in experiment (\(expKey))."
133+
case .userNotInCmabExperiment(let userId, let expKey): message = "User (\(userId)) does not fall into cmab taffic allocation in experiment (\(expKey))."
132134
case .userReceivedDefaultVariableValue(let userId, let feature, let variable): message = "User (\(userId)) is not in any variation or rollout rule. Returning default value for variable (\(variable)) of feature flag (\(feature))."
133135
case .userReceivedAllDefaultVariableValues(let userId, let feature): message = "User (\(userId)) is not in any variation or rollout rule. Returning default value for all variables of feature flag (\(feature))."
134136
case .featureNotEnabledReturnDefaultVariableValue(let userId, let feature, let variable): message = "Feature (\(feature)) is not enabled for user (\(userId)). Returning the default variable value (\(variable))."

0 commit comments

Comments
 (0)