Files
Portfolio/blog-posts/86-feature-flags-progressive-rollout.md
T
damjan_savicandClaude Opus 4.5 e1bbe5455d Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:07:20 +01:00

753 lines
21 KiB
Markdown

# Feature Flags & Progressive Rollout
**Meta-Description:** Feature Flags implementieren mit Next.js. Progressive Rollouts, Canary Releases und A/B Testing mit Feature Management.
**Keywords:** Feature Flags, Feature Toggle, Progressive Rollout, Canary Release, LaunchDarkly, Unleash, Feature Management
---
## Einführung
**Feature Flags** ermöglichen kontinuierliche Deployment ohne Risiko. Features können für bestimmte Nutzer aktiviert, schrittweise ausgerollt oder bei Problemen sofort deaktiviert werden. Dieser Guide zeigt Implementation und Best Practices.
---
## Feature Flags Overview
```
┌─────────────────────────────────────────────────────────────┐
│ FEATURE FLAGS ARCHITECTURE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Flag Types: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Release Flags: │ │
│ │ - Hide unfinished features │ │
│ │ - Temporary, remove after launch │ │
│ │ │ │
│ │ 2. Experiment Flags: │ │
│ │ - A/B testing │ │
│ │ - Feature comparison │ │
│ │ │ │
│ │ 3. Ops Flags: │ │
│ │ - Kill switches │ │
│ │ - Performance toggles │ │
│ │ │ │
│ │ 4. Permission Flags: │ │
│ │ - Plan-based features │ │
│ │ - User-specific access │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Rollout Strategies: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Percentage Rollout: │ │
│ │ ├── 5% → 25% → 50% → 100% │ │
│ │ └── Gradual exposure to reduce risk │ │
│ │ │ │
│ │ User Targeting: │ │
│ │ ├── Specific user IDs │ │
│ │ ├── User attributes (plan, role, country) │ │
│ │ └── Cohort-based │ │
│ │ │ │
│ │ Environment-based: │ │
│ │ ├── Development: all flags on │ │
│ │ ├── Staging: beta flags on │ │
│ │ └── Production: controlled rollout │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Decision Flow: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Request → Get User Context → Evaluate Rules │ │
│ │ → Check Percentage → Return Flag Value │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Custom Feature Flag System
```typescript
// lib/features/types.ts
export interface FeatureFlag {
key: string;
name: string;
description: string;
type: 'boolean' | 'string' | 'number' | 'json';
defaultValue: unknown;
enabled: boolean;
rules: TargetingRule[];
rolloutPercentage: number;
createdAt: Date;
updatedAt: Date;
}
export interface TargetingRule {
id: string;
attribute: string;
operator: 'equals' | 'contains' | 'greaterThan' | 'lessThan' | 'in' | 'notIn';
value: unknown;
enabled: boolean;
}
export interface EvaluationContext {
userId?: string;
email?: string;
plan?: string;
role?: string;
country?: string;
tenantId?: string;
attributes?: Record<string, unknown>;
}
export interface FlagEvaluation {
value: unknown;
reason: 'default' | 'rule' | 'percentage' | 'override' | 'disabled';
ruleId?: string;
}
```
```typescript
// lib/features/evaluator.ts
import { createHash } from 'crypto';
import { FeatureFlag, EvaluationContext, FlagEvaluation, TargetingRule } from './types';
export class FlagEvaluator {
evaluate(
flag: FeatureFlag,
context: EvaluationContext
): FlagEvaluation {
// Check if flag is globally disabled
if (!flag.enabled) {
return {
value: flag.defaultValue,
reason: 'disabled'
};
}
// Check targeting rules
for (const rule of flag.rules) {
if (!rule.enabled) continue;
if (this.matchesRule(rule, context)) {
return {
value: true,
reason: 'rule',
ruleId: rule.id
};
}
}
// Check percentage rollout
if (flag.rolloutPercentage > 0 && flag.rolloutPercentage < 100) {
const inRollout = this.isInRolloutPercentage(
flag.key,
context.userId || '',
flag.rolloutPercentage
);
if (inRollout) {
return {
value: true,
reason: 'percentage'
};
}
}
// 100% rollout
if (flag.rolloutPercentage === 100) {
return {
value: true,
reason: 'percentage'
};
}
// Default value
return {
value: flag.defaultValue,
reason: 'default'
};
}
private matchesRule(rule: TargetingRule, context: EvaluationContext): boolean {
const contextValue = this.getContextValue(rule.attribute, context);
switch (rule.operator) {
case 'equals':
return contextValue === rule.value;
case 'contains':
return String(contextValue).includes(String(rule.value));
case 'greaterThan':
return Number(contextValue) > Number(rule.value);
case 'lessThan':
return Number(contextValue) < Number(rule.value);
case 'in':
return Array.isArray(rule.value) && rule.value.includes(contextValue);
case 'notIn':
return Array.isArray(rule.value) && !rule.value.includes(contextValue);
default:
return false;
}
}
private getContextValue(attribute: string, context: EvaluationContext): unknown {
if (attribute in context) {
return context[attribute as keyof EvaluationContext];
}
return context.attributes?.[attribute];
}
private isInRolloutPercentage(
flagKey: string,
userId: string,
percentage: number
): boolean {
// Deterministic hash for consistent assignment
const hash = createHash('md5')
.update(`${flagKey}:${userId}`)
.digest('hex');
const hashValue = parseInt(hash.substring(0, 8), 16) % 100;
return hashValue < percentage;
}
}
```
```typescript
// lib/features/client.ts
import { FeatureFlag, EvaluationContext, FlagEvaluation } from './types';
import { FlagEvaluator } from './evaluator';
class FeatureFlagClient {
private flags: Map<string, FeatureFlag> = new Map();
private evaluator = new FlagEvaluator();
private context: EvaluationContext = {};
private overrides: Map<string, unknown> = new Map();
async initialize(): Promise<void> {
await this.fetchFlags();
// Poll for updates
setInterval(() => this.fetchFlags(), 60000);
}
private async fetchFlags(): Promise<void> {
try {
const response = await fetch('/api/features');
const flags: FeatureFlag[] = await response.json();
this.flags.clear();
flags.forEach(flag => this.flags.set(flag.key, flag));
} catch (error) {
console.error('Failed to fetch feature flags:', error);
}
}
setContext(context: EvaluationContext): void {
this.context = context;
}
// Override for testing/debugging
setOverride(key: string, value: unknown): void {
this.overrides.set(key, value);
}
clearOverrides(): void {
this.overrides.clear();
}
isEnabled(key: string, defaultValue: boolean = false): boolean {
// Check overrides first
if (this.overrides.has(key)) {
return Boolean(this.overrides.get(key));
}
const flag = this.flags.get(key);
if (!flag) {
return defaultValue;
}
const evaluation = this.evaluator.evaluate(flag, this.context);
return Boolean(evaluation.value);
}
getValue<T>(key: string, defaultValue: T): T {
if (this.overrides.has(key)) {
return this.overrides.get(key) as T;
}
const flag = this.flags.get(key);
if (!flag) {
return defaultValue;
}
const evaluation = this.evaluator.evaluate(flag, this.context);
return (evaluation.value as T) ?? defaultValue;
}
getEvaluation(key: string): FlagEvaluation | null {
const flag = this.flags.get(key);
if (!flag) return null;
return this.evaluator.evaluate(flag, this.context);
}
getAllFlags(): Record<string, unknown> {
const result: Record<string, unknown> = {};
this.flags.forEach((flag, key) => {
const evaluation = this.evaluator.evaluate(flag, this.context);
result[key] = evaluation.value;
});
return result;
}
}
// Singleton
export const featureFlags = new FeatureFlagClient();
```
---
## React Integration
```tsx
// contexts/FeatureFlagContext.tsx
'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { featureFlags } from '@/lib/features/client';
import { EvaluationContext } from '@/lib/features/types';
interface FeatureFlagContextValue {
isEnabled: (key: string, defaultValue?: boolean) => boolean;
getValue: <T>(key: string, defaultValue: T) => T;
isReady: boolean;
}
const FeatureFlagContext = createContext<FeatureFlagContextValue | null>(null);
export function FeatureFlagProvider({
children,
context
}: {
children: ReactNode;
context?: EvaluationContext;
}) {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
if (context) {
featureFlags.setContext(context);
}
featureFlags.initialize().then(() => setIsReady(true));
}, [context]);
const value: FeatureFlagContextValue = {
isEnabled: (key, defaultValue = false) =>
featureFlags.isEnabled(key, defaultValue),
getValue: (key, defaultValue) =>
featureFlags.getValue(key, defaultValue),
isReady
};
return (
<FeatureFlagContext.Provider value={value}>
{children}
</FeatureFlagContext.Provider>
);
}
export function useFeatureFlags() {
const context = useContext(FeatureFlagContext);
if (!context) {
throw new Error('useFeatureFlags must be used within FeatureFlagProvider');
}
return context;
}
// hooks/useFeature.ts
export function useFeature(key: string, defaultValue: boolean = false): boolean {
const { isEnabled, isReady } = useFeatureFlags();
if (!isReady) return defaultValue;
return isEnabled(key, defaultValue);
}
```
```tsx
// components/Feature.tsx
'use client';
import { ReactNode } from 'react';
import { useFeature } from '@/hooks/useFeature';
interface FeatureProps {
flag: string;
children: ReactNode;
fallback?: ReactNode;
}
export function Feature({ flag, children, fallback = null }: FeatureProps) {
const isEnabled = useFeature(flag);
if (!isEnabled) {
return <>{fallback}</>;
}
return <>{children}</>;
}
// Usage
export function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Feature flag="new-analytics-dashboard">
<NewAnalyticsDashboard />
</Feature>
<Feature
flag="ai-insights"
fallback={<p>AI Insights coming soon!</p>}
>
<AIInsightsPanel />
</Feature>
</div>
);
}
```
---
## Server-Side Evaluation
```typescript
// lib/features/server.ts
import { cookies, headers } from 'next/headers';
import { db } from '@/lib/db';
import { FlagEvaluator } from './evaluator';
import { FeatureFlag, EvaluationContext } from './types';
const evaluator = new FlagEvaluator();
// Cache flags in memory
let flagsCache: Map<string, FeatureFlag> = new Map();
let lastFetch = 0;
const CACHE_TTL = 60000; // 1 minute
async function getFlags(): Promise<Map<string, FeatureFlag>> {
if (Date.now() - lastFetch < CACHE_TTL && flagsCache.size > 0) {
return flagsCache;
}
const flags = await db.featureFlag.findMany({
include: { rules: true }
});
flagsCache = new Map(flags.map(f => [f.key, f]));
lastFetch = Date.now();
return flagsCache;
}
export async function isFeatureEnabled(
key: string,
context?: EvaluationContext
): Promise<boolean> {
const flags = await getFlags();
const flag = flags.get(key);
if (!flag) return false;
// Build context from request if not provided
const evalContext = context || await buildContextFromRequest();
const evaluation = evaluator.evaluate(flag, evalContext);
return Boolean(evaluation.value);
}
async function buildContextFromRequest(): Promise<EvaluationContext> {
const headersList = await headers();
const cookieStore = await cookies();
return {
userId: cookieStore.get('userId')?.value,
tenantId: headersList.get('x-tenant-id') || undefined,
country: headersList.get('cf-ipcountry') || undefined
};
}
// Usage in Server Components
export default async function SettingsPage() {
const showBetaFeatures = await isFeatureEnabled('beta-features');
return (
<div>
<h1>Settings</h1>
{showBetaFeatures && <BetaSettings />}
</div>
);
}
```
---
## Progressive Rollout
```typescript
// lib/features/rollout.ts
import { db } from '@/lib/db';
interface RolloutPlan {
stages: RolloutStage[];
currentStage: number;
startedAt: Date;
}
interface RolloutStage {
percentage: number;
duration: number; // hours
metrics: {
errorRateThreshold: number;
latencyThreshold: number;
};
}
export async function progressRollout(flagKey: string): Promise<void> {
const flag = await db.featureFlag.findUnique({
where: { key: flagKey },
include: { rolloutPlan: true }
});
if (!flag?.rolloutPlan) {
throw new Error('No rollout plan found');
}
const plan = flag.rolloutPlan as RolloutPlan;
const currentStage = plan.stages[plan.currentStage];
const nextStage = plan.stages[plan.currentStage + 1];
if (!nextStage) {
console.log(`Rollout complete for ${flagKey}`);
return;
}
// Check metrics before progressing
const metricsOk = await checkRolloutMetrics(flagKey, currentStage.metrics);
if (!metricsOk) {
// Automatic rollback
await rollbackFeature(flagKey);
throw new Error('Metrics threshold exceeded, rolling back');
}
// Progress to next stage
await db.featureFlag.update({
where: { key: flagKey },
data: {
rolloutPercentage: nextStage.percentage,
rolloutPlan: {
...plan,
currentStage: plan.currentStage + 1
}
}
});
console.log(`Rolled out ${flagKey} to ${nextStage.percentage}%`);
}
async function checkRolloutMetrics(
flagKey: string,
thresholds: { errorRateThreshold: number; latencyThreshold: number }
): Promise<boolean> {
// Query your metrics system
const metrics = await getFeatureMetrics(flagKey);
return (
metrics.errorRate < thresholds.errorRateThreshold &&
metrics.p95Latency < thresholds.latencyThreshold
);
}
export async function rollbackFeature(flagKey: string): Promise<void> {
await db.featureFlag.update({
where: { key: flagKey },
data: {
rolloutPercentage: 0,
enabled: false
}
});
// Alert team
await sendSlackAlert(`Feature ${flagKey} automatically rolled back due to metrics`);
}
// Scheduled job for automatic progression
export async function processRollouts(): Promise<void> {
const activeRollouts = await db.featureFlag.findMany({
where: {
rolloutPercentage: { gt: 0, lt: 100 },
enabled: true
}
});
for (const flag of activeRollouts) {
try {
await progressRollout(flag.key);
} catch (error) {
console.error(`Rollout failed for ${flag.key}:`, error);
}
}
}
```
---
## Admin Dashboard
```tsx
// app/admin/features/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { FeatureFlag } from '@/lib/features/types';
export default function FeatureFlagsAdmin() {
const [flags, setFlags] = useState<FeatureFlag[]>([]);
const [selectedFlag, setSelectedFlag] = useState<FeatureFlag | null>(null);
useEffect(() => {
fetch('/api/admin/features')
.then(res => res.json())
.then(setFlags);
}, []);
const updateFlag = async (key: string, updates: Partial<FeatureFlag>) => {
await fetch(`/api/admin/features/${key}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
setFlags(flags.map(f =>
f.key === key ? { ...f, ...updates } : f
));
};
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">Feature Flags</h1>
<div className="bg-white rounded-lg shadow">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left">Flag</th>
<th className="px-4 py-3 text-left">Status</th>
<th className="px-4 py-3 text-left">Rollout</th>
<th className="px-4 py-3 text-left">Actions</th>
</tr>
</thead>
<tbody>
{flags.map(flag => (
<tr key={flag.key} className="border-t">
<td className="px-4 py-3">
<div>
<div className="font-medium">{flag.name}</div>
<div className="text-sm text-gray-500">{flag.key}</div>
</div>
</td>
<td className="px-4 py-3">
<ToggleSwitch
enabled={flag.enabled}
onChange={(enabled) => updateFlag(flag.key, { enabled })}
/>
</td>
<td className="px-4 py-3">
<RolloutSlider
value={flag.rolloutPercentage}
onChange={(percentage) =>
updateFlag(flag.key, { rolloutPercentage: percentage })
}
/>
</td>
<td className="px-4 py-3">
<button
onClick={() => setSelectedFlag(flag)}
className="text-blue-600 hover:underline"
>
Edit Rules
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{selectedFlag && (
<FlagRulesModal
flag={selectedFlag}
onClose={() => setSelectedFlag(null)}
onSave={(rules) => updateFlag(selectedFlag.key, { rules })}
/>
)}
</div>
);
}
```
---
## Best Practices
| Practice | Description |
|----------|-------------|
| **Naming** | Use clear, descriptive flag names |
| **Lifecycle** | Remove flags after full rollout |
| **Testing** | Test both flag states |
| **Documentation** | Document flag purpose and owner |
| **Monitoring** | Track flag evaluations |
| **Defaults** | Safe defaults (usually off) |
---
## Fazit
Feature Flags ermöglichen:
1. **Safe Deployments**: Features schrittweise ausrollen
2. **Quick Rollbacks**: Probleme sofort beheben
3. **Experimentation**: A/B Tests und Experimente
4. **Customization**: Feature-based Pricing
Feature Flags sind essentiell für moderne Software-Entwicklung.
---
## Bildprompts
1. "Feature flag dashboard, toggles and rollout percentages"
2. "Progressive rollout diagram, stages from 1% to 100%"
3. "Feature targeting rules interface, user segmentation"
---
## Quellen
- [Martin Fowler Feature Toggles](https://martinfowler.com/articles/feature-toggles.html)
- [LaunchDarkly Documentation](https://docs.launchdarkly.com/)
- [Unleash Feature Management](https://www.getunleash.io/)
- [Feature Flags Best Practices](https://featureflags.io/)