24
loading...
This website collects cookies to deliver better user experience
if
/else
checks, to choose what code paths are enabled for a user, but even a tiny bit of tooling to manage them can completely turbocharge your development velocity. Which is great! Hopefully that means your users get more useful product that much more quickly. if feature_flag('NEW_FEATURE', 'ENABLED'):
# show new feature
if feature_flag('MY_EXPERIMENT', 'VARIANT_A'):
# show variant a
elif feature_flag('MY_EXPERIMENT', 'VARIANT_B'):
# show variant b
else:
# show control
gk
in code. I'll use it here as well. from gatekeeper_config import FF_CONFIG_MAP
class Gatekeeper(object):
def __init__(self, user_id=None,
app=None,
request=None,
session=None,
config_map=None):
self.user_id = user_id
self.app = app
self.request = request
self.session = session
self.config_map = config_map if config_map else FF_CONFIG_MAP
def ff(self, *args, **kwargs):
'''Shorthand wrapper for `feature_flag`.'''
return self.feature_flag(*args, **kwargs)
def feature_flag(self, flag, variant):
return self.get_feature_flag_variant(flag) == variant
def ff_variant(self, *args, **kwargs):
'''Shorthand wrapper for `get_feature_flag_variant`.'''
return self.get_feature_flag_variant(*args, **kwargs)
def get_feature_flag_variant(self, flag, user_id_override):
config = self.config_map.get(flag)
if not config:
return None
variant = config.get_variant(
user_id=user_id_override or self.user_id,
app=self.app,
request=self.request,
session=self.session)
return variant.name
def get_config_map(self):
return self.config_map
def get_browser_override_variants(self, request):
return json.loads(request.cookies.get('gatekeeper', '{}'))
def set_browser_override_variant(self, request, flag, variant):
config = self.config_map.get(flag)
if not config:
return None
return config.set_browser_override_variant(request, variant)
def initialize_gatekeeper(user_id=None, app=None, config_map=None):
from flask import current_app
from flask import request
from flask import session
from flask_login import current_user
if not app:
app = current_app
if user_id is None and current_user and not current_user.is_anonymous:
user_id = current_user.id
gk = Gatekeeper(
user_id=user_id,
app=app,
request=request,
session=session,
config_map=config_map)
return gk
from abc import ABC
from abc import abstractmethod
from enum import Enum
import json
import flask
class FeatureFlagConfig(ABC):
FLAG_NAME: str
VARIANTS_ENUM_STR: str
def __init__(self, overrides=None):
self.variants_enum = Enum(
f'{self.__class__.__name__}Variants', self.VARIANTS_ENUM_STR)
self.overrides = overrides
def get_variants(self):
return list(self.variants_enum.__members__.keys())
@abstractmethod
def _get_variant(
self,
user_id: Optional[int] = None,
app: Optional[Flask] = None,
request: Optional[Request] = None,
session: Optional[Any] = None):
pass
def get_variant(self, user_id: Optional[int] = None, app: Optional[Flask] = None,
request: Optional[Request] = None, session: Optional[Any] = None):
override_variant = self.get_override_variant(
user_id=user_id, app=app, request=request, session=session)
if override_variant:
return override_variant
else:
return self._get_variant(
user_id=user_id, app=app, request=request, session=session)
def get_override_variant(
self, user_id=None, app=None, request=None, session=None):
if request:
browser_override_variant = self.get_browser_override_variant(request)
if browser_override_variant:
return browser_override_variant
if self.overrides:
for variant, user_ids in self.overrides.items():
if user_id in user_ids:
return self.variants_enum[variant]
return None
def set_browser_override_variant(self, request, variant):
browser_override_variants = self.get_browser_override_variants(request)
if variant == '':
browser_override_variants.pop(self.FLAG_NAME, None)
else:
browser_override_variants[self.FLAG_NAME] = variant
response = flask.make_response()
response.set_cookie(
'gatekeeper',
json.dumps(browser_override_variants))
return response
def get_browser_override_variants(self, request):
return json.loads(request.cookies.get('gatekeeper', '{}'))
def get_browser_override_variant(self, request):
browser_override_variants = self.get_browser_override_variants(request)
browser_override_variant = browser_override_variants.get(self.FLAG_NAME)
if browser_override_variant:
return self.variants_enum[browser_override_variant]
else:
return None
class MyNewFeatureFFConfig(FeatureFlagConfig):
FLAG_NAME = 'MY_NEW_FEATURE'
VARIANTS_ENUM_STR = 'VISIBLE NOT_VISIBLE'
DESCRIPTION = 'Gate visibility of my new feature during development'
def _get_variant(self, user_id=None, app=None, request=None, session=None):
if app and app.config.get('IS_DEV'):
return self.variants_enum.VISIBLE
elif user_id and user_id in [1, 2, 3]: # Team user ids
return self.variants_enum.VISIBLE
else:
return self.variants_enum.NOT_VISIBLE
FF_CONFIG_MAP: FFConfigMap = {
'MY_NEW_FEATURE': MyNewFeatureFFConfig()
}
# All the usual app setup stuff here...
app = create_app()
@app.before_request
def register_gatekeeper():
# Put imports here to avoid circular import issues.
from flask import request
import gatekeeper
request.gk = gatekeeper.initialize_gatekeeper(app=app)
if request.gk.ff('MY_NEW_FEATURE', 'VISIBLE'):
# Show new feature
{% if request.gk.ff('MY_NEW_FEATURE', 'VISIBLE') %}
{# Show new feature #}
{% endif %}