Firebase Crashlyticsを使って通信エラーを記録する

2 min読了の目安(約1900字TECH技術記事

モチベーション

アプリを起動すると通信に失敗したという報告をいただいたのですが、サーバーサイドには特に目立つ記録が残っておらず、何が起きているのかわからないという状況が発生しました。

クライアント側はクラッシュなどの致命的な問題はAppStore ConnectやCrashlyticsから確認できますが、ネットワークなど環境に依存し、さらにログが残らない問題に対しては再現から解決までがとてもやりにくいです。

そこで、こういった非致命的ではあるが解決しづらい問題に対処するべく、Firebase Crashylticsを使ってエラーを記録することにしました。

Firebase Crashlyticsについて

Crashlyticsといえばアプリのクラッシュ情報を把握できるサービスですが、クラッシュ以外にもエラーやログを送信することができます。

送信されたエラー情報はクラッシュログ同様、Crashlytics上で確認ができます。具体的には以下の情報が閲覧できます。

  • エラー発生時のスタックトレース
  • エラーコード、ドメイン、詳細(userInfoの中身)
  • 問題が起きたデバイス・OS情報

これだけ情報が得られると何が起きていたのかがはっきりするため、問題解決もしやすくなりそうです。

エラーを記録する

※この記事ではFirebaseは既に導入済みであることを前提としています。

エラーを記録するには、recordメソッドにErrorを渡します。

Crashlytics.crashlytics().record(err: Error)

このメソッドを呼ぶとCrashlyticsは1セッションで最大8件までエラーを保持します。8件を超えると古いものから失われていきます。

また保持されたエラーはアプリの次回起動時のタイミングで反映されます。リアルタイムではないことに注意してください。

エラーを定義する(Optional)

Crashlyticsは記録されたエラーのdomainとcodeを元にグルーピングを行います。

通信エラーの場合いろいろな発生要因がありますが、ここでは以下のような感じでざっくり定義しました。

enum APIError: Int, Error {
    var _domain: String {
        return "apiError"
    }
    
    case deviceOffline = -101
    case invalidRequestParameter = -102
    case invalidResponseBody = -103
    ...
}

あとは、扱いやすいようにNSErrorにイニシャライザを生やしたりしてよしなに記録します。

extension NSError {
    convenience init(error: Error, userInfo: [String: Any]?) {
        let err = error as NSError
        self.init(domain: err.domain, code: err.code, userInfo: userInfo)
    }
}

// エラーを作成し報告する
let error = NSError(
    error: APIError.deviceOffline,
    userInfo: [
        "error-description": "hogehoge"
    ]
)

Crashlytics.crashlytics().record(error: error)

あとがき

Crashlyticsを用いることでサーバー側だけではみえないクライアントのエラーを検出することができました。
今回はAPIや通信周りを対象としましたが、実際に利用する場合は最初に起こりうるエラーを洗い出し、何を非致命的なエラーとするかを明確に決めた方が良さそうです。

またエラー報告はCPUとI/Oコストを消費するため、なんでもかんでも報告するのも避けましょう。

参考

Firebase Crashlytics のクラッシュ レポートのカスタマイズ