Мобильные feature flags, которые не превращаются в мёртвый код
Мобильные feature flags помогают безопасно выкатывать изменения, но быстро накапливаются. Узнайте про шаги выката, локальные переопределения и привычки очистки, которые сохраняют приложение простым.

Почему флаги превращаются в скрытые ветки
Мобильные feature flags кажутся дешёвым решением, когда вы их добавляете. Один оператор if, одна удалённая настройка, одна удобная возможность быстро откатить изменения. А через пару месяцев этот маленький выбор может оставить в одном приложении сразу два продукта.
Проблемы начинаются после запуска. Команда выпускает новый checkout, видит, что всё работает, и переключается на следующую задачу. Флаг остаётся. Никто не удаляет старый путь, потому что мобильные релизы выходят медленнее, чем веб-релизы, а команда переживает из-за крайних случаев, задержек модерации или пользователей, которые ещё сидят на старых сборках. Такая осторожность имеет смысл неделю или две. Потом она превращается в захламление.
Каждый флаг добавляет ещё одну ветку, о которой нужно помнить. QA теперь должен проверять старый экран и новый. Разработчикам приходится решать, какой путь должна отслеживать аналитика. Поддержке приходится угадывать, какую версию на самом деле видел пользователь. Нескольких флагов хватает, чтобы создать длинный список состояний приложения, и многие из них почти не тестируются.
На мобильных устройствах это ещё хуже, потому что старые версии приложения живут долго. Серверный флаг может держать мёртвый код живым намного дольше, чем кто-либо планировал. Команда уже считает новый путь настоящим продуктом, а приложение всё ещё носит старый как запасную деталь, которую никто не хочет выбрасывать.
Новые участники команды обычно чувствуют боль первыми. Они открывают файл и находят два сценария, встроенные в боевую логику, без какого-либо намёка на то, какой из них ещё важен. Они тратят время на чтение обоих. Иногда они исправляют баг не в той ветке. Иногда добавляют третье условие просто из осторожности — и беспорядок растёт.
Проблему можно заметить заранее. Люди перестают понимать, кто владеет флагом. QA начинает спрашивать, какой путь можно не проверять. Продукт говорит, что выкатывание завершено, но код по-прежнему поддерживает старый сценарий. Разработчики продолжают добавлять новую работу в обе ветки.
Вот почему у мобильных feature flags должна быть дата окончания, а не только дата запуска. Если флаг живёт дольше пары релизных циклов, он перестаёт быть инструментом выката и начинает работать как скрытая продуктовая логика.
Выбирайте правильный флаг для задачи
Флаг должен отвечать на один вопрос. Если он одновременно управляет выкатыванием, безопасностью и логикой эксперимента, код быстро становится грязным. Хорошие флаги маленькие и скучные. У каждого — одна задача, один владелец и одна дата проверки.
Релизные флаги лучше всего подходят для коротких запусков. Используйте их, когда готовую функцию нужно постепенно выкатывать в течение нескольких дней или пары недель. Когда выкатывание достигает 100%, а приложение ведёт себя нормально, удаляйте старую ветку. Если релизный флаг остаётся в приложении месяцами, он уже не снижает риск запуска. Он превращается в скрытую логику приложения.
Аварийные выключатели — это другое. Оставляйте их для рискованных частей приложения, которым может понадобиться быстрое отключение в продакшене, например для шага оплаты, удалённой синхронизации или тяжёлой фоновой задачи. Изменение текста или новая иконка обычно не требует такого запасного выхода. Каждый аварийный выключатель создаёт ещё один путь для тестирования, поэтому используйте их редко.
Экспериментальным флагам тоже нужна финишная линия ещё до начала теста. Выберите метрику, решите, кто отвечает за результат, и назначьте день, когда команда выберет победителя. Без такого срока обе версии остаются живыми дольше, чем планировалось. Тогда каждое небольшое обновление превращается в два обновления.
Один флаг не должен управлять двумя разными продуктовыми решениями. Команды часто делают так, потому что два изменения выходят одновременно. Они прячут новый экран checkout и новое правило ценообразования под один и тот же флаг, а потом хотят оставить одно и убрать другое. В итоге один флаг блокирует два решения. Лучше разделить такие случаи заранее.
Большинству команд хватает нескольких понятных типов флагов: релизные флаги для коротких выкатываний, аварийные выключатели для рискованных сбоев и экспериментальные флаги для тестов с ограниченным сроком. Если новый флаг не подходит ни под одну из этих задач, остановитесь и задайте вопросы. Чем меньше веток, тем меньше уборки потом, меньше ошибок с релизными переключателями и код, который всё ещё имеет смысл через шесть месяцев.
Задайте правила до релиза
Большинство мёртвых флагов начинается с маленьких сокращений пути. Кто-то добавляет переключатель, чтобы выпустить фичу быстрее, релиз проходит нормально, и никто не возвращается удалить ветку. Полгода спустя приложение всё ещё тащит два пути для одной и той же функции, и никто не чувствует себя достаточно уверенно, чтобы трогать любой из них.
Простые правила не дают этому случиться. Для мобильных feature flags важнее не инструмент, а набор правил.
Начните с названия. Делайте его узким и конкретным. new_checkout_ios — понятно. checkout_v2 — нет, потому что никто не понимает, относится ли он к Android, iOS, поведению backend или ко всему сразу.
Назначьте одного человека за флаг ещё до того, как код попадёт в основную ветку. Этот владелец решает, когда начинать выкатывание, следит за результатом и открывает задачу на очистку. Совместное владение звучит справедливо, но обычно означает, что старый путь так никто и не удалит.
Хорошая запись о флаге отвечает на пять вопросов: какой функцией он управляет, какую платформу затрагивает, кто за него отвечает, когда его нужно удалить и какой результат означает, что выкатывание завершено. Дата удаления должна существовать уже в первый день. Назначьте реальную дату, а не «потом» или «после запуска». Если команде нужно больше времени, она может осознанно сдвинуть дату. Этот маленький шаг заставляет обсудить флаг, а такой разговор обычно показывает, какие флаги уже не отрабатывают своё.
Держите значение по умолчанию в одном месте. Поместите его в один конфигурационный файл, реестр флагов или общий wrapper. Не позволяйте одному экрану считать, что там «выключено», а другому helper — что там «включено». Когда значения по умолчанию расползаются по коду, очистка становится медленной и рискованной.
Финишная линия тоже должна быть описана письменно. «Мы это выкатили» — слишком расплывчато. Лучше звучит конкретное правило: у 100% пользователей iOS есть функция, уровень падений остаётся нормальным в течение одного релизного цикла, а поддержка не видит всплеска проблем с checkout. Когда это условие выполнено, владелец удаляет флаг, а не радуется и идёт дальше.
Если взять только одну привычку, пусть это будет она: у каждого нового флага ещё до слияния должен быть тикет на удаление. Так временные ветки остаются временными.
Выкатывайте мобильный флаг по шагам
Безопасный мобильный выкатывание начинается с малого. Даже хороший код может сломаться на реальных устройствах, в слабой сети или на старых версиях приложения. Ранний выкатывание — это не про рост. Это про то, чтобы понять, нормально ли изменение работает вне ваших тестовых телефонов.
Начните с вашей команды, тестовых пользователей или совсем маленькой части продакшн-трафика. Эта группа должна быть достаточно маленькой, чтобы ограничить плохой релиз, но достаточно реальной, чтобы выявить проблемы, которые QA пропустил. Если что-то ломается у 1% пользователей, это можно быстро исправить. Если ломается у всех, поддержка утонет в обращениях.
Простой план мобильного выкатывания обычно выглядит так: сначала включите флаг для внутренних пользователей, проверьте падения и логи ошибок, затем переведите на небольшую публичную группу вроде 1%–5%, а потом увеличивайте охват маленькими шагами, только если показатели остаются спокойными. После одного стабильного релиза удалите флаг.
Не спешите мимо этапа наблюдения. На мобильных есть задержки, о которых веб-команды часто забывают. Некоторые пользователи открывают приложение раз в неделю. Другие остаются на старой версии дольше, чем ожидается. Дайте каждому этапу достаточно времени, чтобы проявились всплески падений, сломанная аналитика, медленные экраны и странные сообщения от пользователей.
Сообщения в поддержку важнее, чем многие команды готовы признать. Панель может выглядеть нормально, пока пользователи продолжают сталкиваться с запутанными состояниями, пустыми экранами или потерянным прогрессом. Посмотрите, что приходит в поддержку в первый день или два. Повторяющиеся жалобы обычно говорят больше, чем зелёный график.
Увеличивайте охват небольшими скачками. Переход с 5% на 50% выглядит эффективно, но он скрывает, когда именно началась проблема. Более медленный путь облегчает решение об откате. Если функция меняется во время выкатывания, поставьте паузу. Не увеличивайте доступ, пока команда правит логику под тем же флагом. Иначе один тест превратится сразу в несколько.
Как только функция стабильно выкатана полностью, удаляйте флаг. Если оставить его «на всякий случай», вы сохраните скрытую ветку для будущих разработчиков, которым придётся её поддерживать, тестировать и помнить. Выкат не завершён, пока старый путь не исчез.
Используйте локальные переопределения, не засоряя код
Локальные переопределения флагов полезны. Тестировщики и разработчики могут принудительно проверять крайние случаи, не дожидаясь удалённой конфигурации. Но они же создают тихие баги, когда приложение прячет их в случайных экранах, разбросанных константах или скрытых жестах.
Держите каждое переопределение в одном debug-меню. Поместите его рядом с другими сведениями о сборке: версией приложения, окружением и активным аккаунтом. Если кому-то нужно принудительно включить флаг для теста, он должен точно знать, куда идти.
Приложение также должно показывать, откуда берётся значение каждого флага. Маленькая метка вроде «по умолчанию», «с сервера» или «локальное переопределение» экономит время. Без такой метки люди тратят минуты на споры о том, изменил ли сервер флаг или тестировщик забыл старую настройку.
Сброс в один тап тоже важен. Тестировщики весь день переключаются между сценариями, а старые переопределения остаются. «Сбросить все локальные флаги» помогает избежать странного поведения после обеда или на следующее утро.
Держите путь кода таким же чистым, как и debug-меню. Элементы управления принадлежат меню, а логика переопределения — одному сервису флагов. Остальная часть приложения должна просто запрашивать текущее значение и двигаться дальше. Когда проверки переопределения расползаются по экранам, презентерам и хелперам, локальные переопределения перестают быть тестовым инструментом и превращаются в скрытые ветки.
Простой пример показывает пользу. Тестировщик принудительно включает новый onboarding-экран в Android-сборке. В debug-меню отображается onboarding_v2 = ON (local override). После теста он нажимает сброс. При следующем запуске приложение возвращается к значению с сервера. Никому не нужно переустанавливать приложение, очищать хранилище или гадать, какое состояние приложение сохранило.
Полезно и логировать каждое переопределение в debug-сборках. Короткой записи с названием флага, выбранным значением и источником достаточно. Когда скриншот или баг-репорт выглядит странно, такой лог часто объясняет всё за секунды.
Не переносите инструмент переопределений в production-сборки. Обычные пользователи не должны его видеть, запускать или носить в себе скрытое локальное состояние, которое поддержка не может объяснить. Если сотрудникам нужен доступ в условиях, похожих на продакшн, используйте отдельную внутреннюю сборку и держите публичное приложение чистым.
Простой пример из реального приложения
Команда приложения для покупок хочет заменить экран checkout. Она не создаёт отдельные флаги для макета, оплаты и промокодов. Она использует один флаг для одного решения: старый checkout или новый checkout.
Так код остаётся читаемым. И это сильно упрощает процесс очистки флага — место, где многие команды ошибаются.
Во время разработки QA пользуется debug-меню с локальными переопределениями флагов. На тестовом телефоне они включают и выключают новый checkout, не дожидаясь удалённого выкатывания. Это позволяет быстро проверить базовые вещи: работает ли кнопка «Назад», открывается ли Apple Pay, совпадает ли сводка заказа со старым сценарием.
Продукт может использовать то же меню для сравнения на одном устройстве. Они оформляют один и тот же заказ дважды — сначала со старым экраном, потом с новым. Небольшие различия быстро всплывают, когда устройство, аккаунт и корзина остаются теми же.
Команда выпускает приложение с новым checkout, скрытым для большинства пользователей. Сначала они открывают его для 10%. Они следят за показателями, которые важны именно для этого экрана: завершение checkout, ошибки оплаты, отчёты о падениях и сообщения в поддержку.
Если эти цифры остаются нормальными, на следующем шаге они переходят к 50%. Они не держат флаг наполовину включённым месяцами. Такой флаг временный, поэтому команда назначает ему дату окончания ещё до начала выкатывания.
Как выглядит очистка
После одного чистого релиза, когда новый экран включён полностью, они удаляют старый путь checkout. Они также убирают флаг из remote config, вынимают его из debug-меню и удаляют любые аналитические разветвления, которые существовали только для сравнения.
Вот здесь команды часто ошибаются. Если старый путь остаётся в приложении, он превращается в скрытую ветку, которую никто не тестирует как следует.
Простое правило помогает держать команду честной: если пользователям больше не нужен старый экран как запасной вариант, код не должен продолжать держать его живым. Локальные переопределения всё ещё могут помогать в тестировании, но готовое приложение должно хранить только те пути, которые команда по-прежнему собирается поддерживать.
Ошибки, из-за которых флаги остаются надолго
Feature flag редко превращается в мёртвый код из-за одной громкой ошибки. Чаще всё происходит из-за нескольких маленьких сокращений пути, которые со временем накапливаются. Команда воспринимает флаг как временный переключатель, но пишет код вокруг него так, будто он будет жить вечно.
Первая проблема — владение. Команда добавляет переключатель для нового onboarding-экрана, выпускает его и идёт дальше. Через несколько недель никто не знает, кто должен его удалить, кто может менять значение по умолчанию и нужен ли вообще старый путь. У каждого флага должен быть один владелец и одна дата удаления, привязанная к релизной работе.
Ещё один частый беспорядок начинается, когда разработчики помещают один флаг внутрь другого. Экран оплаты проверяет новый checkout-флаг, потом флаг экспериментальной цены, потом региональный флаг. Такой код очень быстро становится трудным для чтения. Хуже того, никто не чувствует себя безопасно, удаляя хоть что-то. Если вам нужно больше одного флага в одном потоке, остановитесь и спросите себя, тестируете ли вы поведение продукта или прячете незавершённую архитектуру.
Распространение флага по коду вызывает боль другого рода. Один и тот же флаг читается в пяти файлах, а может и в десяти. Одна проверка сидит в UI, другая в логике view model, ещё одна в аналитике, ещё одна в построении API-запроса. Теперь удаление кажется рискованным, потому что ветка уже не одна ветка. Это фрагменты, разбросанные по всему приложению. Читайте флаг один раз рядом с точкой входа, а потом передавайте вниз понятное состояние.
Изменения данных делают всё ещё хуже. Команды иногда прячут новое поле базы данных или ответ API за UI-флагом и думают, что так изменение остаётся локальным. Это не так. Как только меняются контракты данных, старый и новый пути начинают расходиться. Флаг лишь скрывает это расхождение, пока пользователи не наткнутся на сломанный крайний случай.
Последняя ошибка скучная, но именно она причиняет больше всего боли. Очистка просто не попадает в план релиза. Команда выкатывает флаг, QA один раз проверяет оба пути, никто не выделяет время, чтобы удалить проигравшую ветку, и следующий релиз начинается раньше, чем происходит уборка. Так временный код становится постоянным.
Если флаг остаётся в приложении два релизных цикла без владельца, без тикета на удаление и без одного места, где приложение его читает, считайте это долгом и вырезайте его.
Быстрые проверки перед каждым релизом
Неделя релиза — это время, когда скрытые ветки проскакивают мимо. Короткая проверка помогает мобильным feature flags не превращаться в код, которому никто не доверяет.
Сделайте проверку достаточно простой, чтобы люди действительно её выполняли. Десять сосредоточенных минут лучше длинного процесса, который все пропускают.
Начните с возраста. Соберите все активные флаги в один список и отметьте те, что пережили больше двух релизов приложения. Такому флагу уже нужно решение: удалить его, оставить по понятной причине или назначить дату окончания.
Потом проверьте владельца. Используйте реальное имя, а не только название команды. Если у флага нет владельца, никто не ответит на базовые вопросы: зачем он существует, какой риск закрывает и когда должен исчезнуть.
Проверьте оба пути, которые всё ещё важны. Откройте приложение с встроенным значением по умолчанию, а затем протестируйте путь, который пользователи получают из live config. На мобильных эти пути легко расходятся, потому что пользователи неделями остаются на старых версиях приложения.
Ищите хвосты по всему коду. Старые проверки часто остаются в текстах интерфейса, событиях аналитики, тестовых файлах, обёртках конфигурации и комментариях. Если имя флага всё ещё встречается в местах, которые больше не влияют на поведение, уберите его, пока оно не начало путать ещё больше.
И наконец, создайте одну задачу на удаление для каждого активного флага. Сделайте её обычным элементом бэклога с версией, датой или триггером. «Удалить после 100% выката» гораздо лучше, чем «почистить потом», потому что «потом» обычно означает «никогда».
Небольшой пример делает риск очевидным. Допустим, вы выкатили новую кнопку checkout под флагом три релиза назад. Живой путь работает, но одно старое событие аналитики всё ещё срабатывает из отключённой ветки, один тест по-прежнему мокает старое значение, а никто уже не помнит, кто добавил флаг. Вот так и выживает мёртвый код.
Сама проверка скучная, и именно поэтому она работает. Если флаг проваливает хотя бы один пункт, считайте это проблемой релиза и исправьте её, пока изменение ещё свежее.
Что делать дальше
Большинству команд не нужен новый инструмент. Им нужна одна короткая привычка, которая не даёт флагам превращаться в постоянные ветки. Если вы уже используете мобильные feature flags, начните с быстрого аудита и сделайте результат видимым для всей команды.
Пройдитесь по каждому активному флагу и отметьте две вещи: зачем он существует и сколько ему лет. Релизный флаг, которому три недели, — это нормально. Тестовый флаг без владельца спустя шесть месяцев — нет. Такая простая сортировка обычно быстро показывает настоящую проблему: слишком много флагов живут долго только потому, что никто не чувствует себя ответственным за их удаление.
Достаточно лёгкой рутины. Отмечайте каждый флаг по типу, добавляйте владельца и плановую дату удаления, группируйте флаги по возрасту, удаляйте те, которые больше не меняют поведение пользователей, и открывайте тикеты на очистку для всего, что пережило свой срок.
После этого выберите одно правило, которому будет следовать вся команда. Сделайте его жёстким и скучным. У каждого релизного флага должен быть тикет на удаление уже в момент создания. Или любой флаг старше 90 дней должен иметь письменную причину, почему он остаётся. Одно ясное правило лучше длинной политики, которую никто не читает.
Встройте проверку флагов в уже существующую работу. Добавьте пять минут в планирование спринта или одну строку в чек-лист релиза: «Какие флаги мы можем удалить в этом цикле?» Этот маленький вопрос предотвращает много мёртвого кода. Он также помогает разработчикам вовремя убирать старые локальные переопределения флагов, прежде чем они расползутся в тестовые сборки и работу поддержки.
Привычка реальной команды может выглядеть так: в понедельник мобильный lead смотрит старые флаги; в среду команда закрывает один тикет на очистку; перед релизом QA проверяет, что временные переопределения отключены. Ничего эффектного, но это работает.
Если в вашем приложении уже слишком много скрытых веток, вторая пара глаз может помочь. Oleg Sotnikov at oleg.is работает со стартапами и небольшими командами над правилами выката, привычками очистки и техническими решениями в роли советника Fractional CTO. Короткий разбор ваших флагов, владения и процесса релиза обычно быстро показывает, откуда идёт лишняя сложность и что стоит упростить в первую очередь.
Часто задаваемые вопросы
Что такое мобильный feature flag?
Мобильный feature flag позволяет приложению выбирать между двумя путями кода во время работы — например, между старым checkout и новым. Он помогает безопасно выкатывать изменения и быстро отключать проблемные участки, но если оставить его слишком надолго, он превращается в скрытую продуктовую логику, которую команде всё равно нужно читать, тестировать и поддерживать.
Когда нужно удалять релизный флаг?
Удаляйте релизный флаг после того, как функция достигла 100%, приложение стабильно проработало один релизный цикл, а поддержка не видит новых проблем. Если оставить его «на всякий случай», вы просто держите лишнюю ветку живой без причины.
Сколько типов флагов нам на самом деле нужно?
Большинству команд достаточно трёх типов: релизные флаги для коротких выкатываний, аварийные выключатели для рискованных сбоев и экспериментальные флаги для тестов с ограниченным сроком. Если новый флаг не подходит ни под одну из этих задач, стоит остановиться и понять, зачем он вообще нужен.
Должен ли один флаг управлять несколькими продуктными изменениями?
Нет. У одного флага должна быть одна задача. Если один и тот же флаг управляет новым экраном и новой ценовой логикой, команде потом будет сложно оставить одно изменение и убрать другое без лишней переделки.
Какая схема выката лучше всего подходит для мобильных приложений?
Лучше всего начинать с внутренних пользователей или тестовых аккаунтов, затем переходить к маленькой публичной группе вроде 1%–5% и увеличивать охват небольшими шагами, пока вы следите за сбоями, ошибками, аналитикой и сообщениями в поддержку. После чистого полного выката старый путь нужно удалить, а не тащить его в следующий релиз.
Почему старые версии приложения усложняют очистку флагов?
Пользователи мобильных приложений дольше остаются на старых версиях, чем пользователи веба. Из-за этого серверный флаг может держать мёртвый код живым неделями или месяцами, даже если команда уже считает новый путь настоящим продуктом.
Как лучше работать с локальными переопределениями флагов?
Держите каждое переопределение в одном debug-меню и показывайте, откуда взялось значение — по умолчанию, с сервера или из локального переопределения. Добавьте сброс в один тап, чтобы тестировщики могли быстро очистить старое состояние и не получать странные отчёты об ошибках.
Где приложение должно читать значение флага?
Читайте флаг один раз рядом с точкой входа и передавайте дальше понятное состояние. Когда один и тот же флаг всплывает в UI-коде, аналитике, хелперах и логике API, удаление становится медленным и рискованным.
Какие признаки говорят, что флаг превратился в технический долг?
Следите за флагами без владельца, без даты удаления или без понятной причины оставаться в коде. Тревожный знак — когда QA спрашивает, какая ветка ещё важна, разработчики правят оба пути или никто не знает, какой опыт на самом деле видел пользователь.
Что стоит проверять перед каждым релизом?
Проводите короткую проверку всех активных флагов перед релизом. Смотрите на их возраст, владельца, значение по умолчанию, живой путь, старые ссылки в коде и задачу на очистку. Если флаг не проходит хотя бы один из этих пунктов, исправьте это, пока изменение ещё свежее.