Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component...
npx skills add cosmix/loom --skill "feature-flags"
Install specific skill from multi-skill repository
# Description
Feature flag patterns for controlled rollouts, A/B testing, kill switches, and runtime configuration. Use when implementing feature toggles, feature flags, gradual rollouts, canary releases, percentage rollouts, dark launches, user targeting, A/B tests, experiments, circuit breakers, emergency kill switches, model switching, or infrastructure flags.
# SKILL.md
name: feature-flags
description: Feature flag patterns for controlled rollouts, A/B testing, kill switches, and runtime configuration. Use when implementing feature toggles, feature flags, gradual rollouts, canary releases, percentage rollouts, dark launches, user targeting, A/B tests, experiments, circuit breakers, emergency kill switches, model switching, or infrastructure flags.
keywords: feature flag, feature toggle, LaunchDarkly, Unleash, split, canary, gradual rollout, percentage rollout, dark launch, A/B test, experiment, kill switch, circuit breaker, model switching, infrastructure flag, runtime config, feature gate
Feature Flags
Overview
Feature flags (also called feature toggles or feature gates) enable runtime control over feature availability without code deployments. They support gradual rollouts, A/B testing, user targeting, emergency kill switches, ML model switching, and infrastructure configuration. This skill covers implementation patterns, best practices, integration with LaunchDarkly/Unleash, and flag lifecycle management.
Agents
- senior-software-engineer - Feature flag strategy, architecture decisions, rollout planning
- software-engineer - Implements feature flags, integrations, and flag evaluation logic
- security-engineer - Feature flag security, access controls, audit logging
- senior-infrastructure-engineer - Feature flag infrastructure, distributed systems, caching strategies
Key Concepts
Flag Types
Boolean Flags - Simple on/off toggles:
interface BooleanFlag {
key: string;
enabled: boolean;
description: string;
}
// Usage
if (featureFlags.isEnabled("new-checkout-flow")) {
return <NewCheckoutFlow />;
}
return <LegacyCheckout />;
Percentage Rollout Flags - Gradual exposure:
interface PercentageFlag {
key: string;
percentage: number; // 0-100
salt: string; // For consistent hashing
}
function isEnabledForUser(flag: PercentageFlag, userId: string): boolean {
// Consistent hashing ensures same user always gets same result
const hash = createHash("md5").update(`${flag.salt}:${userId}`).digest("hex");
const bucket = parseInt(hash.substring(0, 8), 16) % 100;
return bucket < flag.percentage;
}
User-Targeted Flags - Specific user segments:
interface TargetedFlag {
key: string;
defaultValue: boolean;
rules: TargetingRule[];
}
interface TargetingRule {
attribute: string; // 'userId', 'email', 'country', 'plan'
operator: "in" | "notIn" | "equals" | "contains" | "startsWith" | "matches";
values: string[];
value: boolean;
}
// Example: Enable for beta users and premium plans
const flag: TargetedFlag = {
key: "advanced-analytics",
defaultValue: false,
rules: [
{
attribute: "email",
operator: "in",
values: ["[email protected]"],
value: true,
},
{
attribute: "plan",
operator: "in",
values: ["premium", "enterprise"],
value: true,
},
{ attribute: "country", operator: "in", values: ["US", "CA"], value: true },
],
};
Multivariate Flags - Multiple variants for A/B testing:
interface MultivariateFlag<T> {
key: string;
variants: Variant<T>[];
defaultVariant: string;
}
interface Variant<T> {
name: string;
value: T;
weight: number; // Percentage allocation
}
// Example: Button color A/B test
const buttonColorFlag: MultivariateFlag<string> = {
key: "checkout-button-color",
defaultVariant: "control",
variants: [
{ name: "control", value: "#007bff", weight: 34 },
{ name: "green", value: "#28a745", weight: 33 },
{ name: "orange", value: "#fd7e14", weight: 33 },
],
};
Custom Implementation
import { Redis } from "ioredis";
import { createHash } from "crypto";
interface FeatureFlag {
key: string;
type: "boolean" | "percentage" | "targeted" | "multivariate";
enabled: boolean;
percentage?: number;
rules?: TargetingRule[];
variants?: Variant<unknown>[];
salt: string;
description: string;
createdAt: Date;
updatedAt: Date;
}
interface EvaluationContext {
userId?: string;
email?: string;
country?: string;
plan?: string;
[key: string]: string | number | boolean | undefined;
}
class FeatureFlagService {
private redis: Redis;
private cache: Map<string, { flag: FeatureFlag; expiresAt: number }> =
new Map();
private cacheTTL = 60000; // 1 minute
constructor(redis: Redis) {
this.redis = redis;
}
async getFlag(key: string): Promise<FeatureFlag | null> {
// Check local cache
const cached = this.cache.get(key);
if (cached && cached.expiresAt > Date.now()) {
return cached.flag;
}
// Fetch from Redis
const data = await this.redis.get(`flag:${key}`);
if (!data) return null;
const flag: FeatureFlag = JSON.parse(data);
this.cache.set(key, { flag, expiresAt: Date.now() + this.cacheTTL });
return flag;
}
async evaluate(
key: string,
context: EvaluationContext = {}
): Promise<boolean> {
const flag = await this.getFlag(key);
if (!flag) return false;
if (!flag.enabled) return false;
switch (flag.type) {
case "boolean":
return true;
case "percentage":
return this.evaluatePercentage(flag, context.userId || "anonymous");
case "targeted":
return this.evaluateTargeting(flag, context);
default:
return false;
}
}
async evaluateVariant<T>(
key: string,
context: EvaluationContext = {}
): Promise<T | null> {
const flag = await this.getFlag(key);
if (!flag || !flag.enabled || !flag.variants) return null;
const userId = context.userId || "anonymous";
const hash = createHash("md5")
.update(`${flag.salt}:${userId}`)
.digest("hex");
const bucket = parseInt(hash.substring(0, 8), 16) % 100;
let cumulative = 0;
for (const variant of flag.variants) {
cumulative += variant.weight;
if (bucket < cumulative) {
return variant.value as T;
}
}
return (flag.variants[0]?.value as T) ?? null;
}
private evaluatePercentage(flag: FeatureFlag, userId: string): boolean {
const hash = createHash("md5")
.update(`${flag.salt}:${userId}`)
.digest("hex");
const bucket = parseInt(hash.substring(0, 8), 16) % 100;
return bucket < (flag.percentage ?? 0);
}
private evaluateTargeting(
flag: FeatureFlag,
context: EvaluationContext
): boolean {
if (!flag.rules || flag.rules.length === 0) return true;
for (const rule of flag.rules) {
const attributeValue = String(context[rule.attribute] ?? "");
let matches = false;
switch (rule.operator) {
case "in":
matches = rule.values.includes(attributeValue);
break;
case "notIn":
matches = !rule.values.includes(attributeValue);
break;
case "equals":
matches = attributeValue === rule.values[0];
break;
case "contains":
matches = rule.values.some((v) => attributeValue.includes(v));
break;
case "startsWith":
matches = rule.values.some((v) => attributeValue.startsWith(v));
break;
case "matches":
matches = rule.values.some((v) => new RegExp(v).test(attributeValue));
break;
}
if (matches) return rule.value;
}
return false;
}
// Admin methods
async setFlag(flag: FeatureFlag): Promise<void> {
await this.redis.set(`flag:${flag.key}`, JSON.stringify(flag));
this.cache.delete(flag.key);
// Publish change for other instances
await this.redis.publish("flag-updates", JSON.stringify({ key: flag.key }));
}
async deleteFlag(key: string): Promise<void> {
await this.redis.del(`flag:${key}`);
this.cache.delete(key);
await this.redis.publish(
"flag-updates",
JSON.stringify({ key, deleted: true })
);
}
}
Gradual Rollouts and Canary Releases
interface RolloutStrategy {
type: "linear" | "exponential" | "manual";
startPercentage: number;
targetPercentage: number;
incrementPercentage: number;
intervalMinutes: number;
currentPercentage: number;
startedAt: Date;
pausedAt?: Date;
}
class RolloutManager {
private flags: FeatureFlagService;
async startRollout(
flagKey: string,
strategy: RolloutStrategy
): Promise<void> {
const flag = await this.flags.getFlag(flagKey);
if (!flag) throw new Error("Flag not found");
flag.percentage = strategy.startPercentage;
flag.rolloutStrategy = strategy;
await this.flags.setFlag(flag);
// Schedule automatic increments
if (strategy.type !== "manual") {
this.scheduleIncrement(flagKey, strategy);
}
}
private async scheduleIncrement(
flagKey: string,
strategy: RolloutStrategy
): Promise<void> {
const incrementJob = async () => {
const flag = await this.flags.getFlag(flagKey);
if (!flag || flag.rolloutStrategy?.pausedAt) return;
const current = flag.percentage ?? 0;
let newPercentage: number;
if (strategy.type === "linear") {
newPercentage = Math.min(
current + strategy.incrementPercentage,
strategy.targetPercentage
);
} else {
// Exponential: double each time
newPercentage = Math.min(current * 2, strategy.targetPercentage);
}
flag.percentage = newPercentage;
await this.flags.setFlag(flag);
// Continue if not at target
if (newPercentage < strategy.targetPercentage) {
setTimeout(incrementJob, strategy.intervalMinutes * 60 * 1000);
}
};
setTimeout(incrementJob, strategy.intervalMinutes * 60 * 1000);
}
async pauseRollout(flagKey: string): Promise<void> {
const flag = await this.flags.getFlag(flagKey);
if (flag?.rolloutStrategy) {
flag.rolloutStrategy.pausedAt = new Date();
await this.flags.setFlag(flag);
}
}
async rollback(flagKey: string): Promise<void> {
const flag = await this.flags.getFlag(flagKey);
if (flag) {
flag.percentage = 0;
flag.enabled = false;
await this.flags.setFlag(flag);
}
}
}
A/B Testing Integration
interface Experiment {
id: string;
flagKey: string;
name: string;
hypothesis: string;
metrics: string[]; // Metrics to track
variants: ExperimentVariant[];
status: "draft" | "running" | "paused" | "completed";
startDate?: Date;
endDate?: Date;
sampleSize: number;
confidenceLevel: number; // e.g., 0.95
}
interface ExperimentVariant {
name: string;
weight: number;
conversions: number;
impressions: number;
}
class ABTestingService {
async trackExposure(
experimentId: string,
variantName: string,
userId: string
): Promise<void> {
// Record that user was exposed to variant
await this.analytics.track({
event: "experiment_exposure",
userId,
properties: {
experimentId,
variant: variantName,
timestamp: new Date(),
},
});
// Increment impression count
await this.redis.hincrby(
`experiment:${experimentId}:${variantName}`,
"impressions",
1
);
}
async trackConversion(
experimentId: string,
userId: string,
metric: string
): Promise<void> {
// Get user's assigned variant
const variant = await this.redis.hget(
`experiment:${experimentId}:assignments`,
userId
);
if (!variant) return;
await this.analytics.track({
event: "experiment_conversion",
userId,
properties: {
experimentId,
variant,
metric,
timestamp: new Date(),
},
});
await this.redis.hincrby(
`experiment:${experimentId}:${variant}`,
"conversions",
1
);
}
async getResults(experimentId: string): Promise<ExperimentResults> {
const experiment = await this.getExperiment(experimentId);
const results = await Promise.all(
experiment.variants.map(async (variant) => {
const data = await this.redis.hgetall(
`experiment:${experimentId}:${variant.name}`
);
return {
name: variant.name,
impressions: parseInt(data.impressions || "0"),
conversions: parseInt(data.conversions || "0"),
conversionRate:
parseInt(data.conversions || "0") /
parseInt(data.impressions || "1"),
};
})
);
// Calculate statistical significance
const control = results.find((r) => r.name === "control");
const treatments = results.filter((r) => r.name !== "control");
return {
experimentId,
results,
winners: treatments.filter((t) =>
this.isStatisticallySignificant(control!, t, experiment.confidenceLevel)
),
};
}
private isStatisticallySignificant(
control: VariantResult,
treatment: VariantResult,
confidenceLevel: number
): boolean {
// Z-test for proportions
const p1 = control.conversionRate;
const p2 = treatment.conversionRate;
const n1 = control.impressions;
const n2 = treatment.impressions;
const pooledP = (p1 * n1 + p2 * n2) / (n1 + n2);
const se = Math.sqrt(pooledP * (1 - pooledP) * (1 / n1 + 1 / n2));
const z = (p2 - p1) / se;
// Z-score for 95% confidence is 1.96
const zThreshold = confidenceLevel === 0.95 ? 1.96 : 2.58; // 99%
return Math.abs(z) > zThreshold;
}
}
Kill Switches
interface KillSwitch {
key: string;
description: string;
affectedServices: string[];
activatedAt?: Date;
activatedBy?: string;
reason?: string;
autoRecoveryMinutes?: number;
}
class KillSwitchService {
private redis: Redis;
private alerting: AlertingService;
async activate(
key: string,
reason: string,
activatedBy: string
): Promise<void> {
const killSwitch = await this.getKillSwitch(key);
if (!killSwitch) throw new Error("Kill switch not found");
killSwitch.activatedAt = new Date();
killSwitch.activatedBy = activatedBy;
killSwitch.reason = reason;
await this.redis.set(`killswitch:${key}`, JSON.stringify(killSwitch));
// Broadcast to all instances immediately
await this.redis.publish(
"killswitch-activated",
JSON.stringify(killSwitch)
);
// Alert on-call
await this.alerting.sendCritical({
title: `Kill Switch Activated: ${key}`,
message: `Reason: ${reason}\nActivated by: ${activatedBy}\nAffected: ${killSwitch.affectedServices.join(
", "
)}`,
});
// Schedule auto-recovery if configured
if (killSwitch.autoRecoveryMinutes) {
setTimeout(
() => this.deactivate(key, "Auto-recovery"),
killSwitch.autoRecoveryMinutes * 60 * 1000
);
}
}
async deactivate(key: string, reason: string): Promise<void> {
const killSwitch = await this.getKillSwitch(key);
if (!killSwitch) return;
killSwitch.activatedAt = undefined;
killSwitch.activatedBy = undefined;
killSwitch.reason = undefined;
await this.redis.set(`killswitch:${key}`, JSON.stringify(killSwitch));
await this.redis.publish(
"killswitch-deactivated",
JSON.stringify({ key, reason })
);
await this.alerting.sendInfo({
title: `Kill Switch Deactivated: ${key}`,
message: `Reason: ${reason}`,
});
}
async isActive(key: string): Promise<boolean> {
const data = await this.redis.get(`killswitch:${key}`);
if (!data) return false;
const killSwitch: KillSwitch = JSON.parse(data);
return !!killSwitch.activatedAt;
}
}
// Usage in application code
async function processPayment(payment: Payment): Promise<PaymentResult> {
// Check kill switch first
if (await killSwitches.isActive("payments-disabled")) {
throw new ServiceUnavailableError(
"Payment processing temporarily disabled"
);
}
// Normal processing
return paymentProcessor.process(payment);
}
Flag Lifecycle Management
interface FlagLifecycle {
key: string;
status:
| "planning"
| "development"
| "testing"
| "rollout"
| "stable"
| "deprecated"
| "removed";
owner: string;
team: string;
createdAt: Date;
plannedRemovalDate?: Date;
jiraTicket?: string;
staleAfterDays: number;
}
class FlagLifecycleManager {
async checkStaleFlags(): Promise<StaleFlag[]> {
const flags = await this.getAllFlags();
const now = new Date();
return flags.filter((flag) => {
const lifecycle = flag.lifecycle;
if (!lifecycle) return false;
const ageInDays =
(now.getTime() - lifecycle.createdAt.getTime()) / (1000 * 60 * 60 * 24);
// Flag is stale if:
// 1. It's older than staleAfterDays and still in development/testing
// 2. It's past its planned removal date
// 3. It's been stable for > 30 days (should be permanent or removed)
if (lifecycle.status === "stable" && ageInDays > 30) return true;
if (lifecycle.plannedRemovalDate && lifecycle.plannedRemovalDate < now)
return true;
if (
["development", "testing"].includes(lifecycle.status) &&
ageInDays > lifecycle.staleAfterDays
)
return true;
return false;
});
}
async generateCleanupReport(): Promise<CleanupReport> {
const staleFlags = await this.checkStaleFlags();
return {
generatedAt: new Date(),
staleFlags: staleFlags.map((flag) => ({
key: flag.key,
status: flag.lifecycle.status,
owner: flag.lifecycle.owner,
age: this.calculateAge(flag.lifecycle.createdAt),
recommendation: this.getRecommendation(flag),
})),
summary: {
total: staleFlags.length,
byStatus: this.groupBy(staleFlags, (f) => f.lifecycle.status),
byTeam: this.groupBy(staleFlags, (f) => f.lifecycle.team),
},
};
}
private getRecommendation(flag: FlagWithLifecycle): string {
const { status, plannedRemovalDate } = flag.lifecycle;
if (plannedRemovalDate && plannedRemovalDate < new Date()) {
return "URGENT: Past planned removal date. Remove flag and clean up code.";
}
if (status === "stable") {
return "Flag is stable. Either make permanent (remove flag, keep feature) or deprecate.";
}
if (status === "development" || status === "testing") {
return "Flag stuck in development. Complete rollout or remove if abandoned.";
}
return "Review flag status and update lifecycle.";
}
}
LaunchDarkly Integration
import * as LaunchDarkly from "launchdarkly-node-server-sdk";
const ldClient = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY!);
interface LDUser {
key: string;
email?: string;
name?: string;
custom?: Record<string, string | number | boolean>;
}
async function evaluateFlag(
flagKey: string,
user: LDUser,
defaultValue: boolean
): Promise<boolean> {
await ldClient.waitForInitialization();
return ldClient.variation(flagKey, user, defaultValue);
}
async function evaluateFlagWithReason(
flagKey: string,
user: LDUser,
defaultValue: boolean
) {
await ldClient.waitForInitialization();
const detail = await ldClient.variationDetail(flagKey, user, defaultValue);
return {
value: detail.value,
reason: detail.reason,
variationIndex: detail.variationIndex,
};
}
// Track custom events for experiments
function trackConversion(
user: LDUser,
eventKey: string,
data?: Record<string, unknown>
): void {
ldClient.track(eventKey, user, data);
}
// React hook for client-side
function useFeatureFlag(
flagKey: string,
defaultValue: boolean = false
): boolean {
const ldClient = useLDClient();
const [value, setValue] = useState(defaultValue);
useEffect(() => {
if (!ldClient) return;
setValue(ldClient.variation(flagKey, defaultValue));
const handler = (newValue: boolean) => setValue(newValue);
ldClient.on(`change:${flagKey}`, handler);
return () => ldClient.off(`change:${flagKey}`, handler);
}, [ldClient, flagKey, defaultValue]);
return value;
}
ML Model Feature Flags
interface ModelFlag {
key: string;
modelVariants: ModelVariant[];
routingStrategy: "percentage" | "performance" | "custom";
fallbackModel: string;
performanceThresholds?: {
latencyMs: number;
errorRate: number;
};
}
interface ModelVariant {
name: string;
modelId: string;
version: string;
weight?: number; // For percentage routing
endpoint: string;
}
class ModelFlagService {
async evaluateModel(
flagKey: string,
context: { userId?: string; inputSize?: number }
): Promise<ModelVariant> {
const flag = await this.getModelFlag(flagKey);
switch (flag.routingStrategy) {
case "percentage":
return this.percentageRouting(flag, context.userId || "anonymous");
case "performance":
return this.performanceRouting(flag);
case "custom":
return this.customRouting(flag, context);
default:
return flag.modelVariants.find((v) => v.name === flag.fallbackModel)!;
}
}
private async performanceRouting(flag: ModelFlag): Promise<ModelVariant> {
// Get real-time performance metrics for each model
const metrics = await Promise.all(
flag.modelVariants.map(async (variant) => ({
variant,
latency: await this.getAverageLatency(variant.modelId),
errorRate: await this.getErrorRate(variant.modelId),
}))
);
// Filter models meeting performance thresholds
const eligible = metrics.filter(
(m) =>
m.latency < flag.performanceThresholds!.latencyMs &&
m.errorRate < flag.performanceThresholds!.errorRate
);
// Return best performing model or fallback
if (eligible.length === 0) {
return flag.modelVariants.find((v) => v.name === flag.fallbackModel)!;
}
return eligible.sort((a, b) => a.latency - b.latency)[0].variant;
}
async trackModelInference(
flagKey: string,
modelVariant: ModelVariant,
metrics: {
latencyMs: number;
success: boolean;
inputTokens: number;
outputTokens: number;
}
): Promise<void> {
// Track metrics for performance-based routing
await this.redis.zadd(
`model:${modelVariant.modelId}:latency`,
Date.now(),
metrics.latencyMs
);
await this.redis.hincrby(
`model:${modelVariant.modelId}:stats`,
metrics.success ? "success" : "failure",
1
);
// Track costs
await this.redis.hincrby(
`model:${modelVariant.modelId}:tokens`,
"input",
metrics.inputTokens
);
await this.redis.hincrby(
`model:${modelVariant.modelId}:tokens`,
"output",
metrics.outputTokens
);
}
}
// Usage example
async function generateResponse(
prompt: string,
userId: string
): Promise<string> {
const modelVariant = await modelFlags.evaluateModel("chat-model", { userId });
const startTime = Date.now();
try {
const response = await fetch(modelVariant.endpoint, {
method: "POST",
body: JSON.stringify({ prompt, model: modelVariant.modelId }),
});
const data = await response.json();
await modelFlags.trackModelInference("chat-model", modelVariant, {
latencyMs: Date.now() - startTime,
success: true,
inputTokens: data.usage.input_tokens,
outputTokens: data.usage.output_tokens,
});
return data.response;
} catch (error) {
await modelFlags.trackModelInference("chat-model", modelVariant, {
latencyMs: Date.now() - startTime,
success: false,
inputTokens: 0,
outputTokens: 0,
});
throw error;
}
}
Infrastructure Feature Flags
interface InfrastructureFlag {
key: string;
type: "database" | "cache" | "queue" | "storage" | "cdn";
variants: InfraVariant[];
healthCheckInterval: number;
autoFailover: boolean;
}
interface InfraVariant {
name: string;
config: {
host?: string;
port?: number;
url?: string;
region?: string;
[key: string]: string | number | boolean | undefined;
};
healthEndpoint?: string;
healthy: boolean;
priority: number; // Lower = higher priority
}
class InfrastructureFlagService {
private healthChecks: Map<string, NodeJS.Timer> = new Map();
async startHealthChecks(flagKey: string): Promise<void> {
const flag = await this.getInfraFlag(flagKey);
const interval = setInterval(async () => {
for (const variant of flag.variants) {
if (!variant.healthEndpoint) continue;
try {
const response = await fetch(variant.healthEndpoint, {
timeout: 5000,
});
variant.healthy = response.ok;
} catch {
variant.healthy = false;
}
}
await this.updateInfraFlag(flag);
}, flag.healthCheckInterval);
this.healthChecks.set(flagKey, interval);
}
async getActiveVariant(flagKey: string): Promise<InfraVariant> {
const flag = await this.getInfraFlag(flagKey);
// Get healthy variants sorted by priority
const healthyVariants = flag.variants
.filter((v) => v.healthy)
.sort((a, b) => a.priority - b.priority);
if (healthyVariants.length === 0) {
if (flag.autoFailover) {
// Use unhealthy variant as last resort
console.error(`No healthy variants for ${flagKey}, using fallback`);
return flag.variants.sort((a, b) => a.priority - b.priority)[0];
} else {
throw new Error(`No healthy variants available for ${flagKey}`);
}
}
return healthyVariants[0];
}
}
// Database migration example
class DatabaseConnection {
private infraFlags: InfrastructureFlagService;
private currentConnection?: Connection;
async getConnection(): Promise<Connection> {
const variant = await this.infraFlags.getActiveVariant("primary-database");
// Reuse connection if config hasn't changed
if (this.currentConnection?.config.host === variant.config.host) {
return this.currentConnection;
}
// Create new connection to migrated database
this.currentConnection = await createConnection({
host: variant.config.host as string,
port: variant.config.port as number,
database: variant.config.database as string,
});
return this.currentConnection;
}
}
// CDN migration example
class AssetService {
async getAssetUrl(assetPath: string): Promise<string> {
const cdnVariant = await infraFlags.getActiveVariant("cdn-provider");
return `${cdnVariant.config.url}/${assetPath}`;
}
}
Flag Cleanup and Technical Debt Management
interface FlagCleanupTask {
flagKey: string;
status: "pending" | "in-progress" | "completed" | "blocked";
type: "remove-flag" | "make-permanent" | "deprecate";
estimatedEffort: "small" | "medium" | "large";
affectedCodePaths: string[];
dependencies: string[];
assignee?: string;
dueDate?: Date;
}
class FlagCleanupService {
async analyzeCodeImpact(flagKey: string): Promise<CodeImpactReport> {
// Scan codebase for flag references
const references = await this.findFlagReferences(flagKey);
return {
flagKey,
totalReferences: references.length,
fileCount: new Set(references.map((r) => r.file)).size,
byFileType: this.groupBy(references, (r) => r.fileExtension),
complexityScore: this.calculateComplexity(references),
recommendations: this.generateRecommendations(references),
};
}
private calculateComplexity(references: CodeReference[]): number {
let score = 0;
for (const ref of references) {
// Nested conditions increase complexity
if (ref.context.includes("if") && ref.context.includes("else")) {
score += 2;
}
// Multiple flags in same file
const otherFlags = this.countOtherFlags(ref.file);
score += otherFlags * 0.5;
// Presence in critical paths
if (ref.file.includes("payment") || ref.file.includes("auth")) {
score += 3;
}
}
return score;
}
async generateCleanupPlan(staleFlags: string[]): Promise<FlagCleanupTask[]> {
const tasks: FlagCleanupTask[] = [];
for (const flagKey of staleFlags) {
const flag = await this.getFlag(flagKey);
const impact = await this.analyzeCodeImpact(flagKey);
// Determine cleanup type
let type: FlagCleanupTask["type"];
if (flag.percentage === 100 && flag.lifecycle.status === "stable") {
type = "make-permanent"; // Remove flag, keep new code path
} else if (flag.percentage === 0) {
type = "remove-flag"; // Remove flag and new code path
} else {
type = "deprecate"; // Mark for future removal
}
tasks.push({
flagKey,
status: "pending",
type,
estimatedEffort:
impact.complexityScore < 5
? "small"
: impact.complexityScore < 15
? "medium"
: "large",
affectedCodePaths:
impact.fileCount > 0
? Array.from(new Set(impact.references.map((r) => r.file)))
: [],
dependencies: await this.findDependentFlags(flagKey),
});
}
// Sort by effort (easiest first for quick wins)
return tasks.sort((a, b) => {
const effortMap = { small: 1, medium: 2, large: 3 };
return effortMap[a.estimatedEffort] - effortMap[b.estimatedEffort];
});
}
async trackCleanupProgress(): Promise<CleanupMetrics> {
const allFlags = await this.getAllFlags();
const tasks = await this.getAllCleanupTasks();
return {
totalFlags: allFlags.length,
staleFlags: allFlags.filter((f) => this.isStale(f)).length,
flagsWithCleanupTasks: tasks.length,
completedCleanups: tasks.filter((t) => t.status === "completed").length,
averageAgeOfStaleFlags: this.calculateAverageAge(
allFlags.filter((f) => this.isStale(f))
),
projectedCleanupDate: this.estimateCompletionDate(tasks),
};
}
}
// Automated cleanup detection
class FlagCleanupMonitor {
async runDailyCleanupCheck(): Promise<void> {
const staleFlags = await this.flagLifecycle.checkStaleFlags();
if (staleFlags.length === 0) return;
// Create Jira tickets for cleanup
for (const flag of staleFlags) {
const impact = await this.cleanup.analyzeCodeImpact(flag.key);
await this.ticketingSystem.createTicket({
title: `Clean up feature flag: ${flag.key}`,
description: `
Flag Status: ${flag.lifecycle.status}
Age: ${this.calculateAge(flag.lifecycle.createdAt)} days
References: ${impact.totalReferences} in ${impact.fileCount} files
Complexity: ${impact.complexityScore}
Recommendations:
${impact.recommendations.join("\n")}
`,
priority:
impact.complexityScore < 5
? "low"
: impact.complexityScore < 15
? "medium"
: "high",
labels: ["technical-debt", "feature-flags", "cleanup"],
assignee: flag.lifecycle.owner,
});
}
// Send weekly digest to engineering team
if (new Date().getDay() === 1) {
// Monday
await this.sendCleanupDigest(staleFlags);
}
}
}
Best Practices
-
Flag Naming Conventions
-
Use descriptive, consistent names:
feature-checkout-v2,experiment-button-color - Include type prefix:
release-*,experiment-*,ops-*,kill-* -
Avoid abbreviations and ensure team-wide understanding
-
Flag Hygiene
-
Set expiration dates for temporary flags
- Remove flags after features are fully rolled out
- Track flag ownership and associated tickets
-
Regular cleanup audits (monthly)
-
Testing
-
Test all flag states (on, off, each variant)
- Include flag states in integration tests
-
Test rollback scenarios
-
Monitoring
-
Track flag evaluation counts and latency
- Alert on unusual patterns (sudden spikes, failures)
-
Log flag decisions for debugging
-
Documentation
- Document what each flag controls
- Include rollback instructions
- Link to related PRs and tickets
Examples
React Feature Flag Provider
import React, { createContext, useContext, useEffect, useState } from "react";
interface FeatureFlagContextType {
isEnabled: (key: string) => boolean;
getVariant: <T>(key: string) => T | null;
loading: boolean;
}
const FeatureFlagContext = createContext<FeatureFlagContextType | null>(null);
export function FeatureFlagProvider({
children,
userId,
}: {
children: React.ReactNode;
userId: string;
}) {
const [flags, setFlags] = useState<Record<string, unknown>>({});
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadFlags() {
const response = await fetch(`/api/flags?userId=${userId}`);
const data = await response.json();
setFlags(data);
setLoading(false);
}
loadFlags();
// Subscribe to real-time updates
const ws = new WebSocket(
`wss://api.example.com/flags/stream?userId=${userId}`
);
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
setFlags((prev) => ({ ...prev, [update.key]: update.value }));
};
return () => ws.close();
}, [userId]);
const value: FeatureFlagContextType = {
isEnabled: (key) => Boolean(flags[key]),
getVariant: (key) => (flags[key] as T) ?? null,
loading,
};
return (
<FeatureFlagContext.Provider value={value}>
{children}
</FeatureFlagContext.Provider>
);
}
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);
}
// Usage
function CheckoutPage() {
const newCheckout = useFeatureFlag("new-checkout-flow");
if (newCheckout) {
return <NewCheckoutFlow />;
}
return <LegacyCheckout />;
}
When to Use This Skill
Use the feature-flags skill for:
- Feature Toggles - Runtime on/off switches for features
- Gradual Rollouts - Percentage-based or canary releases (1% -> 5% -> 25% -> 100%)
- Dark Launches - Deploy code disabled, enable when ready
- A/B Testing - Compare variants to measure impact
- User Targeting - Enable features for specific users, regions, or plans
- Kill Switches - Emergency disable for broken features
- Circuit Breakers - Auto-disable when error thresholds exceeded
- ML Model Flags - Switch between model versions or providers
- Infrastructure Flags - Migrate databases, caches, CDNs without downtime
- Configuration - Runtime config changes without redeployment
- Cleanup - Managing technical debt from old flags
Agent Selection Guide
| Task | Agent | Why |
|---|---|---|
| Design flag architecture | senior-software-engineer | Strategic decisions on flag types, rollout strategy |
| Implement flag service | software-engineer | Build evaluation logic, integrations |
| Review flag security | security-engineer | Access controls, audit logging, sensitive data |
| Scale flag infrastructure | senior-infrastructure-engineer | Distributed caching, performance, failover |
| Integrate LaunchDarkly/Unleash | software-engineer | SDK integration, webhook setup |
| Plan ML model rollout | senior-software-engineer | Performance routing, fallback strategy |
| Implement cleanup automation | software-engineer | Code scanning, impact analysis |
| Design flag lifecycle policy | senior-software-engineer | Governance, technical debt prevention |
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.