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

Claude Codeでフィーチャーフラグを安全に実装する実践ガイド

Claude Codeでフィーチャーフラグを安全に設計・実装する実践ガイド。ロールアウト、計測、失敗例まで解説。

Claude Codeでフィーチャーフラグを安全に実装する実践ガイド

最初に決めるのはスイッチではなく運用ルール

フィーチャーフラグは、デプロイ済みのコードをあとから有効化・無効化するための「実行時スイッチ」です。初心者が最初につまずくのは、if (flag) を入れることではありません。リリース用なのか、実験用なのか、障害時の緊急停止なのかを分けずに作り、誰がいつ消すのかも決めないことです。

Claude Codeに「フィーチャーフラグを実装して」とだけ頼むと、UIの表示切り替えはすぐ出ます。しかし本番運用では、ターゲティング、デフォルト値、サーバー評価とクライアント評価の境界、露出ログ、メトリクス、クリーンアップ期限まで同時に必要です。Masaがコンテンツサイトや小さなSaaSを触るときに一番効いたのは、コードより先に「失敗したとき何を止めるか」を書くことでした。

公式ドキュメントでも、OpenFeatureは評価APIとプロバイダーを分け、評価コンテキストでユーザーや環境の情報を渡す考え方を示しています。LaunchDarklyはリリースフラグ、実験フラグ、キルスイッチなどの使い分けを整理しています。UnleashはDefine、Develop、Production、Cleanup、Archivedというライフサイクルを示し、古いフラグを残さない運用を重視しています。

参考にする一次情報:

リリース、実験、キルスイッチを混ぜない

フラグは名前より寿命で分けると設計しやすくなります。リリースフラグは未完成機能を隠し、段階的に100%へ広げ、安定したら削除します。実験フラグはA/Bテストの仮説検証に使い、露出イベントと成果指標が必須です。キルスイッチは外部API障害、重い推薦処理、課金事故などを即座に止める長寿命の安全装置です。

ユースケース種類成功指標失敗時の操作
SaaSの新チェックアウトをProプランの25%へ公開リリース決済完了率、エラー率checkout_v2_release をoffに戻す
価格ページのCTA文言を比較実験無料登録開始率、有料意向クリック実験を停止しcontrolへ固定
ブログのアフィリエイト枠を記事下から本文中へ移動実験商品リンククリック率、読了率本文中表示を停止
レコメンドAPIが遅いとき推薦枠を消すキルスイッチp95応答時間、5xx率recommendations_enabled をoff

ブログ収益化の文脈では、関連してA/Bテスト実装アナリティクス実装も先に読んでおくと、フラグの効果を数字で判断しやすくなります。チーム導入やレビュー体制まで整えたい場合はClaude Code導入相談、個人で始めるなら教材一覧へ自然につなげられます。

最小のフラグ設定と評価器

最初の実装はベンダー依存にしすぎない方が安全です。OpenFeatureの考え方と同じく、アプリ側は「キー、デフォルト値、評価コンテキスト」を扱い、あとでLaunchDarkly、Unleash、自作JSON、環境変数へ差し替えられる形にします。次の例は1ファイルで動く最小パターンです。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,
    ),
  });
}

本番ではこのregistryを管理画面やJSONファイルから読みます。重要なのは、フラグが見つからないときに安全なデフォルトへ倒すこと、割合ロールアウトに安定したtargetingKeyを使うこと、所有者と削除期限を設定に含めることです。

サーバー評価とクライアント評価を分ける

課金、権限、在庫、API呼び出し量のように悪用されると困る判定はサーバーで評価します。クライアント評価は、すでに許可済みのUI表示、文言、低リスクなレイアウト切り替えに限定します。LaunchDarklyやUnleashのドキュメントでも、コンテキスト属性やターゲティングルールを正しく渡すことが前提です。ブラウザに秘密の条件や全ルールを丸ごと渡す設計は避けます。

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側は受け取ったスナップショットを表示するだけです。Claude Codeに依頼するときも「権限判定はサーバー、UI文言だけクライアント」と明記すると、危険な実装をかなり減らせます。

段階的ロールアウトと監視をセットにする

安全なロールアウトは、1%から始めることではなく、増やす条件と戻す条件が決まっている状態です。Unleashの段階的ロールアウトは割合、stickiness、制約を組み合わせます。LaunchDarklyのguarded rolloutはメトリクスを見ながら回帰時に停止や通知を行う発想です。小さな自作実装でも同じ考え方を真似できます。

最低限、露出ログ、主要指標、ガードレール指標を分けます。露出ログは「誰がどのフラグ値を見たか」、主要指標は「狙った行動が増えたか」、ガードレールは「速度、エラー、収益品質が悪化していないか」です。

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,
    }),
  );
}

運用メモとしては、チェックアウトなら5xx率、決済失敗率、問い合わせ件数を同時に見ます。ブログの収益枠ならクリック率だけでなく読了率、直帰、LCPも見ます。AI機能の解放ならトークン費用、レイテンシ、ユーザーあたり上限を見ます。単一の「クリックが増えた」だけで勝ち判定を出すと、AdSenseやアフィリエイトの長期収益を壊すことがあります。

具体的な失敗例と落とし穴

1つ目は、毎回ランダムに割り当てる実験です。ページを更新するたびにA/Bが変わると、露出ログもコンバージョンも信用できません。targetingKeyで安定したbucketを作ります。

2つ目は、クライアントだけで有料機能を隠す実装です。ボタンを非表示にしてもAPIが開いていれば権限制御になりません。フラグはUXのスイッチであって、認可の代替ではありません。

3つ目は、defaultValue: trueのまま新機能を公開することです。フラグサービス障害やキー名のtypoで全員に新機能が出ます。未検証のリリースフラグはfalseへ倒し、キルスイッチはoff値を明示します。

4つ目は、期限切れフラグを残すことです。削除されないcheckout_v2_releaseは、半年後に誰も意味を知らない分岐になります。Unleashのstale stateやライフサイクルのように、Cleanupへ進んだらPRで分岐を消します。

5つ目は、親子フラグや複数ルールを重ねすぎることです。50%の親と50%の子があると、実際に何%へ出ているか説明しにくくなります。LaunchDarklyやUnleashのベストプラクティスでも、依存関係は単純に保つ方が安全です。

Claude Codeへの安全な依頼文

Claude Codeはファイルを読んで変更し、テストも実行できます。Anthropicのベストプラクティスにもある通り、検証方法を与えると成果物の質が上がります。次のように、所有範囲、失敗時の安全側、確認コマンドを含めて依頼します。

このリポジトリにフィーチャーフラグを追加してください。
目的はcheckout_v2_releaseの段階的ロールアウトです。

制約:
- 権限と課金に関わる判定はサーバー側で評価する
- 未定義フラグは安全側のfalseへ倒す
- targetingKeyで安定した割合割り当てにする
- ownerとremoveAfterを設定に含める
- unrelated filesは変更しない

必要な出力:
- 最小のflag registryとevaluateFlag関数
- 露出ログのイベント型
- 3つ以上のユースケース
- 失敗例とロールバック手順
- 実行したテストコマンド

レビュー依頼はさらに具体化します。

フィーチャーフラグ実装をレビューしてください。
観点は、デフォルト値、サーバー/クライアント境界、
targetingKeyの安定性、計測漏れ、cleanup期限です。
重大度順に指摘し、修正が必要なファイルだけ提案してください。

この2つを使うだけでも、Claude Codeが単なるUI分岐ではなく、運用できるフラグ設計へ寄りやすくなります。

クリーンアップまでが実装

フラグは作った瞬間から負債化が始まります。リリースフラグは100%公開後に削除、実験フラグは判定後に勝者へ固定して削除、キルスイッチだけは運用手順と監視に残す、という線引きをします。PRテンプレートに「removeAfter」「owner」「監視指標」「削除PRの予定」を入れると、半年後の読み手が助かります。

この記事で紹介した内容は、TypeScriptの最小評価器として実行できる形で確認しました。特にtargetingKeyを変えない限り同じユーザーが同じbucketに入り、未知のフラグは安全側に倒れることを手元で見ています。Masaの運営メモとしては、収益導線を触るフラグほど、クリック率だけでなく読了率と有料意向クリックを同時に見るのが実用的でした。実装を自分のプロダクトへ落とす前に、まず1つのリリースフラグ、1つの実験フラグ、1つのキルスイッチだけで小さく始めるのがおすすめです。

#Claude Code #フィーチャーフラグ #安全なリリース #TypeScript #計測
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。