Timber×Crashlytics でログをクラッシュレポート送信（Crashlytics編）

# はじめに

![タイトルスライド.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215832/13b058da-f12d-374f-2461-5e2ce8396a3a.png)

日が空いてしまいましたが、後編です。

前編は「[Timber×Crashlytics でログをクラッシュレポート送信（Timber編）](https://qiita.com/vidro/items/d3473c28be4682abb7fe)」です。

https://qiita.com/vidro/items/d3473c28be4682abb7fe


前編では Timber を用いて、デバッグビルドとリリースビルドで、ログ出力を切り替える基盤を作成しました。
後編では、前編で作成した `ReleaseTree` クラスを用います。
そして、リリースビルド時は、「コンソールへログ出力**しない**」かつ「クラッシュ時にレポートを送信する」機能を組み込んでいきます。

# Firebase Crashlytics

[Firebase Crashlytics](https://firebase.google.com/docs/crashlytics?hl=ja) は、モバイルアプリで発生したクラッシュを検知して、クラッシュが発生するまでのログを送信するクラッシュレポートツールです。
クラッシュレポートはFirebaseコンソールに集約され、クラッシュの分析や調査に役立ちます。

## 導入方法

### 前提

- Googleアカウントの作成

### Firebase Crashlytics のセットアップ

セットアップに関しては、公式ドキュメントがかなり丁寧でわかりやすいので、本稿では省略します。
※ Firebaseの導入と、Firebase Crashlyticsの導入に分かれている点に注意してください。

#### Firebase

[公式 - Android プロジェクトに Firebase を追加する](https://firebase.google.com/docs/android/setup?hl=ja)

#### Firebase Crashlytics

[公式 - Firebase Crashlytics を使ってみる](https://firebase.google.com/docs/crashlytics/get-started?hl=ja&platform=android)

## プログラム変更

上記の公式手順をもとに、[前編のTimber編](https://qiita.com/vidro/items/d3473c28be4682abb7fe) からの変更点を記載します。

### build.gradle(:project)
※ プロジェクト直下
```diff_gradle:build.gradle
buildscript {
    ....
+    dependencies {
+        classpath 'com.google.gms:google-services:4.3.10'
+        classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
+    }
}
....
```

### build.gradle(:app)
※ app直下
```diff_gradle:build.gradle
....
apply plugin: 'com.android.application'
+ apply plugin: 'com.google.gms.google-services'
....
dependencies {
    implementation 'com.jakewharton.timber:timber:4.7.1'
+    implementation platform('com.google.firebase:firebase-bom:29.2.0')
+    implementation 'com.google.firebase:firebase-analytics'
+    implementation 'com.google.firebase:firebase-crashlytics'
}
```

### MainActivity.java
```diff_java:MainActivity.java
public class MainActivity extends AppCompatActivity {

-   private final String TAG = MainActivity.class.getName() + "_TAG";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

　　　　 // 後編では不要なので消しておきます
-       Log.v(TAG, "Log VERBOSE");
-       Log.d(TAG, "Log DEBUG");
-       Log.i(TAG, "Log INFO");
-       Log.w(TAG, "Log WARN");
-       Log.e(TAG, "Log ERROR");
-       Log.println(Log.ASSERT, TAG, "Log ASSERT");
-       Timber.v("Timber VERBOSE");
-       Timber.d("Timber DEBUG");
-       Timber.i("Timber INFO");
-       Timber.w("Timber WARN");
-       Timber.e("Timber ERROR");
-       Timber.wtf("Timber ASSERT");

+       Button crashButton = new Button(this);
+       crashButton.setText("Test Crash");
+       crashButton.setOnClickListener(new View.OnClickListener() {
+           public void onClick(View view) {
+               throw new RuntimeException("Test Crash"); // Force a crash
+           }
+       });

+       addContentView(crashButton, new ViewGroup.LayoutParams(
+               ViewGroup.LayoutParams.MATCH_PARENT,
+               ViewGroup.LayoutParams.WRAP_CONTENT));

    }
}
```

## クラッシュレポート

まずは公式ドキュメントに従って、クラッシュを強制的に起こし、クラッシュレポートを確認します。
※ Test Crash ボタンをタップすると、アプリが落ち、クラッシュレポートが送信されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215832/760a0761-2dd1-f804-0cfb-b54f41d0e3f9.png" width=75%>


# Timber × Firebase Crashlytics

導入が完了したところで、[前編のTimber編](https://qiita.com/vidro/items/d3473c28be4682abb7fe) で作成した `ReleaseTree` へ、Firebase Crashlytics を組み込んでいきます！

## プログラム変更

### MyApplication.java

```diff_java:MyApplication.java

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (BuildConfig.DEBUG) {
            Timber.plant(new Timber.DebugTree());
+           // デバッグビルドの場合はクラッシュレポートを送信しないように設定
+           FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false);
        } else {
            Timber.plant(new ReleaseTree());
        }
    }
}
```

:::note info
ポイント
- ビルド方法によって、Timberの設定を切り替えます
- `FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false)` の一文を書いておくことで、クラッシュレポートを送信しないように設定できます
（この設定をしないと、開発中に実行時例外が出るたびにクラッシュレポートが送信されてしまいます）
:::

### ReleaseTree.java

```diff_java:ReleaseTree.java
public class ReleaseTree extends Timber.Tree {

    @Override
    protected void log(int priority, @Nullable String tag, @NotNull String message, @Nullable Throwable t) {
        switch (priority) {
-           case Log.INFO:
-           case Log.WARN:
-           case Log.ERROR:
-           case Log.ASSERT:
-               // Crashlytics ログ出力処理を記述（Crashlytics編で記載）
-               break;
+           case Log.INFO  : recordLog("I", message); break;
+           case Log.WARN  : recordLog("W", message); break;
+           case Log.ERROR : recordLog("E", message); break;
+           case Log.ASSERT: recordLog("A", message); break;
            default:
                /*
                下記の優先度は出力なし
                ・Log.VERBOSE
                ・Log.DEBUG
                 */
                break;
        }
    }

+    private void recordLog(String prefix, String message) {
+        FirebaseCrashlytics.getInstance().log(String.format("%s: %s", prefix, message));
+    }
}
```

:::note info
ポイント
- どの優先度かを識別したいので、プレフィックスをつける関数を呼び出します
- `recordLog` 関数内の `FirebaseCrashlytics.getInstance().log()` を呼び出すことで、クラッシュレポートのログとして記録されます
:::

## MainActivity.java

```diff_java:MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
+       Timber.i("メイン画面初期処理開始");
+       Timber.d("デバッグログはコンソール出力のみ！");

        Button crashButton = new Button(this);
        crashButton.setText("Test Crash");
        crashButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
+               Timber.e("Test Crash ボタンが押されました！");
                throw new RuntimeException("Test Crash"); // Force a crash
            }
        });

        addContentView(crashButton, new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT));

+       Timber.i("メイン画面初期処理終了");
    }
}
```

:::note info
ポイント
- 初期処理の開始～終了のタイミングでINFOログ、 DEBUGログを出力します
- ボタンを押されたときにERRORログを出力します
:::

## 動作確認

それぞれのビルド方法でクラッシュを発生させます。

### デバッグビルド

#### コンソール出力

AndroidStudio のコンソールには下記のように出力されます。
```
I: メイン画面初期処理開始
D: デバッグログはコンソール出力のみ！
I: メイン画面初期処理終了
E: Test Crash ボタンが押されました！
```

#### クラッシュレポート

クラッシュレポートは送信されません。

### リリースビルド

#### コンソール出力

実装したログはコンソールに出力されません。

#### クラッシュレポート

クラッシュレポートが送信されます。
また、「ログ」タブには実装したログが出力されています！
そして `ReleaseTree` で実装した通り、デバッグログはクラッシュレポートには出力されていません😎

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215832/39547190-ff23-9516-650f-5d9782534a7e.png)

※ #2 はデフォルトで出力

# おわりに

いかがだったでしょうか。
これで、開発中のコンソール上だけで確認したい内容は **DEBUG** 、開発中とクラッシュレポートどちらでも確認したい内容は **INFO** 以上という使い分けができますね！

また、Firebase Crashlytics には、より分析しやすくするために下記のような機能もあります。

- ユーザーIDを設定
- 任意のKeyValueをログの項目として保持
- 任意のタイミングでクラッシュレポートを送信

これらを扱うような Util クラスを作成してもよさそうですね。

以上、より良いログ基盤の構築の一助になりましたら幸いです！