Как часто вы используете
Не так давно я решил, что неплохо бы продолжить наблюдать результаты от этих вызовов не только в рамках запуска приложения в симуляторе / на девайсе, но и от действий реальных пользователей. Кстати, здесь речь может идти и про
Ближе к делу: в коде у меня нашлось около 300 подобных вызовов. Ни один из них не срабатывал при обычном локальном тестировании, и это меня радовало. Но я предположил, что действия пользователя могут все-таки привести к срабатыванию некоторых из вызовов.
С точки зрения пользователя вылетов происходить не должно (опять же мое мнение). В крайнем случае пользователь должен понять, что какой-то сценарий пошел не по плану и команда разработчиков уже работает над починкой. Подобные исключения всегда обрабатывались мной и для пользователя это могло сказаться в самой безобидной форме. Например, одна из сотен ячеек таблицы просто была невидима.
С пользователем более-менее все ясно. Осталось разобраться с доставкой логов до разработчика. Во-первых, требовалось минимальными усилиями заменить в коде текущие вызовы на вызовы, отправляющие логи куда-то за пределы приложения. Во-вторых, требовалось точно локализовать место происшествия, иначе соотнести исключение с реальным кодом было бы практически невозможно. В-третьих, следовало учесть, что видоизмененные вызовы могут сработать при Unit-тестировании, где
Отличный способ отправки событий предоставляет Fabric (Crashlytics). Выглядит это следующим образом:
Осталось упаковать Crashlytics в какой-нибудь фреймворк, который можно подгружать в полноценном виде в приложение и в урезанном виде (без зависимости Crashlytics) в тестовые таргеты.
«Упаковку» я решил сделать через CocoaPods:
Код для «боевого таргета» выглядит следующим образом:
Для «тестового таргета» т.е. без зависимости Crashlytics:
Исключения действительно начали срабатывать. Большая часть уведомляла о некорректном формате получаемых данных:
Спасибо за внимание.
P.S. Если вдруг вы будете слишком часто слать подобные логи в Crashlytics, то сервис может распознать ваши действия как спам. И вы увидите следующее сообщение:
Swift.assert()
в вашем коде? Я, честно, использую довольно часто (Если это плохая практика, то, пожалуйста, напишите в комментариях — почему это плохо?). В моем коде часто можно встретить, например, такой вызов:Swift.assert(Thread.isMainThread)
Не так давно я решил, что неплохо бы продолжить наблюдать результаты от этих вызовов не только в рамках запуска приложения в симуляторе / на девайсе, но и от действий реальных пользователей. Кстати, здесь речь может идти и про
Swift.precondition()
, Swift.fatalError()
и т.п, хотя их я стараюсь избегать. Более подробно про Unrecoverable Errors in Swift я читал в этой публикации и она оказалось очень даже познавательной.Ближе к делу: в коде у меня нашлось около 300 подобных вызовов. Ни один из них не срабатывал при обычном локальном тестировании, и это меня радовало. Но я предположил, что действия пользователя могут все-таки привести к срабатыванию некоторых из вызовов.
С точки зрения пользователя вылетов происходить не должно (опять же мое мнение). В крайнем случае пользователь должен понять, что какой-то сценарий пошел не по плану и команда разработчиков уже работает над починкой. Подобные исключения всегда обрабатывались мной и для пользователя это могло сказаться в самой безобидной форме. Например, одна из сотен ячеек таблицы просто была невидима.
С пользователем более-менее все ясно. Осталось разобраться с доставкой логов до разработчика. Во-первых, требовалось минимальными усилиями заменить в коде текущие вызовы на вызовы, отправляющие логи куда-то за пределы приложения. Во-вторых, требовалось точно локализовать место происшествия, иначе соотнести исключение с реальным кодом было бы практически невозможно. В-третьих, следовало учесть, что видоизмененные вызовы могут сработать при Unit-тестировании, где
Thread.isMainThread
уже должен игнорироваться т.к. я использую RxTest фреймворк для определенных видов тестрирования (здесь я тоже готов выслушать советы и критику). Главным пунктом осталось то, что локально все исключения должны срабатывать как и раньше, т.е. Loggin.assert()
должен срабатывать тогда же, когда бы срабатывал Swift.assert()
Отличный способ отправки событий предоставляет Fabric (Crashlytics). Выглядит это следующим образом:
Crashlytics.sharedInstance().recordCustomExceptionName("", reason: ""...
Осталось упаковать Crashlytics в какой-нибудь фреймворк, который можно подгружать в полноценном виде в приложение и в урезанном виде (без зависимости Crashlytics) в тестовые таргеты.
«Упаковку» я решил сделать через CocoaPods:
Pod::Spec.new do |s|
s.name = 'Logging'
...
s.subspec 'Base' do |ss|
ss.source_files = 'Source/Logging+Base.swift'
ss.dependency 'Crashlytics'
end
s.subspec 'Test' do |ss|
ss.source_files = 'Source/Logging+Test.swift'
end
end
Код для «боевого таргета» выглядит следующим образом:
import Crashlytics
public enum Logging {
public static func send(_ reason: String? = nil, __file: String = #file, __line: Int = #line) {
let file = __file.components(separatedBy: "/").last ?? __file
let line = "\(__line)"
let name = [line, file].joined(separator: "_")
Crashlytics.sharedInstance().recordCustomExceptionName(name, reason: reason ?? "no reason", frameArray: [])
}
public static func assert(_ assertion: @escaping @autoclosure () -> Bool, reason: String? = nil, __file: String = #file, __line: Int = #line) {
if assertion() == false {
self.assertionFailure(reason, __file: __file, __line: __line)
}
}
public static func assert(_ assertion: @escaping () -> Bool, reason: String? = nil, __file: String = #file, __line: Int = #line) {
if assertion() == false {
self.assertionFailure(reason, __file: __file, __line: __line)
}
}
public static func assertionFailure(_ reason: String? = nil, __file: String = #file, __line: Int = #line) {
Swift.assertionFailure(reason ?? "")
self.send(reason, __file: __file, __line: __line)
}
}
Для «тестового таргета» т.е. без зависимости Crashlytics:
import Foundation
public enum Logging {
public static func send(_ reason: String? = nil, __file: String = #file, __line: Int = #line) {
//
}
public static func assert(_ assertion: @escaping @autoclosure () -> Bool, reason: String? = nil, __file: String = #file, __line: Int = #line) {
//
}
public static func assert(_ assertion: @escaping () -> Bool, reason: String? = nil, __file: String = #file, __line: Int = #line) {
//
}
public static func assertionFailure(_ reason: String? = nil, __file: String = #file, __line: Int = #line) {
//
}
}
Результаты:
Исключения действительно начали срабатывать. Большая часть уведомляла о некорректном формате получаемых данных:
Decodable
иногда получал данные с несоответствующим типом. Иногда срабатывали логи для Thread.isMainThread
, которые очень оперативно исправлялись в следующих релизах. Самыми интересными ошибками стали чудом выловленные NSException.Спасибо за внимание.
P.S. Если вдруг вы будете слишком часто слать подобные логи в Crashlytics, то сервис может распознать ваши действия как спам. И вы увидите следующее сообщение:
Due to improper usage, non-fatal reporting has been disabled for multiple builds. Learn how to re-enable reporting in our documentation.Поэтому, стоит заранее продумать частоту отправки логов. Иначе все логи сборки могут оказаться под угрозой игнорирования сервисом Crashlytics
Gargo
Никогда не использую assert, потому что приложение вместо того чтобы падать, если данные не верны, начинает вести себя непредсказуемо. Я, например, не знаю, что хуже — вылет приложения на assert с проверкой главного потока или зависание приложения (из-за чего его все равно нужно перезапускать) — и то, и другое может быть у конечного пользователя.
+ если бы у вас был отдел тестирования, то вы бы возможно поняли, что использовать 2 разных билда для отладки и тестирования усложняет исправление багов
+ начинаются костыли вроде отправки аналитики по вызовам assert в crashlytics
VadikBeglov Автор
Почему вы решили, что используется два разных билда?
Перефразирую ваше высказывание: Если, допустим, при просмотре карточки товара пришли данные в неверном формате и Decodable не справился с задачей, то вы инициируете вылет приложения.
Если вы называете такое поведение «непредсказуемым», то для меня это обычная обработка ошибок. Ошибку от Decodable я отправлю в лог. Пользователю покажу уведомление о том, что что-то пошло не так. При таком сценарии пользователь сможет продолжить использовать приложение и сможет спокойной посмотреть другую карточку товара.
Ваше сообщение, кстати, прозвучало слегка надменно. Поэтому общение с вами я бы не хотел продолжать. Спасибо за советы.
Gargo
Вы в статье рассматриваете один пример, а в комментариях — совершенно другой. Я писал о случаях, когда ошибка в коде именно клиента, а assert ее только прячет (от программиста в том числе). Если Decodable не справился с задачей, то этот код обернуть в try-catch либо реализовать пирсинг так, чтобы он не генерировал исключительных ситуаций вообще. Обошлось без assert, а добавить аналитику можно опционально.
Еще API может меняться так быстро, что для поддержки Decodable нужно писать столько же кода, что и без него.