Однажды, ваше мобильное приложение становится достаточно большим и им ежедневно пользуются десять тысяч — сто тысяч — миллион, не важно, в общем очень много живых и разных людей. Что это значит для вас, как для разработчика?
Да, теперь стало гораздо страшнее нажимать кнопку «Submit», ведь если вы чего-то недоглядели — в отличии от веб-приложений не получится посидеть ночь, обложившись банками ред-булла и пиццами и все исправить — ревью на мобильных платформах занимает время, а если говорить про iOS — аж целую неделю. Неделя — более чем достаточный срок для того, чтобы лояльный ранее пользователь перестал открывать ваше приложение.
А еще, что не менее важно, это значит, что наступило время, когда «мне нравится, как выглядит этот экран» — уже недостаточное оправдание для того, чтобы этот экран действительно присутствовал в приложении.
В этой статье я постараюсь рассказать о том, что мы делаем, чтобы огромное продакшн-приложение продолжало оставаться таковым.
В качестве примечания: материал данной статьи мало подходит для приложений, работа которых не требует соединения с интернетом. Но в наше мобильное время — таких остается все меньше.
Часть 1. Сегментация uber alles
История первая: Мы интегрировали в наше приложение работу с одним большим и очень красивым third-party. У них своя команда, свой бэкенд, даже свой офис в какой-нибудь солнечной стране, куда можно позвонить и записаться на прием. Но в один прекрасный момент весь этот сервис ложится дня так на три после солидного рефакторинга, который по забавной случайности, нарушил обратную совместимость. Да «напишите это сами», «не связывайтесь больше с ними», «попросите их поскорее все исправить» — это, конечно, интересные мысли для размышления, но делать нужно что-то и причем быстро, для того чтобы пользователи, нажимая на какую-нибудь из привычных кнопок не видели постоянное «Простите, у нас обед», или того хуже — какую-нибудь не очень соответствующую правде информацию.
История вторая: Вы деплоите большую и важную фичу и, разумеется, вы ее как следует протестировали, но при этом в ней все-таки что-то оказалось не так! Кого и как поругать — будем решать потом.
История третья: Для того, чтобы больше знать о пользователях вашего приложения, вы отправляете на ваш выделенный лог-сервер много полезной информации. Но кто-ж знал, что их, внезапно, стало несколько миллионов — и ваша лог-сервер радостно падает раз в 10-15 минут, а новый прибудет недели через 2-3.
Это страшные истории, а еще есть много других, не таких страшных.
И для них всех есть одно удобное и полезное средство — сегментация.
Если вкратце — флоу работы нашего приложения можно описать простыми шагами:
- Получение с сервера общего конфига
- Логин на сервер
- Получение данных сегментации
- Работа в приложении
Итак, теперь остановимся подробно на первом и третьем шаге и на том, какие проблемы они решали в те или иные моменты жизни:
Что такое общий конфиг? Это набор настроек приложения, одинаковый для всех пользователей.
Что в нем содержится:
Адрес продакшн-серверов
Entry-point вашего приложения. И теперь, если случится то, что вам потребуется переехать на другой домен или, например, перевести пользователей на резервный сервер — это можно будет сделать через конфиг. В отличии, опять же, от веб-приложений, для мобильных (и для десктопных) систем — одновременно живы могут быть довольно большое количество версий, а значит вынужденное изменение продакшн-сервера можно провести (а у нас это было, значит может случиться и с вами), не сломав клиенты, которые уже в работе.
Таким образом в приложении будет храниться ровно один статический URL — на файл конфига, а за ним уж как-нибудь можно уследить.
Минимальная версия приложения (optional / force)
В целом, люди не очень любят обновляться. И даже с появлением на мобильных платформах автообновлений — ситуация улучшилась, но не исправилась во всей ее полноте. Например у нас — при двухнедельных циклах релизов — используются, как правило 10-15 версий одновременно. Но иногда происходят, так называемые breaking changes — фундаментальные изменения, которые делают невозможной/некомфортной работу на старых версиях клиента. В таком случае этот параметр сигнализирует нам о том, что «было бы неплохо обновиться» в мягком сценарии и «без обновления продолжение работы невозможно» — в жестком, что мы и показываем в UI для пользователей.
Настройки логов
Для этого мы используем отдельный конфиг-файл, который позволяет нам:
- Добавлять/исключать поля, посылаемые в конкретном лог-сообщении.
- Устанавливать, какой процент пользователей должен обрабатывать конкретно эти лог-сообщения на сервер.(и, более того, на какой сервер — некоторые сообщения логируются на third-party сервисы аналитики вроде GoogleAnalytics, некоторые на наш внутренний сервис и так далее)
- Устанавливать лог-левел для системного лога.
Первое помогает нам, имея достаточно компактные лог сообщения — при необходимости добавлять в них ту информацию, которой нам недостает.
Второе помогает нам балансировать нагрузку на лог-сервер. Так как пользователей у вас уже действительно много, то даже, если 1 процент из них будет отправлять эти сообщения на сервер — это уже позволяет нам получить достаточно репрезентативную картину.
Третье же имеет немного другую пользу — иногда в нашей системе крэш-аналитики появляются крэши, обстоятельства которых сложно восстановить по stackTrace, тогда уменьшая лог-левел до предела мы просто прикладываем последние сколько-то строк системного лога к крэш-репорту и уже на следующий день мы можем восстановить действия пользователей и локализовать проблему (если кому интересно, у нас это достигается в связке логгера cocoalumberjack и HockeyApp аналитики).
Ключи и прочие настройки от third-party библиотек
Да, я понимаю, что это фуфуфу, но тем не менее, в случаи каких-либо неоднозначных ситуаций с этими библиотеками — нам несколько раз приходилось пересоздавать наши аккаунты в third-party — и это, опять же, не ломало работу старых клиентов. К тому же, эти ключи вполне можно «посолить» и сделать их достаточно безопасными. (Но мы же все равно помним, что если вредитель задастся целью — он сможет вытащить и из бинарного приложения статически заданные эти ключи — поэтому такой способ их хранения не слишком хуже — тем более что вытащить алгоритм шифрования сложнее, чем один ключ).
К тому же, иногда в случае особенно неприятных breaking changes в third-party можно себе позволить устроить проксирование работы с ними через ваш сервер, сохранив для старых клиентов старый формат.
Параметры глобального А/В
Некоторые решения необходимо принимать для пользователя, который еще не зарегистрирован в системе (например, внешний вид окна регистрации, путь, по которому мы проходим регистрацию и многое-многое другое). В таком случае в качестве такого параметра мы вполне можем хранить процент пользователей, для которых требуется включить/выключить некоторую функциональность. Для определения попадания в тестовую группу неплохо подходит какой-нибудь уникальный идентификатор устройства (как правило в каждой операционной системе такой можно найти), от котого взяли двузначный хэш.
Кроме того очень полезным приобретением для нас оказалась следующая концепция:
Устройства и пользователи делятся на «реальных» и «тестовых». Для определения вторых мы пробовали разные (например по UDID, когда его использование запретили, по идентификатору для рекламы, но ограничения наложили и на него, да и вообще использование таких идентификаторов не исключает коллизий, что однажды реальный пользователь увидит то, что не должен), но в конце-концов остановились на простой схеме: маленькая утилита при запуске на устройстве записывает некоторый ключ в шифрованное хранилище на устройстве — такое есть на каждой мобильной системе, на iOS, например, такой является Keychain. Основное же приложение при запуске проверяет наличие этого ключа и, в случае его наличия считает пользователя тестовым.
ВАЖНО: Если вы вводите такого рода разделение, постарайтесь соблюсти две вещи:
- Реальный пользователь не должен «случайно» стать тестовым. Увиденная информация может сбить его с толку.
- Работа под тестовым пользователем не дает каких-то преференций и возможности опасных действий при работе с приложением.(особенно это касается разработки игр — потому что злоумышленники, которые поставят себе целью обойти защиту — рано или поздно сумеют это сделать и, более того, расшарят этот способ)
А теперь, когда у нас были тесовые пользователи — для чего мы их использовали?
- Дебаг-панель, которая облегчает действия тестировщика.(подменяет ответы от сервера, изменяет лог-левел, показывает фрейм-рейт и лаги UI)
- «Стейджи» — или тестовые сервера — для тестовых пользователей есть возможность выбрать — с какого стартового конфига мы будем загружать приложение — с production, или же с одного из тестовых серверов, что позволяет протестировать работу приложения при различных настройках как сервера, так и сегментации.
Итак, а теперь перейдем к основному сегментационному конфигу:
Он ориентирован на более прагматичные задачи — на то, что и принято в сообществе считать классическим A/B.
Этот конфиг структурирован в виде некоторого словаря (например, JSON), в формате id_фичи:{словарь параметров}.
Здесь нам уже не нужны какие-то проценты и прочие точки ветвления — как мы помним, к этому моменту мы уже зарегистрированы в системе — и, соответственно, на основе заведомо уникального user_id мы возвращаем для пользователя специфичные конкретно для него параметры, на основе имеющегося на сервере модуля сегментации. В нем мы можем опираться на:
- Насколько долго пользователь пользуется приложения (newcomer, лояльность, ...).
- Платящий ли это пользователь.
- Какое устройство, какую версию операционной системы и, непосредственно, приложения использует пользователь.
- В какой часовой зоне пользователь, в какой стране, какую локализацию использует.
И многие другие данные.
Для чего это используется?
Enable/Disable сегментация
Как описывалось в начале статьи — некоторую большую функциональность открывать довольно таки страшно, поэтому таким способом мы можем, во-первых, отключить фичу, которая ведет себя не так, как ожидалось. А во-вторых, обеспечить постепенное открытие этой функциональности для пользователей (открываем для 5 процентов — наблюдаем, потом для 10, 20, 50 и, наконец, для всех).
Сегментация параметров
В каждую проектируемую для А/В фичу можно закладывать ряд переменных — текст на всплывающем окошке, время анимации, время между показами, цвет кнопки, один из вариантов возможного поведения. Чем больше таких параметров — тем больше можно ставить экспериментов в поиске наилучшего решения. Ограничены вы только вашей фантазией. С другой стороны — тем больший объем тестирования необходим для данной функциональности. (Правда, этот объем можно неплохо размазать по времени — протестировав основную работоспособность приложения, в дальнейшем просто проводить краткое тестирование на тестовом сервере того набора параметров, которые собираются выкатить)
Итак, резюмируя эту часть, хотелось бы отметить следующие пункты:
- A/B тестирование должно быть заложено в архитектуру — каждая проектируемая фича, кроме совсем уж монолитных, должна предполагать возможность того, что она будет полностью отключена. Лишнее ветвление лучше убирать тогда, когда функциональность уже зарекомендовала себя на продакшне, во время рефакторинга. Кроме того, в фичу заранее должны быть заложены «точки внедрения» — которые могут принимать некоторые допустимые значения, которые мы получаем от сервера.
- В дополнение к предыдущему пункту — для каждой фичи должен быть некоторый дефолтный конфиг, во-первых, для того, чтобы пересылаемый конфиг был наименьшим по размеру (только переопределяемые параметры), во-вторых потому что сегментация, как и все остальное — может легко и просто отвалиться.
- Поддержание такого рода логики действительно накладно — оно усложняет код. Иногда, значительно.
- Увеличивается объем приемочного тестирования приложения — при ошибках проектирования или просто связанных фичах необходимо проверять значения не одной группы параметров, а возможные взаимосвязи нескольких.
- При всем при этом это способно сделать приложение значительно более устойчивым к ошибкам разработчиков и идеологов добавочной функциональности.
Часть 2. Эй, ты там как, живой?
Спроси себя, а не фигню ли я делаю
В первой части я написал, что мы делаем, в этой же постараюсь описать — а как же нам, собственно, понять, то ли мы делаем.
Крэш-аналитика.
Самый простой способ понять, что происходит что-то не то — это узнать, что после внедрения новой функциональности приложение просто перестало работать — начало радостно падать.
Для нее мы используем HockeyApp — потому что у него достаточно удобный инструментарий по работе с уже существующими крэшами, а, кроме того, он неплохо интегрирован в различные деплоймент-системы — так что поддерживать актуальность информации в нем можно в автоматическом режиме. Но, по факту, таких инструментов на данный момент существует весьма приличное количество — на любой вкус и цвет, выбирайте сами. Как я писал немного выше — работа с ним стала еще более приятней с того момента, как мы ввели возможность прикреплять к крэш-логу кусок из лога устройства.
Мониторинг сессий и платежей.
Пожалуй, основной инструмент, интересующий бизнес. Для него тоже существует довольно много различных инструментов, но мы используем некоторую смесь самописного и существующего. Потому что не каждый бизнес пойдет на то, чтобы расшарить информацию о платежах со сторонними сервисами. И правильно и сделает. Существующие системы позволяют нам достаточно комфортно обнаруживать за самым краткосрочным и самым долгосрочным трендом качества приложения. При деплое существенной функциональности наблюдения заслуживают следующие метрики:
- Количество сессий — если оно резко изменилось — значит что-то не так.
- Revenue — или количество платежей — которое позволяет как понять в краткосрочном смысле — какой эффект имеет внедряемая функциональность на пользователей, так и в стратегическом плане понять — туда ли мы двигаемся вообще.
- Длина сессии — тоже очень важная метрика. Для нее нельзя сказать, что она всегда лучше тогда, когда больше (это верно для игровых проектов), в бизнес-приложениях она должна скорее быть не очень далека от некоторой прогнозируемой.(если сессии слишком долгие — то возможно, стоит подумать, на что пользователи тратят столько времени?)
- Количество сессий в день — ну тут, в общем-то все понятно.
Современные сервисы позволяют неплохо сегментировать эту аналитику (по устройствам, по версиям приложения, по геолокации и еще по куче признаков), что позволяет быть более точным в своих прогнозах.
Мониторинг отзывов.
К сожалению, очень плохо формализуемый и не поддающийся автоматизации процесс, но, думаю, его важность и нужность объяснять не требуется для любых (не только больших) приложений. Хорошим тоном будет дать возможность написать какой-то отзыв-предложение непосредственно в компанию, потому что в большом количестве отзывов он может быть пропущен.
Мониторинг жизни фич и историй.
Для этого, к сожалению, не получилось найти достаточно удобный и функциональный простой публичный инструмент, поэтому этому и посвящена большая часть логов приложения и выделенный лог сервер.
Мы используем для этого Hadoop (и несколько других сервисов) по следующим причинам:
- Будучи NoSQL — он позволяет легко добавлять/удалять поля в лог-сообщения без необходимости менять структуру базы на лог-сервере — это дает необходимую гибкость.
- Есть возможность делать полноценные статистические выборки по интересующему нас набору параметров, чтобы получить наиболее точный срез информации.
- Мы используем такую сущность, как «история», она же internal session, она же funnel — условно говоря, уникальный идентификатор сессии использования приложения.
На основе этого, мы можем получить выборку, показывающую всю последовательность действий пользователя в рамках интересующей нас сессии. (Например, мы делаем выборку пользователей, у которых почему-то не прошел платеж, после этого для любого из этих пользователей выбираем его funnel и получаем необходимый контекст того, почему же это могло произойти) - Есть возможность настроить «нотификации», подвешенные на некоторые time-скрипты — то есть, например, раз в час мы делаем выборку, считающую процентное отношение количества сессий с ошибками к общему количеству сессий — и если это процентное соотношение превышает некоторый интервал, заинтересованные лица получают сообщение об этом и приступают к более детальному анализу возникшей проблемы.
Я полагаю, все эти требования являются достаточно важными, а инструмент, который им удовлетворяет, я полагаю, не один.
И, пожалуй, самый важный пункт, следующий из наличия у вас подобного инструментария — он предоставляет возможность построения формальных (и достаточно точных) метрик для оценки качества и востребованности функциональности. Если кнопку не нажимают — ее нужно убрать, даже если она очень красива. Если после внедрения суперудобной фичи пользователи стали больше жаловаться на приложение, меньше им пользоваться, меньше платить — значит эта фича не суперудобна. И так далее.
Резюмируя все вышесказанное, хотелось бы отметить несколько следующих пунктов:
- Возможно, большинство материала статьи кажется очевидным, но тем не менее мы до всего этого дошли не с первой попытки и эта совокупность подходов работает и решает задачи.
- Вся эта инфраструктура весьма накладна в разработке и поддержании работоспособности — поэтому не стоит внедрять ее ВСЮ, если вы начинаете писать небольшое приложение, которое, как вы верите, заинтересует миллионы. Хотя бы еще и потому, что статистические методы абсолютно неприменимы до какого-то размера аудитории.
- Однако при этом некоторые рецепты несложно поддерживать в работоспособном состоянии, а нервов они экономят прилично.
- Не стоит логировать прям все-все-все. Это быстро превращается в непереваримую кашу. Я предлагаю логировать исходя из гипотез. То есть, приступая к проектированию новой функциональности, вы с позиции пессимиста выписываете варианты, что может пойти не так, а потом на основе этого пишете минимальный набор лог-сообщений с минимальным набором полей, который позволит покрыть аналитикой эти гипотезы. И иногда проводить ревью лог-сообщений и убирать утратившие актуальность.
- Но при этом помните, что логировать можно очень разнообразные метрики и события — от ошибок в сессии и частоты открытия какого-то экрана, до времени загрузки приложения или времени проведенного на той или иной задаче.
- Пробуйте различный инструментарий для организации сегментации — в идеале это должно быть сделано таким образом, чтобы изменениями сегментов занимались не программисты, а маркетинг- и продакт-команды. Дайте им больше возможностей по организации сегментов.
- На рынке присутствует некоторое количество уже готовых решений для организации A/B сегментации в мобильных приложениях, если вы хотите сэкономить время на организации инфраструктуры, например leanplum
- Вам очень повезло, вы работаете над проектом, который интересен людям :-) Всем спасибо.
Автор: i_user