Skip to content

Commit e92a3b6

Browse files
wip: add code documentation
1 parent 7a56d47 commit e92a3b6

File tree

1 file changed

+168
-72
lines changed

1 file changed

+168
-72
lines changed

Sources/Implementation/DefaultDecisionService.swift

Lines changed: 168 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,15 @@ class DefaultDecisionService: OPTDecisionService {
4747
self.userProfileService = userProfileService
4848
}
4949

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.
5159
func getVariation(config: ProjectConfig,
5260
experiment: Experiment,
5361
user: OptimizelyUserContext,
@@ -69,6 +77,14 @@ class DefaultDecisionService: OPTDecisionService {
6977
return response
7078
}
7179

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.
7288
func getVariation(config: ProjectConfig,
7389
experiment: Experiment,
7490
user: OptimizelyUserContext,
@@ -162,62 +178,15 @@ class DefaultDecisionService: OPTDecisionService {
162178
return DecisionResponse(result: bucketedVariation, reasons: reasons)
163179
}
164180

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
219182

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.
221190
func getVariationForFeature(config: ProjectConfig,
222191
featureFlag: FeatureFlag,
223192
user: OptimizelyUserContext,
@@ -233,6 +202,13 @@ class DefaultDecisionService: OPTDecisionService {
233202
return response!
234203
}
235204

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.
236212
func getVariationForFeatureList(config: ProjectConfig,
237213
featureFlags: [FeatureFlag],
238214
user: OptimizelyUserContext,
@@ -250,7 +226,7 @@ class DefaultDecisionService: OPTDecisionService {
250226
var decisions = [DecisionResponse<FeatureDecision>]()
251227

252228
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)
254230

255231
reasons.merge(decisionResponse.reasons)
256232

@@ -277,12 +253,20 @@ class DefaultDecisionService: OPTDecisionService {
277253

278254
return decisions
279255
}
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> {
286270
let reasons = DecisionReasons(options: options)
287271
let holdouts = config.getHoldoutForFlag(id: featureFlag.id)
288272

@@ -326,6 +310,13 @@ class DefaultDecisionService: OPTDecisionService {
326310
return DecisionResponse(result: nil, reasons: reasons)
327311
}
328312

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.
329320
func getVariationForFeatureRollout(config: ProjectConfig,
330321
featureFlag: FeatureFlag,
331322
user: OptimizelyUserContext,
@@ -380,6 +371,17 @@ class DefaultDecisionService: OPTDecisionService {
380371
return DecisionResponse(result: nil, reasons: reasons)
381372
}
382373

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.
383385
func getVariationForHoldout(config: ProjectConfig,
384386
flagKey: String,
385387
holdout: Holdout,
@@ -389,8 +391,6 @@ class DefaultDecisionService: OPTDecisionService {
389391
return DecisionResponse(result: nil, reasons: DecisionReasons(options: options))
390392
}
391393

392-
let userId = user.userId
393-
let attributes = user.attributes
394394
let reasons = DecisionReasons(options: options)
395395

396396
// ---- check if the user passes audience targeting before bucketing ----
@@ -399,15 +399,18 @@ class DefaultDecisionService: OPTDecisionService {
399399
user: user)
400400

401401
reasons.merge(audienceResponse.reasons)
402+
403+
let userId = user.userId
404+
let attributes = user.attributes
402405

403406
// Acquire bucketingId .
404407
let bucketingId = getBucketingId(userId: userId, attributes: attributes)
405408
var bucketedVariation: Variation?
406409

407410
if audienceResponse.result ?? false {
408411
let info = LogMessage.userMeetsConditionsForHoldout(userId, holdout.key)
409-
logger.i(info)
410412
reasons.addInfo(info)
413+
logger.i(info)
411414

412415
// bucket user into holdout variation
413416
let decisionResponse = bucketer.bucketToVariation(experiment: holdout, bucketingId: bucketingId)
@@ -418,23 +421,32 @@ class DefaultDecisionService: OPTDecisionService {
418421

419422
if let variation = bucketedVariation {
420423
let info = LogMessage.userBucketedIntoVariationInHoldout(userId, holdout.key, variation.key)
421-
logger.i(info)
422424
reasons.addInfo(info)
425+
logger.i(info)
423426
} else {
424427
let info = LogMessage.userNotBucketedIntoHoldoutVariation(userId)
425-
logger.i(info)
426428
reasons.addInfo(info)
429+
logger.i(info)
427430
}
428431

429432
} else {
430433
let info = LogMessage.userDoesntMeetConditionsForHoldout(userId, holdout.key)
431-
logger.i(info)
432434
reasons.addInfo(info)
435+
logger.i(info)
433436
}
434437

435438
return DecisionResponse(result: bucketedVariation, reasons: reasons)
436439
}
437440

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.
438450
func getVariationFromExperimentRule(config: ProjectConfig,
439451
flagKey: String,
440452
rule: Experiment,
@@ -461,7 +473,15 @@ class DefaultDecisionService: OPTDecisionService {
461473
return DecisionResponse(result: variation, reasons: reasons)
462474
}
463475

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.
465485
func getVariationFromDeliveryRule(config: ProjectConfig,
466486
flagKey: String,
467487
rules: [Experiment],
@@ -533,8 +553,79 @@ class DefaultDecisionService: OPTDecisionService {
533553
return DecisionResponse(result: (bucketedVariation, skipToEveryoneElse), reasons: reasons)
534554
}
535555

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+
}
537615

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 {
538629
// By default, the bucketing ID should be the user ID .
539630
var bucketingId = userId
540631
// If the bucketing ID key is defined in attributes, then use that
@@ -546,7 +637,12 @@ class DefaultDecisionService: OPTDecisionService {
546637
return bucketingId
547638
}
548639

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.
550646
func findValidatedForcedDecision(config: ProjectConfig,
551647
user: OptimizelyUserContext,
552648
context: OptimizelyDecisionContext) -> DecisionResponse<Variation> {

0 commit comments

Comments
 (0)