Feature Flag System Overview
Implement feature flags to decouple deployment from release, enable gradual rollouts, A/B testing, and provide emergency kill switches.
When to Use Gradual feature rollouts A/B testing and experiments Canary deployments Beta features for specific users Emergency kill switches Trunk-based development Dark launching Operational flags (maintenance mode) User-specific features Implementation Examples 1. Feature Flag Service (TypeScript) interface FlagConfig { key: string; enabled: boolean; description: string; rules?: FlagRule[]; variants?: FlagVariant[]; createdAt: Date; updatedAt: Date; }
interface FlagRule { type: 'user' | 'percentage' | 'attribute' | 'datetime'; operator: 'in' | 'equals' | 'contains' | 'gt' | 'lt' | 'between'; attribute?: string; values: any[]; }
interface FlagVariant { key: string; weight: number; value: any; }
interface EvaluationContext {
userId?: string;
email?: string;
attributes?: Record
class FeatureFlagService {
private flags: Map
constructor() { this.loadFlags(); }
private loadFlags(): void { // Load from database or config this.flags.set('new-dashboard', { key: 'new-dashboard', enabled: true, description: 'New dashboard UI', rules: [ { type: 'percentage', operator: 'lt', values: [25] // 25% rollout } ], createdAt: new Date(), updatedAt: new Date() });
this.flags.set('premium-features', {
key: 'premium-features',
enabled: true,
description: 'Premium features for paid users',
rules: [
{
type: 'attribute',
operator: 'equals',
attribute: 'plan',
values: ['premium', 'enterprise']
}
],
createdAt: new Date(),
updatedAt: new Date()
});
this.flags.set('beta-feature', {
key: 'beta-feature',
enabled: true,
description: 'Beta feature',
rules: [
{
type: 'user',
operator: 'in',
values: ['user1', 'user2', 'user3']
}
],
createdAt: new Date(),
updatedAt: new Date()
});
}
isEnabled(flagKey: string, context: EvaluationContext = {}): boolean { const flag = this.flags.get(flagKey);
if (!flag) {
console.warn(`Flag not found: ${flagKey}`);
return false;
}
if (!flag.enabled) {
return false;
}
if (!flag.rules || flag.rules.length === 0) {
return true;
}
return this.evaluateRules(flag.rules, context);
}
getVariant(flagKey: string, context: EvaluationContext = {}): any { const flag = this.flags.get(flagKey);
if (!flag || !this.isEnabled(flagKey, context)) {
return null;
}
if (!flag.variants || flag.variants.length === 0) {
return true;
}
return this.selectVariant(flag.variants, context);
}
private evaluateRules(rules: FlagRule[], context: EvaluationContext): boolean { return rules.every(rule => this.evaluateRule(rule, context)); }
private evaluateRule(rule: FlagRule, context: EvaluationContext): boolean { switch (rule.type) { case 'user': return this.evaluateUserRule(rule, context);
case 'percentage':
return this.evaluatePercentageRule(rule, context);
case 'attribute':
return this.evaluateAttributeRule(rule, context);
case 'datetime':
return this.evaluateDateTimeRule(rule, context);
default:
return false;
}
}
private evaluateUserRule(rule: FlagRule, context: EvaluationContext): boolean { if (!context.userId) return false;
return rule.values.includes(context.userId);
}
private evaluatePercentageRule(rule: FlagRule, context: EvaluationContext): boolean { const hash = this.hashContext(context); const percentage = (hash % 100) + 1;
return percentage <= rule.values[0];
}
private evaluateAttributeRule(rule: FlagRule, context: EvaluationContext): boolean { if (!rule.attribute || !context.attributes) return false;
const value = context.attributes[rule.attribute];
switch (rule.operator) {
case 'equals':
return rule.values.includes(value);
case 'contains':
return rule.values.some(v => String(value).includes(v));
case 'gt':
return value > rule.values[0];
case 'lt':
return value < rule.values[0];
default:
return false;
}
}
private evaluateDateTimeRule(rule: FlagRule, context: EvaluationContext): boolean { const now = context.timestamp || Date.now();
if (rule.operator === 'between') {
return now >= rule.values[0] && now <= rule.values[1];
}
return false;
}
private selectVariant(variants: FlagVariant[], context: EvaluationContext): any { const hash = this.hashContext(context); const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0); const position = hash % totalWeight;
let cumulative = 0;
for (const variant of variants) {
cumulative += variant.weight;
if (position < cumulative) {
return variant.value;
}
}
return variants[0].value;
}
private hashContext(context: EvaluationContext): number { const str = context.userId || context.email || 'anonymous'; let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
}
async createFlag(config: Omit
async updateFlag(key: string, updates: PartialFlag not found: ${key});
}
this.flags.set(key, {
...flag,
...updates,
updatedAt: new Date()
});
}
async deleteFlag(key: string): Promise
getAllFlags(): FlagConfig[] { return Array.from(this.flags.values()); } }
// Usage const featureFlags = new FeatureFlagService();
// Simple boolean check if (featureFlags.isEnabled('new-dashboard', { userId: 'user123' })) { console.log('Show new dashboard'); }
// With user attributes const hasPremiumFeatures = featureFlags.isEnabled('premium-features', { userId: 'user123', attributes: { plan: 'premium' } });
// Get variant for A/B testing const buttonColor = featureFlags.getVariant('button-color-test', { userId: 'user123' });
- React Hook for Feature Flags import { createContext, useContext, ReactNode } from 'react';
interface FeatureFlagContextType { isEnabled: (key: string) => boolean; getVariant: (key: string) => any; }
const FeatureFlagContext = createContext
export function FeatureFlagProvider({
children,
userId,
attributes
}: {
children: ReactNode;
userId?: string;
attributes?: Record
const context: FeatureFlagContextType = { isEnabled: (key: string) => { return flagService.isEnabled(key, { userId, attributes }); }, getVariant: (key: string) => { return flagService.getVariant(key, { userId, attributes }); } };
return (
export function useFeatureFlag(key: string): boolean { const context = useContext(FeatureFlagContext); if (!context) { throw new Error('useFeatureFlag must be used within FeatureFlagProvider'); } return context.isEnabled(key); }
export function useFeatureVariant(key: string): any { const context = useContext(FeatureFlagContext); if (!context) { throw new Error('useFeatureVariant must be used within FeatureFlagProvider'); } return context.getVariant(key); }
// Feature component wrapper export function Feature({ flag, fallback = null, children }: { flag: string; fallback?: ReactNode; children: ReactNode; }) { const isEnabled = useFeatureFlag(flag);
return isEnabled ? <>{children}</> : <>{fallback}</>; }
// Usage in components function Dashboard() { const hasNewDashboard = useFeatureFlag('new-dashboard'); const theme = useFeatureVariant('theme-experiment');
return (
<Feature flag="premium-features" fallback={<UpgradePrompt />}>
<PremiumContent />
</Feature>
<div style={{ backgroundColor: theme?.backgroundColor }}>
Content with experiment theme
</div>
</div>
); }
- Feature Flag with Analytics interface FlagEvaluationEvent { flagKey: string; userId?: string; result: boolean; variant?: any; timestamp: number; duration: number; }
class FeatureFlagServiceWithAnalytics extends FeatureFlagService { private analytics: Analytics;
constructor(analytics: Analytics) { super(); this.analytics = analytics; }
isEnabled(flagKey: string, context: EvaluationContext = {}): boolean { const startTime = Date.now(); const result = super.isEnabled(flagKey, context); const duration = Date.now() - startTime;
this.trackEvaluation({
flagKey,
userId: context.userId,
result,
timestamp: Date.now(),
duration
});
return result;
}
getVariant(flagKey: string, context: EvaluationContext = {}): any { const startTime = Date.now(); const variant = super.getVariant(flagKey, context); const duration = Date.now() - startTime;
this.trackEvaluation({
flagKey,
userId: context.userId,
result: variant !== null,
variant,
timestamp: Date.now(),
duration
});
return variant;
}
private trackEvaluation(event: FlagEvaluationEvent): void { this.analytics.track('feature_flag_evaluated', { flag_key: event.flagKey, user_id: event.userId, result: event.result, variant: event.variant, duration_ms: event.duration }); }
async getAnalytics(flagKey: string, timeRange: { start: Date; end: Date }): Promise<{
evaluations: number;
uniqueUsers: number;
enabledRate: number;
variantDistribution: Record
- LaunchDarkly-Style SDK from typing import Dict, Any, Optional import hashlib import json
class FeatureFlagClient: def init(self, sdk_key: str, config: Optional[Dict] = None): self.sdk_key = sdk_key self.config = config or {} self.flags: Dict[str, Dict] = {} self.initialize()
def initialize(self):
"""Load flags from API or cache."""
# In production, fetch from API
self.flags = {
'new-feature': {
'enabled': True,
'rollout': {
'percentage': 50
}
},
'premium-feature': {
'enabled': True,
'targeting': {
'attribute': 'plan',
'values': ['premium', 'enterprise']
}
}
}
def variation(
self,
flag_key: str,
user: Dict[str, Any],
default: bool = False
) -> bool:
"""Evaluate flag for user."""
flag = self.flags.get(flag_key)
if not flag or not flag.get('enabled'):
return default
# Check targeting rules
if 'targeting' in flag:
if not self._evaluate_targeting(flag['targeting'], user):
return False
# Check percentage rollout
if 'rollout' in flag:
return self._evaluate_rollout(flag['rollout'], user, flag_key)
return True
def variation_detail(
self,
flag_key: str,
user: Dict[str, Any],
default: Any = None
) -> Dict[str, Any]:
"""Get flag variation with details."""
value = self.variation(flag_key, user, default)
return {
'value': value,
'variation_index': 0 if value else 1,
'reason': {
'kind': 'RULE_MATCH' if value else 'OFF'
}
}
def _evaluate_targeting(self, targeting: Dict, user: Dict) -> bool:
"""Evaluate targeting rules."""
attribute = targeting.get('attribute')
values = targeting.get('values', [])
user_value = user.get(attribute)
return user_value in values
def _evaluate_rollout(
self,
rollout: Dict,
user: Dict,
flag_key: str
) -> bool:
"""Evaluate percentage rollout."""
percentage = rollout.get('percentage', 0)
user_id = user.get('id', user.get('email', 'anonymous'))
# Consistent hashing for stable rollout
hash_value = self._hash_user(user_id, flag_key)
bucket = hash_value % 100
return bucket < percentage
def _hash_user(self, user_id: str, flag_key: str) -> int:
"""Hash user ID for consistent bucketing."""
combined = f"{flag_key}:{user_id}"
hash_bytes = hashlib.sha256(combined.encode()).digest()
return int.from_bytes(hash_bytes[:4], byteorder='big')
def track(self, event_name: str, user: Dict, data: Optional[Dict] = None):
"""Track custom event."""
# Send to analytics
pass
def identify(self, user: Dict):
"""Identify user."""
# Update user context
pass
def flush(self):
"""Flush events."""
pass
def close(self):
"""Close client."""
pass
Usage
client = FeatureFlagClient(sdk_key='your-sdk-key')
user = { 'id': 'user-123', 'email': 'user@example.com', 'plan': 'premium' }
Check if feature is enabled
if client.variation('new-feature', user): print("New feature enabled!")
Get detailed information
detail = client.variation_detail('premium-feature', user) print(f"Value: {detail['value']}, Reason: {detail['reason']}")
Track event
client.track('feature-used', user, {'feature': 'new-feature'})
- Admin UI for Feature Flags
interface FlagFormData {
key: string;
description: string;
enabled: boolean;
rolloutPercentage?: number;
targetUsers?: string[];
targetAttributes?: Record
; }
function FeatureFlagDashboard() {
const [flags, setFlags] = useState
useEffect(() => { loadFlags(); }, []);
const loadFlags = async () => { const allFlags = flagService.getAllFlags(); setFlags(allFlags); };
const toggleFlag = async (key: string) => { const flag = flags.find(f => f.key === key); if (flag) { await flagService.updateFlag(key, { enabled: !flag.enabled }); await loadFlags(); } };
return (
Feature Flags
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
<th>Status</th>
<th>Rollout</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{flags.map(flag => (
<tr key={flag.key}>
<td>{flag.key}</td>
<td>{flag.description}</td>
<td>
<Switch
checked={flag.enabled}
onChange={() => toggleFlag(flag.key)}
/>
</td>
<td>{getRolloutPercentage(flag)}%</td>
<td>
<button onClick={() => editFlag(flag)}>Edit</button>
<button onClick={() => deleteFlag(flag.key)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
); }
Best Practices ✅ DO Use descriptive flag names Document flag purpose and lifecycle Implement gradual rollouts Track flag evaluations Clean up old flags regularly Use feature flags for experiments Implement kill switches for critical features Test both enabled and disabled states Use consistent hashing for stable rollouts Provide admin UI for non-technical users ❌ DON'T Use flags for permanent configuration Accumulate technical debt with old flags Skip flag cleanup Make flags too granular Hard-code flag checks everywhere Skip analytics and monitoring Resources LaunchDarkly Split.io Flagsmith Unleash
← 返回排行榜