А/В-тесты на Android от А до Я
Большая часть статей об A/B-тестах посвящена веб-разработке, и несмотря на актуальность этого инструмента и для других платформ, мобильная разработка несправедливо остаётся в стороне. Мы попытаемся эту несправедливость устранить, описав основные шаги и раскрыв особенности реализации и проведения A/B-тестов на мобильных платформах.
Концепция A/B-тестирования
A/B-тест нужен для проверки гипотез, направленных на улучшение ключевых метрик приложения. В простейшем случае пользователи делятся на 2 группы контрольную (A) и экспериментальную (B). Фича, реализующая гипотезу, раскатывается только на экспериментальную группу. Далее на основе сравнительного анализа показателей метрики для каждой из групп делается вывод о релевантности фичи.
Реализация
1. Делим пользователей на группы
Для начала нам необходимо понять, как мы будем делить пользователей на группы в нужном процентном соотношении с возможностью динамически его менять. Такая возможность будет особенно полезна, если вдруг выяснится, что новая фича повышает конверсию на 146%, а раскатана, например, всего на 5% пользователей! Наверняка нам захочется выкатить её на всех пользователей и прямо сейчас — без обновления приложений в сторе и сопутствующих временных издержек.
Конечно, можно организовать разбивку на сервере и каждый раз при необходимости что-то менять дёргать backend-разработчиков. Но в реальной жизни бэк зачастую разрабатывается на стороне заказчика или третьей компанией, и у серверных разработчиков и так хватает дел, поэтому оперативно регулировать разбивку, работая с третьими лицами, удаётся не всегда, а точнее, почти никогда, поэтому такой вариант нам не подходит. И тут на помощь приходит Firebase Remote Config!
В Firebase Console, в группе Grow есть вкладка Remote Config, где вы можете создать свой конфиг, который Firebase доставит пользователям вашего приложения.
Конфиг представляет собой мапу <ключ параметра, значение параметра> с возможностью присваивать значение параметра по условию. Например, пользователям с конкретной версией приложения значение X, всем остальным — Y. Более подробно о конфиге можно узнать в соответствующем разделе документации.
Также в группе Grow есть вкладка A/B Testing. Здесь мы можем запускать тесты со всеми вышеописанными плюшками. В качестве параметров используются ключи из нашего Remote Config. В теории можно прямо в A/B-тесте создать новые параметры, но это только внесёт лишнюю путаницу, поэтому делать так не стоит, проще добавить соответствующий параметр в конфиг. Значение в нем традиционно является значением по умолчанию и соответствует контрольной группе, а экспериментальное значение параметра, отличное от дефолтного — экспериментальной.
Прим. Контрольная группа обычно называется группой A, экспериментальная — группой B. Как видно на скрине, в Firebase по умолчанию экспериментальная группа называется «Variant A», что вносит некоторую путаницу. Но ничто не мешает изменить её название.
Далее запускаем A/B-тест, Firebase разбивает пользователей на группы, которым соответствуют разные значения параметра, получив конфиг на клиенте, мы достаём из него нужный параметр и на основе значения применяем новую фичу. Традиционно параметр имеет имя соответствующее названию фичи, и 2 значения: True — фича применяется, False — не применяется. Подробнее о настройках A/B-тестов в соответствующем разделе документации.
2. Кодим
Не будем останавливаться непосредственно на интеграции с Firebase Remote Config — она подробно описана здесь.
Разберём способ организации кода для проведения A/B-тестирования. Если мы просто меняем цвет кнопки, то говорить об организации нет смысла, ибо организовывать особенно нечего. Мы рассмотрим вариант, в котором в зависимости от параметра из Remote Config показывается текущий (для контрольной группы) или новый (для экспериментальной) экран.
Нужно понимать, что по истечению A/B-теста один из вариантов экрана надо будет удалить, в связи с этим организовать код необходимо таким образом, чтобы минимизировать изменения в текущей реализации. Все файлы связанные с новым экраном стоит называть с префиксом AB и помещать в папки с таким же префиксом.
Если мы говорим об MVP в Presentation слое, выглядеть это будет примерно так:
Наиболее гибкой и прозрачной представляется следующая иерархия классов:
BaseOrderStatusFragment будет содержать весь функционал текущей реализации, кроме методов, которые не могут быть размещены в абстрактном классе из-за ограничений архитектуры. Они будут располагаться в OrderStatusFragment.
AbOrderStatusFragment будет переопределять методы, различающиеся в реализации, и иметь необходимые дополнительные. Таким образом, в текущей реализации изменится лишь разбивка одного класса на два и некоторые методы в базовом классе станут protected open вместо private.
Примечание: если архитектура и конкретный кейс позволяют, можно обойтись без создания базового класса и напрямую наследовать AbOrderStatusFragment от OrderStatusFragment.
В рамках такой организации скорее всего придётся отступить от принятого CodeStyle, что в данном случае допустимо, ибо соответствующий код будет удалён или отрефакторен по завершению A/B-теста (но, конечно, в местах нарушения CodeStyle стоит оставить коммент)
Подобная организация позволит нам быстро и безболезненно удалить новую фичу, если она окажется нерелевантной, поскольку все связанные с ней файлы легко найти по префиксу и её реализация не влияет на текущий функционал. В случае же, если фича улучшила ключевую метрику и принято решение оставить её, нам всё равно предстоит работа по выпиливанию текущего функционала, что повлияет на код новой фичи.
Для получения конфига стоит создать отдельный репозиторий и заинжектить его на уровень приложения, чтобы он был везде доступен, так как мы не знаем, какие части приложения затронут будущие A/B-тесты. По этим же причинам запрашивать его стоит как можно раньше, например, вместе с основной информацией, необходимой для работы приложения (обычно такие запросы происходят во время показа сплеша, хотя это холиварная тема, но важно что где-то они есть).
Ну, и, естественно, важно не забыть прокинуть значение параметра из конфига в параметры событий аналитики, чтобы была возможность сравнить метрики
Анализ результатов
Есть немало статей, подробно рассказывающих про способы анализа результатов A/B-тестов, например. Чтобы не повторяться, просто укажем суть. Нужно понимать, что разница метрик на контрольной и экспериментальной группе — случайная величина, и мы не можем сделать вывод о релевантности фичи только на основе того, что на экспериментальной группе показатель метрики лучше. Необходимо построить доверительный интервал (выбор уровня надёжности стоит доверить аналитикам) для вышеописанной случайной величины и проводить эксперимент до тех пор, пока интервал полностью не окажется в положительной или отрицательной полуплоскости — тогда можно будет сделать статистически достоверный вывод.
Подводные камни
1. Ошибка при получении Remote Config
Сравнительный анализ проводится на новых пользователях, так как в экспериментах должны участвовать юзеры с одинаковым пользовательским опытом и только те, кто видел единственный вариант реализации. Напомним, что получение конфига является сетевым запросом и может закончится неудачей, в таком случае будет применено значение по умолчанию, традиционно равное значению для контрольной группы.
Рассмотрим следующий кейс: у нас есть пользователь, которого Firebase отнёс к экспериментальной группе. Пользователь первый раз запускает приложение и запрос Remote Config возвращает ошибку — пользователь видит старый экран. При следующем запуске запрос Remote Config отрабатывает корректно и пользователь видит новый экран. Важно понимать, что такой пользователь не является релевантным для эксперимента, поэтому нужно придумать, как отсеивать такого пользователя на стороне системы аналитики, либо доказать, что число таких пользователей пренебрежимо мало.
На самом деле такие ошибки действительно возникают нечасто, и скорее всего вам подойдёт последний вариант, но есть по сути аналогичная, но куда более насущная проблема — время получения конфига. Как было сказано выше, лучше засунуть запрос Remote Config в начало сессии, но если запрос будет идти слишком долго, пользователю надоест ждать, и он выйдет из приложения. Поэтому нужно решить нетривиальную задачу — выбрать тайм-аут, по которому сбрасывается запрос Remote Config. Если он будет слишком мал, то большой процент пользователей может оказаться в списке нерелевантных для теста, если слишком большим — мы рискуем вызвать гнев пользователей. Мы собрали статистику по времени получения конфига:
Примечание. Данные за последние 30 дней. Общее кол-во запросов 673 529. Первый столбец, помимо сетевых запросов, содержит получения конфига из кеша, поэтому выбивается из общей формы распределения.
Миллисекунды |
Кол-во запросов |
200 |
227485 |
400 |
51038 |
600 |
59249 |
800 |
84516 |
1000 |
63891 |
1200 |
39115 |
1400 |
24889 |
1600 |
16763 |
1800 |
12410 |
2000 |
9502 |
2200 |
7636 |
2400 |
6357 |
2600 |
5409 |
2800 |
4545 |
3000 |
3963 |
3200 |
2699 |
3400 |
3184 |
3600 |
2755 |
3800 |
2431 |
4000 |
2176 |
4200 |
1950 |
4400 |
1804 |
4600 |
1607 |
4800 |
1470 |
5000 |
1310 |
> 5000 |
35375 |
2. Накатка обновления Remote Config
Необходимо понимать, что Firebase кеширует запрос Remote Config. Дефолтное время жизни кеша составляет 12 часов. Время можно регулировать, но у Firebase, есть ограничение на частоту запросов, и если его превысить, Firebase нас забанит и будет возвращать ошибку на запрос конфига (Прим. для тестирования можно прописать настройку setDeveloperModeEnabled, в таком случае лимит применяться не будет, но сделать так можно для ограниченного числа устройств).
Поэтому, например, если мы хотим завершить A/B-тест и раскатать новую фичу на 100%, нужно понимать, что переход осуществится только в течение 12 часов, но это не главная проблема. Рассмотрим следующий кейс: мы провели A/B-тест, завершили его и подготовили новый релиз, в котором есть другой A/B-тест с соответствующим конфигом. Мы выпустили новую версию приложения, но у наших пользователей уже есть конфиг, закешированный с прошлого A/B-теста, и, если время жизни кеша ещё не истекло, запрос конфига не подтянет новые параметры, и мы опять получим пользователей отнесённых к экспериментальной группе, которые при первом запросе получат дефолтные значения конфига и в перспективе испортят данные нового эксперимента.
Решение этой проблемы весьма простое — необходимо форсировать запрос конфига при обновлении версии приложения путём обнуления времени жизни кеша:
val cacheExpiration = if (isAppNewVersion) 0L else TWELVE_HOURS_IN_SECONDS
FirebaseRemoteConfig.getInstance().fetch(cacheExpiration)
Так как обновления выпускаются не так часто, лимиты мы не превысим
Подробнее о подобных проблемах здесь.
Выводы
Firebase предоставляет весьма удобный и простой инструмент проведения A/B-тестирования, которым стоит пользоваться, уделяя при этом особое внимание описанным выше узким местам. Предложенная организация кода позволит минимизировать количество ошибок при внесении изменений, связанных с циклом проведения A/B-тестов.
Всем добра, удачного A/B-тестирования и повышения конверсий на 100500%.