- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
753 lines
21 KiB
Markdown
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/)
|