これは エムスリー Advent Calendar 2023 の3日目の記事です。 前日は三浦さん(@yuba)による「9時間足すんだっけ引くんだっけ問題~あるいは、諸プログラミング言語はいかにタイムゾーンと向き合っているか」でした。
こんにちは、エムスリーエンジニアリンググループ・マルチデバイスチームの藤原です。
マルチデバイスチームでは複数のスマートフォンアプリを開発しており、新機能の追加やレイアウト変更をする際はA/Bテストをすることもしばしばです。 今回は弊チームで採用しているA/Bテストの実装方法を2通り紹介します。
スマートフォンアプリのA/Bテスト
A/Bテストとは、特定の要素を変更したAパターンとBパターンをランダムにユーザグループに提供しどちらが優れているか比較する手法です。
ベースとなるパターンを提供するグループをコントロールグループ、変更を加えたパターンを提供するグループをテストグループと呼ぶこともあります。
弊チームで開発しているアプリは会員向けということもあり、A/Bテストを実施するときのユーザグループの分け方は、通常は会員のIDをハッシュ関数に通した値を用います。そのため、A/Bテストの振り分け部分についてはサードパーティのツールなどは使用せずに独自に実装しています。
また、スマートフォンアプリで新しく開発した機能をユーザに届けるためには、新しい機能を含むアプリをリリースしユーザがアップデートする形で変更を反映するのが一般的です。 強制的なアップデートも可能ではありますが、ユーザの体験を損なう事も多く、ちょっとした工夫で回避できるならそうしておきたいところです。 A/Bテストの分析結果からより良い体験を素早く提供するために、マルチデバイスチームではアプリの更新を必要としないサーバサイドを含めた仕組みを整えています。
Remote Configを用いた実装例
1つ目の例としてFirebase Remote Configを使用した実装を紹介します。 Firebase Remote ConfigとはFirebaseの機能の1つで、簡易的なKey-Valueストアとして使うことができます。 Remote ConfigにA/Bテストの設定を保存しておけば、アプリから適当なタイミングで設定値を取得し、A/Bテストの振り分けを行うことができるというわけです。
Remote Configでの設定は以下のようなハッシュ関数に用いるシード値と、グループを分割する閾値などを持たせています。 Remote Config自体にもパーセンテージを指定してランダムにクライアントに値を返すような機能がありますが、振り分けロジックは自前で実装しているという事情があるためその機能は使いません。
{ "seed": 42, "thresholdPercentage": 50, ... }
クライアントは受け取った値を元にどちらのユーザグループに振り分けるか計算します。 下記はクライアント側の実装イメージです。
val hashValue = calcHashValue(seed, userId) % 100 if (hashValue < thresholdPercentage) { // テストグループの実装 } else { // コントロールグループの実装 }
このような準備をしておけば、A/Bテストの結果が出て全ユーザに展開したいとなった際は閾値の設定を変更するだけで済みます。
{ "seed": 42, "thresholdPercentage": 100, ... }
GraphQLを用いた実装例
2つ目の例として、アプリケーションサーバにGraphQLを採用しているスマートフォンアプリの例を紹介します。 GraphQLとはAPIのためのクエリ言語で、クライアントが必要なデータを自由に選択できるという特徴があります。
A/BテストのためのGraphQLスキーマを準備します。認証済みのクライアントが発行できるクエリを以下のように定義しておきます。
# A/Bテストのグループ enum ABTestingGroup { CTRL, TEST } # A/Bテストプロジェクト type ABTestingProject { projectId: String! abTestingGroup: ABTestingGroup! } query { # パラメータで指定したプロジェクトIDのA/Bテストプロジェクトを返す abTestingProjects( # プロジェクトIDの配列 projectIds: [String!]!, ): [ABTestingProject!] }
abTestingProjects
に対応するリゾルバはプロジェクトIDをパラメータに取ってアクセスしたユーザがテストグループかコントロールグループかを返すようなものを実装します。
A/Bテストを実施するクライアントはテスト対象のコンテンツ(この例では記事一覧)と一緒に、あらかじめ準備しておいたA/Bテストのグループを返すフィールドを取得します。
query { # 記事一覧を取得する articles { articleId title thumbnailUrl } # A/Bテストのグループがどちらかも一緒に取得する abTestingProjects(projectIds: ["articles_layout"]) { projectId abTestingGroup } }
こうすることで記事一覧の情報と一緒にA/Bテストに関する情報も取得できます。
{ "data" { "articles": [ { "articleId": 1, "title": ... }, { "articleId": 2, "title": ... }, { "articleId": 3, "title": ... } ], "abTestingProjects": [ { "projectId": "articles_layout", "abTestingGroup": "TEST" } ] } }
あとはサーバから取得した情報を元にクライアントで分岐させるのみです。
GraphQLで実装してみてちょっとした感動があった
Remote Configを用いた実装では、通信環境の悪いユーザの場合にはコンテンツの情報(例では記事一覧)だけ取得できてA/Bテストの振り分けの情報は取得できないような状況がありえます。 そのような場合はデフォルトのパターンを決めてそちらを表示するか、もしくはコンテンツの情報も取得できていないことにすると思いますが、GraphQLの場合は必ず同時に情報を受け取ることができるため考えることが少なくてすみます。
また、A/Bテストでアプリケーションサーバでも動作を振り分けたいとなったとき、Remote Configに設定がある場合は同様の設定をアプリケーションサーバ側にも置くか、もしくは設定をどうにかアプリケーションサーバに連携する必要があり、どちらもシステムを少し複雑にしてしまいますが、GraphQLの場合は設定を置く場所はアプリケーションサーバの一箇所でよく、シンプルさを保つこともできました。
クライアントサイドはA/Bテストが終了すればクエリからA/Bテストに関するフィールド(上記の例では abTestingProjects
)を削除すればよく、違うA/Bテストを開始するときは異なるパラメータで同じフィールドを再利用できます。
サーバサイドもコンテンツのリゾルバとA/Bテストのリゾルバを完全に分けて管理できていい感じです。
GraphQLの「クライアントが必要なデータを取得できる」という特徴をそのまま使ったまでなのですが、A/Bテストのユースケースでぴったりはまったのはちょっとした感動がありました。
以上、スマートフォンアプリのA/Bテスト実装例の紹介でした。
We are hiring!!
マルチデバイスチームはアプリ・サーバサイドどちらの開発にも携われるような環境となっています。 一緒に開発してくれる仲間を募集中です。お気軽にお問い合わせください。