kubell Creator's Note

ビジネスチャット「Chatwork」のエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

FeatureToggleの運用を複雑にしないためにeslintのカスタムルールを活用する

qiita.com これはChatwork Advent Calendar 2023の18日目の記事です。

こんにちはChatworkのフロントエンド開発部にてWebFrontend領域を担当している末竹(magcho)🐧です

先日のChatwork Product Day 2023では「620万*を超えるIDを持つサービスで、本番環境のユーザーに気づかれることなく安全にjQueryからReactに移行するために行っていること」と題しまして実際に業務内で行なっている安全な開発の工夫についてお話しさせていただきました。

セッションの中でもFeatureToggleについてお話しさせていただきました。今回はそのFeatureToggleについてもう少し込み入った話をしたいと思います。

FeatureToggleとは

FeatureToggleとは設定した条件下のみでTrueになる変数を用いて実行されるコードを分岐する仕組みです。名称については様々な呼び方がありFeatureFlag, FeatureSwitchなどと呼ばれることもあります。実装のマージ・リリースタイミングと機能の有効化タイミングを独立させ小さい範囲から検証・リリースを可能にする仕組みです。

以下のコードのように実ユーザーにはoldModalを、先行して一部のユーザーにだけnewModalを表示するといったことが可能です。

const featureToggle = {
  xxFlag: calculateToggle({
    enableEnvironment:['local','preview','beta']
  }),
}

// ~~~~
if(featureToggle.xxFlag){
  newModal()
}else{
  oldModal()
}

戦略的なリリースを行うことができるためとても便利ですが、その反面運用上での難点もあります。運用していると幾つもの検証が同時に行われfeatureToggleが何種類も存在し得ります。その中でflagを入れ子にしてしまうと機能を有効化するためには2つ以上のフラグの組み合わせの考慮が必要になり、組み合わせは4通りあり、さらにネストが深くなると…深く考えたくもありません。解決にはいろいろなアプローチがあると思いますが。ここではとにかくそんなことに頭を悩ませる必要がないように今回は禁止するアプローチをとってみましょう。

FeatureToggleのネストを禁止する

jsのコードに規約を設ける上で手っ取り早いのはeslintのカスタムルールを作成することです。今回はあるオブジェクトのプロパティーをifの判定に用いている箇所がネストしていないかどうかを確認するルールを作成してみました。

module.exports = {
  meta: {
    type: "problem",
    docs: {
      description:
        "Enforce a maximum nested feature toggles allowed in a program",
    },
  },
  create: function (context) {
    const ifStatement = Symbol("not feature toggle if statement");
    const featureFlagIfStatement = Symbol("feature toggle if statement");
    let ifStatementStack = [];

    return {
      IfStatement: function (node) {
        if (
          node.test.type === "MemberExpression" &&
          node.test.object.name === "featureToggles"
        ) {
          ifStatementStack.push(featureFlagIfStatement);
          if (
            ifStatementStack.filter((i) => i === featureFlagIfStatement)
              .length >= 2
          ) {
            context.report({
              node,
              message: "Avoid deep nesting of feature flags conditions",
            });
          }
        } else {
          ifStatementStack.push(ifStatement);
        }
      },
      "IfStatement:exit": function () {
        ifStatementStack.pop();
      },
    };
  },
};

このルールを用いると以下のコードについて

export const featureToggles = {
  "feature-1": true,
  "feature-2": false,
};

function main() {
  if (featureToggles["feature-1"]) {
    console.log("feature-1 is enabled");

    if (featureToggles["feature-2"]) {
      console.log("feature-2 is enabled");
    }
  }
}
main();

以下のようなエラーが出力されるようになりました、このルールがあれば2段階以上のネストを禁止することができます。めでたしめでたし。

$ eslint --rulesdir eslint-rules index.js

//path/to/index.js
  10:5  error  Avoid deep nesting of feature flags conditions  check-nested-featuretoggles

✖ 1 problem (1 error, 0 warnings)

おまけ

この記事を書くためにeslintのドキュメントを見ているとeslint cliの--rulesdirオプションは非推奨になっていることを知りました。

Deprecated: Use rules from plugins instead.

ref: https://eslint.org/docs/latest/use/command-line-interface#--rulesdir

の記載通りflat configにてconfigファイルに上記のruleの実装をimportしてpluginとしてeslintに渡してあげることで利用できるようです。eslintのflat config対応が終わったらこちらに移行しようと思います。