[iOS] Firebase Crashlyticsを設定して使用してみる

# はじめに
[Firebase Crashlytics](https://firebase.google.com/docs/crashlytics?hl=ja)は、リアルタイムのクラッシュレポートツールであり、アプリの品質を保つための問題特定に役立ちます。
簡単に導入できるためアプリに導入してみたので手順をまとめました。
内容が間違っていたり、良い使用ケースなどを知っている方がいれば、ぜひ教えていただけると幸いです。

# 導入手順
####1. Firebaseをプロジェクトに追加する。
ここでは説明は省きます。
まだ追加していない方は[こちら](https://firebase.google.com/docs/crashlytics/get-started?hl=ja&platform=ios)をご参考。

####2. Googleアナリティクスを有効にする (推奨)。
クラッシュに遭遇していないユーザー数の表示、パンくずリストのログ、ベロシティ アラートなどの機能を利用するには、Firebase プロジェクトで Google アナリティクスを有効にする必要があります。
既存の Firebase プロジェクトで Google アナリティクスが有効になっていない場合は、Firebase コンソールで、`settings` > `[プロジェクトの設定]` の `[統合]` タブで Google アナリティクスを有効にできますが、プロジェクト作成時に有効にしている場合は不要です。

####3. Firebase コンソールの [Crashlytics] を有効にする
ここでは説明は省きます。
まだ有効にしていない方は[こちら](https://firebase.google.com/docs/crashlytics/get-started?hl=ja&platform=ios)をご参考。

####4. アプリに Firebase Crashlytics SDK を追加する
[ドキュメント](https://firebase.google.com/docs/crashlytics/get-started?hl=ja&platform=ios)ではCocoaPodsの使用を推奨しているので、ここでもCocoaPodsを利用します。

```:Podfile
pod 'Firebase/Crashlytics'

# Google Analyticsの機能を利用するために推奨されています
pod 'Firebase/Analytics'
```
pod installして、.xcworkspaceファイルを開いてFirebaseをインポートします。

```Swift
import Firebase

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        return true
    }
}
```

####5. Crashlyticsを初期化する
Crashlytics を初期化するために、実行スクリプトをプロジェクトの`Build Phase`に追加します。
実行スクリプトを使用すると、アプリがクラッシュするたびにXcodeが自動的にプロジェクトの`dSYMファイル`をアップロードしてくれるため、Crashlyticsは自動的にクラッシュレポートを生成できます。
Xcodeのプロジェクトの`[Build Phases]`タブを選択し、`+` > `[New Run Script Phase]`を選択し、`Run Script`に以下を記入します。

```
${PODS_ROOT}/FirebaseCrashlytics/run
```

次に、アプリのdSYMファイルとInfo.plistの場所を指定するために以下の２つを`input Files`に記入します。

```
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}
$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/2ddd16e0-cefe-30ed-2817-66c973ea8477.png" width="600">

####6. Crashlyticsのテスト
実際にCrashlyticsが設定されているかをテストする必要があるため、アプリを実行して強制的にクラッシュさせます。
[ドキュメント](https://firebase.google.com/docs/crashlytics/test-implementation?authuser=0&platform=ios)では、以下のコードを追加して強制的にクラッシュさせています。
(ボタンの位置と背景色は調整しています)

```swift:ViewController.swift
import UIKit
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view, typically from a nib.

        let button = UIButton(type: .roundedRect)
        button.frame = CGRect(x: 50, y: 100, width: 100, height: 30)
        button.backgroundColor = .red
        button.setTitle("Crash", for: [])
        button.addTarget(self, action: #selector(self.crashButtonTapped(_:)), for: .touchUpInside)
        view.addSubview(button)
    }

    @IBAction func crashButtonTapped(_ sender: AnyObject) {
        fatalError("Crashlytics test")
    }
}
```

これでアプリを実行して、上記コードで追加したボタンを押してアプリをクラッシュさせるだけですが、**いくつか注意点**があります。

- Build Settingsからデバッグ情報の形式を変更する必要があります  
プロジェクトの`[Build Settings]` タブの`[All]`タブを選択し、検索窓から**「debug information format」**を検索して、`[Debug Information Format] `設定を **`DWARF with dSYM File`**に設定する。

![FBCrashlytics2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/b481362e-f8f7-cca3-ae3c-66bef69469d6.png)

- ビルドしたときにデバッグする場合、Crashlyticsはクラッシュを拾えません
前述したコードで追加したボタンを押してアプリをクラッシュさせる際に、このボタンをデバッガを使用せずに動作させる必要があります。
理由は、最初にRunしたときの初期インスタンスにはCrashlytics の動作を妨げるデバッガが含まれていることによるらしいです。
方法としては簡単で、
 1. Xcodeで一度`[Run]`を押してシミュレータまたは実機でアプリを立ち上げる
 2. 次に`[Stop]`を押してアプリを停止させる
 3. シミュレータまたはデバイスでアプリを開き、追加したボタンを押してクラッシュさせる
 4. 再度アプリを開くとCrashlytics API によってクラッシュが報告される

Crashlyticsに報告されるタイミングが、クラッシュしたときではなく、次にアプリを開いたときであることは頭に置いておいたほうが良さそうですね。

- Firebaseのコンソールに反映されるまで5分ほどかかる  
個人的に5分以上かかった気がしましたが、少し気長に待てばちゃんと反映されます(笑)

# クラッシュレポートのカスタマイズ
使いこなせば便利そうなものもある一方、具体的な使い道が分からないものもあるので今後も追記していきたいです。
(詳しい方に教えて欲しいです笑)

#### カスタムキーの追加
Key-ValueのセットをCrashlyticsに設定することで、キーを使用してクラッシュレポートを検索、フィルターできます。

```swift
Crashlytics.crashlytics().setCustomValue(300, forKey: "int_key")
Crashlytics.crashlytics().setCustomValue("300", forKey: "str_key")
```

既存のキーの値を更新するには、以下のように同じキーを指定して値を設定するだけです。

```swift
Crashlytics.crashlytics().setCustomValue(300, forKey: "int_key") //前
Crashlytics.crashlytics().setCustomValue("200", forKey: "int_key") //後
```

配列を使用して、一括で複数のKey-Valueペアを設定することもできます。

```swift
let keysAndValues = [
                 "str_key" : "string1",
                 "str_key2" : "string2",
                 "bool_key" : true,
                 "bool_key2" : false,
                 "float_key" : 1.41,
                 "float_key2" : 1.73
                ] as [String : Any]

Crashlytics.crashlytics().setCustomKeysAndValues(keysAndValues)
```

コンソールの`イベント`に集計されます。
上手く使用できればデバッグに役立ちそうなので、状況によっては使ってみたいです。 (使用するイメージができないので、こんなケースで使えるよという使用例をぜひ知りたいです。)

#### カスタムログメッセージを追加
Crashlyticsログをアプリに追加することで、Crashlyticsでログを確認できる。

```swift
Crashlytics.crashlytics().log("button tapped!, \(className)")
```

getVaList() の呼び出しから返される値をフォーマットするログを出す場合は、

```swift
Crashlytics.crashlytics().log(format: "%@, %@", arguments: getValist["button tapped!", \(className)])
```

のようにも書けます。
これらのメソッドは、ユーザーからのアクションを受け取ったときや画面遷移、エラーが発生する可能性がある箇所に使用すると問題特定に役立てそうです。
log()メソッドで送信した文字列は、`イベント`の`[ログ]`タブに記録されます。
log()メソッドにクラス名やアクション名を渡すと、ログが下記画像のように記録されるのでクラッシュの原因が特定しやすくなるのかなと思い使用してみました。
![スクリーンショット 2021-08-12 21.49.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/df95d77d-279b-4c1b-bacd-b297db10002b.png)

#### ユーザーIDの設定
どのユーザーがどのクラッシュを発生したかを特定できるように、setUserID()メソッドが用意されています。

```swift
Crashlytics.crashlytics().setUserID("userId")
```

ログインしたタイミングなどでuserIdを設定すると、誰がクラッシュを起こしたか分かって良さそうです。

#### 致命的でない例外を報告する
致命的でないエラーを記録するにはrecordError()メソッドを使用できます。
このとき、ErrorではなくNSErrorオブジェクトが記録されます。

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

NSErrorのロギングでは、CPU と I/O の使用量が高くなりパフォーマンスに影響が出る場合があるので、使用する対象はある程度決めておく必要があります。
[ドキュメント](https://firebase.google.com/docs/crashlytics/customize-crash-reports?hl=ja&platform=ios#add-keys)でNSErrorをロギングする際の注意点やパフォーマンスの問題に言及しているため、一度目を通ししておくと良さそうです。

#「dSYMファイルが不足している」と表示されたときの対処
しばらく使用してコンソールでCrashlyticsを確認すると、「dSYMファイルが不足している」と表示されていました。
[ドキュメント](https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports?hl=ja&platform=ios)でも言及されていますが、自分が試したことを残しておきます。
>ビットコードの構成やコンソールの接続が原因で、Crashlytics がアプリの dSYM を見つけられない場合があります。

#### 試したこと
- 前述した内容ですが、`BuildSettings`の`Debug Information Format`を`DWARF with dSYM file`に設定してあることを再確認しました。
- ビルドの最後にCrashlyticsが走るようにしました (きちんとドキュメントに書かれています)
Run Scriptを確認しましたが、ちゃんと最後に書かれていました。
![スクリーンショット 2021-08-12 22.00.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/e711215b-b934-19f6-86e8-2cb30267ed25.png)

- 手動でdSYMファイルを見つけてアップロードする方法が[ドキュメント](https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports?platform=ios&authuser=0#locate)に示されているので、それに従いました。
具体的には以下をターミナルで実行しただけです。これでCrashlyticsにdSYMファイルをアップロードして警告が消えました。

```
/path/to/pods/directory/FirebaseCrashlytics/upload-symbols -gsp /path/to/GoogleService-Info.plist -p ios /path/to/dSYMs
```

upload-symbols、GoogleService-Info.plist、dSYMsのpathをそれぞれ指定する必要があります。
dSYMファイルはXcodeのDerivedDataのプロダクトフォルダ内を探すと見つかりました(デフォルトらしい)。

#最後に
簡単にCrashlyticsをアプリに導入できました。
Crashlyticsのレポートは、上手くカスタマイズすれば非常に役立ちそうなので今後使用して気づいた点は追記していきます。

#参考文献
この記事は以下の情報を参考にしました。

- [Firebase Crashlytics](https://firebase.google.com/docs/crashlytics?hl=ja)