99from typing import overload
1010from ._defaultfilters import TimeWindowFilter , TargetingFilter
1111from ._featurefilters import FeatureFilter
12- from ._models import FeatureFlag , Variant , EvaluationEvent , TargetingContext
12+ from ._models import FeatureFlag , Variant , EvaluationEvent , VariantAssignmentReason , TargetingContext
1313
1414
1515FEATURE_MANAGEMENT_KEY = "feature_management"
@@ -73,6 +73,8 @@ class FeatureManager:
7373
7474 :param Mapping configuration: Configuration object.
7575 :keyword list[FeatureFilter] feature_filters: Custom filters to be used for evaluating feature flags.
76+ :keyword Callable[EvaluationEvent] on_feature_evaluated: Callback function to be called when a feature flag is
77+ evaluated.
7678 """
7779
7880 def __init__ (self , configuration , ** kwargs ):
@@ -82,6 +84,7 @@ def __init__(self, configuration, **kwargs):
8284 self ._configuration = configuration
8385 self ._cache = {}
8486 self ._copy = configuration .get (FEATURE_MANAGEMENT_KEY )
87+ self ._on_feature_evaluated = kwargs .pop ("on_feature_evaluated" , None )
8588 filters = [TimeWindowFilter (), TargetingFilter ()] + kwargs .pop (PROVIDED_FEATURE_FILTERS , [])
8689
8790 for feature_filter in filters :
@@ -90,54 +93,63 @@ def __init__(self, configuration, **kwargs):
9093 self ._filters [feature_filter .name ] = feature_filter
9194
9295 @staticmethod
93- def _check_default_disabled_variant (feature_flag ):
96+ def _check_default_disabled_variant (evaluation_event ):
9497 """
9598 A method called when the feature flag is disabled, to determine what the default variant should be. If there is
9699 no allocation, then None is set as the value of the variant in the EvaluationEvent.
97100
98- :param FeatureFlag feature_flag: Feature flag object.
99- :return: EvaluationEvent
101+ :param EvaluationEvent evaluation_event: Evaluation event object.
100102 """
101- if not feature_flag .allocation :
102- return EvaluationEvent (enabled = False )
103- return FeatureManager ._check_variant_override (
104- feature_flag .variants , feature_flag .allocation .default_when_disabled , False
103+ evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_DISABLED
104+ if not evaluation_event .feature .allocation :
105+ return
106+ FeatureManager ._check_variant_override (
107+ evaluation_event .feature .variants ,
108+ evaluation_event .feature .allocation .default_when_disabled ,
109+ False ,
110+ evaluation_event ,
105111 )
106112
107113 @staticmethod
108- def _check_default_enabled_variant (feature_flag ):
114+ def _check_default_enabled_variant (evaluation_event ):
109115 """
110116 A method called when the feature flag is enabled, to determine what the default variant should be. If there is
111117 no allocation, then None is set as the value of the variant in the EvaluationEvent.
112118
113- :param FeatureFlag feature_flag: Feature flag object.
114- :return: EvaluationEvent
119+ :param EvaluationEvent evaluation_event: Evaluation event object.
115120 """
116- if not feature_flag .allocation :
117- return EvaluationEvent (enabled = True )
118- return FeatureManager ._check_variant_override (
119- feature_flag .variants , feature_flag .allocation .default_when_enabled , True
121+ evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_ENABLED
122+ if not evaluation_event .feature .allocation :
123+ return
124+ FeatureManager ._check_variant_override (
125+ evaluation_event .feature .variants ,
126+ evaluation_event .feature .allocation .default_when_enabled ,
127+ True ,
128+ evaluation_event ,
120129 )
121130
122131 @staticmethod
123- def _check_variant_override (variants , default_variant_name , status ):
132+ def _check_variant_override (variants , default_variant_name , status , evaluation_event ):
124133 """
125134 A method to check if a variant is overridden to be enabled or disabled by the variant.
126135
127136 :param list[Variant] variants: List of variants.
128137 :param str default_variant_name: Name of the default variant.
129138 :param bool status: Status of the feature flag.
130- :return: EvaluationEvent
139+ :param EvaluationEvent evaluation_event: Evaluation event object.
131140 """
132141 if not variants or not default_variant_name :
133- return EvaluationEvent (enabled = status )
142+ evaluation_event .enabled = status
143+ return
134144 for variant in variants :
135145 if variant .name == default_variant_name :
136146 if variant .status_override == "Enabled" :
137- return EvaluationEvent (enabled = True )
147+ evaluation_event .enabled = True
148+ return
138149 if variant .status_override == "Disabled" :
139- return EvaluationEvent (enabled = False )
140- return EvaluationEvent (enabled = status )
150+ evaluation_event .enabled = False
151+ return
152+ evaluation_event .enabled = status
141153
142154 @staticmethod
143155 def _is_targeted (context_id ):
@@ -147,34 +159,45 @@ def _is_targeted(context_id):
147159
148160 return (context_marker / (2 ** 32 - 1 )) * 100
149161
150- def _assign_variant (self , feature_flag , targeting_context ):
162+ def _assign_variant (self , feature_flag , targeting_context , evaluation_event ):
151163 """
152164 Assign a variant to the user based on the allocation.
153165
154- :param FeatureFlag feature_flag: Feature flag object.
155166 :param TargetingContext targeting_context: Targeting context.
156- :return: Variant name .
167+ :param EvaluationEvent evaluation_event: Evaluation event object .
157168 """
158- if not feature_flag .variants or not feature_flag .allocation :
159- return None
160- if feature_flag .allocation .user and targeting_context .user_id :
161- for user_allocation in feature_flag .allocation .user :
169+ feature = evaluation_event .feature
170+ variant_name = None
171+ if not feature .variants or not feature .allocation :
172+ return
173+ if feature .allocation .user and targeting_context .user_id :
174+ for user_allocation in feature .allocation .user :
162175 if targeting_context .user_id in user_allocation .users :
163- return user_allocation .variant
164- if feature_flag .allocation .group and len (targeting_context .groups ) > 0 :
165- for group_allocation in feature_flag .allocation .group :
176+ evaluation_event .reason = VariantAssignmentReason .USER
177+ variant_name = user_allocation .variant
178+ elif feature .allocation .group and len (targeting_context .groups ) > 0 :
179+ for group_allocation in feature .allocation .group :
166180 for group in targeting_context .groups :
167181 if group in group_allocation .groups :
168- return group_allocation .variant
169- if feature_flag .allocation .percentile :
170- context_id = targeting_context .user_id + "\n " + feature_flag .allocation .seed
182+ evaluation_event .reason = VariantAssignmentReason .GROUP
183+ variant_name = group_allocation .variant
184+ elif feature .allocation .percentile :
185+ context_id = targeting_context .user_id + "\n " + feature .allocation .seed
171186 box = self ._is_targeted (context_id )
172- for percentile_allocation in feature_flag .allocation .percentile :
187+ for percentile_allocation in feature .allocation .percentile :
173188 if box == 100 and percentile_allocation .percentile_to == 100 :
174- return percentile_allocation .variant
189+ variant_name = percentile_allocation .variant
175190 if percentile_allocation .percentile_from <= box < percentile_allocation .percentile_to :
176- return percentile_allocation .variant
177- return None
191+ evaluation_event .reason = VariantAssignmentReason .PERCENTILE
192+ variant_name = percentile_allocation .variant
193+ if not variant_name :
194+ FeatureManager ._check_default_enabled_variant (evaluation_event )
195+ evaluation_event .variant = self ._variant_name_to_variant (
196+ feature_flag , feature_flag .allocation .default_when_enabled
197+ )
198+ return
199+ evaluation_event .variant = self ._variant_name_to_variant (feature_flag , variant_name )
200+ FeatureManager ._check_variant_override (feature_flag .variants , variant_name , True , evaluation_event )
178201
179202 def _variant_name_to_variant (self , feature_flag , variant_name ):
180203 """
@@ -186,6 +209,8 @@ def _variant_name_to_variant(self, feature_flag, variant_name):
186209 """
187210 if not feature_flag .variants :
188211 return None
212+ if not variant_name :
213+ return None
189214 for variant_reference in feature_flag .variants :
190215 if variant_reference .name == variant_name :
191216 configuration = variant_reference .configuration_value
@@ -230,6 +255,9 @@ def is_enabled(self, feature_flag_id, *args, **kwargs):
230255 targeting_context = self ._build_targeting_context (args )
231256
232257 result = self ._check_feature (feature_flag_id , targeting_context , ** kwargs )
258+ if self ._on_feature_evaluated and result .feature .telemetry .enabled :
259+ result .user = targeting_context .user_id
260+ self ._on_feature_evaluated (result )
233261 return result .enabled
234262
235263 @overload
@@ -255,12 +283,15 @@ def get_variant(self, feature_flag_id, *args, **kwargs):
255283 targeting_context = self ._build_targeting_context (args )
256284
257285 result = self ._check_feature (feature_flag_id , targeting_context , ** kwargs )
286+ if self ._on_feature_evaluated and result .feature .telemetry .enabled :
287+ result .user = targeting_context .user_id
288+ self ._on_feature_evaluated (result )
258289 return result .variant
259290
260- def _check_feature_filters (self , feature_flag , targeting_context , ** kwargs ):
291+ def _check_feature_filters (self , evaluation_event , targeting_context , ** kwargs ):
292+ feature_flag = evaluation_event .feature
261293 feature_conditions = feature_flag .conditions
262294 feature_filters = feature_conditions .client_filters
263- evaluation_event = EvaluationEvent (enabled = False )
264295
265296 if len (feature_filters ) == 0 :
266297 # Feature flags without any filters return evaluate
@@ -283,31 +314,24 @@ def _check_feature_filters(self, feature_flag, targeting_context, **kwargs):
283314 elif self ._filters [filter_name ].evaluate (feature_filter , ** kwargs ):
284315 evaluation_event .enabled = True
285316 break
286- return evaluation_event
287-
288- def _assign_allocation (self , feature_flag , evaluation_event , targeting_context ):
289- if feature_flag .allocation and feature_flag .variants :
290- variant_name = self ._assign_variant (feature_flag , targeting_context )
291- if variant_name :
292- evaluation_event .enabled = FeatureManager ._check_variant_override (
293- feature_flag .variants , variant_name , evaluation_event .enabled
294- ).enabled
295- evaluation_event .variant = self ._variant_name_to_variant (feature_flag , variant_name )
296- evaluation_event .feature = feature_flag
297- return evaluation_event
298317
299- variant_name = None
300- if evaluation_event .enabled :
301- evaluation_event = FeatureManager ._check_default_enabled_variant (feature_flag )
302- if feature_flag .allocation :
303- variant_name = feature_flag .allocation .default_when_enabled
304- else :
305- evaluation_event = FeatureManager ._check_default_disabled_variant (feature_flag )
306- if feature_flag .allocation :
307- variant_name = feature_flag .allocation .default_when_disabled
308- evaluation_event .variant = self ._variant_name_to_variant (feature_flag , variant_name )
309- evaluation_event .feature = feature_flag
310- return evaluation_event
318+ def _assign_allocation (self , evaluation_event , targeting_context ):
319+ feature_flag = evaluation_event .feature
320+ if feature_flag .variants :
321+ if not feature_flag .allocation :
322+ if evaluation_event .enabled :
323+ evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_ENABLED
324+ return
325+ evaluation_event .reason = VariantAssignmentReason .DEFAULT_WHEN_DISABLED
326+ return
327+ if not evaluation_event .enabled :
328+ FeatureManager ._check_default_disabled_variant (evaluation_event )
329+ evaluation_event .variant = self ._variant_name_to_variant (
330+ feature_flag , feature_flag .allocation .default_when_disabled
331+ )
332+ return
333+
334+ self ._assign_variant (feature_flag , targeting_context , evaluation_event )
311335
312336 def _check_feature (self , feature_flag_id , targeting_context , ** kwargs ):
313337 """
@@ -327,23 +351,28 @@ def _check_feature(self, feature_flag_id, targeting_context, **kwargs):
327351 else :
328352 feature_flag = self ._cache .get (feature_flag_id )
329353
354+ evaluation_event = EvaluationEvent (feature_flag )
330355 if not feature_flag :
331356 logging .warning ("Feature flag %s not found" , feature_flag_id )
332357 # Unknown feature flags are disabled by default
333- return EvaluationEvent ( enabled = False )
358+ return evaluation_event
334359
335360 if not feature_flag .enabled :
336361 # Feature flags that are disabled are always disabled
337- evaluation_event = FeatureManager ._check_default_disabled_variant (feature_flag )
362+ FeatureManager ._check_default_disabled_variant (evaluation_event )
338363 if feature_flag .allocation :
339364 variant_name = feature_flag .allocation .default_when_disabled
340365 evaluation_event .variant = self ._variant_name_to_variant (feature_flag , variant_name )
341366 evaluation_event .feature = feature_flag
367+
368+ # If a feature flag is disabled and override can't enable it
369+ evaluation_event .enabled = False
342370 return evaluation_event
343371
344- evaluation_event = self ._check_feature_filters (feature_flag , targeting_context , ** kwargs )
372+ self ._check_feature_filters (evaluation_event , targeting_context , ** kwargs )
345373
346- return self ._assign_allocation (feature_flag , evaluation_event , targeting_context )
374+ self ._assign_allocation (evaluation_event , targeting_context )
375+ return evaluation_event
347376
348377 def list_feature_flag_names (self ):
349378 """
0 commit comments