@@ -28,6 +28,13 @@ struct VariationDecision {
2828 var variation : Variation ?
2929 var cmabError : Bool = false
3030 var cmabUUID : String ?
31+ var holdout : ExperimentCore ? = nil // If variation came from a holdout, store the holdout here
32+ }
33+
34+ struct DeliveryRuleDecision {
35+ var variation : Variation ?
36+ var skipToEveryoneElse : Bool
37+ var holdout : ExperimentCore ? = nil // If variation came from a holdout, store the holdout here
3138}
3239
3340typealias UserProfile = OPTUserProfileService . UPProfile
@@ -392,8 +399,8 @@ class DefaultDecisionService: OPTDecisionService {
392399 isAsync: Bool ,
393400 options: [ OptimizelyDecideOption ] ? = nil ) -> DecisionResponse < FeatureDecision > {
394401 let reasons = DecisionReasons ( options: options)
395-
396- let holdouts = config. getHoldoutForFlag ( id : featureFlag . id )
402+
403+ let holdouts = config. getGlobalHoldouts ( )
397404 for holdout in holdouts {
398405 let holdoutDecision = getVariationForHoldout ( config: config,
399406 flagKey: featureFlag. key,
@@ -468,8 +475,14 @@ class DefaultDecisionService: OPTDecisionService {
468475 let featureDecision = FeatureDecision ( experiment: experiment, variation: nil , source: Constants . DecisionSource. featureTest. rawValue, error: true )
469476 return DecisionResponse ( result: featureDecision, reasons: reasons)
470477 } else if let variation = result. variation {
471- let featureDecision = FeatureDecision ( experiment: experiment, variation: variation, source: Constants . DecisionSource. featureTest. rawValue, cmabUUID: result. cmabUUID)
472- return DecisionResponse ( result: featureDecision, reasons: reasons)
478+ // Check if this variation came from a holdout
479+ if let holdout = result. holdout {
480+ let featureDecision = FeatureDecision ( experiment: holdout, variation: variation, source: Constants . DecisionSource. holdout. rawValue, cmabUUID: result. cmabUUID)
481+ return DecisionResponse ( result: featureDecision, reasons: reasons)
482+ } else {
483+ let featureDecision = FeatureDecision ( experiment: experiment, variation: variation, source: Constants . DecisionSource. featureTest. rawValue, cmabUUID: result. cmabUUID)
484+ return DecisionResponse ( result: featureDecision, reasons: reasons)
485+ }
473486 }
474487 }
475488 }
@@ -524,16 +537,22 @@ class DefaultDecisionService: OPTDecisionService {
524537 user: user,
525538 options: options)
526539 reasons. merge ( decisionResponse. reasons)
527- let ( variation , skipToEveryoneElse ) = decisionResponse. result!
528-
529- if let variation = variation {
540+ let result = decisionResponse. result!
541+
542+ if let variation = result . variation {
530543 let rule = rolloutRules [ index]
531- let featureDecision = FeatureDecision ( experiment: rule, variation: variation, source: Constants . DecisionSource. rollout. rawValue)
532- return DecisionResponse ( result: featureDecision, reasons: reasons)
544+ // Check if this variation came from a holdout
545+ if let holdout = result. holdout {
546+ let featureDecision = FeatureDecision ( experiment: holdout, variation: variation, source: Constants . DecisionSource. holdout. rawValue)
547+ return DecisionResponse ( result: featureDecision, reasons: reasons)
548+ } else {
549+ let featureDecision = FeatureDecision ( experiment: rule, variation: variation, source: Constants . DecisionSource. rollout. rawValue)
550+ return DecisionResponse ( result: featureDecision, reasons: reasons)
551+ }
533552 }
534-
553+
535554 // the last rule is special for "Everyone Else"
536- index = skipToEveryoneElse ? ( rolloutRules. count - 1 ) : ( index + 1 )
555+ index = result . skipToEveryoneElse ? ( rolloutRules. count - 1 ) : ( index + 1 )
537556 }
538557
539558 return DecisionResponse ( result: nil , reasons: reasons)
@@ -637,7 +656,23 @@ class DefaultDecisionService: OPTDecisionService {
637656 let variationDecision = VariationDecision ( variation: variation)
638657 return DecisionResponse ( result: variationDecision, reasons: reasons)
639658 }
640-
659+
660+ // check local holdouts targeting this rule
661+ let localHoldouts = config. getHoldoutsForRule ( ruleId: rule. id)
662+ for holdout in localHoldouts {
663+ let holdoutDecision = getVariationForHoldout ( config: config,
664+ flagKey: flagKey,
665+ holdout: holdout,
666+ user: user,
667+ options: options)
668+ reasons. merge ( holdoutDecision. reasons)
669+ if let variation = holdoutDecision. result {
670+ // User is in holdout — return holdout variation immediately, skip this rule
671+ let variationDecision = VariationDecision ( variation: variation, holdout: holdout)
672+ return DecisionResponse ( result: variationDecision, reasons: reasons)
673+ }
674+ }
675+
641676 let decisionResponse = getVariation ( config: config,
642677 experiment: rule,
643678 user: user,
@@ -657,13 +692,13 @@ class DefaultDecisionService: OPTDecisionService {
657692 /// - ruleIndex: The index of the rule to evaluate.
658693 /// - user: The user context.
659694 /// - options: Optional decision options.
660- /// - Returns: A `DecisionResponse` with the variation (if any), a flag indicating whether to skip to the "Everyone Else" rule, and reasons.
695+ /// - Returns: A `DecisionResponse` with the delivery rule decision and reasons.
661696 func getVariationFromDeliveryRule( config: ProjectConfig ,
662697 flagKey: String ,
663698 rules: [ Experiment ] ,
664699 ruleIndex: Int ,
665700 user: OptimizelyUserContext ,
666- options: [ OptimizelyDecideOption ] ? = nil ) -> DecisionResponse < ( Variation ? , Bool ) > {
701+ options: [ OptimizelyDecideOption ] ? = nil ) -> DecisionResponse < DeliveryRuleDecision > {
667702 let reasons = DecisionReasons ( options: options)
668703 var skipToEveryoneElse = false
669704
@@ -676,9 +711,26 @@ class DefaultDecisionService: OPTDecisionService {
676711 reasons. merge ( forcedDecisionResponse. reasons)
677712
678713 if let variation = forcedDecisionResponse. result {
679- return DecisionResponse ( result: ( variation, skipToEveryoneElse) , reasons: reasons)
714+ let decision = DeliveryRuleDecision ( variation: variation, skipToEveryoneElse: skipToEveryoneElse)
715+ return DecisionResponse ( result: decision, reasons: reasons)
680716 }
681-
717+
718+ // check local holdouts targeting this delivery rule
719+ let localHoldouts = config. getHoldoutsForRule ( ruleId: rule. id)
720+ for holdout in localHoldouts {
721+ let holdoutDecision = getVariationForHoldout ( config: config,
722+ flagKey: flagKey,
723+ holdout: holdout,
724+ user: user,
725+ options: options)
726+ reasons. merge ( holdoutDecision. reasons)
727+ if let variation = holdoutDecision. result {
728+ // User is in holdout — return holdout variation with holdout info
729+ let decision = DeliveryRuleDecision ( variation: variation, skipToEveryoneElse: skipToEveryoneElse, holdout: holdout)
730+ return DecisionResponse ( result: decision, reasons: reasons)
731+ }
732+ }
733+
682734 // regular decision
683735
684736 let userId = user. userId
@@ -725,8 +777,9 @@ class DefaultDecisionService: OPTDecisionService {
725777 logger. d ( info)
726778 reasons. addInfo ( info)
727779 }
728-
729- return DecisionResponse ( result: ( bucketedVariation, skipToEveryoneElse) , reasons: reasons)
780+
781+ let decision = DeliveryRuleDecision ( variation: bucketedVariation, skipToEveryoneElse: skipToEveryoneElse)
782+ return DecisionResponse ( result: decision, reasons: reasons)
730783 }
731784
732785 // MARK: - Audience Evaluation
0 commit comments