iOS - CrashlyticsのrecordErrorでカスタムログを送る

# 概要

アプリでハンドリングしたエラーを記録するための仕組みとして、[Crashlytics](https://fabric.io/)の`recordError`が使えたので備忘録として残しておきます。


サンプルは[CrashlyticsLogSample](https://github.com/TakkuMattsu/CrashlyticsLogSample)にあげてあります。

## 前置き
今開発しているアプリでは、アプリでハンドリングしたエラーは`assert`+エラーダイアログで対応していました。
つまり開発中はアプリがクラッシュし、本番ではエラーダイアログが出る仕組みになっています。
ただこの方法だと何故エラーになったのかは再現してみないとわからないという問題がありました。
(*サーバ側のエラー(500エラー等)はサーバ側のログに残っているのですぐ調査できるのですが、200を返しているがレスポンスのJsonが不正だったりするパターンでは原因究明に時間がかかっていました。*)

## 方法

- [iOSアプリでCrashlyticsにカスタムログを送信する方法](http://qiita.com/yo1106/items/08d99d4e2ed83903485e)を参考にさせてもらいました。

上記の記事にあるように`Crashlytics.sharedInstance().recordError()`を利用するのでNSErrorの拡張クラスを作成します。

```swift
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を作成します。

```swift
/// エラーログ用の拡張
protocol CrashlyticsDebuggable {
    /// Request文字列
    var requestDebugStr: String? { get }
    /// レスポンス文字列
    var responseDebugStr :String? { get }
}
```

実際に利用するクラスの例です。
`XXXAPIDebugInfo`というクラスがCrashlyticsのログ用のモデルになります。
今回は`CrashlyticsDebuggable`に**Request**と**Response**の情報を表示するような制約にしていますが、ここの部分を増やしていき`CrashlyticsError`に手を入れればいろんな場合のログに対応できるかと思います。(自分ははRequestパラメータ等を追加して使っています)

```swift
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に送信される
![image.png](https://qiita-image-store.s3.amazonaws.com/0/56178/39db1e2d-0992-29f8-2714-ca1aa454af5d.png)

Crashlyticsでの表示(実際の開発のエラー)
<img width="975" alt="スクリーンショット 2017-09-17 16.31.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/56178/afdadb36-8794-5654-5cc6-96c7b52c9d1a.png">


# まとめ

アプリでハンドリングしたエラーもCrashlyticsに記録されるようになるので原因を調べるのが早くなるといいなぁ。
あとなんかスマートな書き方じゃない気がするのでもっとスマートに書く方法あったら教えてほしい...