Skip to content

Commit 5e81bca

Browse files
authored
prepare 6.4.0 release (#95)
1 parent 55f01c2 commit 5e81bca

File tree

11 files changed

+578
-204
lines changed

11 files changed

+578
-204
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to the LaunchDarkly Python SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
44

5+
## [6.4.0] - 2018-08-29
6+
### Added:
7+
- The new `LDClient` method `variation_detail` allows you to evaluate a feature flag (using the same parameters as you would for `variation`) and receive more information about how the value was calculated. This information is returned in an `EvaluationDetail` object, which contains both the result value and a "reason" object which will tell you, for instance, if the user was individually targeted for the flag or was matched by one of the flag's rules, or if the flag returned the default value due to an error.
8+
9+
### Fixed:
10+
- When evaluating a prerequisite feature flag, the analytics event for the evaluation did not include the result value if the prerequisite flag was off.
11+
512
## [6.3.0] - 2018-08-27
613
### Added:
714
- The new `LDClient` method `all_flags_state()` should be used instead of `all_flags()` if you are passing flag data to the front end for use with the JavaScript SDK. It preserves some flag metadata that the front end requires in order to send analytics events correctly. Versions 2.5.0 and above of the JavaScript SDK are able to use this metadata, but the output of `all_flags_state()` will still work with older versions.

ldclient/client.py

Lines changed: 80 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
import hashlib
44
import hmac
55
import threading
6+
import traceback
67

78
from builtins import object
89

910
from ldclient.config import Config as Config
1011
from ldclient.event_processor import NullEventProcessor
1112
from ldclient.feature_requester import FeatureRequesterImpl
12-
from ldclient.flag import evaluate
13+
from ldclient.flag import EvaluationDetail, evaluate, error_reason
1314
from ldclient.flags_state import FeatureFlagsState
1415
from ldclient.polling import PollingUpdateProcessor
1516
from 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

ldclient/event_processor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def make_output_event(self, e):
8484
out['user'] = self._user_filter.filter_user_props(e['user'])
8585
else:
8686
out['userKey'] = e['user'].get('key')
87+
if e.get('reason'):
88+
out['reason'] = e.get('reason')
8789
return out
8890
elif kind == 'identify':
8991
return {

0 commit comments

Comments
 (0)