Баги из рабочей практики
Самая непредсказуемая часть любого приложения – человек за клавиатурой. Нам предопределено хлебать смузи и думать о вечном, сидя на безусловном базовом мешке. А мы ломаем машинные процессы из-за своей иррациональности.
Но пока, именно благодаря ошибкам я открыл для себя автоматические тесты, обязательное код-ревью и канареечную раскатку.
Как баги всплывают на поверхность:
• отловил самостоятельно в процессе разработки
• упал автоматический тест
• не прошли код-ревью
• тестировщик вернул задачу
• прилетело из крэшлитики
• аудит или отчет безопасников
• обратная связь от пользователей
Также пришлось поработать над двумя соцсетями. В каждой был подсайт или публичный топик, где юзеры описывали баги. Эти посты превращались в джира-тикеты. На одной работе был канал в слаке, куда сыпались оценки с отзывами из Google Play. Если ставили одну звезду, кабаныч тегал нас под сообщением.
А вот и сами баги:
1. В старых версиях Android Studio линтер не сообщал, что метод setForeground появился в Android 6. Вызвал его без проверки уровня API и приложение грохнулось в рантайме на Android 5. Так я узнал, что у нас еще есть такие пользователи.
2. На стартовом экране был какой-то сетевой запрос. Обработку ошибок для него не сделали. Однажды он отвалился совсем. Юзеры запускали апп, пялились на сплэшскрин и не могли навигироваться дальше.
3. Неправильно настроил launchMode и интенты. У пользователей в Recent Menu отображались две копии нашего приложения.
4. Был экран на WebView, замаскированный под нативный интерфейс – позже его собирались переделать. Ссылки с этого экрана должны были открываться в Chrome Custom Tabs, но из-за сломанного перехватчика shouldOverrideUrlLoading открывались в самом WebView.
5. Приложение оперировало квантами, штуками, рублями и копейками. Из-за неправильного округления итоговая сумма расходилась на 1 копейку. Запрос возвращал ошибку. Флоу блокировался. Познакомился с BigDecimal и roundToInt.
6. Забыл удалить моковые данные. Бывает бэк прилег, а разработка должна продолжаться. Повесил на кнопку навигацию на экран, недоступный без интернета. В запрос добавил delay(10_000), чтобы протестировать лоадинг. Моки улетели в репозиторий.
7. Метод бана юзера в API с параметром duration ожидал либо -1 (пермобан), либо таймстамп в секундах. Из-за kotlin nullable в одной из функций значение «на день» превратилось в -1 и юзеры забанились навсегда.
8. Не добавил обработку http-кода 204 и схлопотал NoSuchElementException. Запрос выполнился, но выбранная сигнатура была несовместима с пустым телом.
9. Выпустили релиз с NullPointerException: поспешно добавил ViewBinding в новые фрагменты, не прочитав документацию.
10. Включение вспышки срабатывало с заметной задержкой, потому что вызывало рекомпозицию и пересоздание инстанса камеры. Затащить камеру в Compose было непросто, поскольку API ориентирован на систему View.
11. На экране поста в заголовках добавлялась иконка галочки в конце. Иногда она переносилась отдельно на новую строку, отчего у дизайнера дергался глаз. Я не придумал ничего лучше, чем объединить ее с последним словом: вынул слово, объединил с иконкой в Span, вернул обратно. Работало прекрасно, пока не вышла статья с очень длинным последним словом в заголовке. Контент уехал за пределы экрана.
12. Делал редактор текста с поддержкой markdown: курсив, жирный, жирный курсив и так далее. Неожиданно в сервисе запретили мат. Пользователи стали маскировать его многочисленными звездочками, к чему редактор оказался не готов. Разметка пустилась в пляс.
13. Поехала верстка из-за того, что какая-то библиотека затащила самописный ConstraintLayout. Исключил транзитивную зависимость через exclude.
14. Мутные операторы котлин-коллекций: all, any и none ведут себя по-разному на пустых коллекциях, что провоцирует логические ошибки: all{…} отдаёт true за счёт «вакуумной истины», когда нет ни одного потенциального контрпримера. any() возвращает false, а none() – true. Я уже несколько раз на этом обжигался. Еще и курсор любит неудачно применять эти функции в коде.
15. При рефакторинге класса-обёртки для RxJava нашёл 40 перегрузок метода onSubscribe с разными параметрами. IDE подсветила половину как неиспользуемые и я их радостно удалил. В результате вызовы переключились на другие перегрузки, параметры изменились и асинхронщина поломалась.
16. ExoPlayer оказался для меня самым проблемным SDK: документация никакая, примеров нормальных не найти. Я не знал, что приложение должно отслеживать входящий звонок, чтобы передавать ему аудиофокус, ставя проигрывание на паузу. У наших пользователей после окончания звонка внезапно воспроизводилось видео из свернутого приложения.
17. Делали кэширование видеозаписей. Забыли про пользователей в роуминге. Посыпались жалобы от туристов, у которых наше приложение съело весь трафик. С тех пор эту историю выслушивают все, кто меня собесит.
18. С Paging Library огреб проблем не меньше, чем с эксоплеером. Прокрутка начала троттлиться из-за неправильного сохранения PagingKey в Room.
19. На бэке добавился новый статус, который в таблице Room хранился в поле с типом Enum. Старая версия приложения при чтении передавала эту незнакомую строку в Enum.valueOf(). Вылет с IllegalArgumentException.
20. Бэк стал слать пустой id, надо было в Room для primary-ключа сделать такое: id.ifEmpty { portion }. Перепутал portion с локальным полем position, которое постоянно инкрементируется. Надобавлялась куча дубликатов.
21. Поймал неконсистентную эмиссию данных из таблиц Room. Flow нужно собирать в combine(), а операции вставки выполнять в единой транзакции.
22. Отвалилось отображение пушей. Потому что загрузил метод onMessageReceived тяжёлой асинхронной логикой. FCM держит FirebaseMessagingService в памяти недолго, затем прибивает. Об этом я не знал.
23. Забыл добавить готовые ProGuard-директивы из файла missing_rules, которые указываются при выполнении таски minify. Релизное приложение вылетело в рантайме.
24. Разнес сборку debug и release на отдельные CI-джобы, но не настроил общий Gradle-кэш. Раннеру не хватило памяти: debug собрался, а release отменился.
25. Для тестирования ML Kit временно добавил в манифест атрибут android:debuggable="true". Коммит попал в репозиторий. Специалисты по безопасности потребовали убрать этот флаг из релизной сборки.
26. Загрузил в репозиторий на гитхабе keystore-файл и ключи для подписи релизной сборки, полагая что приватность репозитория гарантирует безопасность.
27. В комментарий к задаче в джире опубликовал фрагмент лога, в котором засветился секретный токен.
28. На одной работе запушил коммит, подписанный корпоративной электронной почтой со второй работы. Теперь не использую git config --global.
29. Сидел на дейли в AirPods. Вышел встретить курьера, который потерял подъезд. Чтобы расслышать его, нажал кнопку на наушнике – не заметив, что тем самым включил микрофон. Обматерил и курьера и всех на созвоне.
30. Конечно же на первой работе я закоммитился сделать таску за два дня, не проверив доступы. Накануне вечером открываю фигму, а там restricted access. Cпас коллега, у которого были права выдавать инвайты.
Список неполный, постоянно пополняется.
Под конец несколько духоподъемных цитат, чтобы снизить чувство бесполезности усилий:
• Не ошибается тот, кто ничего не делает.
• Важно не никогда не ошибаться, а побеждать чаще, чем ошибаться.
• Единственная настоящая ошибка – та, из которой ты ничего не извлёк.
android