33import hashlib
44import hmac
55import threading
6+ import traceback
67
78from builtins import object
89
910from ldclient .config import Config as Config
1011from ldclient .event_processor import NullEventProcessor
1112from ldclient .feature_requester import FeatureRequesterImpl
12- from ldclient .flag import evaluate
13+ from ldclient .flag import EvaluationDetail , evaluate , error_reason
1314from ldclient .flags_state import FeatureFlagsState
1415from ldclient .polling import PollingUpdateProcessor
1516from ldclient .streaming import StreamingUpdateProcessor
@@ -184,69 +185,91 @@ def variation(self, key, user, default):
184185 available from LaunchDarkly
185186 :return: one of the flag's variation values, or the default value
186187 """
188+ return self ._evaluate_internal (key , user , default , False ).value
189+
190+ def variation_detail (self , key , user , default ):
191+ """Determines the variation of a feature flag for a user, like `variation`, but also
192+ provides additional information about how this value was calculated.
193+
194+ The return value is an EvaluationDetail object, which has three properties:
195+
196+ `value`: the value that was calculated for this user (same as the return value
197+ of `variation`)
198+
199+ `variation_index`: the positional index of this value in the flag, e.g. 0 for the
200+ first variation - or `None` if the default value was returned
201+
202+ `reason`: a hash describing the main reason why this value was selected.
203+
204+ The `reason` will also be included in analytics events, if you are capturing
205+ detailed event data for this flag.
206+
207+ :param string key: the unique key for the feature flag
208+ :param dict user: a dictionary containing parameters for the end user requesting the flag
209+ :param object default: the default value of the flag, to be used if the value is not
210+ available from LaunchDarkly
211+ :return: an EvaluationDetail object describing the result
212+ :rtype: EvaluationDetail
213+ """
214+ return self ._evaluate_internal (key , user , default , True )
215+
216+ def _evaluate_internal (self , key , user , default , include_reasons_in_events ):
187217 default = self ._config .get_default (key , default )
188- if user is not None :
189- self ._sanitize_user (user )
190218
191219 if self ._config .offline :
192- return default
220+ return EvaluationDetail (default , None , error_reason ('CLIENT_NOT_READY' ))
221+
222+ if user is not None :
223+ self ._sanitize_user (user )
193224
194- def send_event (value , version = None ):
195- self ._send_event ({'kind' : 'feature' , 'key' : key , 'user' : user , 'variation' : None ,
196- 'value' : value , 'default' : default , 'version' : version ,
197- 'trackEvents' : False , 'debugEventsUntilDate' : None })
225+ def send_event (value , variation = None , flag = None , reason = None ):
226+ self ._send_event ({'kind' : 'feature' , 'key' : key , 'user' : user ,
227+ 'value' : value , 'variation' : variation , 'default' : default ,
228+ 'version' : flag .get ('version' ) if flag else None ,
229+ 'trackEvents' : flag .get ('trackEvents' ) if flag else None ,
230+ 'debugEventsUntilDate' : flag .get ('debugEventsUntilDate' ) if flag else None ,
231+ 'reason' : reason if include_reasons_in_events else None })
198232
199233 if not self .is_initialized ():
200234 if self ._store .initialized :
201235 log .warn ("Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key )
202236 else :
203237 log .warn ("Feature Flag evaluation attempted before client has initialized! Feature store unavailable - returning default: "
204238 + str (default ) + " for feature key: " + key )
205- send_event (default )
206- return default
207-
239+ reason = error_reason ('CLIENT_NOT_READY' )
240+ send_event (default , None , None , reason )
241+ return EvaluationDetail (default , None , reason )
242+
208243 if user is not None and user .get ('key' , "" ) == "" :
209244 log .warn ("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly." )
210245
211- def cb (flag ):
212- try :
213- if not flag :
214- log .info ("Feature Flag key: " + key + " not found in Feature Store. Returning default." )
215- send_event (default )
216- return default
217-
218- return self ._evaluate_and_send_events (flag , user , default )
219-
220- except Exception as e :
221- log .error ("Exception caught in variation: " + e .message + " for flag key: " + key + " and user: " + str (user ))
222- send_event (default )
223-
224- return default
225-
226- return self ._store .get (FEATURES , key , cb )
227-
228- def _evaluate (self , flag , user ):
229- return evaluate (flag , user , self ._store )
230-
231- def _evaluate_and_send_events (self , flag , user , default ):
232- if user is None or user .get ('key' ) is None :
233- log .warn ("Missing user or user key when evaluating Feature Flag key: " + flag .get ('key' ) + ". Returning default." )
234- value = default
235- variation = None
246+ flag = self ._store .get (FEATURES , key , lambda x : x )
247+ if not flag :
248+ reason = error_reason ('FLAG_NOT_FOUND' )
249+ send_event (default , None , None , reason )
250+ return EvaluationDetail (default , None , reason )
236251 else :
237- result = evaluate (flag , user , self ._store )
238- for event in result .events or []:
239- self ._send_event (event )
240- value = default if result .value is None else result .value
241- variation = result .variation
242-
243- self ._send_event ({'kind' : 'feature' , 'key' : flag .get ('key' ),
244- 'user' : user , 'variation' : variation , 'value' : value ,
245- 'default' : default , 'version' : flag .get ('version' ),
246- 'trackEvents' : flag .get ('trackEvents' ),
247- 'debugEventsUntilDate' : flag .get ('debugEventsUntilDate' )})
248- return value
252+ if user is None or user .get ('key' ) is None :
253+ reason = error_reason ('USER_NOT_SPECIFIED' )
254+ send_event (default , None , flag , reason )
255+ return EvaluationDetail (default , None , reason )
249256
257+ try :
258+ result = evaluate (flag , user , self ._store , include_reasons_in_events )
259+ for event in result .events or []:
260+ self ._send_event (event )
261+ detail = result .detail
262+ if detail .is_default_value ():
263+ detail = EvaluationDetail (default , None , detail .reason )
264+ send_event (detail .value , detail .variation_index , flag , detail .reason )
265+ return detail
266+ except Exception as e :
267+ log .error ("Unexpected error while evaluating feature flag \" %s\" : %s" % (key , e ))
268+ log .debug (traceback .format_exc ())
269+ reason = error_reason ('EXCEPTION' )
270+ send_event (default , None , flag , reason )
271+ return EvaluationDetail (default , None , reason )
272+
250273 def all_flags (self , user ):
251274 """Returns all feature flag values for the given user.
252275
@@ -272,7 +295,8 @@ def all_flags_state(self, user, **kwargs):
272295 :param dict user: the end user requesting the feature flags
273296 :param kwargs: optional parameters affecting how the state is computed: set
274297 `client_side_only=True` to limit it to only flags that are marked for use with the
275- client-side SDK (by default, all flags are included)
298+ client-side SDK (by default, all flags are included); set `with_reasons=True` to
299+ include evaluation reasons in the state (see `variation_detail`)
276300 :return: a FeatureFlagsState object (will never be None; its 'valid' property will be False
277301 if the client is offline, has not been initialized, or the user is None or has no key)
278302 :rtype: FeatureFlagsState
@@ -294,6 +318,7 @@ def all_flags_state(self, user, **kwargs):
294318
295319 state = FeatureFlagsState (True )
296320 client_only = kwargs .get ('client_side_only' , False )
321+ with_reasons = kwargs .get ('with_reasons' , False )
297322 try :
298323 flags_map = self ._store .all (FEATURES , lambda x : x )
299324 except Exception as e :
@@ -304,11 +329,14 @@ def all_flags_state(self, user, **kwargs):
304329 if client_only and not flag .get ('clientSide' , False ):
305330 continue
306331 try :
307- result = self ._evaluate (flag , user )
308- state .add_flag (flag , result .value , result .variation )
332+ detail = evaluate (flag , user , self ._store , False ).detail
333+ state .add_flag (flag , detail .value , detail .variation_index ,
334+ detail .reason if with_reasons else None )
309335 except Exception as e :
310336 log .error ("Error evaluating flag \" %s\" in all_flags_state: %s" % (key , e ))
311- state .add_flag (flag , None , None )
337+ log .debug (traceback .format_exc ())
338+ reason = {'kind' : 'ERROR' , 'errorKind' : 'EXCEPTION' }
339+ state .add_flag (flag , None , None , reason if with_reasons else None )
312340
313341 return state
314342
0 commit comments