3
3
import hashlib
4
4
import hmac
5
5
import threading
6
+ import traceback
6
7
7
8
from builtins import object
8
9
9
10
from ldclient .config import Config as Config
10
11
from ldclient .event_processor import NullEventProcessor
11
12
from ldclient .feature_requester import FeatureRequesterImpl
12
- from ldclient .flag import evaluate
13
+ from ldclient .flag import EvaluationDetail , evaluate , error_reason
13
14
from ldclient .flags_state import FeatureFlagsState
14
15
from ldclient .polling import PollingUpdateProcessor
15
16
from ldclient .streaming import StreamingUpdateProcessor
@@ -184,69 +185,91 @@ def variation(self, key, user, default):
184
185
available from LaunchDarkly
185
186
:return: one of the flag's variation values, or the default value
186
187
"""
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 ):
187
217
default = self ._config .get_default (key , default )
188
- if user is not None :
189
- self ._sanitize_user (user )
190
218
191
219
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 )
193
224
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 })
198
232
199
233
if not self .is_initialized ():
200
234
if self ._store .initialized :
201
235
log .warn ("Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key )
202
236
else :
203
237
log .warn ("Feature Flag evaluation attempted before client has initialized! Feature store unavailable - returning default: "
204
238
+ 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
+
208
243
if user is not None and user .get ('key' , "" ) == "" :
209
244
log .warn ("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly." )
210
245
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 )
236
251
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 )
249
256
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
+
250
273
def all_flags (self , user ):
251
274
"""Returns all feature flag values for the given user.
252
275
@@ -272,7 +295,8 @@ def all_flags_state(self, user, **kwargs):
272
295
:param dict user: the end user requesting the feature flags
273
296
:param kwargs: optional parameters affecting how the state is computed: set
274
297
`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`)
276
300
:return: a FeatureFlagsState object (will never be None; its 'valid' property will be False
277
301
if the client is offline, has not been initialized, or the user is None or has no key)
278
302
:rtype: FeatureFlagsState
@@ -294,6 +318,7 @@ def all_flags_state(self, user, **kwargs):
294
318
295
319
state = FeatureFlagsState (True )
296
320
client_only = kwargs .get ('client_side_only' , False )
321
+ with_reasons = kwargs .get ('with_reasons' , False )
297
322
try :
298
323
flags_map = self ._store .all (FEATURES , lambda x : x )
299
324
except Exception as e :
@@ -304,11 +329,14 @@ def all_flags_state(self, user, **kwargs):
304
329
if client_only and not flag .get ('clientSide' , False ):
305
330
continue
306
331
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 )
309
335
except Exception as e :
310
336
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 )
312
340
313
341
return state
314
342
0 commit comments