@@ -19,7 +19,7 @@ import {
1919 USER_HAS_NO_FORCED_VARIATION
2020} from 'log_message' ;
2121import { beforeEach , describe , expect , it , MockInstance , vi } from 'vitest' ;
22- import { CMAB_DUMMY_ENTITY_ID , CMAB_FETCH_FAILED , DecisionService } from '.' ;
22+ import { CMAB_DUMMY_ENTITY_ID , CMAB_FETCH_FAILED , DecisionService , TARGETED_DELIVERY_EXCLUDED_FROM_HOLDOUT } from '.' ;
2323import OptimizelyUserContext from '../../optimizely_user_context' ;
2424import { createProjectConfig , ProjectConfig } from '../../project_config/project_config' ;
2525import { BucketerParams , Experiment , ExperimentBucketMap , Holdout , OptimizelyDecideOption , UserAttributes , UserProfile } from '../../shared_types' ;
@@ -2196,6 +2196,170 @@ describe('DecisionService', () => {
21962196 decisionSource : DECISION_SOURCES . HOLDOUT ,
21972197 } ) ;
21982198 } ) ;
2199+
2200+ describe ( 'excludeTargetedDeliveries' , ( ) => {
2201+ const getExcludeTDDatafile = ( excludeTargetedDeliveries ?: boolean ) => {
2202+ const datafile = getHoldoutTestDatafile ( ) ;
2203+ datafile . holdouts = datafile . holdouts . map ( ( holdout : any ) => {
2204+ if ( holdout . id === 'holdout_running_id' ) {
2205+ return {
2206+ ...holdout ,
2207+ ...( excludeTargetedDeliveries !== undefined ? { excludeTargetedDeliveries } : { } ) ,
2208+ } ;
2209+ }
2210+ return holdout ;
2211+ } ) ;
2212+ return datafile ;
2213+ } ;
2214+
2215+ it ( 'should return holdout variation immediately when excludeTargetedDeliveries is false' , async ( ) => {
2216+ const { decisionService } = getDecisionService ( ) ;
2217+ const config = createProjectConfig ( JSON . stringify ( getExcludeTDDatafile ( false ) ) ) ;
2218+ const user = new OptimizelyUserContext ( {
2219+ optimizely : { } as any ,
2220+ userId : 'tester' ,
2221+ attributes : { age : 20 } ,
2222+ } ) ;
2223+
2224+ const feature = config . featureKeyMap [ 'flag_1' ] ;
2225+ const value = decisionService . resolveVariationsForFeatureList ( 'async' , config , [ feature ] , user , { } ) . get ( ) ;
2226+ const variation = ( await value ) [ 0 ] ;
2227+
2228+ expect ( variation . result ) . toEqual ( {
2229+ experiment : config . holdoutIdMap && config . holdoutIdMap [ 'holdout_running_id' ] ,
2230+ variation : config . variationIdMap [ 'holdout_variation_running_id' ] ,
2231+ decisionSource : DECISION_SOURCES . HOLDOUT ,
2232+ } ) ;
2233+ } ) ;
2234+
2235+ it ( 'should return holdout variation immediately when excludeTargetedDeliveries is undefined' , async ( ) => {
2236+ const { decisionService } = getDecisionService ( ) ;
2237+ const config = createProjectConfig ( JSON . stringify ( getExcludeTDDatafile ( undefined ) ) ) ;
2238+ const user = new OptimizelyUserContext ( {
2239+ optimizely : { } as any ,
2240+ userId : 'tester' ,
2241+ attributes : { age : 20 } ,
2242+ } ) ;
2243+
2244+ const feature = config . featureKeyMap [ 'flag_1' ] ;
2245+ const value = decisionService . resolveVariationsForFeatureList ( 'async' , config , [ feature ] , user , { } ) . get ( ) ;
2246+ const variation = ( await value ) [ 0 ] ;
2247+
2248+ expect ( variation . result ) . toEqual ( {
2249+ experiment : config . holdoutIdMap && config . holdoutIdMap [ 'holdout_running_id' ] ,
2250+ variation : config . variationIdMap [ 'holdout_variation_running_id' ] ,
2251+ decisionSource : DECISION_SOURCES . HOLDOUT ,
2252+ } ) ;
2253+ } ) ;
2254+
2255+ it ( 'should return delivery variation with holdout info when excludeTargetedDeliveries is true and delivery matches' , async ( ) => {
2256+ const { decisionService } = getDecisionService ( ) ;
2257+ const datafile = getExcludeTDDatafile ( true ) ;
2258+
2259+ const config = createProjectConfig ( JSON . stringify ( datafile ) ) ;
2260+ const user = new OptimizelyUserContext ( {
2261+ optimizely : { } as any ,
2262+ userId : 'tester' ,
2263+ attributes : { age : 20 } ,
2264+ } ) ;
2265+
2266+ mockBucket . mockImplementation ( ( param : BucketerParams ) => {
2267+ if ( param . experimentKey === 'delivery_1' ) {
2268+ return { result : '5004' , reasons : [ ] } ;
2269+ }
2270+ if ( param . experimentKey === 'holdout_running' ) {
2271+ return { result : 'holdout_variation_running_id' , reasons : [ ] } ;
2272+ }
2273+ return { result : null , reasons : [ ] } ;
2274+ } ) ;
2275+
2276+ const feature = config . featureKeyMap [ 'flag_1' ] ;
2277+ const value = decisionService . resolveVariationsForFeatureList ( 'async' , config , [ feature ] , user , { } ) . get ( ) ;
2278+ const variation = ( await value ) [ 0 ] ;
2279+
2280+ expect ( variation . result ) . toEqual ( {
2281+ experiment : config . experimentKeyMap [ 'delivery_1' ] ,
2282+ variation : config . variationIdMap [ '5004' ] ,
2283+ decisionSource : DECISION_SOURCES . ROLLOUT ,
2284+ holdout : {
2285+ experiment : config . holdoutIdMap && config . holdoutIdMap [ 'holdout_running_id' ] ,
2286+ variation : config . variationIdMap [ 'holdout_variation_running_id' ] ,
2287+ } ,
2288+ } ) ;
2289+
2290+ const reasonStrings = variation . reasons . map ( ( r : any ) => typeof r === 'string' ? r : r [ 0 ] ) ;
2291+ expect ( reasonStrings ) . toContain ( TARGETED_DELIVERY_EXCLUDED_FROM_HOLDOUT ) ;
2292+ } ) ;
2293+
2294+ it ( 'should return null decision with holdout info when excludeTargetedDeliveries is true but no delivery matches' , async ( ) => {
2295+ const { decisionService } = getDecisionService ( ) ;
2296+ const datafile = getExcludeTDDatafile ( true ) ;
2297+
2298+ const config = createProjectConfig ( JSON . stringify ( datafile ) ) ;
2299+ const user = new OptimizelyUserContext ( {
2300+ optimizely : { } as any ,
2301+ userId : 'tester' ,
2302+ attributes : { age : 20 } ,
2303+ } ) ;
2304+
2305+ mockBucket . mockImplementation ( ( param : BucketerParams ) => {
2306+ if ( param . experimentKey === 'holdout_running' ) {
2307+ return { result : 'holdout_variation_running_id' , reasons : [ ] } ;
2308+ }
2309+ return { result : null , reasons : [ ] } ;
2310+ } ) ;
2311+
2312+ const feature = config . featureKeyMap [ 'flag_1' ] ;
2313+ const value = decisionService . resolveVariationsForFeatureList ( 'async' , config , [ feature ] , user , { } ) . get ( ) ;
2314+ const variation = ( await value ) [ 0 ] ;
2315+
2316+ expect ( variation . result ) . toEqual ( {
2317+ experiment : null ,
2318+ variation : null ,
2319+ decisionSource : DECISION_SOURCES . ROLLOUT ,
2320+ holdout : {
2321+ experiment : config . holdoutIdMap && config . holdoutIdMap [ 'holdout_running_id' ] ,
2322+ variation : config . variationIdMap [ 'holdout_variation_running_id' ] ,
2323+ } ,
2324+ } ) ;
2325+ } ) ;
2326+
2327+ it ( 'should still block experiment rules when excludeTargetedDeliveries is true' , async ( ) => {
2328+ const { decisionService } = getDecisionService ( ) ;
2329+ const datafile = getExcludeTDDatafile ( true ) ;
2330+
2331+ const config = createProjectConfig ( JSON . stringify ( datafile ) ) ;
2332+ const user = new OptimizelyUserContext ( {
2333+ optimizely : { } as any ,
2334+ userId : 'tester' ,
2335+ attributes : { age : 20 } ,
2336+ } ) ;
2337+
2338+ mockBucket . mockImplementation ( ( param : BucketerParams ) => {
2339+ if ( param . experimentKey === 'holdout_running' ) {
2340+ return { result : 'holdout_variation_running_id' , reasons : [ ] } ;
2341+ }
2342+ if ( param . experimentKey === 'exp_1' ) {
2343+ return { result : '5001' , reasons : [ ] } ;
2344+ }
2345+ return { result : null , reasons : [ ] } ;
2346+ } ) ;
2347+
2348+ const feature = config . featureKeyMap [ 'flag_1' ] ;
2349+ const value = decisionService . resolveVariationsForFeatureList ( 'async' , config , [ feature ] , user , { } ) . get ( ) ;
2350+ const variation = ( await value ) [ 0 ] ;
2351+
2352+ expect ( variation . result ) . toEqual ( {
2353+ experiment : null ,
2354+ variation : null ,
2355+ decisionSource : DECISION_SOURCES . ROLLOUT ,
2356+ holdout : {
2357+ experiment : config . holdoutIdMap && config . holdoutIdMap [ 'holdout_running_id' ] ,
2358+ variation : config . variationIdMap [ 'holdout_variation_running_id' ] ,
2359+ } ,
2360+ } ) ;
2361+ } ) ;
2362+ } ) ;
21992363 } ) ;
22002364 } ) ;
22012365
@@ -3101,6 +3265,34 @@ describe('DecisionService', () => {
31013265 expect ( value [ 0 ] . result . decisionSource ) . toBe ( DECISION_SOURCES . FEATURE_TEST ) ;
31023266 expect ( value [ 0 ] . result . variation ?. key ) . toBe ( 'variation_1' ) ;
31033267 } ) ;
3268+
3269+ it ( 'local holdout ignores excludeTargetedDeliveries and applies normally' , async ( ) => {
3270+ const datafile = makeLocalHoldoutDatafile ( '2001' ) ;
3271+ ( datafile as any ) . localHoldouts [ 0 ] . excludeTargetedDeliveries = true ;
3272+ const config = createProjectConfig ( JSON . stringify ( datafile ) ) ;
3273+ const { decisionService } = getDecisionService ( ) ;
3274+
3275+ mockBucket . mockImplementation ( ( params : BucketerParams ) => {
3276+ if ( params . experimentId === 'local_holdout_id' ) {
3277+ return { result : 'local_holdout_variation_id' , reasons : [ ] } ;
3278+ }
3279+ return { result : null , reasons : [ ] } ;
3280+ } ) ;
3281+
3282+ const user = new OptimizelyUserContext ( {
3283+ optimizely : { } as any ,
3284+ userId : 'user1' ,
3285+ attributes : { age : 15 } ,
3286+ } ) ;
3287+
3288+ const feature = config . featureKeyMap [ 'flag_1' ] ;
3289+ const value = await decisionService . resolveVariationsForFeatureList ( 'async' , config , [ feature ] , user , { } ) . get ( ) ;
3290+
3291+ // Local holdout applies normally even with excludeTargetedDeliveries set
3292+ expect ( value [ 0 ] . result . decisionSource ) . toBe ( DECISION_SOURCES . HOLDOUT ) ;
3293+ expect ( value [ 0 ] . result . experiment ?. id ) . toBe ( 'local_holdout_id' ) ;
3294+ expect ( value [ 0 ] . result . variation ?. id ) . toBe ( 'local_holdout_variation_id' ) ;
3295+ } ) ;
31043296 } ) ;
31053297} ) ;
31063298
0 commit comments