概要
アプリでハンドリングしたエラーを記録するための仕組みとして、CrashlyticsのrecordError
が使えたので備忘録として残しておきます。
サンプルはCrashlyticsLogSampleにあげてあります。
前置き
今開発しているアプリでは、アプリでハンドリングしたエラーはassert
+エラーダイアログで対応していました。
つまり開発中はアプリがクラッシュし、本番ではエラーダイアログが出る仕組みになっています。
ただこの方法だと何故エラーになったのかは再現してみないとわからないという問題がありました。
(サーバ側のエラー(500エラー等)はサーバ側のログに残っているのですぐ調査できるのですが、200を返しているがレスポンスのJsonが不正だったりするパターンでは原因究明に時間がかかっていました。)
方法
- iOSアプリでCrashlyticsにカスタムログを送信する方法を参考にさせてもらいました。
上記の記事にあるようにCrashlytics.sharedInstance().recordError()
を利用するのでNSErrorの拡張クラスを作成します。
import Crashlytics
/// Crashlytics用のコード
/// この辺は適宜拡張してください
enum CrashlyticsErrorCode :Int {
/// 不明なエラー
case unknownError = 0
/// xxxAPIのJson不正
case xxxApiJsonInvalid = 100
/// zzzAPIのJson不正
case zzzApiJsonInvalid = 101
/// コード値
var value:Int {
get {
return self.rawValue
}
}
/// エラードメイン
var domain: String {
get {
switch self {
case .unknownError:
return "Unknown Error."
case .xxxApiJsonInvalid:
return "xxx API Json Invalid Error."
case .zzzApiJsonInvalid:
return "yyy API Json Invalid Error."
}
}
}
}
/// Crashlytics用のエラー
class CrashlyticsError : NSError {
// この辺も必要なものを適宜作成
let msgKey:String = "概要"
let requestKey:String = "リクエスト"
let requestParamKey:String = "リクエストパラメータ"
let responseKey:String = "レスポンス"
private override init(domain: String, code: Int, userInfo dict: [AnyHashable : Any]? = nil) {
super.init(domain: domain, code: code, userInfo: dict)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
/// 初期化
///
/// - Parameters:
/// - code: 必須 CrashlyticsErrorCode
/// - errMsg: 任意 errMsg
/// - debuggableObj: 任意 CrashlyticsDebuggable
init(code: CrashlyticsErrorCode, errMsg:String? = nil, debuggableObj:CrashlyticsDebuggable? = nil, file:String = #file, line:Int = #line){
var userInfo:[AnyHashable : Any] = [:]
userInfo[msgKey] = (errMsg != nil) ? "[\(file):\(line)] \(errMsg!)" : "[\(file):\(line)]"
if let debuggableObj = debuggableObj {
// リクエスト
if let requestMsg = debuggableObj.requestDebugStr {
userInfo[requestKey] = requestMsg
}
// レスポンス
if let responseStr = debuggableObj.responseDebugStr {
userInfo[responseKey] = responseStr
}
}
super.init(domain: code.domain, code: code.value, userInfo: userInfo)
}
/// Crashlyticsへログ送信
func sendCrashlytics(){
Crashlytics.sharedInstance().recordError(self)
}
}
デバッグ用のprotocolを作成します。
/// エラーログ用の拡張
protocol CrashlyticsDebuggable {
/// Request文字列
var requestDebugStr: String? { get }
/// レスポンス文字列
var responseDebugStr :String? { get }
}
実際に利用するクラスの例です。
XXXAPIDebugInfo
というクラスがCrashlyticsのログ用のモデルになります。
今回はCrashlyticsDebuggable
にRequestとResponseの情報を表示するような制約にしていますが、ここの部分を増やしていきCrashlyticsError
に手を入れればいろんな場合のログに対応できるかと思います。(自分ははRequestパラメータ等を追加して使っています)
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
dummyRequestMethod(success: nil) { [weak self] (error) in
// エラーダイアログ
let alertController = UIAlertController(title: "エラー!", message: "エラーが発生しました。", preferredStyle: .alert)
let defaultAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(defaultAction)
self?.present(alertController, animated: true, completion: nil)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/// failureブロックがオプショナルだったりしているのはサンプルだからです。本来はオプショナルではない方がいいはず
private func dummyRequestMethod(success:(()->())?, failure:((Error?)->())?){
// 例えばリクエストして失敗した
let url = URL(string:"https://jsonplaceholder.typicode.com/posts/hello")! // 404が返る
let request = URLRequest(url: url)
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate:nil, delegateQueue:OperationQueue.main)
let task = session.dataTask(with: request, completionHandler: {
(data, response, error) -> Void in
if let res = response as? HTTPURLResponse, 400...499 ~= res.statusCode {
// 今回のコードはここを通ることを期待しています
// エラーをCrashlyticsに送信
let debugInfo = XXXAPIDebugInfo(request: request, response: response, data: data)
CrashlyticsError.init(code: .xxxApiJsonInvalid, errMsg: "リクエストが400-499を返した", debuggableObj:debugInfo).sendCrashlytics()
// 開発時は落ちる、本番はスルー
assertionFailure("リクエストが400-499を返した")
// failureの呼び出し側でエラーダイアログ等を出す
failure?(error)
return
}
success?()
return
})
task.resume()
}
// MARK: - XXXAPIDebugInfo
/// XXXAPI用のデバッグクラス
struct XXXAPIDebugInfo: CrashlyticsDebuggable {
let request: URLRequest?
let response: URLResponse?
let data:Any?
// MARK: - CrashlyticsDebuggable
/// Request文字列
var requestDebugStr: String? {
get {
guard let url = request?.url, let method = request?.httpMethod else {
return nil;
}
// GET -> https://jsonplaceholder.typicode.com/posts/hello
return "\(method) -> \(url)"
}
}
/// レスポンス文字列
var responseDebugStr :String? {
get {
guard let res = response as? HTTPURLResponse, let data = data as? Data else {
return nil
}
guard let resStr = String(data: data, encoding: .utf8) else {
return nil
}
// 400 <- "{}"
return "\(res.statusCode) <- \(resStr)"
}
}
}
}
まとめ
アプリでハンドリングしたエラーもCrashlyticsに記録されるようになるので原因を調べるのが早くなるといいなぁ。
あとなんかスマートな書き方じゃない気がするのでもっとスマートに書く方法あったら教えてほしい...