99
1010from .client_initialize_formatter import ClientInitializeResponseFormatter
1111from .config_evaluation import _ConfigEvaluation
12+ from .evaluation_context import EvaluationContext
1213from .evaluation_details import EvaluationDetails , EvaluationReason , DataSource
1314from .globals import logger
1415from .spec_store import _SpecStore , EntityType
@@ -113,6 +114,7 @@ def get_client_initialize_response(
113114 hash : HashingAlgorithm ,
114115 client_sdk_key = None ,
115116 include_local_override = False ,
117+ target_app_id : Optional [str ] = None ,
116118 ):
117119 if not self ._spec_store .is_ready_for_checks ():
118120 return None
@@ -122,7 +124,7 @@ def get_client_initialize_response(
122124
123125 return ClientInitializeResponseFormatter \
124126 .get_formatted_response (self .__eval_config , user , self ._spec_store , self , hash , client_sdk_key ,
125- include_local_override )
127+ include_local_override , target_app_id )
126128
127129 def _create_evaluation_details (self ,
128130 reason : EvaluationReason = EvaluationReason .none ,
@@ -217,7 +219,8 @@ def __lookup_override(self, config_overrides, user):
217219
218220 def __lookup_config_mapping (self , user : StatsigUser , config_name : str , spec_type : EntityType ,
219221 end_result : _ConfigEvaluation ,
220- maybe_config : Union [Dict [str , Any ], None ] = None ) -> bool :
222+ context : EvaluationContext ,
223+ maybe_config : Union [Dict [str , Any ], None ] = None ,) -> bool :
221224 overrides = self ._spec_store .get_overrides ()
222225 if overrides is None or not isinstance (overrides , dict ):
223226 return False
@@ -245,7 +248,8 @@ def __lookup_config_mapping(self, user: StatsigUser, config_name: str, spec_type
245248 continue
246249
247250 end_result .reset ()
248- self .__evaluate_rule (user , rule , end_result )
251+ context .sampling_rate = rule .get ("samplingRate" , None )
252+ self .__evaluate_rule (user , rule , end_result , context )
249253 if not end_result .boolean_value or end_result .evaluation_details .reason in (
250254 EvaluationReason .unsupported , EvaluationReason .unrecognized ):
251255 end_result .reset ()
@@ -257,7 +261,7 @@ def __lookup_config_mapping(self, user: StatsigUser, config_name: str, spec_type
257261 config_pass = self .__eval_pass_percentage (user , rule , new_config , spec_salt )
258262 if config_pass :
259263 end_result .override_config_name = override_config_name
260- self .__evaluate (user , override_config_name , spec_type , end_result )
264+ self .__evaluate (user , override_config_name , spec_type , end_result , context )
261265 if end_result .evaluation_details .reason == EvaluationReason .none :
262266 return True
263267 return False
@@ -278,14 +282,16 @@ def unsupported_or_unrecognized(self, config_name, end_result):
278282 return self ._create_evaluation_details (EvaluationReason .unsupported )
279283 return self ._create_evaluation_details (EvaluationReason .unrecognized )
280284
281- def check_gate (self , user , gate , end_result = None , is_nested = False ):
285+ def check_gate (self , user , gate , end_result = None , is_nested = False , context : Optional [ EvaluationContext ] = None ):
282286 override = self .__lookup_gate_override (user , gate )
283287 if override is not None :
284288 return override
285289
286290 if end_result is None :
287291 end_result = _ConfigEvaluation ()
288- self .__eval_config (user , gate , EntityType .GATE , end_result , is_nested )
292+ if context is None :
293+ context = EvaluationContext ()
294+ self .__eval_config (user , gate , EntityType .GATE , end_result , context , is_nested )
289295 return end_result
290296
291297 def get_config (self , user , config_name ):
@@ -294,7 +300,7 @@ def get_config(self, user, config_name):
294300 return override
295301
296302 result = _ConfigEvaluation ()
297- self .__eval_config (user , config_name , EntityType .CONFIG , result )
303+ self .__eval_config (user , config_name , EntityType .CONFIG , result , EvaluationContext () )
298304 return result
299305
300306 def get_layer (self , user , layer_name ):
@@ -303,16 +309,16 @@ def get_layer(self, user, layer_name):
303309 return override
304310
305311 result = _ConfigEvaluation ()
306- self .__eval_config (user , layer_name , EntityType .LAYER , result )
312+ self .__eval_config (user , layer_name , EntityType .LAYER , result , EvaluationContext () )
307313 return result
308314
309- def __eval_config (self , user , config_name , entity_type : EntityType , end_result , is_nested = False ):
315+ def __eval_config (self , user , config_name , entity_type : EntityType , end_result , context : EvaluationContext , is_nested = False ):
310316 try :
311317 if not entity_type :
312318 logger .warning ("invalid entity type in evaluation: %s" , config_name )
313319 end_result .rule_id = "error"
314320 return
315- self .__evaluate (user , config_name , entity_type , end_result , is_nested )
321+ self .__evaluate (user , config_name , entity_type , end_result , context , is_nested )
316322 except RecursionError :
317323 raise
318324 except Exception :
@@ -328,10 +334,10 @@ def __check_id_in_list(self, id, list_name):
328334 sha256 (str (id ).encode ('utf-8' )).digest ()).decode ('utf-8' )[0 :8 ]
329335 return hashed in ids
330336
331- def __evaluate (self , user , config_name , entity_type , end_result , is_nested = False ):
337+ def __evaluate (self , user , config_name , entity_type , end_result , context : EvaluationContext , is_nested = False ):
332338 maybe_config_spec = self .__get_config_by_entity_type (config_name , entity_type )
333339
334- override_config = self .__lookup_config_mapping (user , config_name , entity_type , end_result , maybe_config_spec )
340+ override_config = self .__lookup_config_mapping (user , config_name , entity_type , end_result , context , maybe_config_spec )
335341 if override_config :
336342 return
337343
@@ -344,9 +350,10 @@ def __evaluate(self, user, config_name, entity_type, end_result, is_nested=False
344350 return
345351
346352 for rule in maybe_config_spec .get ("rules" , []):
347- self .__evaluate_rule (user , rule , end_result )
353+ context .sampling_rate = rule .get ("samplingRate" , None )
354+ self .__evaluate_rule (user , rule , end_result , context )
348355 if end_result .boolean_value :
349- if self .__evaluate_delegate (user , rule , end_result ) is not None :
356+ if self .__evaluate_delegate (user , rule , end_result , context ) is not None :
350357 self .__finalize_exposures (end_result )
351358 return
352359
@@ -389,15 +396,15 @@ def __finalize_exposures(self, end_result):
389396 end_result .secondary_exposures = self .clean_exposures (end_result .secondary_exposures )
390397 end_result .undelegated_secondary_exposures = self .clean_exposures (end_result .undelegated_secondary_exposures )
391398
392- def __evaluate_rule (self , user , rule , end_result ):
399+ def __evaluate_rule (self , user , rule , end_result , context : EvaluationContext ):
393400 total_eval_result = True
394401 for condition in rule .get ("conditions" , []):
395- eval_result = self .__evaluate_condition (user , condition , end_result , rule . get ( "samplingRate" , None ) )
402+ eval_result = self .__evaluate_condition (user , condition , end_result , context )
396403 if not eval_result :
397404 total_eval_result = False
398405 end_result .boolean_value = total_eval_result
399406
400- def __evaluate_delegate (self , user , rule , end_result ):
407+ def __evaluate_delegate (self , user , rule , end_result , context : EvaluationContext ):
401408 config_delegate = rule .get ("configDelegate" , None )
402409 if config_delegate is None :
403410 return None
@@ -408,23 +415,23 @@ def __evaluate_delegate(self, user, rule, end_result):
408415
409416 end_result .undelegated_secondary_exposures = end_result .secondary_exposures [:]
410417
411- self .__evaluate (user , config_delegate , EntityType .CONFIG , end_result , True )
418+ self .__evaluate (user , config_delegate , EntityType .CONFIG , end_result , context , True )
412419 end_result .explicit_parameters = config .get (
413420 "explicitParameters" , [])
414421 end_result .allocated_experiment = config_delegate
415422 return end_result
416423
417- def __evaluate_condition (self , user , condition , end_result , sampling_rate = None ):
424+ def __evaluate_condition (self , user , condition , end_result , context : EvaluationContext ):
418425 value = None
419426 type = condition .get ("type" , "" ).upper ()
420427 target = condition .get ("targetValue" )
421428 field = condition .get ("field" , "" )
422429 id_Type = condition .get ("idType" , "userID" )
423430 if type == "PUBLIC" :
424- end_result .analytical_condition = sampling_rate is None
431+ end_result .analytical_condition = context . sampling_rate is None
425432 return True
426433 if type in ("FAIL_GATE" , "PASS_GATE" ):
427- delegated_gate = self .check_gate (user , target , end_result , True )
434+ delegated_gate = self .check_gate (user , target , end_result , True , context )
428435
429436 new_exposure = {
430437 "gate" : target ,
@@ -438,15 +445,15 @@ def __evaluate_condition(self, user, condition, end_result, sampling_rate=None):
438445
439446 pass_gate = delegated_gate .boolean_value if type == "PASS_GATE" else not delegated_gate .boolean_value
440447
441- end_result .analytical_condition = sampling_rate is None
448+ end_result .analytical_condition = context . sampling_rate is None
442449 return pass_gate
443450 if type in ("MULTI_PASS_GATE" , "MULTI_FAIL_GATE" ):
444451 if target is None or len (target ) == 0 :
445- end_result .analytical_condition = sampling_rate is None
452+ end_result .analytical_condition = context . sampling_rate is None
446453 return False
447454 pass_gate = False
448455 for gate in target :
449- other_result = self .check_gate (user , gate )
456+ other_result = self .check_gate (user , gate , context = context )
450457
451458 new_exposure = {
452459 "gate" : gate ,
@@ -461,7 +468,7 @@ def __evaluate_condition(self, user, condition, end_result, sampling_rate=None):
461468 if pass_gate :
462469 break
463470
464- end_result .analytical_condition = sampling_rate is None
471+ end_result .analytical_condition = context . sampling_rate is None
465472 return pass_gate
466473 if type == "IP_BASED" :
467474 value = self .__get_from_user (user , field )
@@ -478,7 +485,7 @@ def __evaluate_condition(self, user, condition, end_result, sampling_rate=None):
478485 self ._country_lookup = CountryLookup ()
479486 value = self ._country_lookup .lookupStr (ip )
480487 if value is None :
481- end_result .analytical_condition = sampling_rate is None
488+ end_result .analytical_condition = context . sampling_rate is None
482489 return False
483490 elif type == "UA_BASED" :
484491 value = self .__get_from_user (user , field )
@@ -499,8 +506,13 @@ def __evaluate_condition(self, user, condition, end_result, sampling_rate=None):
499506 salt_str + "." + unit_id ) % 1000 )
500507 elif type == "UNIT_ID" :
501508 value = self .__get_unit_id (user , id_Type )
509+ elif type == "TARGET_APP" :
510+ if context .client_key is not None :
511+ value = context .target_app_id
512+ else :
513+ value = self ._spec_store .get_app_id ()
502514
503- end_result .analytical_condition = sampling_rate is None
515+ end_result .analytical_condition = context . sampling_rate is None
504516
505517 op = condition .get ("operator" )
506518 user_bucket = condition .get ("user_bucket" )
0 commit comments