こんにちは。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報告の詳細を知ることができます。[鍵]タブをクリックするとuserInfo
のkey, 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!
ミラティブではユーザーにより価値を届けるためにチームを巻き込んで共に成長していける方を募集中です!