@@ -47,7 +47,15 @@ class DefaultDecisionService: OPTDecisionService {
47
47
self . userProfileService = userProfileService
48
48
}
49
49
50
- /// Public Method
50
+ // MARK: - Experiment Decision
51
+
52
+ /// Determines the variation for a user in a given experiment.
53
+ /// - Parameters:
54
+ /// - config: The project configuration containing experiment and feature details.
55
+ /// - experiment: The experiment to evaluate.
56
+ /// - user: The user context containing user ID and attributes.
57
+ /// - options: Optional decision options (e.g., ignore user profile service).
58
+ /// - Returns: A `DecisionResponse` containing the assigned variation (if any) and decision reasons.
51
59
func getVariation( config: ProjectConfig ,
52
60
experiment: Experiment ,
53
61
user: OptimizelyUserContext ,
@@ -69,6 +77,14 @@ class DefaultDecisionService: OPTDecisionService {
69
77
return response
70
78
}
71
79
80
+ /// Determines the variation for a user in an experiment, considering user profile and decision rules.
81
+ /// - Parameters:
82
+ /// - config: The project configuration.
83
+ /// - experiment: The experiment to evaluate.
84
+ /// - user: The user context.
85
+ /// - options: Optional decision options.
86
+ /// - userProfileTracker: Optional tracker for user profile data.
87
+ /// - Returns: A `DecisionResponse` with the variation (if any) and decision reasons.
72
88
func getVariation( config: ProjectConfig ,
73
89
experiment: Experiment ,
74
90
user: OptimizelyUserContext ,
@@ -162,62 +178,15 @@ class DefaultDecisionService: OPTDecisionService {
162
178
return DecisionResponse ( result: bucketedVariation, reasons: reasons)
163
179
}
164
180
165
- func doesMeetAudienceConditions( config: ProjectConfig ,
166
- experiment: ExperimentCore ,
167
- user: OptimizelyUserContext ,
168
- logType: Constants . EvaluationLogType = . experiment,
169
- loggingKey: String ? = nil ) -> DecisionResponse < Bool > {
170
- let reasons = DecisionReasons ( )
171
-
172
- var result = true // success as default (no condition, etc)
173
- let evType = logType. rawValue
174
- let finalLoggingKey = loggingKey ?? experiment. key
175
-
176
- do {
177
- if let conditions = experiment. audienceConditions {
178
- logger. d { ( ) -> String in
179
- return LogMessage . evaluatingAudiencesCombined ( evType, finalLoggingKey, Utils . getConditionString ( conditions: conditions) ) . description
180
- }
181
- switch conditions {
182
- case . array( let arrConditions) :
183
- if arrConditions. count > 0 {
184
- result = try conditions. evaluate ( project: config. project, user: user)
185
- } else {
186
- // empty conditions (backward compatibility with "audienceIds" is ignored if exists even though empty
187
- result = true
188
- }
189
- case . leaf:
190
- result = try conditions. evaluate ( project: config. project, user: user)
191
- default :
192
- result = true
193
- }
194
- }
195
- // backward compatibility with audienceIds list
196
- else if experiment. audienceIds. count > 0 {
197
- var holder = [ ConditionHolder] ( )
198
- holder. append ( . logicalOp( . or) )
199
- for id in experiment. audienceIds {
200
- holder. append ( . leaf( . audienceId( id) ) )
201
- }
202
- logger. d { ( ) -> String in
203
- return LogMessage . evaluatingAudiencesCombined ( evType, finalLoggingKey, Utils . getConditionString ( conditions: holder) ) . description
204
- }
205
- result = try holder. evaluate ( project: config. project, user: user)
206
- }
207
- } catch {
208
- if let error = error as? OptimizelyError {
209
- logger. i ( error)
210
- reasons. addInfo ( error)
211
- }
212
- result = false
213
- }
214
-
215
- logger. i ( . audienceEvaluationResultCombined( evType, finalLoggingKey, result. description) )
216
-
217
- return DecisionResponse ( result: result, reasons: reasons)
218
- }
181
+ // MARK: - Feature Flag Decision
219
182
220
- /// Public Method
183
+ /// Determines the feature decision for a user for a specific feature flag.
184
+ /// - Parameters:
185
+ /// - config: The project configuration.
186
+ /// - featureFlag: The feature flag to evaluate.
187
+ /// - user: The user context.
188
+ /// - options: Optional decision options.
189
+ /// - Returns: A `DecisionResponse` with the feature decision (if any) and reasons.
221
190
func getVariationForFeature( config: ProjectConfig ,
222
191
featureFlag: FeatureFlag ,
223
192
user: OptimizelyUserContext ,
@@ -233,6 +202,13 @@ class DefaultDecisionService: OPTDecisionService {
233
202
return response!
234
203
}
235
204
205
+ /// Determines feature decisions for a list of feature flags.
206
+ /// - Parameters:
207
+ /// - config: The project configuration.
208
+ /// - featureFlags: The list of feature flags to evaluate.
209
+ /// - user: The user context.
210
+ /// - options: Optional decision options.
211
+ /// - Returns: An array of `DecisionResponse` objects, each containing a feature decision and reasons.
236
212
func getVariationForFeatureList( config: ProjectConfig ,
237
213
featureFlags: [ FeatureFlag ] ,
238
214
user: OptimizelyUserContext ,
@@ -250,7 +226,7 @@ class DefaultDecisionService: OPTDecisionService {
250
226
var decisions = [ DecisionResponse < FeatureDecision > ] ( )
251
227
252
228
for featureFlag in featureFlags {
253
- var decisionResponse = getVariationForFeatureExperiment ( config: config, featureFlag: featureFlag, user: user, userProfileTracker: profileTracker)
229
+ var decisionResponse = getVariationForFeature ( config: config, featureFlag: featureFlag, user: user, userProfileTracker: profileTracker)
254
230
255
231
reasons. merge ( decisionResponse. reasons)
256
232
@@ -277,12 +253,20 @@ class DefaultDecisionService: OPTDecisionService {
277
253
278
254
return decisions
279
255
}
280
-
281
- func getVariationForFeatureExperiment( config: ProjectConfig ,
282
- featureFlag: FeatureFlag ,
283
- user: OptimizelyUserContext ,
284
- userProfileTracker: UserProfileTracker ? = nil ,
285
- options: [ OptimizelyDecideOption ] ? = nil ) -> DecisionResponse < FeatureDecision > {
256
+
257
+ /// Determines the feature decision for a feature flag, considering experiments and holdouts.
258
+ /// - Parameters:
259
+ /// - config: The project configuration.
260
+ /// - featureFlag: The feature flag to evaluate.
261
+ /// - user: The user context.
262
+ /// - userProfileTracker: Optional tracker for user profile data.
263
+ /// - options: Optional decision options.
264
+ /// - Returns: A `DecisionResponse` with the feature decision (if any) and reasons.
265
+ func getVariationForFeature( config: ProjectConfig ,
266
+ featureFlag: FeatureFlag ,
267
+ user: OptimizelyUserContext ,
268
+ userProfileTracker: UserProfileTracker ? = nil ,
269
+ options: [ OptimizelyDecideOption ] ? = nil ) -> DecisionResponse < FeatureDecision > {
286
270
let reasons = DecisionReasons ( options: options)
287
271
let holdouts = config. getHoldoutForFlag ( id: featureFlag. id)
288
272
@@ -326,6 +310,13 @@ class DefaultDecisionService: OPTDecisionService {
326
310
return DecisionResponse ( result: nil , reasons: reasons)
327
311
}
328
312
313
+ /// Determines the feature decision for a feature flag's rollout rules.
314
+ /// - Parameters:
315
+ /// - config: The project configuration.
316
+ /// - featureFlag: The feature flag to evaluate.
317
+ /// - user: The user context.
318
+ /// - options: Optional decision options.
319
+ /// - Returns: A `DecisionResponse` with the feature decision (if any) and reasons.
329
320
func getVariationForFeatureRollout( config: ProjectConfig ,
330
321
featureFlag: FeatureFlag ,
331
322
user: OptimizelyUserContext ,
@@ -380,6 +371,17 @@ class DefaultDecisionService: OPTDecisionService {
380
371
return DecisionResponse ( result: nil , reasons: reasons)
381
372
}
382
373
374
+
375
+ // MARK: - Holdout and Rule Decisions
376
+
377
+ /// Determines the variation for a holdout group.
378
+ /// - Parameters:
379
+ /// - config: The project configuration.
380
+ /// - flagKey: The feature flag key.
381
+ /// - holdout: The holdout group to evaluate.
382
+ /// - user: The user context.
383
+ /// - options: Optional decision options.
384
+ /// - Returns: A `DecisionResponse` with the variation (if any) and reasons.
383
385
func getVariationForHoldout( config: ProjectConfig ,
384
386
flagKey: String ,
385
387
holdout: Holdout ,
@@ -389,8 +391,6 @@ class DefaultDecisionService: OPTDecisionService {
389
391
return DecisionResponse ( result: nil , reasons: DecisionReasons ( options: options) )
390
392
}
391
393
392
- let userId = user. userId
393
- let attributes = user. attributes
394
394
let reasons = DecisionReasons ( options: options)
395
395
396
396
// ---- check if the user passes audience targeting before bucketing ----
@@ -399,15 +399,18 @@ class DefaultDecisionService: OPTDecisionService {
399
399
user: user)
400
400
401
401
reasons. merge ( audienceResponse. reasons)
402
+
403
+ let userId = user. userId
404
+ let attributes = user. attributes
402
405
403
406
// Acquire bucketingId .
404
407
let bucketingId = getBucketingId ( userId: userId, attributes: attributes)
405
408
var bucketedVariation : Variation ?
406
409
407
410
if audienceResponse. result ?? false {
408
411
let info = LogMessage . userMeetsConditionsForHoldout ( userId, holdout. key)
409
- logger. i ( info)
410
412
reasons. addInfo ( info)
413
+ logger. i ( info)
411
414
412
415
// bucket user into holdout variation
413
416
let decisionResponse = bucketer. bucketToVariation ( experiment: holdout, bucketingId: bucketingId)
@@ -418,23 +421,32 @@ class DefaultDecisionService: OPTDecisionService {
418
421
419
422
if let variation = bucketedVariation {
420
423
let info = LogMessage . userBucketedIntoVariationInHoldout ( userId, holdout. key, variation. key)
421
- logger. i ( info)
422
424
reasons. addInfo ( info)
425
+ logger. i ( info)
423
426
} else {
424
427
let info = LogMessage . userNotBucketedIntoHoldoutVariation ( userId)
425
- logger. i ( info)
426
428
reasons. addInfo ( info)
429
+ logger. i ( info)
427
430
}
428
431
429
432
} else {
430
433
let info = LogMessage . userDoesntMeetConditionsForHoldout ( userId, holdout. key)
431
- logger. i ( info)
432
434
reasons. addInfo ( info)
435
+ logger. i ( info)
433
436
}
434
437
435
438
return DecisionResponse ( result: bucketedVariation, reasons: reasons)
436
439
}
437
440
441
+ /// Determines the variation for an experiment rule within a feature flag.
442
+ /// - Parameters:
443
+ /// - config: The project configuration.
444
+ /// - flagKey: The feature flag key.
445
+ /// - rule: The experiment rule to evaluate.
446
+ /// - user: The user context.
447
+ /// - userProfileTracker: Optional tracker for user profile data.
448
+ /// - options: Optional decision options.
449
+ /// - Returns: A `DecisionResponse` with the variation (if any) and reasons.
438
450
func getVariationFromExperimentRule( config: ProjectConfig ,
439
451
flagKey: String ,
440
452
rule: Experiment ,
@@ -461,7 +473,15 @@ class DefaultDecisionService: OPTDecisionService {
461
473
return DecisionResponse ( result: variation, reasons: reasons)
462
474
}
463
475
464
-
476
+ /// Determines the variation for a delivery rule in a rollout.
477
+ /// - Parameters:
478
+ /// - config: The project configuration.
479
+ /// - flagKey: The feature flag key.
480
+ /// - rules: The list of rollout rules.
481
+ /// - ruleIndex: The index of the rule to evaluate.
482
+ /// - user: The user context.
483
+ /// - options: Optional decision options.
484
+ /// - Returns: A `DecisionResponse` with the variation (if any), a flag indicating whether to skip to the "Everyone Else" rule, and reasons.
465
485
func getVariationFromDeliveryRule( config: ProjectConfig ,
466
486
flagKey: String ,
467
487
rules: [ Experiment ] ,
@@ -533,8 +553,79 @@ class DefaultDecisionService: OPTDecisionService {
533
553
return DecisionResponse ( result: ( bucketedVariation, skipToEveryoneElse) , reasons: reasons)
534
554
}
535
555
536
- func getBucketingId( userId: String , attributes: OptimizelyAttributes ) -> String {
556
+ // MARK: - Audience Evaluation
557
+
558
+ /// Evaluates whether a user meets the audience conditions for an experiment or rule.
559
+ /// - Parameters:
560
+ /// - config: The project configuration.
561
+ /// - experiment: The experiment or rule to evaluate.
562
+ /// - user: The user context.
563
+ /// - logType: The type of evaluation for logging (e.g., experiment or rollout rule).
564
+ /// - loggingKey: Optional key for logging.
565
+ /// - Returns: A `DecisionResponse` with a boolean indicating whether conditions are met and reasons.
566
+ func doesMeetAudienceConditions( config: ProjectConfig ,
567
+ experiment: ExperimentCore ,
568
+ user: OptimizelyUserContext ,
569
+ logType: Constants . EvaluationLogType = . experiment,
570
+ loggingKey: String ? = nil ) -> DecisionResponse < Bool > {
571
+ let reasons = DecisionReasons ( )
572
+
573
+ var result = true // success as default (no condition, etc)
574
+ let evType = logType. rawValue
575
+ let finalLoggingKey = loggingKey ?? experiment. key
576
+
577
+ do {
578
+ if let conditions = experiment. audienceConditions {
579
+ logger. d { ( ) -> String in
580
+ return LogMessage . evaluatingAudiencesCombined ( evType, finalLoggingKey, Utils . getConditionString ( conditions: conditions) ) . description
581
+ }
582
+ switch conditions {
583
+ case . array( let arrConditions) :
584
+ if arrConditions. count > 0 {
585
+ result = try conditions. evaluate ( project: config. project, user: user)
586
+ } else {
587
+ // empty conditions (backward compatibility with "audienceIds" is ignored if exists even though empty
588
+ result = true
589
+ }
590
+ case . leaf:
591
+ result = try conditions. evaluate ( project: config. project, user: user)
592
+ default :
593
+ result = true
594
+ }
595
+ }
596
+ // backward compatibility with audienceIds list
597
+ else if experiment. audienceIds. count > 0 {
598
+ var holder = [ ConditionHolder] ( )
599
+ holder. append ( . logicalOp( . or) )
600
+ for id in experiment. audienceIds {
601
+ holder. append ( . leaf( . audienceId( id) ) )
602
+ }
603
+ logger. d { ( ) -> String in
604
+ return LogMessage . evaluatingAudiencesCombined ( evType, finalLoggingKey, Utils . getConditionString ( conditions: holder) ) . description
605
+ }
606
+ result = try holder. evaluate ( project: config. project, user: user)
607
+ }
608
+ } catch {
609
+ if let error = error as? OptimizelyError {
610
+ logger. i ( error)
611
+ reasons. addInfo ( error)
612
+ }
613
+ result = false
614
+ }
537
615
616
+ logger. i ( . audienceEvaluationResultCombined( evType, finalLoggingKey, result. description) )
617
+
618
+ return DecisionResponse ( result: result, reasons: reasons)
619
+ }
620
+
621
+ // MARK: - Utilities
622
+
623
+ /// Retrieves the bucketing ID for a user, defaulting to user ID unless overridden in attributes.
624
+ /// - Parameters:
625
+ /// - userId: The user's ID.
626
+ /// - attributes: The user's attributes.
627
+ /// - Returns: The bucketing ID to use for variation assignment.
628
+ func getBucketingId( userId: String , attributes: OptimizelyAttributes ) -> String {
538
629
// By default, the bucketing ID should be the user ID .
539
630
var bucketingId = userId
540
631
// If the bucketing ID key is defined in attributes, then use that
@@ -546,7 +637,12 @@ class DefaultDecisionService: OPTDecisionService {
546
637
return bucketingId
547
638
}
548
639
549
- /// Public Method
640
+ /// Finds and validates a forced decision for a given context.
641
+ /// - Parameters:
642
+ /// - config: The project configuration.
643
+ /// - user: The user context.
644
+ /// - context: The decision context (flag and rule keys).
645
+ /// - Returns: A `DecisionResponse` with the forced variation (if valid) and reasons.
550
646
func findValidatedForcedDecision( config: ProjectConfig ,
551
647
user: OptimizelyUserContext ,
552
648
context: OptimizelyDecisionContext ) -> DecisionResponse < Variation > {
0 commit comments