Tips & Tricks (更新: 2026/6/2)

用 Claude Code 安全实现 Feature Flags:发布、实验与 Kill Switch

用 Claude Code 设计可运营的 feature flags:灰度发布、实验、kill switch、监控与清理。

用 Claude Code 安全实现 Feature Flags:发布、实验与 Kill Switch

先定义运营规则,而不是先写开关

Feature flag,也就是功能开关,是把已经部署的代码按条件打开、关闭或逐步放量的机制。初学者最容易犯的错不是不会写 if (flag),而是把所有开关都当成同一种东西:发布开关、实验开关和 kill switch 的生命周期、负责人、监控指标和清理方式完全不同。

Claude Code 很擅长快速生成 UI 分支,但生产环境需要更多约束:安全默认值、targeting context(用于判断目标用户和环境的上下文)、服务端/客户端边界、曝光日志、护栏指标、回滚步骤,以及短期 flag 的删除日期。Masa 在内容站和小型 SaaS 里踩过的坑是,只看“按钮是否显示”,却没有定义“失败时关什么、看什么数字”。所以 prompt 应该从运营规则开始。

一次资料建议固定看官方文档。OpenFeature 把应用侧 evaluation API 和背后的 provider 分开,并用 evaluation context 传递用户、应用和环境信息。LaunchDarkly 文档清楚区分发布、实验、kill switch 等用法。Unleash 的生命周期从 Define、Develop、Production 到 Cleanup、Archived,强调不要让过期 flag 长期留在代码里。Claude Code 的官方实践也强调给代理明确的验证路径。

参考链接:

不要混淆发布、实验和 Kill Switch

先按寿命分类,再写代码。发布 flag 用来隐藏未完成能力,逐步从内部用户扩到 100%,稳定后就删除。实验 flag 用来验证假设,必须记录曝光和结果指标。Kill switch 是更长期的安全装置,用来在外部 API 故障、成本暴涨、推荐服务变慢或自动化出错时立即关闭相关路径。

场景类型成功指标失败时动作
新 checkout 只给 25% Pro 用户发布支付完成率、支付错误率关闭 checkout_v2_release
比较价格页 CTA 文案实验注册开始率、付费意向点击停止实验并固定 control
把博客 affiliate 区块移入正文实验商品点击、读完率恢复到文章底部
供应商故障时关闭推荐模块Kill switchp95 延迟、5xx 率关闭 recommendations_enabled

如果你做的是内容变现,建议同时阅读 Claude Code A/B 测试Claude Code 分析实现。没有测量的 flag 只是猜测。CTA 不应只看点击率,还要保护 AdSense 质量、读完率、affiliate 收入和咨询意向。需要系统化落地时,可以查看 ClaudeCodeLab 产品咨询方案

最小配置与 Evaluator 模式

一开始不要把业务代码绑死在某个供应商上。应用侧只需要按 key、默认值和 context 进行 evaluation;底层以后可以替换成 LaunchDarkly、Unleash、OpenFeature provider、JSON 文件或内部服务。下面的例子可以保存成 flag-demo.ts,用 npx tsx flag-demo.ts 直接运行。

type FlagValue = boolean | string | number;
type FlagKind = "release" | "experiment" | "kill_switch";
type Plan = "free" | "pro" | "enterprise";
type Role = "user" | "admin";
type Operator = "equals" | "in";

type FlagContext = {
  targetingKey: string;
  plan: Plan;
  country: string;
  role: Role;
  appVersion: string;
};

type FlagRule = {
  attribute: keyof Omit<FlagContext, "targetingKey">;
  operator: Operator;
  values: string[];
  value: FlagValue;
  percentage?: number;
};

type FlagConfig = {
  key: string;
  kind: FlagKind;
  enabled: boolean;
  defaultValue: FlagValue;
  offValue: FlagValue;
  owner: string;
  removeAfter?: string;
  rules: FlagRule[];
};

const registry: Record<string, FlagConfig> = {
  checkout_v2_release: {
    key: "checkout_v2_release",
    kind: "release",
    enabled: true,
    defaultValue: false,
    offValue: false,
    owner: "growth-platform",
    removeAfter: "2026-07-15",
    rules: [
      {
        attribute: "role",
        operator: "equals",
        values: ["admin"],
        value: true,
      },
      {
        attribute: "plan",
        operator: "in",
        values: ["pro", "enterprise"],
        value: true,
        percentage: 25,
      },
    ],
  },
  pricing_copy_2026_06: {
    key: "pricing_copy_2026_06",
    kind: "experiment",
    enabled: true,
    defaultValue: "control",
    offValue: "control",
    owner: "monetization",
    removeAfter: "2026-06-30",
    rules: [
      {
        attribute: "country",
        operator: "in",
        values: ["JP", "US", "DE"],
        value: "simple",
        percentage: 50,
      },
    ],
  },
  recommendations_enabled: {
    key: "recommendations_enabled",
    kind: "kill_switch",
    enabled: true,
    defaultValue: true,
    offValue: false,
    owner: "sre",
    rules: [],
  },
};

function bucketFor(flagKey: string, targetingKey: string): number {
  const input = `${flagKey}:${targetingKey}`;
  let hash = 0;

  for (const char of input) {
    hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
  }

  return hash % 100;
}

function ruleMatches(
  flagKey: string,
  rule: FlagRule,
  context: FlagContext,
): boolean {
  const actual = String(context[rule.attribute]);
  const matched =
    rule.operator === "equals"
      ? actual === rule.values[0]
      : rule.values.includes(actual);

  if (!matched) return false;
  if (rule.percentage === undefined) return true;

  return bucketFor(flagKey, context.targetingKey) < rule.percentage;
}

export function evaluateFlag<T extends FlagValue = FlagValue>(
  key: string,
  context: FlagContext,
): T {
  const flag = registry[key];
  if (!flag) return false as T;
  if (!flag.enabled) return flag.offValue as T;

  for (const rule of flag.rules) {
    if (ruleMatches(flag.key, rule, context)) {
      return rule.value as T;
    }
  }

  return flag.defaultValue as T;
}

const demoContexts: FlagContext[] = [
  {
    targetingKey: "user_001",
    plan: "pro",
    country: "JP",
    role: "user",
    appVersion: "1.8.0",
  },
  {
    targetingKey: "user_002",
    plan: "free",
    country: "BR",
    role: "admin",
    appVersion: "1.8.0",
  },
];

for (const context of demoContexts) {
  console.log(context.targetingKey, {
    checkout: evaluateFlag<boolean>("checkout_v2_release", context),
    pricingCopy: evaluateFlag<string>("pricing_copy_2026_06", context),
    recommendations: evaluateFlag<boolean>(
      "recommendations_enabled",
      context,
    ),
  });
}

这个例子故意保持简单。未知 flag 返回安全 fallback,百分比放量使用稳定的 targetingKey,短期 flag 写明 owner 和 removeAfter。后续你可以把 registry 换成后台配置,但应用侧 contract 不需要变。

服务端 Evaluation 与客户端展示要分开

计费、权限、配额、库存、后端成本相关的判断应该在服务端 evaluation。客户端 flag 适合已授权后的 UI 文案、布局、onboarding 提示或低风险视觉变化。不要把完整 targeting 规则发到浏览器,也不要用“隐藏按钮”代替后端鉴权。

type User = {
  id: string;
  plan: "free" | "pro" | "enterprise";
  role: "user" | "admin";
};

type RequestLike = {
  headers: {
    get(name: string): string | null;
  };
};

export function buildFlagContext(
  user: User,
  request: RequestLike,
): FlagContext {
  return {
    targetingKey: user.id,
    plan: user.plan,
    role: user.role,
    country: request.headers.get("x-country") ?? "US",
    appVersion: process.env.NEXT_PUBLIC_APP_VERSION ?? "dev",
  };
}

export function getServerFlagSnapshot(context: FlagContext) {
  return {
    checkoutV2: evaluateFlag<boolean>("checkout_v2_release", context),
    pricingCopy: evaluateFlag<string>("pricing_copy_2026_06", context),
  };
}
type PricingFlags = {
  pricingCopy: string;
};

export function PricingCta({ flags }: { flags: PricingFlags }) {
  const label =
    flags.pricingCopy === "simple"
      ? "从免费计划开始"
      : "开始免费试用";

  return <a href="/signup">{label}</a>;
}

这样 React 组件只负责展示服务端已经算好的 snapshot。给 Claude Code 的 prompt 里要写清楚:权限和计费必须在服务端,客户端只能消费预先 evaluation 的结果。

灰度发布必须配监控

安全 rollout 不是“从 1% 开始”这么简单,而是要有放量计划、指标和回滚阈值。Unleash 的 gradual rollout 会结合百分比、stickiness 和约束。LaunchDarkly guarded rollout 会把发布和指标连接起来,在指标回退时暂停或回滚。即使你用自建 evaluator,也应该复制这种运营模型。

至少跟踪三层数据:曝光、主要指标、护栏指标。曝光回答“谁看到了哪个 flag 值”。主要指标回答“目标行为是否改善”。护栏指标回答“速度、错误、收入质量、支持压力或用户信任是否受损”。

type FlagExposure = {
  flagKey: string;
  value: FlagValue;
  targetingKey: string;
  route: string;
  evaluatedAt: string;
};

export function trackFlagExposure(event: FlagExposure) {
  console.log(
    JSON.stringify({
      event_name: "feature_flag_exposure",
      ...event,
    }),
  );
}

Checkout 要同时看 5xx、支付失败和支持工单。内容站不能只看 affiliate 点击,还要看读完率、跳出率、Core Web Vitals 和付费咨询点击。AI 功能要看 token 成本、p95 延迟和每用户配额。一个提高点击却降低购买质量的 flag,不应该算成功。

具体失败例

第一种是每次加载都随机分配。用户刷新后从 A 变成 B,曝光和转化数据都会坏掉。必须用稳定 targeting key 做 bucket。

第二种是只在客户端隐藏付费功能。React 里没有按钮,不代表后端 API 被保护。Feature flag 可以改善体验,但不是鉴权系统。

第三种是不安全默认值。未定义的发布 flag 通常应该返回 false。如果拼写错误返回 true,就等于意外全量发布。

第四种是过期 flag 不删除。半年后没人知道 checkout_v2_release 是什么逻辑。实验结束或发布完成后,要进入 cleanup 并删掉分支。

第五种是规则嵌套太深。父 flag、子 flag、重叠百分比会让团队说不清实际触达比例。依赖关系越少越容易运营。

给 Claude Code 的安全 Prompt

Claude Code 可以读文件、改代码、跑测试,所以要提前给出边界和验证命令。

请给这个仓库添加 feature flag 工作流。
第一个 flag 是 checkout_v2_release,用于灰度发布。

约束:
- 计费和权限相关 flag 必须在服务端 evaluation
- 未知发布 flag 必须返回 false
- 百分比 rollout 必须使用稳定 targetingKey
- registry 必须包含 owner 和 removeAfter
- 不要修改无关文件

需要输出:
- 最小 flag registry 和 evaluateFlag 函数
- 曝光事件类型
- 至少 3 个产品场景
- 失败例和回滚步骤
- 实际运行的测试命令

合并前再用 review prompt:

请 review 这个 feature flag 实现。
重点检查默认值、服务端/客户端边界、稳定分桶、
曝光事件遗漏、cleanup 日期和回滚行为。
按严重程度列出问题,并指出具体文件。

这样 Claude Code 更容易输出可运营的设计,而不是只加一个 UI toggle。

清理也是上线的一部分

每个 flag 从创建那天开始就会变成维护成本。发布 flag 在全量后删除,实验 flag 在选出胜者后删除,kill switch 可以保留但必须有负责人、runbook 和告警。把 ownerremoveAfter、监控指标和计划删除 PR 写进 PR 模板,后续维护会轻很多。

本文的 evaluator 已按 TypeScript demo 形式验证:同一个 targetingKey 会进入同一个 bucket,未知 flag 返回安全 fallback,kill switch 有明确 off value。Masa 的实践经验是,变现路径上的 flag 不能只看点击,还要同时看阅读质量和付费意向。先落地一个发布 flag、一个实验 flag、一个 kill switch,再考虑完整平台。

#Claude Code #Feature Flags #灰度发布 #TypeScript #可观测性
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。