名称: 功能标志 描述: 用于控制发布、A/B测试、紧急关闭开关和运行时配置的功能标志模式。适用于实现功能开关、功能标志、渐进式发布、金丝雀发布、百分比发布、暗启动、用户定向、A/B测试、实验、熔断器、紧急关闭开关、模型切换或基础设施标志。 关键词: 功能标志, 功能开关, LaunchDarkly, Unleash, 分割测试, 金丝雀发布, 渐进式发布, 百分比发布, 暗启动, A/B测试, 实验, 关闭开关, 熔断器, 模型切换, 基础设施标志, 运行时配置, 功能门
功能标志
概述
功能标志(也称为功能开关或功能门)允许在运行时控制功能可用性,无需代码部署。它们支持渐进式发布、A/B测试、用户定向、紧急关闭开关、ML模型切换和基础设施配置。本技能涵盖实现模式、最佳实践、与LaunchDarkly/Unleash的集成以及标志生命周期管理。
代理
- 高级软件工程师 - 功能标志策略、架构决策、发布规划
- 软件工程师 - 实现功能标志、集成和标志评估逻辑
- 高级软件工程师 - 功能标志安全性、访问控制、审计日志
- 高级软件工程师 - 功能标志基础设施、分布式系统、缓存策略
关键概念
标志类型
布尔标志 - 简单开关:
interface BooleanFlag {
key: string;
enabled: boolean;
description: string;
}
// 用法
if (featureFlags.isEnabled("new-checkout-flow")) {
return <NewCheckoutFlow />;
}
return <LegacyCheckout />;
百分比发布标志 - 渐进式暴露:
interface PercentageFlag {
key: string;
percentage: number; // 0-100
salt: string; // 用于一致性哈希
}
function isEnabledForUser(flag: PercentageFlag, 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;
}
用户定向标志 - 特定用户段:
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;
}
// 示例:为Beta用户和高级计划启用
const flag: TargetedFlag = {
key: "advanced-analytics",
defaultValue: false,
rules: [
{
attribute: "email",
operator: "in",
values: ["beta@example.com"],
value: true,
},
{
attribute: "plan",
operator: "in",
values: ["premium", "enterprise"],
value: true,
},
{ attribute: "country", operator: "in", values: ["US", "CA"], value: true },
],
};
多变量标志 - 用于A/B测试的多个变体:
interface MultivariateFlag<T> {
key: string;
variants: Variant<T>[];
defaultVariant: string;
}
interface Variant<T> {
name: string;
value: T;
weight: number; // 百分比分配
}
// 示例:按钮颜色A/B测试
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 },
],
};
自定义实现
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分钟
constructor(redis: Redis) {
this.redis = redis;
}
async getFlag(key: string): Promise<FeatureFlag | null> {
// 检查本地缓存
const cached = this.cache.get(key);
if (cached && cached.expiresAt > Date.now()) {
return cached.flag;
}
// 从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;
}
// 管理方法
async setFlag(flag: FeatureFlag): Promise<void> {
await this.redis.set(`flag:${flag.key}`, JSON.stringify(flag));
this.cache.delete(flag.key);
// 发布变更以供其他实例使用
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 }),
);
}
}
渐进式发布和金丝雀发布
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);
// 调度自动增量
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 {
// 指数式:每次翻倍
newPercentage = Math.min(current * 2, strategy.targetPercentage);
}
flag.percentage = newPercentage;
await this.flags.setFlag(flag);
// 如果未达到目标则继续
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测试集成
interface Experiment {
id: string;
flagKey: string;
name: string;
hypothesis: string;
metrics: string[]; // 要跟踪的指标
variants: ExperimentVariant[];
status: "draft" | "running" | "paused" | "completed";
startDate?: Date;
endDate?: Date;
sampleSize: number;
confidenceLevel: number; // 例如,0.95
}
interface ExperimentVariant {
name: string;
weight: number;
conversions: number;
impressions: number;
}
class ABTestingService {
async trackExposure(
experimentId: string,
variantName: string,
userId: string,
): Promise<void> {
// 记录用户暴露于变体
await this.analytics.track({
event: "experiment_exposure",
userId,
properties: {
experimentId,
variant: variantName,
timestamp: new Date(),
},
});
// 增量印象计数
await this.redis.hincrby(
`experiment:${experimentId}:${variantName}`,
"impressions",
1,
);
}
async trackConversion(
experimentId: string,
userId: string,
metric: string,
): Promise<void> {
// 获取用户分配的变体
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"),
};
}),
);
// 计算统计显著性
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检验
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;
// 95%置信度的Z分数为1.96
const zThreshold = confidenceLevel === 0.95 ? 1.96 : 2.58; // 99%
return Math.abs(z) > zThreshold;
}
}
关闭开关
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));
// 立即广播到所有实例
await this.redis.publish(
"killswitch-activated",
JSON.stringify(killSwitch),
);
// 报警值班人员
await this.alerting.sendCritical({
title: `关闭开关激活: ${key}`,
message: `原因: ${reason}
激活者: ${activatedBy}
影响: ${killSwitch.affectedServices.join(
", ",
)}`,
});
// 如果配置则调度自动恢复
if (killSwitch.autoRecoveryMinutes) {
setTimeout(
() => this.deactivate(key, "自动恢复"),
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: `关闭开关停用: ${key}`,
message: `原因: ${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;
}
}
// 应用程序代码中的用法
async function processPayment(payment: Payment): Promise<PaymentResult> {
// 首先检查关闭开关
if (await killSwitches.isActive("payments-disabled")) {
throw new ServiceUnavailableError(
"支付处理暂时禁用",
);
}
// 正常处理
return paymentProcessor.process(payment);
}
标志生命周期管理
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);
// 标志过时如果:
// 1. 它比staleAfterDays更旧且仍在开发/测试中
// 2. 它已过计划移除日期
// 3. 它已稳定超过30天(应永久保留或移除)
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 "紧急:已过计划移除日期。移除标志并清理代码。";
}
if (status === "stable") {
return "标志已稳定。要么永久化(移除标志,保留功能)要么弃用。";
}
if (status === "development" || status === "testing") {
return "标志卡在开发中。完成发布或移除如果已放弃。";
}
return "审查标志状态并更新生命周期。";
}
}
LaunchDarkly集成
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,
};
}
// 为实验跟踪自定义事件
function trackConversion(
user: LDUser,
eventKey: string,
data?: Record<string, unknown>,
): void {
ldClient.track(eventKey, user, data);
}
// 客户端端的React钩子
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模型功能标志
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; // 用于百分比路由
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> {
// 获取每个模型的实时性能指标
const metrics = await Promise.all(
flag.modelVariants.map(async (variant) => ({
variant,
latency: await this.getAverageLatency(variant.modelId),
errorRate: await this.getErrorRate(variant.modelId),
})),
);
// 过滤满足性能阈值的模型
const eligible = metrics.filter(
(m) =>
m.latency < flag.performanceThresholds!.latencyMs &&
m.errorRate < flag.performanceThresholds!.errorRate,
);
// 返回最佳性能模型或回退
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> {
// 为基于性能的路由跟踪指标
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,
);
// 跟踪成本
await this.redis.hincrby(
`model:${modelVariant.modelId}:tokens`,
"input",
metrics.inputTokens,
);
await this.redis.hincrby(
`model:${modelVariant.modelId}:tokens`,
"output",
metrics.outputTokens,
);
}
}
// 用法示例
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;
}
}
基础设施功能标志
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; // 更低 = 更高优先级
}
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);
// 获取按优先级排序的健康变体
const healthyVariants = flag.variants
.filter((v) => v.healthy)
.sort((a, b) => a.priority - b.priority);
if (healthyVariants.length === 0) {
if (flag.autoFailover) {
// 使用不健康变体作为最后手段
console.error(`没有健康变体用于 ${flagKey},使用回退`);
return flag.variants.sort((a, b) => a.priority - b.priority)[0];
} else {
throw new Error(`没有可用的健康变体用于 ${flagKey}`);
}
}
return healthyVariants[0];
}
}
// 数据库迁移示例
class DatabaseConnection {
private infraFlags: InfrastructureFlagService;
private currentConnection?: Connection;
async getConnection(): Promise<Connection> {
const variant = await this.infraFlags.getActiveVariant("primary-database");
// 如果配置未更改则重用连接
if (this.currentConnection?.config.host === variant.config.host) {
return this.currentConnection;
}
// 创建新连接到迁移数据库
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迁移示例
class AssetService {
async getAssetUrl(assetPath: string): Promise<string> {
const cdnVariant = await infraFlags.getActiveVariant("cdn-provider");
return `${cdnVariant.config.url}/${assetPath}`;
}
}
标志清理和技术债务管理
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> {
// 扫描代码库以查找标志引用
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) {
// 嵌套条件增加复杂性
if (ref.context.includes("if") && ref.context.includes("else")) {
score += 2;
}
// 同一文件中的多个标志
const otherFlags = this.countOtherFlags(ref.file);
score += otherFlags * 0.5;
// 关键路径中的存在
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);
// 确定清理类型
let type: FlagCleanupTask["type"];
if (flag.percentage === 100 && flag.lifecycle.status === "stable") {
type = "make-permanent"; // 移除标志,保留新代码路径
} else if (flag.percentage === 0) {
type = "remove-flag"; // 移除标志和新代码路径
} else {
type = "deprecate"; // 标记为未来移除
}
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),
});
}
// 按努力排序(最简单优先以快速获胜)
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),
};
}
}
// 自动清理检测
class FlagCleanupMonitor {
async runDailyCleanupCheck(): Promise<void> {
const staleFlags = await this.flagLifecycle.checkStaleFlags();
if (staleFlags.length === 0) return;
// 为清理创建Jira工单
for (const flag of staleFlags) {
const impact = await this.cleanup.analyzeCodeImpact(flag.key);
await this.ticketingSystem.createTicket({
title: `清理功能标志: ${flag.key}`,
description: `
标志状态: ${flag.lifecycle.status}
年龄: ${this.calculateAge(flag.lifecycle.createdAt)} 天
引用: ${impact.totalReferences} 在 ${impact.fileCount} 个文件中
复杂性: ${impact.complexityScore}
建议:
${impact.recommendations.join("
")}
`,
priority:
impact.complexityScore < 5
? "低"
: impact.complexityScore < 15
? "中"
: "高",
labels: ["技术债务", "功能标志", "清理"],
assignee: flag.lifecycle.owner,
});
}
// 每周向工程团队发送摘要
if (new Date().getDay() === 1) {
// 星期一
await this.sendCleanupDigest(staleFlags);
}
}
}
最佳实践
-
标志命名约定
- 使用描述性、一致的名称:
feature-checkout-v2,experiment-button-color - 包含类型前缀:
release-*,experiment-*,ops-*,kill-* - 避免缩写并确保团队范围理解
- 使用描述性、一致的名称:
-
标志卫生
- 为临时标志设置到期日期
- 功能完全发布后移除标志
- 跟踪标志所有权和相关工单
- 定期清理审计(每月)
-
测试
- 测试所有标志状态(开、关、每个变体)
- 在集成测试中包含标志状态
- 测试回滚场景
-
监控
- 跟踪标志评估计数和延迟
- 异常模式报警(突然峰值、失败)
- 记录标志决策以调试
-
文档
- 记录每个标志控制什么
- 包括回滚说明
- 链接到相关PR和工单
示例
React功能标志提供者
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();
// 订阅实时更新
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必须在FeatureFlagProvider内使用");
return context.isEnabled(key);
}
// 用法
function CheckoutPage() {
const newCheckout = useFeatureFlag("new-checkout-flow");
if (newCheckout) {
return <NewCheckoutFlow />;
}
return <LegacyCheckout />;
}
何时使用此技能
使用功能标志技能用于:
- 功能开关 - 运行时开/关功能切换
- 渐进式发布 - 基于百分比或金丝雀发布(1% -> 5% -> 25% -> 100%)
- 暗启动 - 部署代码禁用,准备就绪时启用
- A/B测试 - 比较变体以衡量影响
- 用户定向 - 为特定用户、区域或计划启用功能
- 关闭开关 - 紧急禁用损坏功能
- 熔断器 - 当错误阈值超过时自动禁用
- ML模型标志 - 在模型版本或提供商之间切换
- 基础设施标志 - 迁移数据库、缓存、CDN无需停机
- 配置 - 运行时配置更改无需重新部署
- 清理 - 管理旧标志的技术债务
代理选择指南
| 任务 | 代理 | 为什么 |
|---|---|---|
| 设计标志架构 | 高级软件工程师 | 标志类型、发布策略的战略决策 |
| 实现标志服务 | 软件工程师 | 构建评估逻辑、集成 |
| 审查标志安全 | 高级软件工程师 | 访问控制、审计日志、敏感数据 |
| 扩展标志基础设施 | 高级软件工程师 | 分布式缓存、性能、故障转移 |
| 集成LaunchDarkly/Unleash | 软件工程师 | SDK集成、Webhook设置 |
| 规划ML模型发布 | 高级软件工程师 | 性能路由、回退策略 |
| 实现清理自动化 | 软件工程师 | 代码扫描、影响分析 |
| 设计标志生命周期政策 | 高级软件工程师 | 治理、技术债务预防 |