Skip to content

Commit 17335e1

Browse files
Merge branch 'master' into muzahid/cmab-impression-event
2 parents eff199b + 5baf45e commit 17335e1

File tree

7 files changed

+157
-111
lines changed

7 files changed

+157
-111
lines changed

Sources/Implementation/DefaultDecisionService.swift

Lines changed: 61 additions & 53 deletions
Large diffs are not rendered by default.

Sources/Optimizely+Decide/OptimizelyClient+Decide.swift

Lines changed: 78 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ extension OptimizelyClient {
4848
return createUserContext(userId: userId,
4949
attributes: (attributes ?? [:]) as [String: Any])
5050
}
51-
51+
5252
/// Create a user context to be used internally without sending an ODP identify event.
5353
///
5454
/// - Parameters:
@@ -62,6 +62,13 @@ extension OptimizelyClient {
6262
identify: false)
6363
}
6464

65+
/// Returns a decision result for a given flag key
66+
///
67+
/// - Parameters:
68+
/// - user: The user context for which the decision is being made
69+
/// - key: The feature flag key to evaluate
70+
/// - options: An array of options for decision-making.
71+
/// - Returns: An OptimizelyDecision representing the flag decision
6572
func decide(user: OptimizelyUserContext,
6673
key: String,
6774
options: [OptimizelyDecideOption]? = nil) -> OptimizelyDecision {
@@ -73,14 +80,22 @@ extension OptimizelyClient {
7380
guard let _ = config.getFeatureFlag(key: key) else {
7481
return OptimizelyDecision.errorDecision(key: key, user: user, error: .featureKeyInvalid(key))
7582
}
76-
83+
7784
var allOptions = defaultDecideOptions + (options ?? [])
85+
// Filtering out `enabledFlagsOnly` to ensure users always get a result.
7886
allOptions.removeAll(where: { $0 == .enabledFlagsOnly })
7987

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

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

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

104-
let decisionMap = self.decide(user: user, keys: [key], options: allOptions, opType: .async, ignoreDefaultOptions: true)
120+
let decisionMap = self.decide(user: user, keys: [key], options: allOptions, isAsync: true, ignoreDefaultOptions: true)
105121
let decision = decisionMap[key] ?? OptimizelyDecision.errorDecision(key: key, user: user, error: .generic)
106122
completion(decision)
107123
}
108124
}
109125

126+
/// Returns a key-map of decision results for multiple flag keys
127+
///
128+
/// - Parameters:
129+
/// - user: The user context for which the decisions are being made
130+
/// - keys: The feature flag keys to evaluate
131+
/// - options: An array of options for decision-making.
132+
/// - Returns: A dictionary of all decision results, mapped by flag keys.
110133
func decide(user: OptimizelyUserContext,
111134
keys: [String],
112135
options: [OptimizelyDecideOption]? = nil) -> [String: OptimizelyDecision] {
113-
return decide(user: user, keys: keys, options: options, opType: .sync, ignoreDefaultOptions: false)
114-
}
115-
116-
func decideAsync(user: OptimizelyUserContext,
117-
keys: [String],
118-
options: [OptimizelyDecideOption]? = nil,
119-
completion: @escaping DecideForKeysCompletion) {
120-
decisionQueue.async {
121-
let decisions = self.decide(user: user, keys: keys, options: options, opType: .async, ignoreDefaultOptions: false)
122-
completion(decisions)
123-
}
124-
}
125-
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)
136+
return decide(user: user, keys: keys, options: options, isAsync: false)
131137
}
132138

139+
/// Returns a decision result for a given key asynchronously
140+
///
141+
/// - Parameters:
142+
/// - user: The user context for which the decision is being made
143+
/// - keys: The feature flag keys to evaluate
144+
/// - options: An array of options for decision-making
145+
/// - completion: Handler will be called with a dictionary mapping feature flag keys to OptimizelyDecision
133146
func decideAsync(user: OptimizelyUserContext,
134147
keys: [String],
135148
options: [OptimizelyDecideOption]? = nil,
136-
ignoreDefaultOptions: Bool,
137149
completion: @escaping DecideForKeysCompletion) {
138150
decisionQueue.async {
139-
let decisions = self.decide(user: user, keys: keys, options: options, opType: .async, ignoreDefaultOptions: ignoreDefaultOptions)
151+
let decisions = self.decide(user: user, keys: keys, options: options, isAsync: true)
140152
completion(decisions)
141153
}
142154
}
143155

156+
/// Returns a key-map of decision results for multiple flag keys
157+
///
158+
/// - Parameters:
159+
/// - user: The user context for which to make the decision
160+
/// - keys: Array of feature flag keys to decide upon
161+
/// - options: Optional array of decision options that override default behavior
162+
/// - isAsync: Boolean indicating whether the operation is asynchronous
163+
/// - ignoreDefaultOptions: Boolean indicating whether to ignore default decide options
164+
/// - Returns: A dictionary of all decision results, mapped by flag keys.
144165
private func decide(user: OptimizelyUserContext,
145166
keys: [String],
146167
options: [OptimizelyDecideOption]? = nil,
147-
opType: OPType,
148-
ignoreDefaultOptions: Bool) -> [String: OptimizelyDecision] {
168+
isAsync: Bool,
169+
ignoreDefaultOptions: Bool = false) -> [String: OptimizelyDecision] {
149170
guard let config = self.config else {
150171
logger.e(OptimizelyError.sdkNotReady)
151172
return [:]
@@ -187,7 +208,7 @@ extension OptimizelyClient {
187208
}
188209
}
189210

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

192213
for index in 0..<flagsWithoutForceDecision.count {
193214
if decisionList?.indices.contains(index) ?? false {
@@ -221,6 +242,16 @@ extension OptimizelyClient {
221242
return decisionMap
222243
}
223244

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

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

245-
let decision = self.decide(user: user, keys: config.featureFlagKeys, options: options, opType: .async, ignoreDefaultOptions: false)
284+
let decision = self.decide(user: user, keys: config.featureFlagKeys, options: options, isAsync: true, ignoreDefaultOptions: false)
246285
completion(decision)
247286
}
248287
}
249-
288+
250289
private func createOptimizelyDecision(flagKey: String,
251290
user: OptimizelyUserContext,
252291
flagDecision: FeatureDecision?,
@@ -358,16 +397,16 @@ extension OptimizelyClient {
358397

359398
if let valueType = Constants.VariableValueType(rawValue: type) {
360399
switch valueType {
361-
case .string:
362-
break
363-
case .integer:
364-
valueParsed = Int(value)
365-
case .double:
366-
valueParsed = Double(value)
367-
case .boolean:
368-
valueParsed = Bool(value)
369-
case .json:
370-
valueParsed = OptimizelyJSON(payload: value)?.toMap()
400+
case .string:
401+
break
402+
case .integer:
403+
valueParsed = Int(value)
404+
case .double:
405+
valueParsed = Double(value)
406+
case .boolean:
407+
valueParsed = Bool(value)
408+
case .json:
409+
valueParsed = OptimizelyJSON(payload: value)?.toMap()
371410
}
372411
}
373412

Sources/Utils/LogMessage.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ extension LogMessage: CustomStringConvertible {
130130
case .userNotBucketedIntoAnyExperimentInGroup(let userId, let group): message = "User (\(userId)) is not in any experiment of group (\(group))."
131131
case .userBucketedIntoInvalidExperiment(let id): message = "Bucketed into an invalid experiment id (\(id))"
132132
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))."
133+
case .userNotInCmabExperiment(let userId, let expKey): message = "User (\(userId)) does not fall into cmab traffic allocation in experiment (\(expKey))."
134134
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))."
135135
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))."
136136
case .featureNotEnabledReturnDefaultVariableValue(let userId, let feature, let variable): message = "Feature (\(feature)) is not enabled for user (\(userId)). Returning the default variable value (\(variable))."

Tests/OptimizelyTests-Common/DecisionListenerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1267,7 +1267,7 @@ class FakeDecisionService: DefaultDecisionService {
12671267
return DecisionResponse.responseNoReasons(result: featureDecision)
12681268
}
12691269

1270-
override func getVariationForFeatureExperiments(config: ProjectConfig, featureFlag: FeatureFlag, user: OptimizelyUserContext, userProfileTracker: UserProfileTracker? = nil, opType: OPType = .sync, options: [OptimizelyDecideOption]? = nil) -> DecisionResponse<FeatureDecision> {
1270+
override func getVariationForFeatureExperiments(config: ProjectConfig, featureFlag: FeatureFlag, user: OptimizelyUserContext, userProfileTracker: UserProfileTracker? = nil, isAsync: Bool = false, options: [OptimizelyDecideOption]? = nil) -> DecisionResponse<FeatureDecision> {
12711271
guard let experiment = self.experiment, let tmpVariation = self.variation else {
12721272
return DecisionResponse.nilNoReasons()
12731273
}

Tests/OptimizelyTests-Common/DecisionServiceTests_Experiments.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ extension DecisionServiceTests_Experiments {
726726
experiment: cmabExperiment,
727727
user: user,
728728
options: nil,
729-
opType: .async,
729+
isAsync: true,
730730
userProfileTracker: nil)
731731
let variation = decision.result?.variation
732732
XCTAssertNotNil(variation)
@@ -754,7 +754,7 @@ extension DecisionServiceTests_Experiments {
754754
experiment: cmabExperiment,
755755
user: user,
756756
options: nil,
757-
opType: .async,
757+
isAsync: true,
758758
userProfileTracker: nil)
759759
XCTAssertNil(decision.result, "Should return nil for 0% traffic allocation")
760760
XCTAssertEqual(expectedReasons.toReport(), decision.reasons.toReport())
@@ -790,7 +790,7 @@ extension DecisionServiceTests_Experiments {
790790
experiment: cmabExperiment,
791791
user: user,
792792
options: nil,
793-
opType: .async,
793+
isAsync: true,
794794
userProfileTracker: nil)
795795

796796
XCTAssertNotNil(decision.result)
@@ -821,7 +821,7 @@ extension DecisionServiceTests_Experiments {
821821
experiment: cmabExperiment,
822822
user: user,
823823
options: nil,
824-
opType: .sync,
824+
isAsync: false,
825825
userProfileTracker: nil)
826826
XCTAssertNil(decision.result?.variation)
827827
XCTAssertEqual(expectedReasons.toReport(), decision.reasons.toReport())
@@ -849,7 +849,7 @@ extension DecisionServiceTests_Experiments {
849849
experiment: cmabExperiment,
850850
user: user,
851851
options: nil,
852-
opType: .async,
852+
isAsync: true,
853853
userProfileTracker: nil)
854854
XCTAssertNil(decision.result?.variation)
855855
XCTAssertEqual(expectedReasons.toReport(), decision.reasons.toReport())

Tests/OptimizelyTests-Common/DecisionServiceTests_Features.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -259,19 +259,19 @@ extension DecisionServiceTests_Features {
259259

260260
func testGetVariationForFeatureExperimentWhenMatched() {
261261
let pair = self.decisionService.getVariationForFeatureExperiments(config: config,
262-
featureFlag: featureFlag,
263-
user: optimizely.createUserContext(userId: kUserId,
264-
attributes: kAttributesCountryMatch)).result
262+
featureFlag: featureFlag,
263+
user: optimizely.createUserContext(userId: kUserId, attributes: kAttributesCountryMatch),
264+
isAsync: false).result
265265
XCTAssert(pair?.experiment?.key == kExperimentKey)
266266
XCTAssert(pair?.variation?.key == kVariationKeyD)
267267
XCTAssert(pair?.source == Constants.DecisionSource.featureTest.rawValue)
268268
}
269269

270270
func testGetVariationForFeatureExperimentWhenNotMatched() {
271271
let pair = self.decisionService.getVariationForFeatureExperiments(config: config,
272-
featureFlag: featureFlag,
273-
user: optimizely.createUserContext(userId: kUserId,
274-
attributes: kAttributesCountryNotMatch)).result
272+
featureFlag: featureFlag,
273+
user: optimizely.createUserContext(userId: kUserId, attributes: kAttributesCountryNotMatch),
274+
isAsync: false).result
275275
XCTAssertNil(pair)
276276
}
277277

@@ -281,9 +281,9 @@ extension DecisionServiceTests_Features {
281281
self.config.project.featureFlags = [featureFlag]
282282

283283
let pair = self.decisionService.getVariationForFeatureExperiments(config: config,
284-
featureFlag: featureFlag,
285-
user: optimizely.createUserContext(userId: kUserId,
286-
attributes: kAttributesCountryMatch)).result
284+
featureFlag: featureFlag,
285+
user: optimizely.createUserContext(userId: kUserId, attributes: kAttributesCountryMatch),
286+
isAsync: false).result
287287
XCTAssertNil(pair)
288288
}
289289

@@ -330,8 +330,8 @@ extension DecisionServiceTests_Features {
330330
let pair = ups_service.getVariationForFeatureList(
331331
config: config,
332332
featureFlags: [flag1, flag2, flag3],
333-
user: optimizely.createUserContext(userId: kUserId,
334-
attributes: kAttributesCountryMatch)
333+
user: optimizely.createUserContext(userId: kUserId, attributes: kAttributesCountryMatch),
334+
isAsync: false
335335
)
336336

337337

Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_CMAB.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ class OptimizelyUserContextTests_Decide_CMAB: XCTestCase {
9898

9999
expectation.fulfill()
100100
}
101-
102101
}
103102

104103
wait(for: [expectation], timeout: 5) // Increased timeout for reliability

0 commit comments

Comments
 (0)