リモート設定に最初のフラグを入れた日のことは、よく覚えています。新しいオンボーディング画面を一部のユーザーにだけ出すための new_onboarding というキーでした。便利でした。あまりに便利だったので、半年後には30個近いフラグが並び、test2 や enable_v3_real のような、もう誰も意味を説明できないキーがいくつも残っていました。
フィーチャーフラグそのものは強力です。問題は、増やすのが簡単で消すのが面倒という非対称性にあります。6本のアプリで同じことが同時に起きると、設定画面はあっという間に荒れ地になりました。ここでは、その荒れ地を作らないために決めた統治の仕組みを、実際の命名規約と段階公開・キルスイッチの実装とともに共有します。
命名規約 — 半年後の自分が読める形に
最初に決めたのは命名規約です。フラグ名から「どのアプリの」「何の」「いつまでの」フラグかが読めるようにしました。
<scope>.<domain>.<name>.<kind>
例:
wallpaper.onboarding.skip_tutorial.release # 全アプリ共通・恒久
fortune.paywall.intro_offer_v2.experiment # 占い系のみ・実験
all.admob.interstitial_enabled.ops # 全アプリ・運用スイッチ
末尾の kind が肝です。フラグの寿命を3種類に分けています。release は新機能を段階的に出すための一時的なもの、experiment はA/Bテスト用でテスト終了とともに消すもの、ops はキルスイッチのように恒久的に残す運用スイッチです。この分類があると、後述する棚卸しのときに「これは消していいのか」を機械的に判断できます。
scope でアプリ群を指定できるようにしたのは、6本のうち占い系3本にだけ効かせたい設定が頻繁にあったからです。all は全アプリ、それ以外はアプリグループIDで絞ります。
デフォルトは必ず安全側へ
リモート設定の最大の落とし穴は、通信に失敗したときの挙動です。起動直後やオフラインでは、フラグの値がまだ取れていません。ここで「フラグが取れていない=機能ON」と書いてしまうと、まだ検証中の機能が全ユーザーに漏れます。
原則として、フラグが取れないときは「これまでどおりの安全な状態」に倒れるようにします。新機能のフラグなら既定はOFF、機能を止めるための運用スイッチなら既定はON(=止めない)です。
import remoteConfig from '@react-native-firebase/remote-config';
// 取得できないときに倒す先を、フラグごとに明示する
const DEFAULTS = {
'wallpaper.onboarding.skip_tutorial.release': false, // 新機能 → 既定OFF
'all.admob.interstitial_enabled.ops': true, // 運用 → 既定は止めない
} as const;
export async function initFlags(): Promise<void> {
await remoteConfig().setDefaults(DEFAULTS);
await remoteConfig().setConfigSettings({
minimumFetchIntervalMillis: 60 * 60 * 1000, // 通常は1時間キャッシュ
});
// 失敗しても DEFAULTS が効くので、ここでは握りつぶさず記録だけ残す
try {
await remoteConfig().fetchAndActivate();
} catch (e) {
logFlagFetchError(e);
}
}
export function flag(key: keyof typeof DEFAULTS): boolean {
return remoteConfig().getValue(key).asBoolean();
}setDefaults にすべてのフラグの安全側の値を明示しておくのが要点です。これを書いておけば、サーバーに一度も到達できていない端末でも、必ず定義済みの安全な挙動になります。私はこの「既定値の一覧」をコードのなかで唯一の真実として扱い、新しいフラグを足すときは必ずここに既定を書いてからリモート側に作る、という順番を守っています。