FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

Firebase Crashlytics を Crash report 以外で活用しているお話

みなさん、こんにちは。

ピクトリンク事業部 商品技術開発部 の 足立 です。

約1年ぶりの記事の投稿になるそうで、記事の書かなさすぎにちょっとだけ驚愕しています 🫢

今回は、テックブログリレーを開催することになり、2日目を担当させていただきます。

記事の内容としては、担当しているプロジェクトの中で Firebase Crashlytics の 致命的ではない例外報告 を活用しているので実例などを踏まえ紹介していきたいと思います。

Firebase Crashlytics とは

Google が提供している mBaaS の一つで、アプリのクラッシュ情報をリアルタイムで集計・分析し、クラッシュの原因や影響度を特定するために活用されるサービスです。

ライブラリを導入し、設定ファイルを読み込ませ初期化処理を たった1行 書くだけで、特別な実装などせずにアプリのクラッシュ情報を収集できるようになります😍スゴイ!

firebase.google.com

「Firebase Crashlytics の 致命的ではない例外報告」 とはなにか?

前述の通り Firebase Crashlytics はアプリのクラッシュ情報を収集・分析を行うためのサービスになります。

そのため、集計対象のデータは「クラッシュした情報 = 致命的なエラーの情報」となります。

クラッシュが発生するまで集計もされず、集計処理自体も自動でされるので「送りたい情報」を任意のタイミングで送ることはできません。

そこで利用すると良いのが「致命的ではない例外報告」になります。

クラッシュした情報としてではなく「エラー情報の収集」や「特定処理における状態を収集」など使い方はさまざまですが、任意のタイミングで Firebase Crashlytics にデータを送信することができます。

私が担当していたピクトリンクフォトでは主に「エラー」や「ワーニング」などを送り、私たちの目の届かないところで起きている、意図しない状態(エラーが発生して処理が中断するなど)が発生しているのか?どれくらい発生しているのか?を検知するために利用しています。

ピクトリンクフォトでの活用例

ピクトリンクフォトはいわゆるクラウドストレージサービスのため、端末にある画像・動画データを扱います。

アップロード及びダウンロード処理では、画像や動画データを適切な状態に変換させているため、以下のような様々な意図しない状態に遭遇することも多々あります。

  • 処理対象のデータが意図しないデータ形式だった
  • パケットロスによりダウンロードデータの欠損
  • 端末の空き容量が少なく処理が中断する
  • なんかよくわんないんだけど、処理が中断している

などなど、多岐にわたります。

開発者であれば実機転送を行い、エラーの原因など確認することはできますが、リリースされたアプリでは不可能です。

ユーザーが利用しているアプリの中で何かしらのエラーが発生し、問い合わせしていただいても「動きません。表示されません。」という内容が多いです。

どんなタイミングでどんな操作をしていて、端末の状態はどんなものだったかが一切わかりません。

この問題の原因調査を行うための手段として「Firebase Crashlytics の 致命的ではない例外報告 」を以下の方針に絞り実装しています。

  • 根幹機能の中で一連の流れを阻害する状態が発生しそうな部分
  • 再現性の低い挙動が関連しそうな部分
  • ユーザーからの問い合わせで、原因が不明だが関連しそうな部分

実装例

「Firebase Crashlytics の 致命的ではない例外報告 」の基本的な実装は以下のようになります。

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

「例外報告」とあるように基本的には「エラーの情報」を記録するためのI/Fになっています。

このまま使うだけでも良いのですが、もう少し使いやすくするために、以下のような構成にしています。

final class CrashlyticsLogger {
    enum RecordType {
        case error(Error)
        case warning(LoggerWarningType)
    }

    enum LoggerWarningType: String {
        case memoryWarning
        case network

        var domain: String { /** 省略 */ }
        var code: Int { /** 省略 */ }
        }
    }

    static func recordLog(_ type: RecordType, message: String? = nil, userInfo: [String: Any]? = nil, _ file: String = #file, _ function: String = #function, line: Int = #line) {
        var additionalInfo: [String: Any] = [:]
        if let userInfo {
            additionalInfo = additionalInfo.merging(userInfo) { a, _ in a }
        }

        if let message {
            additionalInfo["message"] = message
        }

        let domain: String
        let code: Int
        let info: [String: Any]

        switch type {
        case let .error(error):
            let nsError = error as NSError
            domain = nsError.domain
            code = nsError.code
            info = nsError.userInfo.merging(additionalInfo) { a, _ in a }
        case let .warning(warn):
            domain = warn.domain
            code = warn.code
            info = additionalInfo
        }

        let recordError = NSError(domain: domain, code: code, userInfo: info)
        Crashlytics.crashlytics().record(error: recordError)
    }
}

簡単に説明を書いていきます

RecordType

「例外報告」として記録したいものを使う側が選べるように定義。

  • error: 発生したエラーをそのまま記録
  • warning: エラーではないが今後の調査などで使う情報を記録。
LoggerWarningType

エラー以外の情報を記録する場合に、実装方針から外れ不用意に乱用させないために定義。

  • memoryWarning: メモリ不足関連
  • network: ネットワーク通信関連
recordLog(_:message:userInfo:)

パラメータに応じて、必要な情報を組み立て Crashlytics にデータ送信を行う Crashlytics.crashlytics().record(error:) のラッパーメソッドになります。

  • 発生したエラーをそのまま記録
do {
    // 省略
} catch {
    CrashlyticsLogger.recordLog(.error(error))
}
  • エラーではないが今後の調査などで使う情報を記録
func session() {
    // 省略

    CrashlyticsLogger.recordLog(.warning(.network),
        message: "通信処理の遅延が閾値以上",
        userInfo: [
            "request_time": String(describing: requestTime),
            "response_time": String(describing: responseTime),
            "network_speed": String(format: " %.2f Mbps", speed) + 
        ]
    )
}

Firebase Crashlytics Console での見え方

送ったデータがコンソール上でどう表示されるかを見ていきます。

スタックトレース情報

基本的な表示はクラッシュ情報と同じフォーマットです。

Domain や Code(エラーコード) は記録する際に任意に設定した内容になります。

キー情報

一緒に送ったエラー以外の情報などの表示が確認できます。

数値などを確認できるようになるため、アプリ(ユーザー)ごとのエラーの発生状況・状態などを確認し、原因調査をする際の仮説立てや検証に使います。

実際の活用事例の紹介

いくつかある活用事例なかで、2つほどですが簡単に紹介します。

業務に関わるところはちょっとぼかしつつふんわり書いていますが、イメージだけでも掴めてもらえれば幸いです。

データ一覧画面でサムネイルが表示されない

サムネイルの取得・表示をするために、専用の認証情報を使っていました。

有効期限の設定もしており、有効期限がきれる直前に再取得を行い新たな認証情報をつかってサムネイルの取得・表示を行っていたので「認証情報が正しく使われていない可能性」「サムネイル取得で関係ないエラーが発生している可能性」などの仮説を立てつつ、以下の情報を収集し検証を行いました。

  • サムネイルを表示した時間
  • 認証情報の有効期限
  • サムネイル取得結果のエラー情報
データがアップロードされない

クラウドストレージサービスですので、写真や動画をアップロードしてもらう機能があります。

アップロード機能は根幹機能になるので、初めからある程度のワーニングログの収集はしていました。

  • 処理ID
  • 処理状況コード
  • 対象データの容量
  • ログインユーザーの状態
  • クラウドストレージの状態

他にもありますが、これらの情報を使い「処理がどこまで動いているのか」「ユーザー・クラウドストレージの状態による処理の中断」などの検証を行いました。

さいごに

今回はターゲットを「致命的ではない例外報告」を使った事例を紹介させていただきました。

今まで使っていなかった方は、ぜひこの記事をきっかけに導入してみてはいかがでしょうか?

今後は、まだ使えていない機能である「カスタムキー」や「カスタムログメッセージ」なども導入し、クラッシュ・原因不明の動作などを素早く解決できる状態を目指したいと思います。