import logging
|
|
import ldclient
|
from django.conf import settings
|
from django.contrib.auth.models import AnonymousUser
|
from ldclient.config import Config, HTTPConfig
|
from ldclient.feature_store import CacheConfig
|
from ldclient.integrations import Files, Redis
|
|
from label_studio.core.current_request import get_current_request
|
from label_studio.core.utils.common import load_func
|
from label_studio.core.utils.io import find_node
|
from label_studio.core.utils.params import get_all_env_with_prefix, get_bool_env
|
|
from .stale_feature_flags import STALE_FEATURE_FLAGS
|
|
logger = logging.getLogger(__name__)
|
|
get_user_repr = load_func(settings.FEATURE_FLAGS_GET_USER_REPR)
|
get_user_repr_from_organization = load_func(settings.FEATURE_FLAGS_GET_USER_REPR_FROM_ORGANIZATION)
|
|
|
def get_feature_file_path():
|
package_name = 'label_studio' if settings.VERSION_EDITION == 'Community' else 'label_studio_enterprise'
|
if settings.FEATURE_FLAGS_FILE.startswith('/'):
|
return settings.FEATURE_FLAGS_FILE
|
else:
|
return find_node(package_name, settings.FEATURE_FLAGS_FILE, 'file')
|
|
|
if settings.FEATURE_FLAGS_FROM_FILE:
|
# Feature flags from file
|
if not settings.FEATURE_FLAGS_FILE:
|
raise ValueError(
|
'When "FEATURE_FLAGS_FROM_FILE" is set, you have to specify a valid path for feature flags file, e.g.'
|
'FEATURE_FLAGS_FILE=my_flags.yml'
|
)
|
|
feature_flags_file = get_feature_file_path()
|
|
logger.info(f'Read flags from file {feature_flags_file}')
|
data_source = Files.new_data_source(paths=[feature_flags_file])
|
config = Config(
|
sdk_key=settings.FEATURE_FLAGS_API_KEY or 'whatever', update_processor_class=data_source, send_events=False
|
)
|
ldclient.set_config(config)
|
client = ldclient.get()
|
elif settings.FEATURE_FLAGS_OFFLINE:
|
# On-prem usage, without feature flags file
|
ldclient.set_config(Config(settings.FEATURE_FLAGS_API_KEY or 'whatever', offline=True))
|
client = ldclient.get()
|
else:
|
# Production usage
|
if hasattr(settings, 'REDIS_LOCATION'):
|
logger.debug(f'Set LaunchDarkly config with Redis feature store at {settings.REDIS_LOCATION}')
|
store_kwargs = {
|
'url': settings.REDIS_LOCATION,
|
'prefix': 'feature-flags',
|
'caching': CacheConfig(expiration=30),
|
}
|
if settings.REDIS_LOCATION.startswith('rediss'):
|
store_kwargs['redis_opts'] = settings.REDIS_SSL_SETTINGS
|
store = Redis.new_feature_store(**store_kwargs)
|
ldclient.set_config(
|
Config(settings.FEATURE_FLAGS_API_KEY, feature_store=store, http=HTTPConfig(connect_timeout=5))
|
)
|
else:
|
logger.debug('Set LaunchDarkly config without Redis...')
|
ldclient.set_config(Config(settings.FEATURE_FLAGS_API_KEY, http=HTTPConfig(connect_timeout=5)))
|
client = ldclient.get()
|
|
|
def flag_set(feature_flag, user=None, override_system_default=None, organization=None):
|
"""Use this method to check whether this flag is set ON to the current user, to split the logic on backend
|
For example,
|
```
|
if flag_set('ff-dev-123-some-fixed-issue-231221-short', user):
|
run_new_code()
|
else:
|
run_old_code()
|
```
|
`override_default` is used to override any system defaults in place in case no files or LD API flags provided
|
|
stale_feature_flags will be checked to confirm if the feature flags are still active
|
|
stale feature flags are considered "deprecated" and should not be changeable in any circumstance.
|
They are an intermediary step before code references to the flag being removed completely.
|
"""
|
|
if feature_flag in STALE_FEATURE_FLAGS:
|
return STALE_FEATURE_FLAGS[feature_flag]
|
|
if user is None:
|
user = AnonymousUser
|
elif user == 'auto':
|
user = AnonymousUser
|
request = get_current_request()
|
if request and getattr(request, 'user', None) and request.user.is_authenticated:
|
user = request.user
|
|
if organization is None:
|
user_dict = get_user_repr(user)
|
else:
|
user_dict = get_user_repr_from_organization(organization)
|
|
env_value = get_bool_env(feature_flag, default=None)
|
if env_value is not None:
|
return env_value
|
if override_system_default is not None:
|
system_default = override_system_default
|
else:
|
system_default = settings.FEATURE_FLAGS_DEFAULT_VALUE
|
return client.variation(feature_flag, user_dict, system_default)
|
|
|
def all_flags(user):
|
"""Return the output of this method in API response, to bootstrap client-side flags.
|
More on https://docs.launchdarkly.com/sdk/features/bootstrapping#javascript
|
stale_feature_flags will override any client configuration
|
"""
|
user_dict = get_user_repr(user)
|
logger.debug(f'Resolve all flags state for user {user_dict}')
|
state = client.all_flags_state(user_dict)
|
flags = state.to_json_dict()
|
|
env_ff = get_all_env_with_prefix('ff_', is_bool=True)
|
env_fflag = get_all_env_with_prefix('fflag_', is_bool=True)
|
env_fflag2 = get_all_env_with_prefix('fflag-', is_bool=True)
|
env_fflag3 = get_all_env_with_prefix('feat_', is_bool=True)
|
env_ff.update(env_fflag)
|
env_ff.update(env_fflag2)
|
env_ff.update(env_fflag3)
|
|
for env_flag_name, env_flag_on in env_ff.items():
|
flags[env_flag_name] = env_flag_on
|
|
for feature_flag, value in STALE_FEATURE_FLAGS.items():
|
flags[feature_flag] = value
|
|
return flags
|