Когда мобильное приложение ломается, потому что пользователи не обновляются

Вы выкатили новый эндпоинт бэкенда. Последняя версия приложения обрабатывает его идеально. В CI/CD пайплайне всё зелено. А потом начинают сыпаться краш-репорты.

Пользователи старых версий приложения стучатся в тот же эндпоинт, но их приложение не может распарсить новый формат ответа. Они видят пустой экран, падение или, что хуже, тихую потерю данных. Вы не меняли бэкенд для всех. Вы изменили его для последней версии приложения. Но бэкенд не различает клиентов. Он отдаёт один и тот же ответ каждому, кто спросит.

Это скрытая цена мобильной доставки. В отличие от веб-приложений, где вы контролируете клиент, мобильные приложения живут на устройствах, которые вам не принадлежат. Пользователи решают, когда обновляться. Одни обновляются сразу. Другие ждут неделями. Третьи всё ещё используют версию, которую вы выпустили полгода назад. А ваш бэкенд тем временем продолжает эволюционировать.

Проблема разрыва версий

Основное противоречие просто: бэкенд меняется непрерывно, а мобильные клиенты обновляются по своему расписанию. Каждый раз, когда вы добавляете новый эндпоинт, меняете структуру ответа или помечаете поле как устаревшее, вы создаёте потенциальную точку отказа для старых версий приложения.

Следующая диаграмма последовательности показывает, как изменение бэкенда может сломать старые версии приложения, в то время как новые продолжают работать:

sequenceDiagram participant AppV1 as App v1.0 participant AppV2 as App v2.0 participant Backend as Backend Note over Backend: Бэкенд развивается AppV1->>Backend: GET /api/orders (старый формат) Backend-->>AppV1: 200 OK (старый ответ) Note over AppV1: Работает AppV2->>Backend: GET /api/v2/orders (новый формат) Backend-->>AppV2: 200 OK (новый ответ) Note over AppV2: Работает AppV1->>Backend: GET /api/v2/orders (новый формат) Backend-->>AppV1: 200 OK (новый ответ) Note over AppV1: КРАШ - не может распарсить новый формат Note over Backend: Решение: сохранить старый эндпоинт для обратной совместимости

Большинство команд не осознают этого, пока прод не ломается. Они тестируют последнюю версию приложения с последней версией бэкенда, всё проходит, и они выкатывают. Но тестовый набор никогда не запускал старое приложение с новым бэкендом. Эта комбинация остаётся невидимой, пока на неё не наткнутся реальные пользователи.

Проблема усугубляется по мере роста вашей пользовательской базы. Больше пользователей — больше версий в дикой природе. У каждой версии свои ожидания относительно того, что должен возвращать бэкенд. Ваш бэкенд должен удовлетворять их все одновременно, по крайней мере какое-то время.

Знайте, какие версии существуют

Прежде чем управлять совместимостью, вам нужна видимость. Магазины приложений дают некоторые данные — Google Play Console и App Store Connect показывают распределение версий. Но эти данные запаздывают и агрегированы. Они говорят, что пользователи установили, а не что они активно используют.

Лучший подход: отправлять версию приложения с каждым запросом. Добавьте кастомный заголовок вроде X-App-Version или закодируйте его в строке User-Agent. Ваш бэкенд логирует эту информацию, и вы можете агрегировать её в дашборде, показывающем принятие версий в реальном времени.

Эти данные отвечают на критические вопросы:

  • Какой процент активных пользователей сидит на каждой версии?
  • Как быстро пользователи принимают последний релиз?
  • Какие старые версии всё ещё имеют значительный трафик?
  • Когда можно безопасно прекратить поддержку устаревшей версии?

Без этих данных вы принимаете решения вслепую. Вы можете объявить устаревшей версию, у которой всё ещё 30% активных пользователей, или продолжать поддерживать версию, которую используют только 2%.

Обеспечьте обратную совместимость бэкенда

Стандартный подход — обратная совместимость. Когда вы меняете эндпоинт, не удаляйте старый формат ответа сразу. Добавляйте новые поля рядом со старыми или версионируйте эндпоинты API явно.

Например, вместо изменения /api/orders создайте /api/v2/orders и оставьте /api/v1/orders работающим. Ваше последнее приложение общается с v2, старые приложения продолжают использовать v1. Это даёт пользователям время на обновление, не ломая их опыт.

Но у обратной совместимости есть пределы. Вы не можете поддерживать пять версий каждого эндпоинта вечно. Стоимость растёт с каждой поддерживаемой версией. В какой-то момент нужно отключать старые версии.

Вот где мониторинг версий становится критически важным. Когда данные показывают, что устаревшая версия упала ниже приемлемого порога — скажем, 5% активных пользователей — вы можете объявить о прекращении поддержки. Отправьте внутриприложенческое уведомление с просьбой обновиться. Дайте им дедлайн. После этой даты старый эндпоинт перестаёт работать.

Принудительные обновления, когда это необходимо

Иногда нельзя ждать постепенного принятия. Патчи безопасности, критические исправления багов или изменения в регулировании могут потребовать немедленного обновления. В таких случаях нужен механизм принудительного обновления.

Паттерн прост: ваш бэкенд проверяет заголовок X-App-Version в каждом запросе. Если версия ниже минимального порога, бэкенд возвращает специальный код ответа или полезную нагрузку. Приложение обнаруживает это и показывает обязательный экран обновления. Пользователь не может продолжить, пока не скачает последнюю версию из магазина.

Это грубый инструмент. Используйте его редко. Каждое принудительное обновление создаёт трения и рискует негативными отзывами. Но когда оно нужно, это лучше, чем оставлять пользователей на уязвимой или сломанной версии.

Используйте Remote Config как страховочную сеть

Remote Config даёт вам золотую середину между полной совместимостью и принудительными обновлениями. Вместо изменения кода вы меняете конфигурацию с сервера. Приложение периодически загружает эту конфигурацию — URL-адреса, таймауты, фича-тогглы, версии эндпоинтов — без необходимости обновления в магазине.

Вот как это помогает с проблемами совместимости. Предположим, в последней версии приложения есть баг, который проявляется только с конкретным эндпоинтом бэкенда. Вы не можете быстро исправить приложение, потому что ревью в магазине занимает дни. Но вы можете изменить remote config, чтобы перенаправить эту версию приложения на старый, стабильный эндпоинт. Баг исчезает без единой строки изменённого кода.

Remote Config также помогает при постепенных раскатках. Если вы замечаете, что пользователи версии 4.2 падают на новой функции, вы можете отключить эту функцию только для версии 4.2, оставив её активной для версии 4.3. Конфигурация учитывает версии, поэтому каждая версия приложения получает поведение, которое может обработать.

Вот как может выглядеть полезная нагрузка remote config с учётом версий:

{
  "config": {
    "new_checkout_flow": {
      "enabled": true,
      "disabled_versions": ["4.2.0", "4.2.1"]
    },
    "api_base_url": "https://api.example.com/v2",
    "legacy_api_base_url": "https://api.example.com/v1",
    "timeout_ms": 10000
  },
  "flags": {
    "dark_mode": true,
    "experimental_search": false
  }
}

Приложение читает список disabled_versions и пропускает новый флоу оформления заказа для версий 4.2.0 и 4.2.1, возвращаясь к старому флоу. Обновление приложения не требуется.

Фича-флаги для экстренного восстановления

Фича-флаги работают аналогично, но фокусируются на включении/выключении функциональности, а не на значениях конфигурации. Когда новая функция вызывает проблемы в проде, вы выключаете флаг из своего дашборда. Функция исчезает из приложения. Пользователи её не видят, не падают на ней и не жалуются на неё.

Преимущество перед remote config — гранулярность. Вы можете нацеливаться на конкретные сегменты пользователей, регионы или версии приложений. Вы можете раскатывать постепенно. Вы можете проводить A/B-тесты. И когда что-то идёт не так, вы можете мгновенно отключить функцию без нового релиза.

Фича-флаги особенно ценны для мобильных приложений, потому что они разделяют деплой и релиз. Вы можете выкатить код, скрытый за флагом, активировать его, когда будете готовы, и деактивировать, если что-то пойдёт не так. Приложению не нужно меняться. Всем управляет сервер флагов.

Практический чек-лист для управления версиями

  • Отправляйте версию приложения с каждым запросом через кастомный заголовок
  • Постройте дашборд с распределением версий в реальном времени
  • Держите старые эндпоинты API работающими, пока использование устаревших версий не упадёт ниже 5%
  • Установите минимальную поддерживаемую версию и обеспечьте её соблюдение механизмом принудительного обновления
  • Внедрите remote config для изменений поведения на стороне сервера без обновлений приложения
  • Используйте фича-флаги для мгновенного отключения проблемных функций
  • Объявляйте дедлайны прекращения поддержки через внутриприложенческие уведомления перед отключением

Вывод

Мобильная доставка не заканчивается, когда билд подписан и загружен в магазин. Она заканчивается, когда каждый пользователь сидит на версии, которая работает с вашим текущим бэкендом. Это может занять недели или месяцы. За это время ваш бэкенд изменится, и каждое изменение — потенциальная поломка для кого-то, кто всё ещё использует старую версию.

Мониторьте распределение версий. Поддерживайте обратную совместимость, пока это практично. Используйте remote config и фича-флаги, чтобы выиграть время, когда что-то идёт не так. И когда вы вынуждены принудительно обновлять, делайте это осознанно, а не реактивно.

Цель не в том, чтобы устранить фрагментацию версий — это невозможно. Цель — знать, что есть в поле, поддерживать это в рабочем состоянии и иметь план на случай, если что-то пойдёт не так.