Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 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
eab9755
Update code documentation
muzahidul-opti Jul 9, 2025
771f279
Correct spelling mistake
muzahidul-opti Jul 9, 2025
2cd2981
Add explanation about async behavior
muzahidul-opti Jul 10, 2025
45140d9
Add comment about cmab error handling
muzahidul-opti Jul 11, 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
140 changes: 64 additions & 76 deletions Sources/Implementation/DefaultDecisionService.swift

Large diffs are not rendered by default.

121 changes: 80 additions & 41 deletions Sources/Optimizely+Decide/OptimizelyClient+Decide.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ extension OptimizelyClient {
return createUserContext(userId: userId,
attributes: (attributes ?? [:]) as [String: Any])
}

/// Create a user context to be used internally without sending an ODP identify event.
///
/// - Parameters:
Expand All @@ -62,6 +62,13 @@ extension OptimizelyClient {
identify: false)
}

/// Returns a decision result for a given flag key
///
/// - Parameters:
/// - user: The user context for which the decision is being made
/// - key: The feature flag key to evaluate
/// - options: An array of options for decision-making.
/// - Returns: An OptimizelyDecision representing the flag decision
func decide(user: OptimizelyUserContext,
key: String,
options: [OptimizelyDecideOption]? = nil) -> OptimizelyDecision {
Expand All @@ -73,14 +80,22 @@ extension OptimizelyClient {
guard let _ = config.getFeatureFlag(key: key) else {
return OptimizelyDecision.errorDecision(key: key, user: user, error: .featureKeyInvalid(key))
}

var allOptions = defaultDecideOptions + (options ?? [])
// Filtering out `enabledFlagsOnly` to ensure users always get a result.
allOptions.removeAll(where: { $0 == .enabledFlagsOnly })

let decisionMap = decide(user: user, keys: [key], options: allOptions, opType: .sync, ignoreDefaultOptions: true)
let decisionMap = decide(user: user, keys: [key], options: allOptions, isAsync: false, ignoreDefaultOptions: true)
return decisionMap[key] ?? OptimizelyDecision.errorDecision(key: key, user: user, error: .generic)
}

/// Returns a decision result for a given key asynchronously
///
/// - Parameters:
/// - user: The user context for which the decision is being made
/// - key: The feature flag key to evaluate
/// - options: An array of options for decision-making.
/// - completion: Handler will be called with a OptimizelyDecision
func decideAsync(user: OptimizelyUserContext,
key: String,
options: [OptimizelyDecideOption]? = nil,
Expand All @@ -99,53 +114,59 @@ extension OptimizelyClient {
}

var allOptions = self.defaultDecideOptions + (options ?? [])
// Filtering out `enabledFlagsOnly` to ensure users always get a result.
allOptions.removeAll(where: { $0 == .enabledFlagsOnly })

let decisionMap = self.decide(user: user, keys: [key], options: allOptions, opType: .async, ignoreDefaultOptions: true)
let decisionMap = self.decide(user: user, keys: [key], options: allOptions, isAsync: true, ignoreDefaultOptions: true)
let decision = decisionMap[key] ?? OptimizelyDecision.errorDecision(key: key, user: user, error: .generic)
completion(decision)
}
}

/// Returns a key-map of decision results for multiple flag keys
///
/// - Parameters:
/// - user: The user context for which the decisions are being made
/// - keys: The feature flag keys to evaluate
/// - options: An array of options for decision-making.
/// - Returns: A dictionary of all decision results, mapped by flag keys.
func decide(user: OptimizelyUserContext,
keys: [String],
options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] {
return decide(user: user, keys: keys, options: options, opType: .sync, ignoreDefaultOptions: false)
}

func decideAsync(user: OptimizelyUserContext,
keys: [String],
options: [OptimizelyDecideOption]? = nil,
completion: @escaping DecideForKeysCompletion) {
decisionQueue.async {
let decisions = self.decide(user: user, keys: keys, options: options, opType: .async, ignoreDefaultOptions: false)
completion(decisions)
}
}

func decide(user: OptimizelyUserContext,
keys: [String],
options: [OptimizelyDecideOption]? = nil,
ignoreDefaultOptions: Bool) -> [String: OptimizelyDecision] {
return self.decide(user: user, keys: keys, options: options, opType: .sync, ignoreDefaultOptions: ignoreDefaultOptions)
return decide(user: user, keys: keys, options: options, isAsync: false)
}

/// Returns a decision result for a given key asynchronously
///
/// - Parameters:
/// - user: The user context for which the decision is being made
/// - keys: The feature flag keys to evaluate
/// - options: An array of options for decision-making
/// - completion: Handler will be called with a dictionary mapping feature flag keys to OptimizelyDecision
func decideAsync(user: OptimizelyUserContext,
keys: [String],
options: [OptimizelyDecideOption]? = nil,
ignoreDefaultOptions: Bool,
completion: @escaping DecideForKeysCompletion) {
decisionQueue.async {
let decisions = self.decide(user: user, keys: keys, options: options, opType: .async, ignoreDefaultOptions: ignoreDefaultOptions)
let decisions = self.decide(user: user, keys: keys, options: options, isAsync: true)
completion(decisions)
}
}

/// Returns a key-map of decision results for multiple flag keys
///
/// - Parameters:
/// - user: The user context for which to make the decision
/// - keys: Array of feature flag keys to decide upon
/// - options: Optional array of decision options that override default behavior
/// - isAsync: Boolean indicating whether the operation is asynchronous
/// - ignoreDefaultOptions: Boolean indicating whether to ignore default decide options
/// - Returns: A dictionary of all decision results, mapped by flag keys.
private func decide(user: OptimizelyUserContext,
keys: [String],
options: [OptimizelyDecideOption]? = nil,
opType: OPType,
ignoreDefaultOptions: Bool) -> [String: OptimizelyDecision] {
isAsync: Bool,
ignoreDefaultOptions: Bool = false) -> [String: OptimizelyDecision] {
guard let config = self.config else {
logger.e(OptimizelyError.sdkNotReady)
return [:]
Expand Down Expand Up @@ -187,7 +208,7 @@ extension OptimizelyClient {
}
}

let decisionList = (decisionService as? DefaultDecisionService)?.getVariationForFeatureList(config: config, featureFlags: flagsWithoutForceDecision, user: user, opType: opType, options: allOptions)
let decisionList = (decisionService as? DefaultDecisionService)?.getVariationForFeatureList(config: config, featureFlags: flagsWithoutForceDecision, user: user, isAsync: isAsync, options: allOptions)

for index in 0..<flagsWithoutForceDecision.count {
if decisionList?.indices.contains(index) ?? false {
Expand Down Expand Up @@ -221,6 +242,16 @@ extension OptimizelyClient {
return decisionMap
}

/// Returns a key-map of decision results for all flag keys
///
/// This method evaluates all feature flags in the current configuration for the provided user.
/// It returns a dictionary mapping feature flag keys to their respective decisions.
///
/// - Parameters:
/// - user: The user context for which decisions are made.
/// - options: Optional array of decision options that affect how decisions are made. Default is nil.
/// - Returns: A dictionary of all decision results, mapped by flag keys.
/// - Returns an empty dictionary if the SDK is not ready.
func decideAll(user: OptimizelyUserContext,
options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] {
guard let config = self.config else {
Expand All @@ -231,6 +262,14 @@ extension OptimizelyClient {
return decide(user: user, keys: config.featureFlagKeys, options: options)
}

/// Asynchronously evaluates all feature flags and returns the decisions.
///
/// This method will return decisions for all feature flags in the project.
///
/// - Parameters:
/// - user: The user context for which to evaluate the feature flags
/// - options: An array of options for decision-making. Default is nil.
/// - completion: Handler will be called with a dictionary mapping feature flag keys to OptimizelyDecision
func decideAllAsync(user: OptimizelyUserContext,
options: [OptimizelyDecideOption]? = nil,
completion: @escaping DecideForKeysCompletion) {
Expand All @@ -242,11 +281,11 @@ extension OptimizelyClient {
return
}

let decision = self.decide(user: user, keys: config.featureFlagKeys, options: options, opType: .async, ignoreDefaultOptions: false)
let decision = self.decide(user: user, keys: config.featureFlagKeys, options: options, isAsync: true, ignoreDefaultOptions: false)
completion(decision)
}
}

private func createOptimizelyDecision(flagKey: String,
user: OptimizelyUserContext,
flagDecision: FeatureDecision?,
Expand All @@ -260,7 +299,7 @@ extension OptimizelyClient {

let userId = user.userId
let attributes = user.attributes
let flagEnabled = flagDecision?.variation.featureEnabled ?? false
let flagEnabled = flagDecision?.variation?.featureEnabled ?? false

logger.i("Feature \(flagKey) is enabled for user \(userId) \(flagEnabled)")

Expand Down Expand Up @@ -312,7 +351,7 @@ extension OptimizelyClient {
reasons: reasonsToReport,
decisionEventDispatched: decisionEventDispatched))

return OptimizelyDecision(variationKey: flagDecision?.variation.key,
return OptimizelyDecision(variationKey: flagDecision?.variation?.key,
enabled: flagEnabled,
variables: optimizelyJSON,
ruleKey: ruleKey,
Expand Down Expand Up @@ -357,16 +396,16 @@ extension OptimizelyClient {

if let valueType = Constants.VariableValueType(rawValue: type) {
switch valueType {
case .string:
break
case .integer:
valueParsed = Int(value)
case .double:
valueParsed = Double(value)
case .boolean:
valueParsed = Bool(value)
case .json:
valueParsed = OptimizelyJSON(payload: value)?.toMap()
case .string:
break
case .integer:
valueParsed = Int(value)
case .double:
valueParsed = Double(value)
case .boolean:
valueParsed = Bool(value)
case .json:
valueParsed = OptimizelyJSON(payload: value)?.toMap()
}
}

Expand Down
29 changes: 18 additions & 11 deletions Sources/Optimizely+Decide/OptimizelyUserContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,28 +131,31 @@ public class OptimizelyUserContext {
/// - options: Optional array of decision options that will be used for this decision only
/// - completion: A callback that receives the resulting OptimizelyDecision
///
/// - Note: If the SDK is not ready, this method will immediately return an error decision through the completion handler.
/// - Note:
/// - If the SDK is not ready, this method will immediately return an error decision through the completion handler.
/// - 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.
public func decideAsync(key: String,
options: [OptimizelyDecideOption]? = nil,
completion: @escaping DecideCompletion) {
options: [OptimizelyDecideOption]? = nil,
completion: @escaping DecideCompletion) {

guard let optimizely = self.optimizely, let clone = self.clone else {
let decision = OptimizelyDecision.errorDecision(key: key, user: self, error: .sdkNotReady)
completion(decision)
return
}
optimizely.decideAsync(user: clone, key: key, options: options, completion: completion)

}

/// Returns a decision result asynchronously for a given flag key
/// - Parameters:
/// - key: A flag key for which a decision will be made
/// - options: An array of options for decision-making
/// - Returns: A decision result
///
/// - 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.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public func decideAsync(key: String,
options: [OptimizelyDecideOption]? = nil) async -> OptimizelyDecision {
public func decideAsync(key: String,
options: [OptimizelyDecideOption]? = nil) async -> OptimizelyDecision {
return await withCheckedContinuation { continuation in
decideAsync(key: key, options: options) { decision in
continuation.resume(returning: decision)
Expand Down Expand Up @@ -187,8 +190,9 @@ public class OptimizelyUserContext {
/// - options: An array of options for decision-making.
/// - completion: A callback that receives a dictionary mapping each feature flag key to its corresponding decision result.
///
/// - Note: If the SDK is not ready, this method will immediately return an empty dictionary through the completion handler.

/// - Note:
/// - If the SDK is not ready, this method will immediately return an error decision through the completion handler.
/// - 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.
public func decideAsync(keys: [String],
options: [OptimizelyDecideOption]? = nil,
completion: @escaping DecideForKeysCompletion) {
Expand All @@ -207,9 +211,11 @@ public class OptimizelyUserContext {
/// - keys: An array of flag keys for which decisions will be made
/// - options: An array of options for decision-making
/// - Returns: A dictionary of all decision results, mapped by flag keys
///
/// - 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.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public func decideAsync(keys: [String],
options: [OptimizelyDecideOption]? = nil) async -> [String: OptimizelyDecision] {
public func decideAsync(keys: [String],
options: [OptimizelyDecideOption]? = nil) async -> [String: OptimizelyDecision] {
return await withCheckedContinuation { continuation in
decideAsync(keys: keys, options: options) { decisions in
continuation.resume(returning: decisions)
Expand Down Expand Up @@ -239,7 +245,6 @@ public class OptimizelyUserContext {
/// The closure takes a dictionary of feature/experiment keys to their corresponding decision results.
///
/// - 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.

public func decideAllAsync(options: [OptimizelyDecideOption]? = nil, completion: @escaping DecideForKeysCompletion) {
guard let optimizely = self.optimizely, let clone = self.clone else {
logger.e(OptimizelyError.sdkNotReady)
Expand All @@ -253,6 +258,8 @@ public class OptimizelyUserContext {
/// Returns decisions for all active flag keys asynchronously
/// - Parameter options: An array of options for decision-making
/// - Returns: A dictionary of all decision results, mapped by flag keys
///
/// - 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.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public func decideAllAsync(options: [OptimizelyDecideOption]? = nil) async -> [String: OptimizelyDecision] {
return await withCheckedContinuation { continuation in
Expand Down
10 changes: 5 additions & 5 deletions Sources/Optimizely/OptimizelyClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ open class OptimizelyClient: NSObject {
options: nil).result

let source = pair?.source ?? Constants.DecisionSource.rollout.rawValue
let featureEnabled = pair?.variation.featureEnabled ?? false
let featureEnabled = pair?.variation?.featureEnabled ?? false
if featureEnabled {
logger.i(.featureEnabledForUser(featureKey, userId))
} else {
Expand Down Expand Up @@ -588,8 +588,8 @@ open class OptimizelyClient: NSObject {
user: makeInternalUserContext(userId: userId, attributes: attributes),
options: nil).result
if let decision = decision {
if let featureVariable = decision.variation.variables?.filter({$0.id == variable.id}).first {
if let featureEnabled = decision.variation.featureEnabled, featureEnabled {
if let featureVariable = decision.variation?.variables?.filter({$0.id == variable.id}).first {
if let featureEnabled = decision.variation?.featureEnabled, featureEnabled {
featureValue = featureVariable.value
logger.i(.userReceivedVariableValue(featureValue, variableKey, featureKey))
} else {
Expand Down Expand Up @@ -678,7 +678,7 @@ open class OptimizelyClient: NSObject {
featureFlag: featureFlag,
user: makeInternalUserContext(userId: userId, attributes: attributes),
options: nil).result
if let featureEnabled = decision?.variation.featureEnabled {
if let featureEnabled = decision?.variation?.featureEnabled {
enabled = featureEnabled
if featureEnabled {
logger.i(.featureEnabledForUser(featureKey, userId))
Expand All @@ -691,7 +691,7 @@ open class OptimizelyClient: NSObject {

for v in featureFlag.variables {
var featureValue = v.defaultValue ?? ""
if enabled, let variable = decision?.variation.getVariable(id: v.id) {
if enabled, let variable = decision?.variation?.getVariable(id: v.id) {
featureValue = variable.value
}

Expand Down
Loading
Loading