def get_all_data(self): uri = self._poll_uri hdrs = _headers(self._config) cache_entry = self._cache.get(uri) if cache_entry is not None: hdrs['If-None-Match'] = cache_entry.etag r = self._http.request('GET', uri, headers=hdrs, timeout=urllib3.Timeout( connect=self._config.connect_timeout, read=self._config.read_timeout), retries=1) throw_if_unsuccessful_response(r) if r.status == 304 and cache_entry is not None: data = cache_entry.data etag = cache_entry.etag from_cache = True else: data = json.loads(r.data.decode('UTF-8')) etag = r.getheader('ETag') from_cache = False if etag is not None: self._cache[uri] = CacheEntry(data=data, etag=etag) log.debug("%s response status:[%d] From cache? [%s] ETag:[%s]", uri, r.status, from_cache, etag) return {FEATURES: data['flags'], SEGMENTS: data['segments']}
def do_send(should_retry): # noinspection PyBroadException try: if isinstance(events, dict): body = [events] else: body = events json_body = jsonpickle.encode(body, unpicklable=False) log.debug('Sending events payload: ' + json_body) hdrs = _headers(self.sdk_key) uri = self._config.events_uri r = self._session.post(uri, headers=hdrs, timeout=(self._config.connect_timeout, self._config.read_timeout), data=json_body) r.raise_for_status() except ProtocolError as e: if e.args is not None and len( e.args) > 1 and e.args[1] is not None: inner = e.args[1] if inner.errno is not None and inner.errno == errno.ECONNRESET and should_retry: log.warning( 'ProtocolError exception caught while sending events. Retrying.' ) do_send(False) else: log.exception( 'Unhandled exception in event consumer. Analytics events were not processed.' ) except: log.exception( 'Unhandled exception in event consumer. Analytics events were not processed.' )
def _do_request(self, uri, allow_cache): hdrs = _headers(self._config.sdk_key) if allow_cache: cache_entry = self._cache.get(uri) if cache_entry is not None: hdrs['If-None-Match'] = cache_entry.etag r = self._http.request('GET', uri, headers=hdrs, timeout=urllib3.Timeout( connect=self._config.connect_timeout, read=self._config.read_timeout), retries=1) throw_if_unsuccessful_response(r) if r.status == 304 and allow_cache and cache_entry is not None: data = cache_entry.data etag = cache_entry.etag from_cache = True else: data = json.loads(r.data.decode('UTF-8')) etag = r.getheader('ETag') from_cache = False if allow_cache and etag is not None: self._cache[uri] = CacheEntry(data=data, etag=etag) log.debug("%s response status:[%d] From cache? [%s] ETag:[%s]", uri, r.status, from_cache, etag) return data
def process_message(store, requester, msg, ready): payload = json.loads(msg.data) log.debug("Received stream event {}".format(msg.event)) if msg.event == 'put': store.init(payload) if not ready.is_set() and store.initialized: ready.set() log.info("StreamingUpdateProcessor initialized ok") elif msg.event == 'patch': key = payload['path'][1:] feature = payload['data'] log.debug("Updating feature {}".format(key)) store.upsert(key, feature) elif msg.event == "indirect/patch": key = payload['data'] store.upsert(key, requester.get_one(key)) elif msg.event == "indirect/put": store.init(requester.get_all()) if not ready.is_set() and store.initialized: ready.set() log.info("StreamingUpdateProcessor initialized ok") elif msg.event == 'delete': key = payload['path'][1:] # noinspection PyShadowingNames version = payload['version'] store.delete(key, version) else: log.warning('Unhandled event in stream processor: ' + msg.event)
def init(self, features): try: self._lock.lock() self._features = dict(features) self._initialized = True log.debug("Initialized feature store with " + str(len(features)) + " features") finally: self._lock.unlock()
def upsert(self, key, feature): try: self._lock.lock() f = self._features.get(key) if f is None or f['version'] < feature['version']: self._features[key] = feature log.debug("Updated feature {0} to version {1}".format(key, feature['version'])) finally: self._lock.unlock()
def _evaluate_internal(self, key, user, default, include_reasons_in_events): default = self._config.get_default(key, default) if self._config.offline: return EvaluationDetail(default, None, error_reason('CLIENT_NOT_READY')) if user is not None: self._sanitize_user(user) 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) 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.") 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: 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 run(self): log.debug("Starting stream processor") self._running = True hdrs = _stream_headers(self._api_key) uri = self._config.stream_uri + "/" messages = SSEClient(uri, verify=self._config.verify, headers=hdrs) for msg in messages: if not self._running: break self.process_message(self._store, msg)
def variation(self, key, user, default): default = self._config.get_default(key, default) self._sanitize_user(user) if self._config.offline: return default def send_event(value, version=None): self._send_event({ 'kind': 'feature', 'key': key, 'user': user, 'value': value, 'default': default, 'version': version }) if not self.is_initialized(): log.warn( "Feature Flag evaluation attempted before client has finished initializing! Returning default: " + str(default) + " for feature key: " + key) send_event(default) return default if user is None or user.get('key') is None: log.warn( "Missing user or user key when evaluating Feature Flag key: " + key + ". Returning default.") send_event(default) return default if user.get('key', "") == "": log.warn( "User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly." ) flag = self._store.get(key) if not flag: log.warn("Feature Flag key: " + key + " not found in Feature Store. Returning default.") send_event(default) return default value, events = evaluate(flag, user, self._store) for event in events or []: self._send_event(event) log.debug("Sending event: " + str(event)) if value is not None: send_event(value, flag.get('version')) return value send_event(default, flag.get('version')) return default
def get_one(self, key): hdrs = _headers(self._sdk_key) uri = self._config.get_latest_features_uri + '/' + key log.debug("Getting one feature flag using uri: " + uri) r = self._session.get(uri, headers=hdrs, timeout=(self._config.connect_timeout, self._config.read_timeout)) r.raise_for_status() feature = r.json() return feature
def init(self, all_data): try: self._lock.rlock() self._items.clear() self._items.update(all_data) self._initialized = True for k in all_data: log.debug("Initialized '%s' store with %d items", k.namespace, len(all_data[k])) finally: self._lock.runlock()
def get_all(self): hdrs = _headers(self._sdk_key) uri = self._config.get_latest_features_uri log.debug("Getting all flags using uri: " + uri) r = self._session.get(uri, headers=hdrs, timeout=(self._config.connect_timeout, self._config.read_timeout)) r.raise_for_status() features = r.json() return features
def init(self, all_data): """ """ try: self._lock.rlock() self._items.clear() self._items.update(all_data) self._initialized = True for k in all_data: log.debug("Initialized '%s' store with %d items", k.namespace, len(all_data[k])) finally: self._lock.runlock()
def upsert(self, kind, item): key = item['key'] try: self._lock.rlock() itemsOfKind = self._items[kind] i = itemsOfKind.get(key) if i is None or i['version'] < item['version']: itemsOfKind[key] = item log.debug("Updated %s in '%s' to version %d", key, kind.namespace, item['version']) finally: self._lock.runlock()
def all_flags_state(self, user, **kwargs): """Returns an object that encapsulates the state of all feature flags for a given user, including the flag values and also metadata that can be used on the front end. This method does not send analytics events back to LaunchDarkly. :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); 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 """ if self._config.offline: log.warn("all_flags_state() called, but client is in offline mode. Returning empty state") return FeatureFlagsState(False) if not self.is_initialized(): if self._store.initialized: log.warn("all_flags_state() called before client has finished initializing! Using last known values from feature store") else: log.warn("all_flags_state() called before client has finished initializing! Feature store unavailable - returning empty state") return FeatureFlagsState(False) if user is None or user.get('key') is None: log.warn("User or user key is None when calling all_flags_state(). Returning empty state.") return FeatureFlagsState(False) 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: log.error("Unable to read flags for all_flag_state: %s" % e) return FeatureFlagsState(False) for key, flag in flags_map.items(): if client_only and not flag.get('clientSide', False): continue try: 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)) log.debug(traceback.format_exc()) reason = {'kind': 'ERROR', 'errorKind': 'EXCEPTION'} state.add_flag(flag, None, None, reason if with_reasons else None) return state
def run(self): # noinspection PyBroadException try: json_body = json.dumps(self._event_body) log.debug('Sending diagnostic event: ' + json_body) _post_events_with_retry( self._http, self._config, self._config.events_base_uri + '/diagnostic', None, json_body, "diagnostic event") except Exception as e: log.warning( 'Unhandled exception in event processor. Diagnostic event was not sent. [%s]', e)
def upsert(self, kind, item): """ """ key = item['key'] try: self._lock.rlock() itemsOfKind = self._items[kind] i = itemsOfKind.get(key) if i is None or i['version'] < item['version']: itemsOfKind[key] = item log.debug("Updated %s in '%s' to version %d", key, kind.namespace, item['version']) finally: self._lock.runlock()
def get(self, key, callback): try: self._lock.rlock() f = self._features.get(key) if f is None: log.debug("Attempted to get missing feature: " + str(key) + " Returning None") return callback(None) if 'deleted' in f and f['deleted']: log.debug("Attempted to get deleted feature: " + str(key) + " Returning None") return callback(None) return callback(f) finally: self._lock.runlock()
def dispatch_event(self): """ Dispatch the event """ # If last character is LF, strip it. if self.data.endswith('\n'): self.data = self.data[:-1] log.debug("Dispatching event %s[%s]: %s", self.event, self.id, self.data) event = Event(self.data, self.event, self.id, self.retry) self.on_event(event) if self.id: self.last_id = self.id self.reset()
def process_message(store, requester, msg): if msg.event == 'put': all_data = json.loads(msg.data) init_data = { FEATURES: all_data['data']['flags'], SEGMENTS: all_data['data']['segments'] } log.debug("Received put event with %d flags and %d segments", len(init_data[FEATURES]), len(init_data[SEGMENTS])) store.init(init_data) return True elif msg.event == 'patch': payload = json.loads(msg.data) path = payload['path'] obj = payload['data'] log.debug("Received patch event for %s, New version: [%d]", path, obj.get("version")) target = StreamingUpdateProcessor._parse_path(path) if target is not None: store.upsert(target.kind, obj) else: log.warning("Patch for unknown path: %s", path) elif msg.event == "indirect/patch": path = msg.data log.debug("Received indirect/patch event for %s", path) target = StreamingUpdateProcessor._parse_path(path) if target is not None: store.upsert(target.kind, requester.get_one(target.kind, target.key)) else: log.warning("Indirect patch for unknown path: %s", path) elif msg.event == "indirect/put": log.debug("Received indirect/put event") store.init(requester.get_all_data()) return True elif msg.event == 'delete': payload = json.loads(msg.data) path = payload['path'] # noinspection PyShadowingNames version = payload['version'] log.debug("Received delete event for %s, New version: [%d]", path, version) target = StreamingUpdateProcessor._parse_path(path) if target is not None: store.delete(target.kind, target.key, version) else: log.warning("Delete for unknown path: %s", path) else: log.warning('Unhandled event in stream processor: ' + msg.event) return False
def run(self): # noinspection PyBroadException try: json_body = json.dumps(self._event_body) log.debug('Sending diagnostic event: ' + json_body) hdrs = _headers(self._config) uri = self._config.events_base_uri + '/diagnostic' r = self._http.request('POST', uri, headers=hdrs, timeout=urllib3.Timeout(connect=self._config.connect_timeout, read=self._config.read_timeout), body=json_body, retries=1) except Exception as e: log.warning( 'Unhandled exception in event processor. Diagnostic event was not sent. [%s]', e)
def _do_send(self, output_events): # noinspection PyBroadException try: json_body = json.dumps(output_events) log.debug('Sending events payload: ' + json_body) payload_id = str(uuid.uuid4()) r = _post_events_with_retry( self._http, self._config, self._config.events_uri, payload_id, json_body, "%d events" % len(self._payload.events)) if r: self._response_fn(r) return r except Exception as e: log.warning( 'Unhandled exception in event processor. Analytics events were not processed. [%s]', e)
def get(self, kind, key, callback): """ """ try: self._lock.rlock() itemsOfKind = self._items[kind] item = itemsOfKind.get(key) if item is None: log.debug("Attempted to get missing key %s in '%s', returning None", key, kind.namespace) return callback(None) if 'deleted' in item and item['deleted']: log.debug("Attempted to get deleted key %s in '%s', returning None", key, kind.namespace) return callback(None) return callback(item) finally: self._lock.runlock()
def get(self, kind, key, callback): try: self._lock.rlock() itemsOfKind = self._items[kind] item = itemsOfKind.get(key) if item is None: log.debug( "Attempted to get missing key %s in '%s', returning None", key, kind.namespace) return callback(None) if 'deleted' in item and item['deleted']: log.debug( "Attempted to get deleted key %s in '%s', returning None", key, kind.namespace) return callback(None) return callback(item) finally: self._lock.runlock()
def _do_send(self, output_events): # noinspection PyBroadException try: json_body = json.dumps(output_events) log.debug('Sending events payload: ' + json_body) hdrs = _headers(self._config.sdk_key) hdrs['X-LaunchDarkly-Event-Schema'] = str(__CURRENT_EVENT_SCHEMA__) uri = self._config.events_uri r = self._http.request('POST', uri, headers=hdrs, timeout=urllib3.Timeout(connect=self._config.connect_timeout, read=self._config.read_timeout), body=json_body, retries=1) self._response_fn(r) return r except Exception as e: log.warning( 'Unhandled exception in event processor. Analytics events were not processed. [%s]', e)
def _do_request(self, base_uri, allow_cache): hdrs = _headers(self._config.sdk_key, client='PythonMobile') method = "GET" body = None uri = base_uri cache_uri = uri + EVALX_GET + '/' + self._config.user_b64 if self._config.use_report: method = 'REPORT' body = self._config.user_json hdrs.update({'Content-Type': 'application/json'}) uri = uri + EVALX_REPORT else: uri = cache_uri if self._config.evaluation_reasons: uri += '?withReasons=true' cache_uri += '?withReasons=true' if allow_cache: cache_entry = self._cache.get(cache_uri) if cache_entry is not None: hdrs['If-None-Match'] = cache_entry.etag r = self._http.request(method, uri, headers=hdrs, timeout=urllib3.Timeout( connect=self._config.connect_timeout, read=self._config.read_timeout), retries=1, body=body) throw_if_unsuccessful_response(r) if r.status == 304 and allow_cache and cache_entry is not None: data = cache_entry.data etag = cache_entry.etag from_cache = True else: data = json.loads(r.data.decode('UTF-8')) etag = r.getheader('ETag') from_cache = False if allow_cache and etag is not None: self._cache[uri] = CacheEntry(data=data, etag=etag) log.debug("%s response status:[%d] From cache? [%s] ETag:[%s]", uri, r.status, from_cache, etag) return data
def process_message(store, requester, msg): if msg.event == 'put': all_data = json.loads(msg.data) for k,v in all_data.items(): v['key'] = k init_data = { FEATURES: all_data } log.debug("Received put event with %d flags", len(init_data[FEATURES])) store.init(init_data) return True elif msg.event == 'patch': recv_time = time.time() * 1000 payload = json.loads(msg.data) if payload.get('key') == 'locust-heartbeat': value = int(payload.get('value') or 0) duration = int((recv_time - value)) request_success.fire(request_type='sse:flag-update', name='/meval', response_time=duration, response_length=0) log.debug("Received patch event for %s, New version: [%d]", payload.get('key'), payload.get("version")) store.upsert(FEATURES, payload) elif msg.event == 'ping': log.debug('Received ping event') all_data = requester.get_all_data() store.init(all_data) log.debug("Received flags after ping event with %d flags", len(all_data[FEATURES])) return True elif msg.event == 'delete': payload = json.loads(msg.data) key = payload.get('key') # noinspection PyShadowingNames version = payload['version'] log.debug("Received delete event for %s, New version: [%d]", key, version) target = ParsedPath(kind = FEATURES, key = key) store.delete(target.kind, target.key, version) else: log.warning('Unhandled event in stream processor: ' + msg.event) return False
def get(self, kind: VersionedDataKind, key: str, callback: Callable[[Any], Any] = lambda x: x) -> Any: """ """ try: self._lock.rlock() itemsOfKind = self._items[kind] item = itemsOfKind.get(key) if item is None: log.debug( "Attempted to get missing key %s in '%s', returning None", key, kind.namespace) return callback(None) if 'deleted' in item and item['deleted']: log.debug( "Attempted to get deleted key %s in '%s', returning None", key, kind.namespace) return callback(None) return callback(item) finally: self._lock.runlock()
def _do_request(self, uri, allow_cache): hdrs = _headers(self._config.sdk_key) if allow_cache: cache_entry = self._cache.get(uri) if cache_entry is not None: hdrs['If-None-Match'] = cache_entry.etag r = self._http.request('GET', uri, headers=hdrs, timeout=urllib3.Timeout(connect=self._config.connect_timeout, read=self._config.read_timeout), retries=1) throw_if_unsuccessful_response(r) if r.status == 304 and cache_entry is not None: data = cache_entry.data etag = cache_entry.etag from_cache = True else: data = json.loads(r.data.decode('UTF-8')) etag = r.getheader('ETag') from_cache = False if allow_cache and etag is not None: self._cache[uri] = CacheEntry(data=data, etag=etag) log.debug("%s response status:[%d] From cache? [%s] ETag:[%s]", uri, r.status, from_cache, etag) return data
def do_send(should_retry): # noinspection PyBroadException try: json_body = self._serializer.serialize_events(events) log.debug('Sending events payload: ' + json_body) hdrs = _headers(self._config.sdk_key) uri = self._config.events_uri r = self._session.post(uri, headers=hdrs, timeout=(self._config.connect_timeout, self._config.read_timeout), data=json_body) if r.status_code == 401: log.error( 'Received 401 error, no further events will be posted since SDK key is invalid' ) self.stop() return r.raise_for_status() except ProtocolError as e: if e.args is not None and len( e.args) > 1 and e.args[1] is not None: inner = e.args[1] if inner.errno is not None and inner.errno == errno.ECONNRESET and should_retry: log.warning( 'ProtocolError exception caught while sending events. Retrying.' ) do_send(False) else: log.warning( 'Unhandled exception in event consumer. Analytics events were not processed.', exc_info=True) except: log.warning( 'Unhandled exception in event consumer. Analytics events were not processed.', exc_info=True)
def run(self): log.debug("Starting event consumer") self._running = True while self._running: self.send()
def all_flags_state(self, user, **kwargs): """Returns an object that encapsulates the state of all feature flags for a given user, including the flag values and also metadata that can be used on the front end. See the JavaScript SDK Reference Guide on `Bootstrapping <https://docs.launchdarkly.com/docs/js-sdk-reference#section-bootstrapping>`_. This method does not send analytics events back to LaunchDarkly. :param dict user: the end user requesting the feature flags :param kwargs: optional parameters affecting how the state is computed - see below :Keyword Arguments: * **client_side_only** (*boolean*) -- set to True to limit it to only flags that are marked for use with the client-side SDK (by default, all flags are included) * **with_reasons** (*boolean*) -- set to True to include evaluation reasons in the state (see :func:`variation_detail()`) * **details_only_for_tracked_flags** (*boolean*) -- set to True to omit any metadata that is normally only used for event generation, such as flag versions and evaluation reasons, unless the flag has event tracking or debugging turned on :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 """ if self._config.offline: log.warn( "all_flags_state() called, but client is in offline mode. Returning empty state" ) return FeatureFlagsState(False) if not self.is_initialized(): if self._store.initialized: log.warn( "all_flags_state() called before client has finished initializing! Using last known values from feature store" ) else: log.warn( "all_flags_state() called before client has finished initializing! Feature store unavailable - returning empty state" ) return FeatureFlagsState(False) if user is None or user.get('key') is None: log.warn( "User or user key is None when calling all_flags_state(). Returning empty state." ) return FeatureFlagsState(False) state = FeatureFlagsState(True) client_only = kwargs.get('client_side_only', False) with_reasons = kwargs.get('with_reasons', False) details_only_if_tracked = kwargs.get('details_only_for_tracked_flags', False) try: flags_map = self._store.all(FEATURES, lambda x: x) if flags_map is None: raise ValueError("feature store error") except Exception as e: log.error("Unable to read flags for all_flag_state: %s" % repr(e)) return FeatureFlagsState(False) for key, flag in flags_map.items(): if client_only and not flag.get('clientSide', False): continue try: detail = evaluate(flag, user, self._store, False).detail state.add_flag(flag, detail.value, detail.variation_index, detail.reason if with_reasons else None, details_only_if_tracked) except Exception as e: log.error( "Error evaluating flag \"%s\" in all_flags_state: %s" % (key, repr(e))) log.debug(traceback.format_exc()) reason = {'kind': 'ERROR', 'errorKind': 'EXCEPTION'} state.add_flag(flag, None, None, reason if with_reasons else None, details_only_if_tracked) return state
def _evaluate_internal(self, key, user, default, event_factory): default = self._config.get_default(key, default) if self._config.offline: return EvaluationDetail(default, None, error_reason('CLIENT_NOT_READY')) if not self.is_initialized(): if self._store.initialized: log.warning( "Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key) else: log.warning( "Feature Flag evaluation attempted before client has initialized! Feature store unavailable - returning default: " + str(default) + " for feature key: " + key) reason = error_reason('CLIENT_NOT_READY') self._send_event( event_factory.new_unknown_flag_event( key, user, default, reason)) return EvaluationDetail(default, None, reason) if user is not None and user.get('key', "") == "": log.warning( "User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly." ) try: flag = self._store.get(FEATURES, key, lambda x: x) except Exception as e: log.error( "Unexpected error while retrieving feature flag \"%s\": %s" % (key, repr(e))) log.debug(traceback.format_exc()) reason = error_reason('EXCEPTION') self._send_event( event_factory.new_unknown_flag_event(key, user, default, reason)) return EvaluationDetail(default, None, reason) if not flag: reason = error_reason('FLAG_NOT_FOUND') self._send_event( event_factory.new_unknown_flag_event(key, user, default, reason)) return EvaluationDetail(default, None, reason) else: if user is None or user.get('key') is None: reason = error_reason('USER_NOT_SPECIFIED') self._send_event( event_factory.new_default_event(flag, user, default, reason)) return EvaluationDetail(default, None, reason) try: result = evaluate(flag, user, self._store, event_factory) for event in result.events or []: self._send_event(event) detail = result.detail if detail.is_default_value(): detail = EvaluationDetail(default, None, detail.reason) self._send_event( event_factory.new_eval_event(flag, user, detail, default)) return detail except Exception as e: log.error( "Unexpected error while evaluating feature flag \"%s\": %s" % (key, repr(e))) log.debug(traceback.format_exc()) reason = error_reason('EXCEPTION') self._send_event( event_factory.new_default_event(flag, user, default, reason)) return EvaluationDetail(default, None, reason)