🚩 Feature Flags¶
Enable A/B testing, gradual rollouts, and feature toggles with django-waffle.
Overview¶
Feature flags allow you to:
- A/B test new features with a subset of users
- Gradually roll out features to minimize risk
- Toggle features on/off without deploying code
- Test in production safely with specific users/groups
- Emergency rollback by disabling a flag
Django Keel integrates django-waffle for production-ready feature flagging.
Types of Flags¶
1. Flags (User-based)¶
Enable features for specific users:
from waffle import flag_is_active
if flag_is_active(request, 'new_dashboard'):
# Show new dashboard
return render(request, 'dashboard_v2.html')
else:
# Show old dashboard
return render(request, 'dashboard_v1.html')
2. Switches (Global)¶
Enable features globally for everyone:
from waffle import switch_is_active
if switch_is_active('maintenance_mode'):
return render(request, 'maintenance.html')
3. Samples (Percentage-based)¶
Enable features for a percentage of users:
from waffle import sample_is_active
if sample_is_active('beta_features'):
# 10% of users see beta features
show_beta_features()
Setup¶
1. Enable Feature Flags¶
When generating your project, feature flags are included by default in Django Keel projects.
Check config/settings/base.py:
INSTALLED_APPS = [
# ...
'waffle',
# ...
]
MIDDLEWARE = [
# ...
'waffle.middleware.WaffleMiddleware',
# ...
]
2. Run Migrations¶
3. Access Admin Interface¶
Visit /admin/waffle/ to manage flags, switches, and samples.
Creating Flags¶
Via Admin Interface¶
- Go to
/admin/waffle/flag/ - Click "Add Flag"
- Configure:
- Name:
new_dashboard - Everyone: Off (default)
- Percent: 0 (or 10 for 10% rollout)
- Superusers: On (enable for staff testing)
- Groups: Select specific groups
- Users: Add specific users
Via Django Shell¶
from waffle.models import Flag
# Create a flag
flag = Flag.objects.create(
name='new_dashboard',
everyone=False,
percent=10.0, # 10% of users
superusers=True
)
# Enable for specific users
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.get(email='beta@example.com')
flag.users.add(user)
Via Management Command¶
Django Keel provides helper commands:
# Create a flag
python manage.py waffle_flag new_dashboard --create
# Enable for everyone
python manage.py waffle_flag new_dashboard --everyone
# Enable for 25% of users
python manage.py waffle_flag new_dashboard --percent 25
# Disable
python manage.py waffle_flag new_dashboard --deactivate
Usage in Views¶
Function-Based Views¶
from waffle.decorators import waffle_flag
@waffle_flag('new_dashboard')
def dashboard_view(request):
"""Only accessible when flag is active."""
return render(request, 'dashboard_v2.html')
Class-Based Views¶
from waffle.mixins import WaffleFlagMixin
class DashboardView(WaffleFlagMixin, TemplateView):
waffle_flag = 'new_dashboard'
template_name = 'dashboard_v2.html'
Conditional Logic¶
from waffle import flag_is_active
def dashboard_view(request):
if flag_is_active(request, 'new_dashboard'):
context = get_new_dashboard_context()
template = 'dashboard_v2.html'
else:
context = get_old_dashboard_context()
template = 'dashboard_v1.html'
return render(request, template, context)
Usage in Templates¶
Check Flag Status¶
{% load waffle_tags %}
{% flag "new_dashboard" %}
<div class="new-dashboard">
<!-- New dashboard content -->
</div>
{% else %}
<div class="old-dashboard">
<!-- Old dashboard content -->
</div>
{% endflag %}
Check Switch Status¶
{% load waffle_tags %}
{% switch "maintenance_mode" %}
<div class="alert alert-warning">
Site is under maintenance. Some features may be unavailable.
</div>
{% endswitch %}
Usage in API (DRF)¶
Conditional Endpoints¶
from rest_framework.decorators import api_view
from waffle import flag_is_active
@api_view(['GET'])
def api_dashboard(request):
if flag_is_active(request, 'new_dashboard_api'):
serializer = DashboardV2Serializer
else:
serializer = DashboardV1Serializer
data = get_dashboard_data()
return Response(serializer(data).data)
Feature Availability in Response¶
from waffle import flag_is_active
class FeatureListView(APIView):
def get(self, request):
return Response({
'features': {
'new_dashboard': flag_is_active(request, 'new_dashboard'),
'dark_mode': flag_is_active(request, 'dark_mode'),
'ai_assistant': flag_is_active(request, 'ai_assistant'),
}
})
Common Patterns¶
Gradual Rollout¶
Start with 1% and increase gradually:
# Day 1: 1% of users
flag.percent = 1.0
flag.save()
# Day 3: 10% of users (monitor metrics)
flag.percent = 10.0
flag.save()
# Day 7: 50% of users
flag.percent = 50.0
flag.save()
# Day 14: 100% of users
flag.everyone = True
flag.save()
Canary Releases¶
Enable for internal users first:
# Enable for superusers only
flag.superusers = True
flag.everyone = False
# Then enable for beta testers group
beta_group = Group.objects.get(name='Beta Testers')
flag.groups.add(beta_group)
# Finally, gradual rollout to all users
flag.percent = 10.0
flag.save()
A/B Testing¶
Split users into control and treatment groups:
# Create two flags for A/B test
flag_a = Flag.objects.create(name='checkout_v1', percent=50.0)
flag_b = Flag.objects.create(name='checkout_v2', percent=50.0)
# In view
if flag_is_active(request, 'checkout_v2'):
return checkout_v2(request)
else:
return checkout_v1(request)
# Track conversions
if flag_is_active(request, 'checkout_v2'):
analytics.track('purchase', {'variant': 'v2'})
Emergency Kill Switch¶
Quickly disable problematic features:
from waffle.models import Switch
# Create kill switch
Switch.objects.create(name='new_payment_processor', active=True)
# In code
from waffle import switch_is_active
if switch_is_active('new_payment_processor'):
process_payment_v2()
else:
process_payment_v1() # Fallback to old processor
# Emergency disable (via admin or shell)
switch = Switch.objects.get(name='new_payment_processor')
switch.active = False
switch.save()
Testing with Feature Flags¶
Override Flags in Tests¶
from waffle.testutils import override_flag
class DashboardTestCase(TestCase):
@override_flag('new_dashboard', active=True)
def test_new_dashboard(self):
response = self.client.get('/dashboard/')
self.assertContains(response, 'New Dashboard')
@override_flag('new_dashboard', active=False)
def test_old_dashboard(self):
response = self.client.get('/dashboard/')
self.assertContains(response, 'Old Dashboard')
Test Both Variants¶
from waffle.testutils import override_flag
class CheckoutTestCase(TestCase):
def test_checkout_variants(self):
# Test new checkout
with override_flag('checkout_v2', active=True):
response = self.client.post('/checkout/', data)
self.assertEqual(response.status_code, 200)
# Test old checkout
with override_flag('checkout_v2', active=False):
response = self.client.post('/checkout/', data)
self.assertEqual(response.status_code, 200)
Monitoring & Analytics¶
Track Flag Usage¶
import logging
from waffle import flag_is_active
logger = logging.getLogger(__name__)
def my_view(request):
flag_active = flag_is_active(request, 'new_feature')
# Log flag usage
logger.info(
'feature_flag_check',
extra={
'flag_name': 'new_feature',
'user_id': request.user.id,
'is_active': flag_active,
}
)
if flag_active:
return new_feature_view(request)
return old_feature_view(request)
Integration with Analytics¶
from waffle import flag_is_active
def checkout_view(request):
variant = 'v2' if flag_is_active(request, 'checkout_v2') else 'v1'
# Track with your analytics service
analytics.track(request.user.id, 'checkout_started', {
'variant': variant,
})
return render(request, f'checkout_{variant}.html')
Best Practices¶
- Use descriptive names -
new_dashboard_redesignnotflag_123 - Document flags - Add notes in admin about what each flag does
- Clean up old flags - Remove flags after full rollout
- Monitor metrics - Track conversion rates for each variant
- Test both paths - Write tests for flag on AND off
- Use switches for kill switches - Faster than flags
- Avoid flag sprawl - Consolidate related flags
- Set expiration dates - Remove temporary flags after rollout
Troubleshooting¶
Flag Not Working¶
Check middleware is installed:
# config/settings/base.py
MIDDLEWARE = [
# ...
'waffle.middleware.WaffleMiddleware', # Must be here
# ...
]
Check flag configuration:
from waffle.models import Flag
flag = Flag.objects.get(name='my_flag')
print(f"Everyone: {flag.everyone}")
print(f"Percent: {flag.percent}")
print(f"Active: {flag.is_active_for_user(user)}")
Flag Always Returns False¶
Check user authentication:
# Flags require authenticated users for user-based checks
if not request.user.is_authenticated:
# Flag will be False unless everyone=True
pass
Use switches for anonymous users:
from waffle import switch_is_active
# Works for all users (anonymous or authenticated)
if switch_is_active('new_feature'):
show_feature()
Performance Issues¶
Cache flag lookups:
from django.core.cache import cache
from waffle import flag_is_active
def get_flag_status(request, flag_name):
cache_key = f'flag_{flag_name}_{request.user.id}'
status = cache.get(cache_key)
if status is None:
status = flag_is_active(request, flag_name)
cache.set(cache_key, status, 60) # Cache for 1 minute
return status
API Reference¶
Helper Functions¶
Django Keel provides helper functions in apps/core/feature_flags.py:
from apps.core.feature_flags import (
is_feature_enabled,
get_active_features,
enable_feature_for_user,
)
# Check if feature is enabled
if is_feature_enabled('new_dashboard', user=request.user):
show_new_dashboard()
# Get all active features for user
features = get_active_features(request.user)
# Returns: {'new_dashboard': True, 'dark_mode': False, ...}
# Enable feature for specific user
enable_feature_for_user('beta_access', user)
Further Reading¶
Related Documentation¶
- Feature Gating - Subscription-based access control
- Stripe Integration - Payment and subscription management
- Teams & Organizations - Multi-tenant team management