マルチモジュール構成での Crashlytics の導入

# はじめに
マルチモジュール構成でCrashlyticsを導入したときに、デバッグ環境でCrashlyticsの機能が効いているか確認しようとしても、
出力されたクラッシュのスタックトレースにファイル名や行数が記載されていなかったり、クラッシュのタイトルが全て同じになってしまったりと、
期待したスタックトレースが表示されずデバッグできない状態になってしまいました。
こういったハマりがあったので、今回はマルチモジュール構成でのCrashlyticsの導入方法を記事にしてみます。

# 前提

## ツール

- IDE: Xcode16.2
- 言語: Swift5.10
- パッケージ管理ツール: SPM

## マルチモジュール構成

SPM によるマルチモジュール構成

構成は以下の通り

- TcaSampleProjct(プロジェクトバンドル)

  - TcaSampleProjct(アプリケーションターゲット)

    - dependencies
      - SampleTargetFramework(Embed)
      - SampleFramework(Embed)
      - SampleLibraryView
      - SampleLibraryModel

  - SampleTargetFramework(フレームワークターゲット)

- SPM モジュール
  - SampleFramework(動的ライブラリ)
  - SampleLibraryView(静的ライブラリ)
  - SampleLibraryModel(静的ライブラリ)

SPM モジュールの Package.swift

```swift
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "SampleLibrary",
    platforms: [.iOS(.v17)],
    products: [
        .library(name: "SampleFramework",
                 type: .dynamic,
                 targets: ["SampleFramework"]),
        .library(
            name: "SampleLibraryView",
            type: .static,
            targets: [
                "SampleLibraryView",
            ]
        ),
        .library(
            name: "SampleLibraryModel",
            type: .static,
            targets: [
                "SampleLibraryModel",
            ]
        )
    ],
    targets: [
        .target(name: "SampleFramework",
                dependencies: ["SampleLibraryView", "SampleLibraryModel"]),
        .target(name: "SampleLibraryView",
                dependencies: ["SampleLibraryCore"]),
        .target(name: "SampleLibraryModel",
                dependencies: ["SampleLibraryCore"]),
        .target(name: "SampleLibraryCore"),
        .testTarget(name: "SampleLibraryViewTests"),
        .testTarget(name: "SampleLibraryModelTests")
    ]
)
```

# やりたいこと

上述の環境で[Firebase Crashlytics]()を導入して、クラッシュしたことを Firebase コンソールから確認してスタックトレースからデバッグができるようにしたい。

# ドキュメントの通りにやってみた

Crashlytics の導入方法は[公式ドキュメント](https://firebase.google.com/docs/crashlytics/get-started?hl=ja&authuser=0&_gl=1*1p7oqu9*_ga*MTI0MzIwNTkyLjE3MzQ1ODA5MDI.*_ga_CW55HF8NVT*MTczODM4ODE5OC44LjEuMTczODM4OTAxNC41Ny4wLjA.&platform=ios)がなんだかんだで一番わかりやすいと思っているので、これをみてやってみました。

詳細は公式ドキュメントにわかりやすく載っているので、ざっくりと手順だけ説明します。

## 1. Firebase を iOS プロジェクトに導入する([ドキュメント](https://firebase.google.com/docs/ios/setup?hl=ja))

SPM で`https://github.com/firebase/firebase-ios-sdk.git`を導入するときに`firebase-ios-sdk`のどのターゲットを選択するところがあります。

Crashlytics を導入する場合は、以下ターゲットを依存させます。

- `FirebaseAnalytics`
- `FirebaseCrashlytics`

SPM で Firebase を導入する手順は、以下公式ドキュメントにも記載されていますので、詳細は以下を参照してください。

https://firebase.google.com/docs/ios/swift-package-manager?hl=ja

## 2. dSYM ファイルをアップロードする Run Script フェーズを追加する

アプリケーションターゲットのビルドフェーズの最後に、以下スクリプトを実行する Run Script フェーズを追加します。

```console
${BUILD_DIR%Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run
```

https://firebase.google.com/docs/ios/swift-package-manager?hl=ja#crashlytics

作成した Run Script フェーズの`Input Files`に以下ドキュメントに記載されている通りの設定を行います。

今回はマルチモジュールのため、アプリケーションターゲットのdSYMファイルのパスだけでなく、関係する全てのモジュールのdSYMファイルのパスを`Input Files`に追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/d4b66a0c-f765-8882-7e8d-627bfba619e0.png)

以下Firebaseの公式ドキュメントの説明の通り、Input Filesで指定したファイル以外アクセスできないようにしているため、関係するモジュールのdSYMファイルに関連するファイルやディレクトリについてもアクセスできるように指定しておきます。

>Xcode は、実行スクリプトでビルドファイルを使用できるように、これらの入力ファイルの指定場所を探します。また、ユーザー スクリプト サンドボックスが有効になっている場合、Xcode は実行スクリプトが Input Files で指定されたファイルにのみアクセスできるようにします。
>
>プロジェクトの dSYM※ ファイルの場所を指定すると、Crashlytics は dSYM※ を処理できます。
アプリのビルド済み GoogleService-Info.plist ファイルの場所を指定すると、Crashlytics は dSYM※ を Firebase アプリに関連付けることができます。
アプリの実行可能ファイルの場所を指定することで、実行スクリプトで同じ dSYM※ の重複アップロードを防ぐことができます。アプリのバイナリはアップロードされません。

https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports?hl=ja&platform=ios#check-input-files-setup

※dSYMとは？って感じですが、後続で軽く説明しますので、この時点ではCrashlyticsを導入するにあたって必要なファイルがあるというぐらいで考えてください。


# クラッシュを検知できるか試してみる

## デバッグビルドで行う方法

Crashlytics でクラッシュした情報を監視するには、dSYM というファイルを生成する必要があります。

dSYM はデバッグするために必要な情報で、これによってスタックトレースでどのファイルの何行目でクラッシュしたといったソースファイルレベルのデバッグができるようになります。
詳しくは[公式ドキュメント](https://developer.apple.com/documentation/xcode/building-your-app-to-include-debugging-information)を参照してください。

Debug Configuration ではデフォルトでは dSYM を生成するようになっていないので、アプリケーションターゲット(TcaSampleProjct)や依存するフレームワークターゲット(SampleTargetFramework)は以下ビルド設定を更新する必要がある。

[Debug Information Format](https://developer.apple.com/documentation/xcode/build-settings-reference#Debug-Information-Format) = DWARF with dSYM File

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/fd889988-f187-4ebc-774e-39406614cfbb.png)

[Firebase の公式ドキュメント](https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports?hl=ja&platform=ios#check-xcode)でも方法が記載されています。


:::note warn
このやり方では SPM の依存(SampleFramework, SampleLibraryView, SampleLibraryModel)は dSYM 生成してくれなかったです、、
Debug Configuration でも dSYM 生成できる方法知っている方教えて欲しいです、、
:::

## リリースビルドで行う方法

これに関しては、何かを行う必要はなく、追加でビルド設定をいじる必要ありません。(これまでの設定で一通りやることは終わっています)
なぜなら、上述した dSYM を生成するビルド設定は Release Configuration ではデフォルトで生成するようになっているからです。

ただ、シミュレーターでアプリを実行するとき、デフォルトではDebugビルドになっているので、
以下手順でscheme の Run の設定を Release に変更する必要があります。

[Edit Scheme] -> [Run] -> [Build Configuration]を Debug から Release に変更する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/a228f784-c553-5306-a904-ed353e93f6ef.png)

:::note warn
この設定変更はあくまで Crashlytics をローカル環境で試したい時だけ行うこと！
Release ビルドは配布用に最適化する設定になっているので、その分ビルド時間がめちゃくちゃかかりますので、開発時にビルドする分にはConfigurationはDebugの方が都合がいいです。
:::

# 困ったこと

上記の手順で、シミュレーターでクラッシュしたらCrashlyticsでクラッシュ情報が表示されるようになったかと思います。(もしクラッシュ情報が表示されなかった場合は、後に記述するトラブルシュートの記載を参考にしてみてください。)

しかし、アプリケーションターゲット(TcaSampleProjct)以外の箇所でクラッシュした時、クラッシュしたファイル名と行数が出ず、クラッシュのタイトルも原因問わず同じになってしまいました。

具体的には以下キャプチャのように、とてもクラッシュ箇所を特定できるような情報でてくれません。
クラッシュした発生したモジュールはわかりますが、具体的にどの箇所で発生したかはわからないので、いざクラッシュの原因特定しようにもこれではまともな調査はできないでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/4edec270-8cba-a738-d504-8992346e2dc8.png)

理想としては、以下のようにクラッシュ発生箇所のファイル名や行数が表示されるようになりたいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/89d0fc28-9cdb-601b-7b5f-e787f00fad02.png)

また、クラッシュのタイトルも以下のような名前に丸められてしまいます。(アプリケーションターゲット以外の依存モジュール内で起きるクラッシュは全て以下のタイトルにまとめられます、、)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/a54d122e-a97f-055c-f1b7-b0609bb7d132.png)

タイトルが丸められてしまうということは、どんなクラッシュが起きているかを分析する時に、大きな阻害となってしまいます。
例えば、以下で4件のクラッシュのタイトルがありますが、タイトルが一つのクラッシュの原因に紐づいていれば4種類のクラッシュ原因があるとわかりますが、一つのタイトルの中に複数のクラッシュの原因が混在していれば今このアプリではどれぐらいクラッシュする原因があるか特定するのは困難になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/b3632059-98bc-d6f1-1869-366f1c3b8f02.png)

そういうわけで、上記問題はアプリを運用する上で許容しかねる問題と判断したので、なんとかしたいと思いました。


# 原因

アプリケーションターゲット以外のモジュールのdSYMがCrashlyticsにアップロードされていなかったと思われます。

以下Issueでも同じ事象ぽい報告がありました。

https://github.com/firebase/firebase-ios-sdk/issues/5569

>We've encountered the same issue. It appears that upload-symbols with the —build-phase flag only uploads the dSYM for the build target, not all the dSYMs in DWARF_DSYM_FOLDER_PATH.
>
>The workaround is to call it manually with something like ${PROJECT_DIR}/../scripts/FirebaseCrashlytics/upload-symbols -d -p ios -gsp ${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleService-Info.plist ${DWARF_DSYM_FOLDER_PATH}

日本語訳

>私たちも同じ問題に遭遇しました。フラグupload-symbolsを使用すると、—build-phase内のすべての dSYM ではなく、ビルド ターゲットの dSYM のみがアップロードされるようですDWARF_DSYM_FOLDER_PATH。
>
>回避策としては、次のように手動で呼び出すことです。${PROJECT_DIR}/../scripts/FirebaseCrashlytics/upload-symbols -d -p ios -gsp ${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleService-Info.plist ${DWARF_DSYM_FOLDER_PATH}

上記Issueで、Run Scriptで設定したスクリプト(`run`)では、`${DWARF_DSYM_FOLDER_PATH}`に含まれるすべてのdSYMではなく、アプリケーションターゲットのdSYMしかアップロードしてくれないという情報がありました。

Issue内で提案されている回避方法として、`upload-symbol`を用いて`GoogleService-Info.plist`とアップロードするdSYMが配置されているディレクトリを指定するというのがあるそうです。

# dSYMをアップロードするRun Scriptを修正する

Crashlyticsの導入時に追加した以下Run Scriptを修正していきます。

```console
${BUILD_DIR%Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run
```

## 修正後のScript

```console
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols" -gsp "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleService-Info.plist" -p ios "${DWARF_DSYM_FOLDER_PATH}"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/16b3ed56-b47b-16a6-38bc-5ec7db135239.png)

Run Script修正後にシミュレーターでクラッシュを確認してみた結果、Crashlyticsで表示されるクラッシュのスタックトレースが以下です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/754e843c-0b84-266e-9e11-2299eb82f70e.png)

ご覧のように、きちんとアプリケーションターゲットから依存しているフレームワークでクラッシュしても、フレームワークのコード内のファイルと行数までわかるようになっています。

# 今回の確認でもハマりポイント

## シミュレーターでクラッシュを起こしても、Crashlyticsのページでクラッシュの情報が表示されない

自分の環境では、以下2つが原因で、Crashlyticsにクラッシュ情報が表示されないという事象が起こりました。

### アプリケーションターゲットの依存に、`FirebaseCrashlytics`が含まれていなかった。

SPMで`firebase-ios-sdk`を追加したときに、`FirebaseCrashlytics`を依存に追加しないと、Crashlyticsが機能しなかったので注意です。
アプリケーションターゲットのGeneralタブの`Framworks, Libraries, and Embedded Contents`に`FirebaseCrashlytics`が含まれていることを確認してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/9c1cbfb3-286d-781b-ac1e-7dacb70c8da1.png)

### シミュレーターでクラッシュを起こす時に、デバッグ機能を有効にしている状態だった

Runで起動したシミュレーターでそのままクラッシュを起こしても、Crashlyticsにクラッシュ情報はアップロードされません。
Runは一度停止した上で、シミュレーターから直接インストールされた対象のアプリをタップして起動した上で、クラッシュを起こすと、Crashlyticsにもクラッシュ情報が表示されるようになります。
これは、公式ドキュメントにも記載されているので確認してみてください。

https://firebase.google.com/docs/crashlytics/get-started?hl=ja&authuser=0&_gl=1*1p7oqu9*_ga*MTI0MzIwNTkyLjE3MzQ1ODA5MDI.*_ga_CW55HF8NVT*MTczODM4ODE5OC44LjEuMTczODM4OTAxNC41Ny4wLjA.&platform=ios#force-test-crash

#　おわり
Crashlyticsの導入自体はFirebaseの公式ドキュメントで詳しく説明されていますが、意外とハマりポイントも多いので、注意が必要だなと思いました。
ただ、アプリを公開して運用するならクラッシュしているかどうかの情報は必須になると思うので、導入を検討する人は多いと思います。
参考になった方が少しでもいれば幸いです。
記載内容で、誤りなどありましたらコメントよりご指摘いただけますと幸いです！

今回確認のために作ったサンプルプロジェクトをGithubのリポジトリに挙げているので、こちらもよければご参考に見ていただければと思います！

https://github.com/stotic-dev/MultiModuleiOSProjectWithCrashlytics