Перезапуск TeamReach: как OTA-обновления спасли миграцию 5 000 000 пользователей

Источник: Expo Blog•

Перезапуск TeamReach: как OTA-обновления спасли миграцию 5 000 000 пользователей

TeamReach перевела 5 000 000 активных пользователей на пересозданное приложение за одну ночь. Двенадцать OTA-обновлений за три дня исправили то, что пропустило тестирование, примерно за 4 000 долларов.

Это гостевой пост Джина Корнфилда (Gene Cornfield), в настоящее время президента TeamReach и многолетнего бизнес-лидера в Accenture.

Если вам когда-либо доставалось чужое приложение, вы наверняка знакомы с особым чувством тревоги перед тем, что вас ждет. Две нативные кодовые базы, не разделяющие никакой общей логики. Уровень сбоев, на который никто не заглядывал уже год. Рекламные блоки, которые тихо перестали работать (что становится проблемой, когда реклама — ваш источник дохода). Бэкэнд на PHP без ORM.

Ставки взлетают до небес вместе с пульсом, когда у приложения огромная аудитория. Когда мы купили TeamReach, почти 800 000 человек открывали его каждый божий день с одной и той же уверенностью: оно просто будет работать.

У этой истории счастливый конец, но детали стоит прочитать, если вы столкнулись со сложностями, преимуществами (и рисками!) перезапуска популярного приложения с миллионами пользователей.

Мы переписали TeamReach с нуля на Expo всего за три месяца и перевели каждого из наших пользователей на новое приложение в ночь на 28 июня. Затем мы потратили три дня на исправление неполадок в продакшене, не выпуская ни единой новой сборки в App Store (наша команда спала мало).

За эти три дня мы выпустили двенадцать беспроводных (OTA) обновлений. Вероятно, это обошлось нам примерно в 4000 долларов. Без Expo, Claude и Codex этот проект занял бы восемь месяцев и стоил сотни тысяч долларов.

Сразу скажу: я здесь не инженер. Я управляю компанией. Далее описывается то, как разворачивались эти события, с техническими деталями от Натана Дионна (Nathan Dionne), нашего ведущего разработчика, чье агентство и выполняло переработку.

Что мы купили

В марте 2026 года мои партнеры и я купили TeamReach. Это приложение для спортивной коммуникации и планирования. Если вы родитель ребенка, занимающегося спортом, вы понимаете, о чем речь. Если нет, просто представьте: составы команд, календари, групповые чаты и логистика, которая сопутствует детским спортивным командам или клубам, играющим в пиклбол, теннис, футбол, гольф и другие виды спорта для взрослых. Примерно 5 миллионов человек использовали его за год, 2,5 миллиона в отдельный месяц и почти миллион в пиковый день.

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

Наследованное приложение

Команда, которая изначально создала TeamReach, сделала это для управления спортивными командами своих детей. Они и представить не могли, что их личный побочный проект привлечет столько пользователей не только со всех США, но и из более чем 200 стран и территорий по всему миру!

Приложение состояло из двух отдельных нативных кодовых баз: приложения на Objective-C для iOS и устаревшего для Android. Бэкэнд представлял собой PHP на MariaDB без ORM и без миграций. Но опять же, никто не ожидал, что приложение привлечет столь крупную и активную базу пользователей, причем органически, без какого-либо маркетинга. Поэтому почти за десятилетие разработчики поочередно дописывали данные в одни и те же поля базы данных по мере того, как пользовательская база росла от сотен до тысяч, десятков и сотен тысяч, а затем и миллионов. Я помню, как наш ведущий разработчик Натан Дионн прямо заявил: «В базе данных есть одна таблица со 100 миллиардами строк. Я никогда ничего подобного не видел».

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

Почему мы переписали приложение на Expo

Current Technology (агентство Натана — и чертовски хорошее!) выполнило полный редизайн на Expo примерно за три месяца силами всего четырех человек. База данных одновременно была перенесена на Postgres.

Я хочу внести ясность в то, почему мы решили, что масштабный редизайн в такие сроки — это хорошая идея, и дело вовсе не в недостатке уверенности в нашем тестировании. Скорее, наоборот.

«Я запустил тысячу приложений в своей жизни и знаю, что что-то обязательно сломается», — сказал мне Натан перед переходом. «Этого просто не избежать, когда делаешь что-то подобное». Так что причиной нашего согласия на полный редизайн стала возможность использовать OTA-обновления Expo для отправки обновлений по воздуху.

Это означало часы боли вместо нескольких недель. По словам Натана: «Мы не смогли бы запуститься еще целый квартал, если бы не были уверены в том, что можем делать OTA».

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

От Натана я узнал, что этот механизм работает в более узких рамках, чем принято считать. Сборка состоит из двух слоев: нативного слоя, скомпилированного в бинарный файл, и слоя обновлений поверх него, который можно заменить на любое совместимое обновление. OTA-обновление заменяет второй слой — то есть JavaScript, стили и такие ресурсы, как изображения. Нативный код остается нетронутым, а все, что его затрагивает, проходит через сборку Expo и проверку в магазине в обычном режиме. Вы получаете логику приложения и интерфейс... именно там, где миграции данных терпят крах.

Ночь перехода

Мы переключились на новую версию в 2 часа ночи по восточному времени. Новое приложение первыми получили в Новой Зеландии — странная фраза для американского спортивного приложения, если бы Новая Зеландия не была одним из наших крупнейших рынков: 1,6% населения пользуются TeamReach.

Отчеты об ошибках начали поступать уже через несколько минут. Джордан (Jordan), который руководит службой поддержки клиентов и которого когда-нибудь канонизируют, просидел в Zendesk примерно 36 часов подряд, занимаясь триажем и группировкой тикетов во что-то, с чем инженеры могли работать.

За первые четыре часа вышло три OTA-обновления. После этого мы вошли в ритм обновлений в полдень и полночь на протяжении трех дней: сбор ночных отчетов об ошибках, диагностика, исправление, контроль качества, развертывание, повтор. Всего двенадцать обновлений. Весь класс проблем, способных потопить бизнес, был устранен и попал в руки пользователей менее чем за 24 часа — и я говорю о критических проблемах функционала, а не о шлифовке интерфейса.

Чего я до конца не осознавал перед началом: публикация обновления не равна его доставке. Библиотека expo-updates проверяет наличие нового обновления при запуске приложения, скачивает манифест с его описанием, а затем подтягивает недостающие ресурсы. Если этот процесс завершается достаточно быстро, обновление запускается в рамках текущей сессии. Если нет, загрузка продолжается в фоновом режиме, а обновление применяется при следующем открытии приложения пользователем.

Для нас это означало, что исправление доходило до аудитории в течение 24 часов по мере того, как люди в разных часовых поясах открывали приложение перед утренними матчами или дневными тренировками, а не мгновенно для всех. Именно поэтому стоит заботиться о том, чтобы каждое обновление было небольшим по размеру.

Ошибки, которые наши тесты не смогли найти

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

Когда вы выполняете миграцию схемы десятилетней давности, вы проводите тестирование на тех данных, которые видите. Чего вы не видите, так это того, что поле, с которым текущий код работает как с логическим, три года назад было типом tinyint, принимающим 0 и 1, некоторое время после этого содержало JSON-строку, а в горстке строк и вовсе содержало сырой HTML из еще более ранних времен. Не было ни одного ORM, который бы это контролировал, и ни одной миграции, которая бы когда-либо очистила эти данные.

У наших новых пользователей все было в порядке. У пользователей с самым большим стажем, чья история записей уходила вглубь времен дальше всего, обнаружились поля, которые новый клиент не мог разобрать. Несколько проблем было решено на стороне API, но большинство — во фронтенде: мы научили клиент принимать те формы, которые база данных возвращала на самом деле, а не те, которые заявляла схема.

Вы не найдете такого в QA. Вы обнаруживаете это, когда тренер в Окленде, зарегистрировавшийся в 2019 году, открывает свой список, а экран пуст.

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

Изменения, которые наши пользователи отвергли

Этот рерайт был именно этим: та же функциональность и тот же интерфейс, к которым привыкли пользователи TeamReach на iOS или Android, переписанные на React Native. Паритет был приоритетом. Большая стабильность или скорость — это единственные изменения, которые пользователи должны заметить. Новые функции и серьезные улучшения пользовательского интерфейса должны были подождать до тех пор, пока новая версия не получит широкое распространение. Нашей мантрой была та же клятва, которую дают врачи: не навреди.

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

Например, в разделе сообщений приложения, над текстовым полем для написания сообщения, находилась горизонтальная панель иконок: прикрепить фото или файл к составляемому сообщению; добавить вопрос для опроса или геолокацию; установить важность или запланировать время отправки. Эта панель была видна (и перекрывала около 15% области просмотра) независимо от того, пишите ли вы сообщение или читаете сообщения других в ленте.

Поэтому мы решили добавить иконку «плюс» слева от текстового поля сообщения, которая раскрывала бы панель иконок, когда пользователю нужно что-то добавить к создаваемому сообщению. В остальном же эта панель не мешалась, предоставляя пользователям больше пространства для чтения сообщений в ленте. Очень стандартный сценарий взаимодействия, который вы видите в мессенджерах, слева от строки поиска Google или в ChatGPT, Claude — везде. Единственное, пожалуй, что более стандартно — это иконка шестеренки для настроек.

Конечно, наши пользователи должны быть знакомы с таким привычным сценарием взаимодействия и, как мы предполагали, оценить дополнительное пространство для чтения, верно? Как бы не так. Мы получили десятки запросов в службу поддержки с вопросами о том, куда подевались иконки прикрепления фото или добавления геолокации.

Другой пример: когда пользователи загружали видео, они сохранялись в «Файлах». Мы решили, что имеет больше смысла сохранять видео в разделе «Фотографии». Мы ошиблись.

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

Доставка этих изменений в продакшен в тот же день, а не две недели спустя — это и есть главная цель. Руководящий принцип дизайна и стратегии нашего приложения заключается в том, чтобы позволить людям тратить свое время на то, что они любят, с теми, кого они любят, а не управлять этим в приложении. Изменение интерфейса, которое объективно лучше, но замедляет пользователя, не проходит эту проверку.

Во что это обошлось

Около 4000 долларов на доставку обновлений за все время окна запуска.

То, как складывается эта цифра, важнее самой цифры. Expo OTA Update выставляет счета на основе активных пользователей за месяц, где один пользователь — это одна установка, которая загружает хотя бы одно обновление в течение расчетного периода. Оплата идет не за каждое обновление. Двенадцать обновлений для одной и той же аудитории стоят примерно столько же, сколько одно. Неудачный запуск полностью меняет математику, потому что ограничением на частоту релизов является способность вашей команды разбираться с проблемами, а не счетчик, тикающий при каждом пуске.

Оценка Натана по переводу приложения нашего размера на рельсы традиционным способом составляет более 1 миллиона долларов, а программа QA, которая вам понадобится, чтобы отловить эти проблемы с данными до запуска, а не после, сама по себе исчисляется шестизначной суммой. Мы потратили 4000 долларов. (И три дня с минимальным количеством сна.)

Как мы выпускаем обновления теперь

Двухнедельные спринты создают обычную сборку для магазина приложений. Между ними OTA обрабатывает критические исправления. Мы также используем его для экспериментов, в основном с изменениями интерфейса и оптимизацией рекламы, чтобы видеть, как изменения воспринимаются реальными пользователями, прежде чем включать их в релиз.

Лежащая в основе модель представляет собой три свойства, заложенные в каждую сборку: канал, версия среды выполнения (runtime version) и платформа. Каналы — это имена, которые вы даете группам сборок: наши сборки для магазинов используют продакшн-канал, в то время как сборки TestFlight используют стейджинг-канал. Обновления публикуются в ветки, а канал указывает на ту ветку, из которой его сборки должны считывать обновления. Платформа и версия среды выполнения должны совпадать точь-в-точь. Это вся модель совместимости, и на ее понимание у меня ушло больше времени, чем следовало бы.

Включение этого механизма сводится к двум вещам: блок конфигурации и одна команда.

Правило выбора версии среды выполнения (runtimeVersion) — это то, что стоит понять до того, как оно понадобится вам в 3 часа ночи. Оно определяет, до каких установленных бинарных файлов может дотянуться обновление, поэтому устройство получает только то обновление, которое его нативный код действительно может запустить. Политика fingerprint повышает эту версию всякий раз, когда меняется что-либо, затрагивающее нативную среду выполнения — именно то, что нужно, когда человек, отправляющий исправление в 3 часа ночи, не спал. Существует также политика appVersion, которая неплоха, но возлагает на вас обязанность не забывать самостоятельно повышать версию приложения.

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

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

Чего OTA-обновления не исправляют

Они содержат только ненативные части приложения: JavaScript, стили и ассеты. Новый нативный модуль, изменение разрешений, обновление Expo SDK, все, что требует нового бинарного файла… все это проходит через сборку для магазина и проверку по обычному графику.

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

Существует встроенный механизм восстановления после ошибок, и полезно знать его устройство, прежде чем полагаться на него. Если обновление вызывает критическую ошибку JavaScript на раннем этапе запуска, expo-updates перехватывает ее, дает приложению около пяти секунд на загрузку более свежего обновления и, если сбой произошел до отрисовки первого представления, помечает неудачное обновление как неисправное и возвращается к последнему работающему.

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

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

Когда перезапуск уже позади, полное отсутствие технического долга позволило нам очень быстро внедрить новый функционал, о котором пользователи просили годами. Непрошеные слова благодарности, которые мы получаем, приносят даже больше радости, чем сокращение количества обращений в поддержку и рост вовлеченности пользователей с началом нового учебного года, когда мы впервые перешагнули отметку в миллион ежедневных пользователей (и это число растет).

С чего начать, если вы готовите собственный перезапуск

Три вещи, которые я бы сказал себе шестимесячной давности:

  • Устаревшие данные — это риск, а не код. Закладывайте в бюджет первой недели после запуска время на разбор структур, которые скрывает ваша старая база данных, а не на только что написанные функции.
  • Ваш путь быстрого исправления ценнее тестового покрытия, потому что вы не сможете затестить схему, которую никто не документировал.
  • Если вы сами не являетесь активным пользователем своего приложения, поймите: все, что кажется вам очевидным улучшением, может оказаться совершенно ненужным для людей, чье мнение действительно важно. Пользователи, выбравшие вас за простоту, могут счесть небольшие изменения, объективно улучшающие UX, потерей, а не приобретением. Поэтому либо тестируйте даже незначительные изменения на реальных пользователях перед запуском, либо планируйте быстро и без эгоизма откатывать некоторые из них.

Если вы раздумываете над перезапуском, конкретный первый шаг прост: запустите npx expo install expo-updates и eas update:configure на вашем тестовом приложении, соберите билд и отправьте одно обновление на устройство, которое держите в руках. Дистанция между «мы могли бы исправить это за пару часов» и «мы могли бы исправить это за две недели» меняет ваше представление о том, что вы готовы выпустить в продакшен. Погрузитесь в это с головой до того вечера, когда это будет иметь значение.

Даже несмотря на то, что я сделал это (а наша команда наверняка была раздражена многочисленными просьбами «сдвинуть те пять пикселей»), помните: люди, которые каждый день полагаются на ваше приложение в важных для них делах, все равно укажут на проблемы, которые упустили даже самое тщательное тестирование и лучшие эвристические практики.

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

О чём эта статья

Ещё в разделе «Разработка ПО»

Все →

Ещё от Expo