こんにちは@hatuyuki4です。この記事はロコガイド Advent Calendar6日目です。健康診断結果がすこぶる悪かったので、お魚中心の食生活を送っています。
トクバイでは新機能をリリースしたり、既存機能を改善したりする際に、A/Bテストをしながらユーザに公開することが多いです。今回はモバイルアプリでスムーズにA/Bテストを行うために、実施しているtipsについて書きたいと思います。
モバイルアプリでのA/Bテスト
モバイルアプリでA/Bテストを行おうと思ったら、まずFirebaseの A/B Testing
の利用を検討すると思います。しかし A/B Testing
は登場して以来β版が続いていますし(2020年12月現在)、計測まで時間がかかることもあり、なかなかスピーディな検証をするのには向いていません。ただし、数日後の定着率を自前で計測するにはログ計測基盤を自前で用意する必要があるため、その辺りのコストとのバランスを考えると有効なツールともいえます。
そのためトクバイでは、アプリ側に一工夫入れることでシンプルで使いやすいA/Bテスト機能を実装しています。
シンプルなA/Bテスト
トクバイでは A/B Testing
ではなく通常の Firebase RemoteConfig
を利用してA/Bテストを行っています。通常RemoteConfig
でのA/BテストはParameterが際限なく増えていったり、Conditionsの管理が煩雑になったりと難点が多いのですが、アプリ側にも実装を加えることで、シンプルで扱いやすいA/Bテストを実現できます。
1. Remote Config上にA/Bテストの条件だけをjsonで記載する 2. 1つのjsonに複数のTestGroup(テスト条件)を作成する 3. アプリ内で疑似乱数を生成し、保存する 4. Remote Configから取得したTestGroupの条件から、アプリでパターンの出し分けを行う
RemoteConfig実装例
RemoteConfigでは1つのParameterに辞書型の配列を用意しておきます。title
は行うA/Bテストを識別するためのもの、scope
は対象ユーザの設定(後述)、targets
にA/Bテストで出し分ける条件を書きます。targets
内のrate
は合計で100になるように記述します。これがそのまま何%のユーザに公開するのかという値になります。また合計が100であれば3つ以上同時にテストを行えます。
RemoteConfigのConditions
では公開範囲をいじらず、全ユーザに同じ配列を渡します。これにより、Parameterも1つですみ、RemoteConfig側の設定が煩雑になることを防げます。
[ { "title": "map_search", "scope": "all", "targets": [ { "target": "A", "rate": 50 }, { "target": "B", "rate": 50 } ] } ]
アプリ実装例
RemoteConfigからJsonを受け取ったら、アプリ側では1つのA/BテストをABTestingGroup
、 A/Bテストの掲出条件をABTestingTarget
という構造体に変換します。Scope
には現在all
とnew_user
のみを受け付けているので、この2つの値を定義した列挙型で持たせておいてます。(Scope
の利用については後述します)。
struct ABTestingGroup: Codable { enum Scope: String, Codable { case all case newUser = "new_user" } let title: String let scope: Scope let targets: [ABTestingTarget] // 1~100の値がどのターゲットに属するのか返す func testingTarget(random: Int) -> String? { var targetCount = 0 for testingTarget in targets { targetCount += testingTarget.rate if random <= targetCount { return testingTarget.target } } return nil } } struct ABTestingTarget: Codable { let target: String let rate: Int }
そして取得できたABTestingGroup
ごとに 1~100までの乱数を生成し、それをabtesting_\(title)
という名前でUserDefaultsに保存します(title
は利用したユーザグループから取ってきます)。そう、RemoteConfig側で掲出制御をするのではなく、アプリ側で振り分けるようにしているのですね。
乱数を保存したら、その乱数がTestTarget
のどのスコープに収まるのかチェックして、出し分けを判定するという流れになります。
// Testing Groupごとに1~100までのランダムな値を保存する private func setRandomValue(_ title: String) { setValue(title, value: Int.random(in: 1...100)) } // Testing Groupのtitleを使って値を保存する func setValue(_ title: String, value: Int) { UserDefaults.standard.set(value, forKey: "abtesting_\(title)") } // Testing Groupのtitleを使って値を取得する func randomValue(_ title: String) -> Int { return UserDefaults.standard.integer(forKey: "abtesting_\(title)") } // どのターゲットかどうか判定する func isTarget(_ title: String, target: String) -> Bool { guard let testingGroup = testingGroups.first { $0.title == title } let value = randomValue(title) guard let testingTarget = searchGroup(by: title)?.testingTarget(random: randomValue(title)) else { return false } return testingTarget.target.rawValue == target.rawValue }
Advance!
デバッグ機能
アプリの検証をしていると、A/Bテストの各パターンを素早く切り替えて動作確認したくなると思います。今回ご紹介した例では、A/Bの判定ロジックをRemoteConfig
やバックエンドではなくアプリ内部に実装しているため、パターンの切り替えもアプリ内のUserDefaultsの値を変更するだけでできてしまいます。
そのためトクバイアプリの開発メニューにもパターン切り替え機能が実装されており、誰でもすべてのパターンを素早くチェックできるようになっています。
(UserDefautlsの値を0~100の値に動かすだけなので、Sliderを利用しています)
新規ユーザ限定リリース
時には、A/Bテスト内容を既存ユーザに提示したくない場合があります。例えばオンボーディングに関する施策であるとか、ドラスティックな変更であるとかを、新規にインストールしたユーザにのみ提示したい時などです。そのため先述の scope
を使い、アプリ内で新規ユーザかどうかを判定し、その50%にだけ公開するという機能も実装しています。これはRemote ConfigのConditions
だけでは出し分けできませんね。
ちなみに、新規ユーザかどうかというのは、ユーザがアプリを初めて起動した時のバージョンを内部で保持し、それと現在のバージョンとを比較しています。これにより大掛かりな検証も、ためらわず行うことができます。
まとめ
新しい機能をリリースするとき、それがユーザにとっていいものかどうかは実際に出してみないとわからないのが実情です。そんな中でやるべきことは、実際にA/Bテストを行って、仮説が正しいのか間違っていたのか考察するサイクルを可能な限り早めることです。
ロコガイドでは今後も、ユーザが使いやすくなるようにA/Bテストなどを繰り返しながら、よりよいアプリをリリースしていきたいと思います。