@@ -74,10 +74,12 @@ import {
7474} from 'log_message' ;
7575import { OptimizelyError } from '../../error/optimizly_error' ;
7676import { CmabService } from './cmab/cmab_service' ;
77- import { Maybe , OpType , OpValue } from '../../utils/type' ;
77+ import { Maybe , OpType } from '../../utils/type' ;
7878import { Value } from '../../utils/promise/operation_value' ;
7979import { Platform } from '../../platform_support' ;
8080
81+ import { localHoldout } from '../../feature_toggle' ;
82+
8183export const EXPERIMENT_NOT_RUNNING = 'Experiment %s is not running.' ;
8284export const RETURNING_STORED_VARIATION =
8385 'Returning previously activated variation "%s" of experiment "%s" for user "%s" from user profile.' ;
@@ -135,36 +137,48 @@ interface DecisionServiceOptions {
135137 cmabService : CmabService ;
136138}
137139
138- interface DeliveryRuleResponse < T , K > extends DecisionResponse < T > {
139- skipToEveryoneElse : K ;
140- /** Set when a local holdout was hit for this delivery rule (FSSDK-12369). */
141- localHoldoutDecision ?: DecisionObj ;
142- }
143140
144- interface UserProfileTracker {
145- userProfile : ExperimentBucketMap | null ;
146- isProfileUpdated : boolean ;
147- }
141+ export type DecisionReason = [ string , ...any [ ] ] ;
148142
149- type VarationKeyWithCmabParams = {
150- variationKey ? : string ;
143+ export type VariationIdWithCmabParams = {
144+ variationId ? : string ;
151145 cmabUuid ?: string ;
152- /**
153- * Set when the variation comes from a local holdout (FSSDK-12369).
154- * When present, the caller should use this as the full decision result
155- * instead of constructing one from the experiment.
156- */
157- localHoldoutDecision ?: DecisionObj ;
158146} ;
159- export type DecisionReason = [ string , ...any [ ] ] ;
160- export type VariationResult = DecisionResponse < VarationKeyWithCmabParams > ;
161- export type DecisionResult = DecisionResponse < DecisionObj > ;
162- type VariationIdWithCmabParams = {
163- variationId ? : string ;
147+
148+ export type VariationKeyWithCmabParams = {
149+ variationKey ? : string ;
164150 cmabUuid ?: string ;
165151} ;
152+
153+ export type DecisionResult = DecisionResponse < DecisionObj > ;
154+
155+
156+ type LocalHoldoutResult = DecisionResponse < DecisionObj > & {
157+ holdoutDecision : true
158+ }
159+
160+ type ExperimentEvaluationResult = DecisionResponse < VariationKeyWithCmabParams > & {
161+ holdoutDecision ?: false ;
162+ }
163+
164+ export type ExperimentRuleResponse = ExperimentEvaluationResult | LocalHoldoutResult ;
165+
166+ type DeliveryEvaluationResult = DecisionResponse < Variation | null > & {
167+ holdoutDecision ?: false ;
168+ skipToEveryoneElse : boolean ;
169+ }
170+
171+ type DeliveryRuleResponse = DeliveryEvaluationResult | LocalHoldoutResult ;
172+
173+ type VariationKeyResult = DecisionResponse < VariationKeyWithCmabParams >
174+
166175export type DecideOptionsMap = Partial < Record < OptimizelyDecideOption , boolean > > ;
167176
177+ interface UserProfileTracker {
178+ userProfile : ExperimentBucketMap | null ;
179+ isProfileUpdated : boolean ;
180+ }
181+
168182export const CMAB_DUMMY_ENTITY_ID = '$'
169183
170184export const LOGGER_NAME = 'DecisionService' ;
@@ -222,7 +236,7 @@ export class DecisionService {
222236 user : OptimizelyUserContext ,
223237 decideOptions : DecideOptionsMap ,
224238 userProfileTracker ?: UserProfileTracker ,
225- ) : Value < OP , VariationResult > {
239+ ) : Value < OP , VariationKeyResult > {
226240 const userId = user . getUserId ( ) ;
227241
228242 const experimentKey = experiment . key ;
@@ -310,7 +324,7 @@ export class DecisionService {
310324 this . getDecisionForCmabExperiment ( op , configObj , experiment , user , decideOptions ) :
311325 this . getDecisionFromBucketer ( op , configObj , experiment , user ) ;
312326
313- return decisionVariationValue . then ( ( variationResult ) : Value < OP , VariationResult > => {
327+ return decisionVariationValue . then ( ( variationResult ) : Value < OP , VariationKeyResult > => {
314328 decideReasons . push ( ...variationResult . reasons ) ;
315329 if ( variationResult . error ) {
316330 return Value . of ( op , {
@@ -1083,14 +1097,22 @@ export class DecisionService {
10831097 op , configObj , feature , fromIndex + 1 , user , decideReasons , decideOptions , userProfileTracker ) ;
10841098 }
10851099
1086- const decisionVariationValue = this . getVariationFromExperimentRule (
1100+ const experimentResponse = this . getVariationFromExperimentRule (
10871101 op , configObj , feature . key , experiment , user , decideOptions , userProfileTracker ,
10881102 ) ;
10891103
1090- return decisionVariationValue . then ( ( decisionVariation ) => {
1091- decideReasons . push ( ...decisionVariation . reasons ) ;
1104+ return experimentResponse . then ( ( experimentResponse ) => {
1105+ decideReasons . push ( ...experimentResponse . reasons ) ;
10921106
1093- if ( decisionVariation . error ) {
1107+ // If a local holdout was hit, return the holdout decision directly (preserves holdout as experiment).
1108+ if ( experimentResponse . holdoutDecision ) {
1109+ return Value . of ( op , {
1110+ result : experimentResponse . result ,
1111+ reasons : decideReasons ,
1112+ } ) ;
1113+ }
1114+
1115+ if ( experimentResponse . error ) {
10941116 return Value . of ( op , {
10951117 error : true ,
10961118 result : {
@@ -1102,28 +1124,20 @@ export class DecisionService {
11021124 } ) ;
11031125 }
11041126
1105- // If a local holdout was hit, return the holdout decision directly (preserves holdout as experiment).
1106- if ( decisionVariation . result . localHoldoutDecision ) {
1107- return Value . of ( op , {
1108- result : decisionVariation . result . localHoldoutDecision ,
1109- reasons : decideReasons ,
1110- } ) ;
1111- }
1112-
1113- if ( ! decisionVariation . result . variationKey ) {
1127+ if ( ! experimentResponse . result . variationKey ) {
11141128 return this . traverseFeatureExperimentList (
11151129 op , configObj , feature , fromIndex + 1 , user , decideReasons , decideOptions , userProfileTracker ) ;
11161130 }
11171131
1118- const variationKey = decisionVariation . result . variationKey ;
1132+ const variationKey = experimentResponse . result . variationKey ;
11191133 let variation : Variation | null = experiment . variationKeyMap [ variationKey ] ;
11201134 if ( ! variation ) {
11211135 variation = getFlagVariationByKey ( configObj , feature . key , variationKey ) ;
11221136 }
11231137
11241138 return Value . of ( op , {
11251139 result : {
1126- cmabUuid : decisionVariation . result . cmabUuid ,
1140+ cmabUuid : experimentResponse . result . cmabUuid ,
11271141 experiment,
11281142 variation,
11291143 decisionSource : DECISION_SOURCES . FEATURE_TEST ,
@@ -1139,11 +1153,10 @@ export class DecisionService {
11391153 user : OptimizelyUserContext ,
11401154 ) : DecisionResponse < DecisionObj > {
11411155 const decideReasons : DecisionReason [ ] = [ ] ;
1142- let decisionObj : DecisionObj ;
11431156 if ( ! feature . rolloutId ) {
11441157 this . logger ?. debug ( NO_ROLLOUT_EXISTS , feature . key ) ;
11451158 decideReasons . push ( [ NO_ROLLOUT_EXISTS , feature . key ] ) ;
1146- decisionObj = {
1159+ const decisionObj = {
11471160 experiment : null ,
11481161 variation : null ,
11491162 decisionSource : DECISION_SOURCES . ROLLOUT ,
@@ -1163,7 +1176,7 @@ export class DecisionService {
11631176 feature . key ,
11641177 ) ;
11651178 decideReasons . push ( [ INVALID_ROLLOUT_ID , feature . rolloutId , feature . key ] ) ;
1166- decisionObj = {
1179+ const decisionObj = {
11671180 experiment : null ,
11681181 variation : null ,
11691182 decisionSource : DECISION_SOURCES . ROLLOUT ,
@@ -1181,7 +1194,7 @@ export class DecisionService {
11811194 feature . rolloutId ,
11821195 ) ;
11831196 decideReasons . push ( [ ROLLOUT_HAS_NO_EXPERIMENTS , feature . rolloutId ] ) ;
1184- decisionObj = {
1197+ const decisionObj = {
11851198 experiment : null ,
11861199 variation : null ,
11871200 decisionSource : DECISION_SOURCES . ROLLOUT ,
@@ -1191,26 +1204,34 @@ export class DecisionService {
11911204 reasons : decideReasons ,
11921205 } ;
11931206 }
1194- let decisionVariation ;
1195- let skipToEveryoneElse ;
1196- let variation ;
1197- let rolloutRule ;
1207+
11981208 let index = 0 ;
11991209 while ( index < rolloutRules . length ) {
1200- decisionVariation = this . getVariationFromDeliveryRule ( configObj , feature . key , rolloutRules , index , user ) ;
1201- decideReasons . push ( ...decisionVariation . reasons ) ;
1202- variation = decisionVariation . result ;
1203- skipToEveryoneElse = decisionVariation . skipToEveryoneElse ;
1204- if ( variation ) {
1205- // If a local holdout was hit for this delivery rule, use its decision object directly.
1206- if ( decisionVariation . localHoldoutDecision ) {
1207- return {
1208- result : decisionVariation . localHoldoutDecision ,
1209- reasons : decideReasons ,
1210- } ;
1210+ const deliveryRuleResponse = this . getVariationFromDeliveryRule ( configObj , feature . key , rolloutRules , index , user ) ;
1211+
1212+ decideReasons . push ( ...deliveryRuleResponse . reasons ) ;
1213+
1214+ // If a local holdout was hit for this delivery rule, use its decision object directly.
1215+ if ( deliveryRuleResponse . holdoutDecision ) {
1216+ return {
1217+ reasons : decideReasons ,
1218+ result : deliveryRuleResponse . result ,
12111219 }
1212- rolloutRule = configObj . experimentIdMap [ rolloutRules [ index ] . id ] ;
1213- decisionObj = {
1220+ }
1221+
1222+
1223+ const variation = deliveryRuleResponse . result ;
1224+ const skipToEveryoneElse = deliveryRuleResponse . skipToEveryoneElse ;
1225+
1226+ if ( variation ) {
1227+ // if (decisionVariation.localHoldoutDecision) {
1228+ // return {
1229+ // result: decisionVariation.localHoldoutDecision,
1230+ // reasons: decideReasons,
1231+ // };
1232+ // }
1233+ const rolloutRule = configObj . experimentIdMap [ rolloutRules [ index ] . id ] ;
1234+ const decisionObj = {
12141235 experiment : rolloutRule ,
12151236 variation : variation ,
12161237 decisionSource : DECISION_SOURCES . ROLLOUT
@@ -1224,7 +1245,7 @@ export class DecisionService {
12241245 index = skipToEveryoneElse ? ( rolloutRules . length - 1 ) : ( index + 1 ) ;
12251246 }
12261247
1227- decisionObj = {
1248+ const decisionObj = {
12281249 experiment : null ,
12291250 variation : null ,
12301251 decisionSource : DECISION_SOURCES . ROLLOUT ,
@@ -1573,7 +1594,7 @@ export class DecisionService {
15731594 user : OptimizelyUserContext ,
15741595 decideOptions : DecideOptionsMap ,
15751596 userProfileTracker ?: UserProfileTracker ,
1576- ) : Value < OP , VariationResult > {
1597+ ) : Value < OP , ExperimentRuleResponse > {
15771598 const decideReasons : DecisionReason [ ] = [ ] ;
15781599
15791600 // check forced decision first
@@ -1588,44 +1609,34 @@ export class DecisionService {
15881609 } ) ;
15891610 }
15901611
1591- // Check local holdouts targeting this specific experiment rule (FSSDK-12369).
1592- // Inserted immediately after the forced-decision block, before regular rule evaluation .
1593- const localHoldoutsForExperiment = getHoldoutsForRule ( configObj , rule . id ) ;
1594- for ( const holdout of localHoldoutsForExperiment ) {
1595- const holdoutDecision = this . getVariationForHoldout ( configObj , holdout , user ) ;
1596- decideReasons . push ( ... holdoutDecision . reasons ) ;
1597- if ( holdoutDecision . result . variation ) {
1598- // Signal the caller to use the holdout decision directly, preserving the holdout as experiment.
1599- return Value . of ( op , {
1600- result : {
1601- variationKey : holdoutDecision . result . variation . key ,
1602- localHoldoutDecision : holdoutDecision . result ,
1603- } ,
1604- reasons : decideReasons ,
1605- } ) ;
1612+ if ( localHoldout ( ) ) {
1613+ // Check local holdouts targeting this specific experiment rule.
1614+ // Inserted immediately after the forced-decision block, before regular rule evaluation.
1615+ const localHoldoutsForExperiment = getHoldoutsForRule ( configObj , rule . id ) ;
1616+ for ( const holdout of localHoldoutsForExperiment ) {
1617+ const holdoutDecision = this . getVariationForHoldout ( configObj , holdout , user ) ;
1618+ decideReasons . push ( ... holdoutDecision . reasons ) ;
1619+ if ( holdoutDecision . result . variation ) {
1620+ // Signal the caller to use the holdout decision directly, preserving the holdout as experiment.
1621+ return Value . of < OP , LocalHoldoutResult > ( op , {
1622+ result : holdoutDecision . result ,
1623+ reasons : decideReasons ,
1624+ holdoutDecision : true ,
1625+ } ) ;
1626+ }
16061627 }
16071628 }
16081629
16091630 const decisionVariationValue = this . resolveVariation ( op , configObj , rule , user , decideOptions , userProfileTracker ) ;
16101631
16111632 return decisionVariationValue . then ( ( variationResult ) => {
16121633 decideReasons . push ( ...variationResult . reasons ) ;
1613- return Value . of ( op , {
1634+ return Value . of < OP , ExperimentEvaluationResult > ( op , {
16141635 error : variationResult . error ,
16151636 result : variationResult . result ,
16161637 reasons : decideReasons ,
16171638 } ) ;
16181639 } ) ;
1619-
1620- // return response;
1621-
1622- // decideReasons.push(...decisionVariation.reasons);
1623- // const variationKey = decisionVariation.result;
1624-
1625- // return {
1626- // result: variationKey,
1627- // reasons: decideReasons,
1628- // };
16291640 }
16301641
16311642 private getVariationFromDeliveryRule (
@@ -1634,7 +1645,7 @@ export class DecisionService {
16341645 rules : Experiment [ ] ,
16351646 ruleIndex : number ,
16361647 user : OptimizelyUserContext
1637- ) : DeliveryRuleResponse < Variation | null , boolean > {
1648+ ) : DeliveryRuleResponse {
16381649 const decideReasons : DecisionReason [ ] = [ ] ;
16391650 let skipToEveryoneElse = false ;
16401651
@@ -1652,19 +1663,20 @@ export class DecisionService {
16521663 } ;
16531664 }
16541665
1655- // Check local holdouts targeting this specific delivery rule (FSSDK-12369).
1656- // Inserted immediately after the forced-decision block, before audience and traffic allocation checks.
1657- const localHoldoutsForDelivery = getHoldoutsForRule ( configObj , rule . id ) ;
1658- for ( const holdout of localHoldoutsForDelivery ) {
1659- const holdoutDecision = this . getVariationForHoldout ( configObj , holdout , user ) ;
1660- decideReasons . push ( ...holdoutDecision . reasons ) ;
1661- if ( holdoutDecision . result . variation ) {
1662- return {
1663- result : holdoutDecision . result . variation ,
1664- reasons : decideReasons ,
1665- skipToEveryoneElse,
1666- localHoldoutDecision : holdoutDecision . result ,
1667- } ;
1666+ if ( localHoldout ( ) ) {
1667+ // Check local holdouts targeting this specific delivery rule (FSSDK-12369).
1668+ // Inserted immediately after the forced-decision block, before audience and traffic allocation checks.
1669+ const localHoldoutsForDelivery = getHoldoutsForRule ( configObj , rule . id ) ;
1670+ for ( const holdout of localHoldoutsForDelivery ) {
1671+ const holdoutDecision = this . getVariationForHoldout ( configObj , holdout , user ) ;
1672+ decideReasons . push ( ...holdoutDecision . reasons ) ;
1673+ if ( holdoutDecision . result . variation ) {
1674+ return {
1675+ result : holdoutDecision . result ,
1676+ reasons : decideReasons ,
1677+ holdoutDecision : true ,
1678+ } ;
1679+ }
16681680 }
16691681 }
16701682
0 commit comments