功能标志管理Skill feature-flags

功能标志管理技能用于实现运行时功能控制,无需代码部署即可管理功能发布、A/B测试、用户定向和紧急开关。支持渐进式滚动发布、金丝雀发布、多变量实验、ML模型切换和基础设施配置。关键词包括:功能标志、功能开关、A/B测试、滚动发布、LaunchDarkly、Unleash、DevOps、发布管理、代码部署、运行时配置。

DevOps 0 次安装 0 次浏览 更新于 3/24/2026

名称: 功能标志 描述: 用于控制发布、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);
    }
  }
}

最佳实践

  1. 标志命名约定

    • 使用描述性、一致的名称:feature-checkout-v2, experiment-button-color
    • 包含类型前缀:release-*, experiment-*, ops-*, kill-*
    • 避免缩写并确保团队范围理解
  2. 标志卫生

    • 为临时标志设置到期日期
    • 功能完全发布后移除标志
    • 跟踪标志所有权和相关工单
    • 定期清理审计(每月)
  3. 测试

    • 测试所有标志状态(开、关、每个变体)
    • 在集成测试中包含标志状态
    • 测试回滚场景
  4. 监控

    • 跟踪标志评估计数和延迟
    • 异常模式报警(突然峰值、失败)
    • 记录标志决策以调试
  5. 文档

    • 记录每个标志控制什么
    • 包括回滚说明
    • 链接到相关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模型发布 高级软件工程师 性能路由、回退策略
实现清理自动化 软件工程师 代码扫描、影响分析
设计标志生命周期政策 高级软件工程师 治理、技术债务预防