Source code for ldclient.client

"""
This submodule contains the client class that provides most of the SDK functionality.
"""

import os
import threading
import traceback
from typing import Any, Callable, Dict, List, Optional, Tuple
from uuid import uuid4

from ldclient.config import Config
from ldclient.context import Context
from ldclient.evaluation import EvaluationDetail, FeatureFlagsState
from ldclient.hook import (
    EvaluationSeriesContext,
    Hook,
    _EvaluationWithHookResult
)
from ldclient.impl.big_segments import BigSegmentStoreManager
from ldclient.impl.client_common import (
    get_environment_metadata,
    get_plugin_hooks
)
from ldclient.impl.client_common import secure_mode_hash as _secure_mode_hash
from ldclient.impl.datasource.feature_requester import FeatureRequesterImpl
from ldclient.impl.datasource.polling import PollingUpdateProcessor
from ldclient.impl.datasource.streaming import StreamingUpdateProcessor
from ldclient.impl.datasystem import DataAvailability, DataSystem
from ldclient.impl.datasystem.fdv2 import FDv2
from ldclient.impl.evaluator import Evaluator, error_reason
from ldclient.impl.events.diagnostics import (
    _DiagnosticAccumulator,
    create_diagnostic_id
)
from ldclient.impl.events.event_processor import DefaultEventProcessor
from ldclient.impl.events.types import EventFactory
from ldclient.impl.flag_tracker import FlagTrackerImpl
from ldclient.impl.model.feature_flag import FeatureFlag
from ldclient.impl.rwlock import ReadWriteLock
from ldclient.impl.stubs import NullEventProcessor, NullUpdateProcessor
from ldclient.impl.util import check_uwsgi, log
from ldclient.interfaces import (
    BigSegmentStoreStatusProvider,
    DataSourceStatusProvider,
    DataStoreStatusProvider,
    FlagTracker
)
from ldclient.migrations import OpTracker, Stage
from ldclient.plugin import EnvironmentMetadata
from ldclient.versioned_data_kind import FEATURES, SEGMENTS

from .impl import AnyNum

_FORK_WARNING_MESSAGE = (
    "This process was forked after the LDClient was created. Background threads do not survive a fork, "
    "so flag updates and analytics events will not work in this process. Call LDClient.postfork() after forking."
)


[docs] class LDClient: """The LaunchDarkly SDK client object. Applications should configure the client at startup time and continue to use it throughout the lifetime of the application, rather than creating instances on the fly. The best way to do this is with the singleton methods :func:`ldclient.set_config()` and :func:`ldclient.get()`. However, you may also call the constructor directly if you need to maintain multiple instances. Client instances are thread-safe. """ def __init__(self, config: Config, start_wait: float = 5): """Constructs a new LDClient instance. :param config: optional custom configuration :param start_wait: the number of seconds to wait for a successful connection to LaunchDarkly """ check_uwsgi() self._config = config self._config._instance_id = str(uuid4()) self._config._validate() self._event_processor = None self._event_factory_default = EventFactory(False) self._event_factory_with_reasons = EventFactory(True) # Python has no lock-free atomic flag in the standard library. Code reaches this # lock only when data availability is cached, and only until the flag is set. self._cached_data_warning_lock = threading.Lock() self._eval_cached_data_warned = False self._all_flags_cached_data_warned = False self._owner_pid = os.getpid() self._fork_warned_pids: Dict[int, object] = {} self.__start_up(start_wait)
[docs] def postfork(self, start_wait: float = 5): """ Re-initializes an existing client after a process fork. The SDK relies on multiple background threads to operate correctly. When a process forks, `these threads are not available to the child <https://pythondev.readthedocs.io/fork.html#reinitialize-all-locks-after-fork>`. As a result, the SDK will not function correctly in the child process until it is re-initialized. This method is effectively equivalent to instantiating a new client. Future iterations of the SDK will provide increasingly efficient re-initializing improvements. Note that any configuration provided to the SDK will need to survive the forking process independently. For this reason, it is recommended that any listener or hook integrations be added postfork unless you are certain it can survive the forking process. If the SDK is used in a forked child process before this method is called, it logs a warning once per process. Calling this method clears that condition for the current process. :param start_wait: the number of seconds to wait for a successful connection to LaunchDarkly """ self._owner_pid = os.getpid() self._fork_warned_pids = {} self.__start_up(start_wait)
def _check_forked(self): """ Log a warning once per process if this client is used in a process that was forked after the client was created. The "already warned" state is keyed by pid, not stored as a boolean. A boolean would be copied into every child on fork, so a grandchild process would never get its own warning. The state is a dict written with ``dict.setdefault``, not a ``threading.Lock``. A lock held at the moment of a fork is inherited locked by the child and would hang its first evaluation. ``dict.setdefault`` is atomic in CPython, so many threads log exactly one warning. """ pid = os.getpid() if pid == self._owner_pid: return if not self._fork_breaks_client(): return token = object() if self._fork_warned_pids.setdefault(pid, token) is token: log.warning(_FORK_WARNING_MESSAGE) def _fork_breaks_client(self) -> bool: """ Return true if this configuration needs background threads. Offline mode has none. LDD mode with events disabled reads flags from the persistent store and sends nothing, so a fork does not break it. """ if self._config.offline: return False if self._config.use_ldd and not self._config.send_events: return False return True def __start_up(self, start_wait: float): environment_metadata = get_environment_metadata(self._config, "python-server-sdk") plugin_hooks = get_plugin_hooks(self._config.plugins, environment_metadata) self.__hooks_lock = ReadWriteLock() self.__hooks = self._config.hooks + plugin_hooks # type: List[Hook] datasystem_config = self._config.datasystem_config if datasystem_config is None: # Initialize data system (FDv1) to encapsulate v1 data plumbing from ldclient.impl.datasystem.fdv1 import ( # local import to avoid circular dependency FDv1 ) self._data_system: DataSystem = FDv1(self._config) else: self._data_system = FDv2(self._config, datasystem_config) self.__flag_tracker = FlagTrackerImpl( self._data_system.flag_change_listeners, lambda key, context: self.variation(key, context, None) ) # Expose providers and store from data system self.__data_store_status_provider = self._data_system.data_store_status_provider self.__data_source_status_provider = ( self._data_system.data_source_status_provider ) big_segment_store_manager = BigSegmentStoreManager(self._config.big_segments) self.__big_segment_store_manager = big_segment_store_manager self._evaluator = Evaluator( lambda key: self._data_system.store.get(FEATURES, key), lambda key: self._data_system.store.get(SEGMENTS, key), lambda key: big_segment_store_manager.get_user_membership(key), log, ) if self._config.offline: log.info("Started LaunchDarkly Client in offline mode") if self._config.use_ldd: log.info("Started LaunchDarkly Client in LDD mode") diagnostic_accumulator = self._set_event_processor(self._config) # Pass diagnostic accumulator to data system for streaming metrics self._data_system.set_diagnostic_accumulator(diagnostic_accumulator) # type: ignore self.__register_plugins(environment_metadata) update_processor_ready = threading.Event() self._data_system.start(update_processor_ready) if not self._config.offline and not self._config.use_ldd: if start_wait > 60: log.warning(f"Client was configured to block for up to {start_wait} seconds when initializing. We recommend blocking no longer than 60.") if start_wait > 0: log.info("Waiting up to " + str(start_wait) + " seconds for LaunchDarkly client to initialize...") update_processor_ready.wait(start_wait) if self.is_initialized() is True: log.info("Started LaunchDarkly Client: OK") else: log.warning("Initialization timeout exceeded for LaunchDarkly Client or an error occurred. " "Feature Flags may not yet be available.") def __register_plugins(self, environment_metadata: EnvironmentMetadata): for plugin in self._config.plugins: try: plugin.register(self, environment_metadata) except Exception as e: log.error("Error registering plugin %s: %s", plugin.metadata.name, e) def _set_event_processor(self, config): if config.offline or not config.send_events: self._event_processor = NullEventProcessor() return None if not config.event_processor_class: diagnostic_id = create_diagnostic_id(config) diagnostic_accumulator = None if config.diagnostic_opt_out else _DiagnosticAccumulator(diagnostic_id) self._event_processor = DefaultEventProcessor(config, diagnostic_accumulator=diagnostic_accumulator) return diagnostic_accumulator self._event_processor = config.event_processor_class(config) return None def _make_update_processor(self, config, store, ready, diagnostic_accumulator): if config.update_processor_class: log.info("Using user-specified update processor: " + str(config.update_processor_class)) return config.update_processor_class(config, store, ready) if config.offline or config.use_ldd: return NullUpdateProcessor(config, store, ready) if config.stream: return StreamingUpdateProcessor(config, store, ready, diagnostic_accumulator) log.info("Disabling streaming API") log.warning("You should only disable the streaming API if instructed to do so by LaunchDarkly support") if config.feature_requester_class: feature_requester = config.feature_requester_class(config) else: feature_requester = FeatureRequesterImpl(config) # type: FeatureRequester return PollingUpdateProcessor(config, feature_requester, store, ready)
[docs] def get_sdk_key(self) -> Optional[str]: """Returns the configured SDK key.""" return self._config.sdk_key
[docs] def close(self): """Releases all threads and network connections used by the LaunchDarkly client. Do not attempt to use the client after calling this method. """ self._check_forked() log.info("Closing LaunchDarkly client..") self._event_processor.stop() self._data_system.stop() self.__big_segment_store_manager.stop()
# These magic methods allow a client object to be automatically cleaned up by the "with" scope operator def __enter__(self): return self def __exit__(self, type, value, traceback): self.close() def _send_event(self, event): self._event_processor.send_event(event)
[docs] def track_migration_op(self, tracker: OpTracker): """ Tracks the results of a migrations operation. This event includes measurements which can be used to enhance the observability of a migration within the LaunchDarkly UI. Customers making use of the :class:`ldclient.MigrationBuilder` should not need to call this method manually. Customers not using the builder should provide this method with the tracker returned from calling :func:`migration_variation`. """ self._check_forked() event = tracker.build() if isinstance(event, str): log.error("error generting migration op event %s; no event will be emitted", event) return self._send_event(event)
[docs] def track(self, event_name: str, context: Context, data: Optional[Any] = None, metric_value: Optional[AnyNum] = None): """Tracks that an application-defined event occurred. This method creates a "custom" analytics event containing the specified event name (key) and context properties. You may attach arbitrary data or a metric value to the event with the optional ``data`` and ``metric_value`` parameters. Note that event delivery is asynchronous, so the event may not actually be sent until later; see :func:`flush()`. :param event_name: the name of the event :param context: the evaluation context associated with the event :param data: optional additional data associated with the event :param metric_value: a numeric value used by the LaunchDarkly experimentation feature in numeric custom metrics; can be omitted if this event is used by only non-numeric metrics """ self._check_forked() if not context.valid: log.warning("Invalid context for track (%s)" % context.error) else: self._send_event(self._event_factory_default.new_custom_event(event_name, context, data, metric_value))
[docs] def identify(self, context: Context): """Reports details about an evaluation context. This method simply creates an analytics event containing the context properties, to that LaunchDarkly will know about that context if it does not already. Evaluating a flag, by calling :func:`variation()` or :func:`variation_detail()`, also sends the context information to LaunchDarkly (if events are enabled), so you only need to use :func:`identify()` if you want to identify the context without evaluating a flag. :param context: the context to register """ self._check_forked() if not context.valid: log.warning("Invalid context for identify (%s)" % context.error) else: self._send_event(self._event_factory_default.new_identify_event(context))
[docs] def is_offline(self) -> bool: """Returns true if the client is in offline mode.""" return self._config.offline
[docs] def is_initialized(self) -> bool: """Returns whether the client is initialized and has flag data available to serve requests. If this returns true, it means the client has data it can use to evaluate flags. That could be because it connected to LaunchDarkly at least once and received flag data, or because it has cached data from a persistent store, or because it was configured for offline or LDD (daemon) mode. It could still have encountered a connection problem after that point, and cached data may not be current, so this does not guarantee that the flag data is up to date; if you need to know the connection status in more detail, use :attr:`data_source_status_provider`. If this returns false, it means the client has not yet obtained any flag data. It might still be starting up, or attempting to reconnect after an unsuccessful attempt, or it might have received an error that needs to be fixed (such as an invalid SDK key). In this state, feature flag evaluations will return default values -- unless you are using a persistent store integration and flag data had already been stored by a successfully connected SDK in the past. You can use :attr:`data_source_status_provider` to get information on errors, or to wait for a successful retry. :return: true if the client is initialized and has flag data available """ if self.is_offline() or self._config.use_ldd: return True return self._data_system.data_availability.at_least(DataAvailability.CACHED)
[docs] def flush(self): """Flushes all pending analytics events. Normally, batches of events are delivered in the background at intervals determined by the ``flush_interval`` property of :class:`ldclient.config.Config`. Calling ``flush()`` schedules the next event delivery to be as soon as possible; however, the delivery still happens asynchronously on a worker thread, so this method will return immediately. """ self._check_forked() if self._config.offline: return return self._event_processor.flush()
[docs] def variation(self, key: str, context: Context, default: Any) -> Any: """Calculates the value of a feature flag for a given context. :param key: the unique key for the feature flag :param context: the evaluation context :param default: the default value of the flag, to be used if the value is not available from LaunchDarkly :return: the variation for the given context, or the ``default`` value if the flag cannot be evaluated """ def evaluate(): detail, _ = self._evaluate_internal(key, context, default, self._event_factory_default) return _EvaluationWithHookResult(evaluation_detail=detail) return self.__evaluate_with_hooks(key=key, context=context, default_value=default, method="variation", block=evaluate).evaluation_detail.value
[docs] def variation_detail(self, key: str, context: Context, default: Any) -> EvaluationDetail: """Calculates the value of a feature flag for a given context, and returns an object that describes the way the value was determined. The ``reason`` property in the result will also be included in analytics events, if you are capturing detailed event data for this flag. :param key: the unique key for the feature flag :param context: the evaluation context :param default: the default value of the flag, to be used if the value is not available from LaunchDarkly :return: an :class:`ldclient.evaluation.EvaluationDetail` object that includes the feature flag value and evaluation reason """ def evaluate(): detail, _ = self._evaluate_internal(key, context, default, self._event_factory_with_reasons) return _EvaluationWithHookResult(evaluation_detail=detail) return self.__evaluate_with_hooks(key=key, context=context, default_value=default, method="variation_detail", block=evaluate).evaluation_detail
[docs] def migration_variation(self, key: str, context: Context, default_stage: Stage) -> Tuple[Stage, OpTracker]: """ This method returns the migration stage of the migration feature flag for the given evaluation context. This method returns the default stage if there is an error or the flag does not exist. If the default stage is not a valid stage, then a default stage of :class:`ldclient.migrations.Stage.OFF` will be used instead. """ if not isinstance(default_stage, Stage) or default_stage not in Stage: log.error(f"default stage {default_stage} is not a valid stage; using 'off' instead") default_stage = Stage.OFF def evaluate(): detail, flag = self._evaluate_internal(key, context, default_stage.value, self._event_factory_default) if isinstance(detail.value, str): stage = Stage.from_str(detail.value) if stage is not None: tracker = OpTracker(key, flag, context, detail, default_stage) return _EvaluationWithHookResult(evaluation_detail=detail, results={'default_stage': stage, 'tracker': tracker}) detail = EvaluationDetail(default_stage.value, None, error_reason('WRONG_TYPE')) tracker = OpTracker(key, flag, context, detail, default_stage) return _EvaluationWithHookResult(evaluation_detail=detail, results={'default_stage': default_stage, 'tracker': tracker}) hook_result = self.__evaluate_with_hooks(key=key, context=context, default_value=default_stage.value, method="migration_variation", block=evaluate) return hook_result.results['default_stage'], hook_result.results['tracker']
def _evaluate_internal(self, key: str, context: Context, default: Any, event_factory) -> Tuple[EvaluationDetail, Optional[FeatureFlag]]: default = self._config.get_default(key, default) if self._config.offline: return EvaluationDetail(default, None, error_reason('CLIENT_NOT_READY')), None availability = self._data_system.data_availability if availability != DataAvailability.REFRESHED: if availability == DataAvailability.CACHED: if not self._eval_cached_data_warned: with self._cached_data_warning_lock: if not self._eval_cached_data_warned: self._eval_cached_data_warned = True log.warning("Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key + ". This message is logged once.") 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, context, default, reason)) return EvaluationDetail(default, None, reason), None if not context.valid: log.warning("Context was invalid for flag evaluation (%s); returning default value" % context.error) return EvaluationDetail(default, None, error_reason('USER_NOT_SPECIFIED')), None try: flag = self._data_system.store.get(FEATURES, key) 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, context, default, reason)) return EvaluationDetail(default, None, reason), None if not flag: reason = error_reason('FLAG_NOT_FOUND') self._send_event(event_factory.new_unknown_flag_event(key, context, default, reason)) return EvaluationDetail(default, None, reason), None else: try: result = self._evaluator.evaluate(flag, context, 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, context, detail, default)) return detail, flag 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, context, default, reason)) return EvaluationDetail(default, None, reason), flag
[docs] def all_flags_state(self, context: Context, **kwargs) -> FeatureFlagsState: """Returns an object that encapsulates the state of all feature flags for a given context, 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/sdk/features/bootstrapping#javascript>`_. This method does not send analytics events back to LaunchDarkly. :param context: the end context 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 context is invalid) """ if self._config.offline: log.warning("all_flags_state() called, but client is in offline mode. Returning empty state") return FeatureFlagsState(False) self._check_forked() availability = self._data_system.data_availability if availability != DataAvailability.REFRESHED: if availability == DataAvailability.CACHED: if not self._all_flags_cached_data_warned: with self._cached_data_warning_lock: if not self._all_flags_cached_data_warned: self._all_flags_cached_data_warned = True log.warning("all_flags_state() called before client has finished initializing! Using last known values from feature store. This message is logged once.") else: log.warning("all_flags_state() called before client has finished initializing! Feature store unavailable - returning empty state") return FeatureFlagsState(False) if not context.valid: log.warning("Context was invalid for all_flags_state (%s); returning default value" % context.error) 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._data_system.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: result = self._evaluator.evaluate(flag, context, self._event_factory_default) detail = result.detail prerequisites = result.prerequisites 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'} detail = EvaluationDetail(None, None, reason) prerequisites = [] requires_experiment_data = EventFactory.is_experiment(flag, detail.reason) flag_state = { 'key': flag['key'], 'value': detail.value, 'variation': detail.variation_index, 'reason': detail.reason, 'version': flag['version'], 'prerequisites': prerequisites, 'trackEvents': flag.get('trackEvents', False) or requires_experiment_data, 'trackReason': requires_experiment_data, 'debugEventsUntilDate': flag.get('debugEventsUntilDate', None), } state.add_flag(flag_state, with_reasons, details_only_if_tracked) return state
[docs] def secure_mode_hash(self, context: Context) -> str: """Creates a hash string that can be used by the JavaScript SDK to identify a context. For more information, see the documentation on `Secure mode <https://docs.launchdarkly.com/sdk/features/secure-mode#configuring-secure-mode-in-the-javascript-client-side-sdk>`_. :param context: the evaluation context :return: the hash string """ return _secure_mode_hash(self._config, context)
[docs] def add_hook(self, hook: Hook): """ Add a hook to the client. In order to register a hook before the client starts, please use the `hooks` property of `Config`. Hooks provide entrypoints which allow for observation of SDK functions. :param hook: """ if not isinstance(hook, Hook): return with self.__hooks_lock.write(): self.__hooks.append(hook)
def __evaluate_with_hooks(self, key: str, context: Context, default_value: Any, method: str, block: Callable[[], _EvaluationWithHookResult]) -> _EvaluationWithHookResult: """ # evaluate_with_hook will run the provided block, wrapping it with evaluation hook support. # # :param key: # :param context: # :param default: # :param method: # :param block: # :return: """ # variation, variation_detail, and migration_variation all pass through here, so this one # check covers every evaluation entry point. self._check_forked() hooks = [] # type: List[Hook] with self.__hooks_lock.read(): if len(self.__hooks) == 0: return block() hooks = self.__hooks.copy() series_context = EvaluationSeriesContext(key=key, context=context, default_value=default_value, method=method, environment_id=self._data_system.environment_id) hook_data = self.__execute_before_evaluation(hooks, series_context) evaluation_result = block() self.__execute_after_evaluation(hooks, series_context, hook_data, evaluation_result.evaluation_detail) return evaluation_result def __execute_before_evaluation(self, hooks: List[Hook], series_context: EvaluationSeriesContext) -> List[dict]: return [self.__try_execute_stage("beforeEvaluation", hook.metadata.name, lambda: hook.before_evaluation(series_context, {})) for hook in hooks] def __execute_after_evaluation(self, hooks: List[Hook], series_context: EvaluationSeriesContext, hook_data: List[dict], evaluation_detail: EvaluationDetail) -> List[dict]: return [ self.__try_execute_stage("afterEvaluation", hook.metadata.name, lambda: hook.after_evaluation(series_context, data, evaluation_detail)) for (hook, data) in reversed(list(zip(hooks, hook_data))) ] def __try_execute_stage(self, method: str, hook_name: str, block: Callable[[], dict]) -> dict: try: return block() except BaseException as e: log.error(f"An error occurred in {method} of the hook {hook_name}: #{e}") return {} @property def big_segment_store_status_provider(self) -> BigSegmentStoreStatusProvider: """ Returns an interface for tracking the status of a Big Segment store. The :class:`ldclient.interfaces.BigSegmentStoreStatusProvider` has methods for checking whether the Big Segment store is (as far as the SDK knows) currently operational and tracking changes in this status. """ return self.__big_segment_store_manager.status_provider @property def data_source_status_provider(self) -> DataSourceStatusProvider: """ Returns an interface for tracking the status of the data source. The data source is the mechanism that the SDK uses to get feature flag configurations, such as a streaming connection (the default) or poll requests. The :class:`ldclient.interfaces.DataSourceStatusProvider` has methods for checking whether the data source is (as far as the SDK knows) currently operational and tracking changes in this status. :return: The data source status provider """ return self.__data_source_status_provider @property def data_store_status_provider(self) -> DataStoreStatusProvider: """ Returns an interface for tracking the status of a persistent data store. The provider has methods for checking whether the data store is (as far as the SDK knows) currently operational, tracking changes in this status, and getting cache statistics. These are only relevant for a persistent data store; if you are using an in-memory data store, then this method will return a stub object that provides no information. :return: The data store status provider """ return self.__data_store_status_provider @property def flag_tracker(self) -> FlagTracker: """ Returns an interface for tracking changes in feature flag configurations. The :class:`ldclient.interfaces.FlagTracker` contains methods for requesting notifications about feature flag changes using an event listener model. """ return self.__flag_tracker
__all__ = ['LDClient', 'Config']