Feature Flags Implementierung
Feature Flags: Kontrolliertes Ausrollen
Feature Flags ermöglichen sichere Deployments und graduelle Rollouts. Lernen Sie, wie Sie Features kontrolliert aktivieren.
Was sind Feature Flags?
// Ohne Feature Flag
function checkout() {
processPayment();
}
// Mit Feature Flag
function checkout() {
if (featureFlags.isEnabled('new_payment_flow')) {
processNewPayment();
} else {
processPayment();
}
}
// Vorteile:
// - Deployment ≠ Release
// - Schnelles Rollback (Flag aus)
// - A/B Testing
// - Canary Releases
Flag-Typen
| Typ | Beschreibung | Lebensdauer |
|---|---|---|
| Release Flag | Feature ein/ausschalten | Kurz (nach Rollout löschen) |
| Experiment Flag | A/B Tests | Mittel (bis Entscheidung) |
| Ops Flag | Circuit Breaker, Kill Switch | Lang/permanent |
| Permission Flag | Feature für User-Gruppen | Lang/permanent |
Einfache Implementierung
// featureFlags.js
class FeatureFlags {
constructor() {
this.flags = new Map();
}
// Flag definieren
define(name, options = {}) {
this.flags.set(name, {
enabled: options.enabled ?? false,
percentage: options.percentage ?? 100,
users: options.users ?? [],
groups: options.groups ?? []
});
}
// Flag prüfen
isEnabled(name, context = {}) {
const flag = this.flags.get(name);
if (!flag) return false;
// Komplett deaktiviert
if (!flag.enabled) return false;
// Spezifische User
if (flag.users.length && context.userId) {
if (flag.users.includes(context.userId)) return true;
}
// User-Gruppen
if (flag.groups.length && context.userGroup) {
if (flag.groups.includes(context.userGroup)) return true;
}
// Prozentuale Ausrollung
if (flag.percentage < 100 && context.userId) {
const hash = this.hashString(name + context.userId);
return (hash % 100) < flag.percentage;
}
return flag.enabled;
}
hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
}
// Verwendung
const flags = new FeatureFlags();
flags.define('new_checkout', {
enabled: true,
percentage: 10 // 10% der User
});
flags.define('beta_features', {
enabled: true,
groups: ['beta_testers', 'internal']
});
// In Code
if (flags.isEnabled('new_checkout', { userId: user.id })) {
// Neuer Checkout
}
Konfiguration über Umgebung
// Config aus Environment
const featureConfig = {
new_checkout: {
enabled: process.env.FF_NEW_CHECKOUT === 'true',
percentage: parseInt(process.env.FF_NEW_CHECKOUT_PERCENT) || 0
},
dark_mode: {
enabled: process.env.FF_DARK_MODE === 'true'
}
};
// JSON Config Datei
// features.json
{
"new_checkout": {
"enabled": true,
"percentage": 25,
"groups": ["beta"]
},
"experimental_search": {
"enabled": true,
"users": ["user_123", "user_456"]
}
}
Feature Flag Service (LaunchDarkly-Style)
// Server
class FeatureFlagService {
constructor(config) {
this.flags = new Map(Object.entries(config));
this.overrides = new Map();
}
evaluate(flagKey, context) {
// Override prüfen
const overrideKey = `${flagKey}:${context.userId}`;
if (this.overrides.has(overrideKey)) {
return this.overrides.get(overrideKey);
}
const flag = this.flags.get(flagKey);
if (!flag) return { value: false, reason: 'FLAG_NOT_FOUND' };
// Evaluierung
if (!flag.enabled) {
return { value: false, reason: 'DISABLED' };
}
// Targeting Rules
for (const rule of flag.rules || []) {
if (this.matchesRule(rule, context)) {
return { value: rule.value, reason: 'RULE_MATCH' };
}
}
// Percentage Rollout
if (flag.percentage < 100) {
const bucket = this.getBucket(flagKey, context.userId);
if (bucket >= flag.percentage) {
return { value: false, reason: 'PERCENTAGE_ROLLOUT' };
}
}
return { value: true, reason: 'DEFAULT' };
}
// Override für Testing
setOverride(flagKey, userId, value) {
this.overrides.set(`${flagKey}:${userId}`, { value, reason: 'OVERRIDE' });
}
getBucket(flagKey, userId) {
const hash = crypto.createHash('md5')
.update(`${flagKey}:${userId}`)
.digest('hex');
return parseInt(hash.substring(0, 8), 16) % 100;
}
}
React Integration
// FeatureFlagProvider.jsx
import { createContext, useContext } from 'react';
const FeatureFlagContext = createContext();
export function FeatureFlagProvider({ flags, children }) {
const isEnabled = (flagName) => {
return flags[flagName]?.enabled ?? false;
};
return (
<FeatureFlagContext.Provider value={{ isEnabled, flags }}>
{children}
</FeatureFlagContext.Provider>
);
}
export function useFeatureFlag(flagName) {
const { isEnabled } = useContext(FeatureFlagContext);
return isEnabled(flagName);
}
// Verwendung
function CheckoutButton() {
const newCheckout = useFeatureFlag('new_checkout');
if (newCheckout) {
return <NewCheckoutButton />;
}
return <LegacyCheckoutButton />;
}
// Oder als Component
function FeatureFlag({ name, children, fallback = null }) {
const enabled = useFeatureFlag(name);
return enabled ? children : fallback;
}
<FeatureFlag name="new_dashboard" fallback={<OldDashboard />}>
<NewDashboard />
</FeatureFlag>
Graduelle Rollouts
# Canary Release Plan Tag 1: 1% der User → Monitoring Tag 2: 5% der User → Monitoring Tag 3: 25% der User → Monitoring Tag 4: 50% der User → Monitoring Tag 5: 100% aller User # Bei Problemen: Sofort auf 0% zurück
Best Practices
✅ Do:
- Flags nach Rollout entfernen (Tech Debt!)
- Konsistente Naming-Convention
- Dokumentieren wofür jeder Flag ist
- Default immer "aus" (sicher)
❌ Don't:
- Flags ewig behalten
- Verschachtelte Flag-Logik
- Flags in kritischen Pfaden ohne Fallback