Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

Firebase Crashlyticsを用いたError検知のすすめ

こんにちは。shogo4405です。本エントリーは、Firebase Crashlytics(以下Crashlytics)を利用しているiOSエンジニア向けにError情報の保存および活用についてのミラティブ社の事例を紹介したいと思います。

Errorの収集

ここで言うError情報とは、protocol Error : Sendableのことを指しています。Mirrativでは、主に次のError情報を収集してアプリケーションの品質向上につなげています。

  • URLSession#dataTaskでコールバックで得られるError
    • 通信に関わるエラーを検知する用途
  • DecodableのDecodingError
    • クライアントとサーバー間でデータ交換がうまくいっているか検知する用途
  • try AVAudioSession.shared.setCategoryでスローされるError
    • 音声系統が意図通りに変更できたかを確認する用途

Crashlytics への報告

さてErrorレポートのソースコード側をみていきましょう。Crashlyticsのドキュメントによれば、次のメソッドで報告することができます。加えてNSError.init(domain: NSCocoaErrorDomain, code: -1001, userInfo: userInfo)のErrorのdomainおよびcode毎に分類して表示するという記載があります。

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

Mirrativではどのコード変更に伴うエラー情報なのかを Crashlytics上で表示しやすくするため、クラッシュの分類と同じになるように、domainにはソースコードファイル名。codeには行番号をセットして報告する運用を行なっています。この方法ではオリジナルのError情報が欠落するためuserInfoにError情報を展開してあります。

var userInfo: [String: Any] = [:]

// CustomNSError非継承のErrorではuserInfoが取得できないのでリフレクションで取る
let mirror = Mirror(reflecting: originalError)
userInfo["type"] = String(describing: type(of: error))
for child in mirror.children {
  if let label = child.label {
    userInfo[label] = String(describing: child.value)
  }
}

let error = NSError(domain: filename(#filename), code: #line, userInfo: userInfo)
Crashlytics.crashlytics().record(error: error)

// #filename はフルパスで取得になるためディレクトリは除外する
private func filename(_ file: String) -> String {
   file.components(separatedBy: "/").last ?? file
}

Crashlytics上の表示

実際のアプリケーション上で送ると、 Crashlyticsのダッシュボードに次のように表示されます。

各々項目をクリックするとError報告の詳細を知ることができます。[鍵]タブをクリックするとuserInfokey, value値を確認することができます。

Errorの集計と分析

Crashlyticsのダッシュボードの表示で、Error情報のメンバー間での共有を行う。詳細の確認といったことはできると思います。一方でより詳細に分類をしたい。どのようなErrorが発生している個別に集計確認したいケースがあります。

CrashlyticsとBigQueryとの連携行いSQLベースで集計によって解決しています。どのようなテーブル構造なのかは、こちらのページで確認することが可能です。また、Error情報は次のようにマップされます。

カラム Error 備考
issue_type domain
issue_subtitle code
custom_keys userInfo JSON型

サンプルとしてErrorのuserInfo[任意のキー]をissue_title, issue_subtitle毎に集計するクエリーを掲載します。BigQueryではJavaScriptを使えるのでエンジニアフレンドリーで便利ですね。

CREATE TEMP FUNCTION e(x STRING, k STRING)
RETURNS STRING
LANGUAGE js AS r""" 
    var data = JSON.parse(x)
    for (var i = 0; i < data.length; ++i) {
        if (data[i].key == k) {
            return data[i].value
        }
    }
    return null
""";
SELECT
  e(TO_JSON_STRING(custom_keys), 'type') as type,
  issue_title,
  issue_subtitle,
  COUNT(*) as total
FROM
  -- xxxxxxのところは、アプリのBundleIDになります。
  `firebase_crashlytics.xxxxxxxxxxxx_IOS`
WHERE
  e(TO_JSON_STRING(custom_keys), 'type') IS NOT NULL
  AND DATE(event_timestamp, "Japan") >= DATE_SUB(CURRENT_DATE("Japan"), INTERVAL 14 DAY)
GROUP BY type, issue_title, issue_subtitle

むすびに

Mirrativでは、このようにクライアントのErrorを定量的にデータを収集しながらサービスの品質向上に努めています。クライアント領域で発生するDecodingErrorの検知は特に有益であると感じており、画面が意図通りに表示できているか、否かの判断する材料になっています。

We are hiring!

ミラティブではユーザーにより価値を届けるためにチームを巻き込んで共に成長していける方を募集中です!

www.mirrativ.co.jp

speakerdeck.com