Add blog posts, cleanup unused files, update components
- 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>
This commit is contained in:
@@ -0,0 +1,752 @@
|
||||
# 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/)
|
||||
Reference in New Issue
Block a user