Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
029529a
add Python versions to readme
eli-darkly Jun 26, 2018
8a3f700
Merge pull request #63 from launchdarkly/eb/ch19355/readme
eli-darkly Jun 26, 2018
ed20f3c
add Python 3.7-rc to build
eli-darkly Jun 28, 2018
53599b1
fix config
eli-darkly Jun 28, 2018
515aa3c
Merge pull request #64 from launchdarkly/eb/ch19764/py3.7
eli-darkly Jun 28, 2018
8b8d87b
use final 3.7 release in CI build
eli-darkly Jul 5, 2018
3af872e
Merge pull request #65 from launchdarkly/eb/ch19764/py3.7
eli-darkly Jul 5, 2018
ca88418
update readme
eli-darkly Jul 5, 2018
9fb607c
Merge branch 'eb/ch19764/py3.7'
eli-darkly Jul 5, 2018
ff280b2
Remove @ashanbrown from CODEOWNERS
ashanbrown Jul 24, 2018
f0d7570
better log output for stream failures
eli-darkly Aug 2, 2018
1cb2355
Merge pull request #66 from launchdarkly/eb/ch18405/backoff-logging
eli-darkly Aug 3, 2018
2cdb92a
Merge branch 'master' of github.com:launchdarkly/python-client
eli-darkly Aug 3, 2018
4d03e32
add new version of all_flags that captures more metadata
eli-darkly Aug 18, 2018
f64fd29
provide a method that returns a JSONable dictionary instead of just a…
eli-darkly Aug 20, 2018
f6e019a
misc fixes
eli-darkly Aug 21, 2018
3b9efb6
add ability to filter for only client-side flags
eli-darkly Aug 21, 2018
7e5fa8a
Merge pull request #67 from launchdarkly/eb/ch22308/all-flags-state
eli-darkly Aug 22, 2018
fb1d857
Merge pull request #68 from launchdarkly/eb/ch12124/client-side-filter
eli-darkly Aug 22, 2018
5523d2d
implement evaluation with explanations
eli-darkly Aug 23, 2018
0b088d4
simplify default logic & add tests
eli-darkly Aug 24, 2018
ed69b55
add missing docstrings to client methods
eli-darkly Aug 24, 2018
eb1282a
revert accidental change
eli-darkly Aug 24, 2018
9bb5843
typo
eli-darkly Aug 24, 2018
a32a1a1
Merge pull request #70 from launchdarkly/eb/ch22348/docstrings
eli-darkly Aug 24, 2018
b256091
Merge branch 'explanation' into eb/ch19976/explanation
eli-darkly Aug 24, 2018
cd82aa3
comment formatting
eli-darkly Aug 24, 2018
97622a3
comment formatting
eli-darkly Aug 24, 2018
2d41316
Merge branch 'explanation' into eb/ch19976/explanation
eli-darkly Aug 24, 2018
b460881
Merge branch 'master' of github.com:launchdarkly/python-client
eli-darkly Aug 27, 2018
60e46f9
Merge pull request #69 from launchdarkly/eb/ch19976/explanation
eli-darkly Aug 29, 2018
3ba9352
fix event value when prerequisite flag is off
eli-darkly Aug 29, 2018
1d13ab4
comment
eli-darkly Aug 29, 2018
e27adfb
Merge pull request #71 from launchdarkly/eb/ch22995/prereq-off
eli-darkly Aug 29, 2018
22d4a5b
Merge branch 'explanation'
eli-darkly Aug 30, 2018
816bf47
version 6.4.0
eli-darkly Aug 30, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

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

## [6.4.0] - 2018-08-29
### Added:
- 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.

### Fixed:
- When evaluating a prerequisite feature flag, the analytics event for the evaluation did not include the result value if the prerequisite flag was off.

## [6.3.0] - 2018-08-27
### Added:
- 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.
Expand Down
132 changes: 80 additions & 52 deletions ldclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import hashlib
import hmac
import threading
import traceback

from builtins import object

from ldclient.config import Config as Config
from ldclient.event_processor import NullEventProcessor
from ldclient.feature_requester import FeatureRequesterImpl
from ldclient.flag import evaluate
from ldclient.flag import EvaluationDetail, evaluate, error_reason
from ldclient.flags_state import FeatureFlagsState
from ldclient.polling import PollingUpdateProcessor
from ldclient.streaming import StreamingUpdateProcessor
Expand Down Expand Up @@ -184,69 +185,91 @@ def variation(self, key, user, default):
available from LaunchDarkly
:return: one of the flag's variation values, or the default value
"""
return self._evaluate_internal(key, user, default, False).value

def variation_detail(self, key, user, default):
"""Determines the variation of a feature flag for a user, like `variation`, but also
provides additional information about how this value was calculated.

The return value is an EvaluationDetail object, which has three properties:

`value`: the value that was calculated for this user (same as the return value
of `variation`)

`variation_index`: the positional index of this value in the flag, e.g. 0 for the
first variation - or `None` if the default value was returned

`reason`: a hash describing the main reason why this value was selected.

The `reason` will also be included in analytics events, if you are capturing
detailed event data for this flag.

:param string key: the unique key for the feature flag
:param dict user: a dictionary containing parameters for the end user requesting the flag
:param object default: the default value of the flag, to be used if the value is not
available from LaunchDarkly
:return: an EvaluationDetail object describing the result
:rtype: EvaluationDetail
"""
return self._evaluate_internal(key, user, default, True)

def _evaluate_internal(self, key, user, default, include_reasons_in_events):
default = self._config.get_default(key, default)
if user is not None:
self._sanitize_user(user)

if self._config.offline:
return default
return EvaluationDetail(default, None, error_reason('CLIENT_NOT_READY'))

if user is not None:
self._sanitize_user(user)

def send_event(value, version=None):
self._send_event({'kind': 'feature', 'key': key, 'user': user, 'variation': None,
'value': value, 'default': default, 'version': version,
'trackEvents': False, 'debugEventsUntilDate': None})
def send_event(value, variation=None, flag=None, reason=None):
self._send_event({'kind': 'feature', 'key': key, 'user': user,
'value': value, 'variation': variation, 'default': default,
'version': flag.get('version') if flag else None,
'trackEvents': flag.get('trackEvents') if flag else None,
'debugEventsUntilDate': flag.get('debugEventsUntilDate') if flag else None,
'reason': reason if include_reasons_in_events else None})

if not self.is_initialized():
if self._store.initialized:
log.warn("Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key)
else:
log.warn("Feature Flag evaluation attempted before client has initialized! Feature store unavailable - returning default: "
+ str(default) + " for feature key: " + key)
send_event(default)
return default

reason = error_reason('CLIENT_NOT_READY')
send_event(default, None, None, reason)
return EvaluationDetail(default, None, reason)

if user is not None and user.get('key', "") == "":
log.warn("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly.")

def cb(flag):
try:
if not flag:
log.info("Feature Flag key: " + key + " not found in Feature Store. Returning default.")
send_event(default)
return default

return self._evaluate_and_send_events(flag, user, default)

except Exception as e:
log.error("Exception caught in variation: " + e.message + " for flag key: " + key + " and user: " + str(user))
send_event(default)

return default

return self._store.get(FEATURES, key, cb)

def _evaluate(self, flag, user):
return evaluate(flag, user, self._store)

def _evaluate_and_send_events(self, flag, user, default):
if user is None or user.get('key') is None:
log.warn("Missing user or user key when evaluating Feature Flag key: " + flag.get('key') + ". Returning default.")
value = default
variation = None
flag = self._store.get(FEATURES, key, lambda x: x)
if not flag:
reason = error_reason('FLAG_NOT_FOUND')
send_event(default, None, None, reason)
return EvaluationDetail(default, None, reason)
else:
result = evaluate(flag, user, self._store)
for event in result.events or []:
self._send_event(event)
value = default if result.value is None else result.value
variation = result.variation

self._send_event({'kind': 'feature', 'key': flag.get('key'),
'user': user, 'variation': variation, 'value': value,
'default': default, 'version': flag.get('version'),
'trackEvents': flag.get('trackEvents'),
'debugEventsUntilDate': flag.get('debugEventsUntilDate')})
return value
if user is None or user.get('key') is None:
reason = error_reason('USER_NOT_SPECIFIED')
send_event(default, None, flag, reason)
return EvaluationDetail(default, None, reason)

try:
result = evaluate(flag, user, self._store, include_reasons_in_events)
for event in result.events or []:
self._send_event(event)
detail = result.detail
if detail.is_default_value():
detail = EvaluationDetail(default, None, detail.reason)
send_event(detail.value, detail.variation_index, flag, detail.reason)
return detail
except Exception as e:
log.error("Unexpected error while evaluating feature flag \"%s\": %s" % (key, e))
log.debug(traceback.format_exc())
reason = error_reason('EXCEPTION')
send_event(default, None, flag, reason)
return EvaluationDetail(default, None, reason)

def all_flags(self, user):
"""Returns all feature flag values for the given user.

Expand All @@ -272,7 +295,8 @@ def all_flags_state(self, user, **kwargs):
:param dict user: the end user requesting the feature flags
:param kwargs: optional parameters affecting how the state is computed: set
`client_side_only=True` to limit it to only flags that are marked for use with the
client-side SDK (by default, all flags are included)
client-side SDK (by default, all flags are included); set `with_reasons=True` to
include evaluation reasons in the state (see `variation_detail`)
:return: a FeatureFlagsState object (will never be None; its 'valid' property will be False
if the client is offline, has not been initialized, or the user is None or has no key)
:rtype: FeatureFlagsState
Expand All @@ -294,6 +318,7 @@ def all_flags_state(self, user, **kwargs):

state = FeatureFlagsState(True)
client_only = kwargs.get('client_side_only', False)
with_reasons = kwargs.get('with_reasons', False)
try:
flags_map = self._store.all(FEATURES, lambda x: x)
except Exception as e:
Expand All @@ -304,11 +329,14 @@ def all_flags_state(self, user, **kwargs):
if client_only and not flag.get('clientSide', False):
continue
try:
result = self._evaluate(flag, user)
state.add_flag(flag, result.value, result.variation)
detail = evaluate(flag, user, self._store, False).detail
state.add_flag(flag, detail.value, detail.variation_index,
detail.reason if with_reasons else None)
except Exception as e:
log.error("Error evaluating flag \"%s\" in all_flags_state: %s" % (key, e))
state.add_flag(flag, None, None)
log.debug(traceback.format_exc())
reason = {'kind': 'ERROR', 'errorKind': 'EXCEPTION'}
state.add_flag(flag, None, None, reason if with_reasons else None)

return state

Expand Down
2 changes: 2 additions & 0 deletions ldclient/event_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ def make_output_event(self, e):
out['user'] = self._user_filter.filter_user_props(e['user'])
else:
out['userKey'] = e['user'].get('key')
if e.get('reason'):
out['reason'] = e.get('reason')
return out
elif kind == 'identify':
return {
Expand Down
Loading