[Flutter x Firebase] Crashlyticsと連携してクラッシュレポートを取得する
やること
- 前回作成したソーシャルログイン(Google)機能付きカウンターアプリをFirebaseのCrashlyticsと連携させて、クラッシュレポートを取得できるようにする
- iOSのみ
前回
※ 前回作った認証付きのカウンターアプリを使うが、FirebaseとFlutterアプリの連携が済んでいれば認証機能はなくてもCrashlyticsの機能を使うことはできる(と思う)
成果物
大きな動作は前回・前々回と変わらないので割愛。
ソースコード
使い方
参考ページ: Crashlytics | FlutterFire
準備
パッケージのインストール
~~
dependencies:
firebase_crashlytics: ^0.2.2
~~
エラー対処ログ
firebase_crashlyticsを追加してからビルドするとエラーになる
firebase_crashlytics
をpackage.yaml
に追加してビルドしようとしたら、エラーが発生。
pod install
を実行したところ、さらに次のようなエラーが。。。
pod update
を実行したら正常にビルドできた。
参考記事: ios - CocoaPods could not find compatible versions for pod "Firebase/CoreOnly" - Stack Overflow
Crashlyticsを有効にする
ステップ 1: Firebase コンソールで Crashlytics を設定する
- Firebase コンソールの左側のナビゲーション パネルにある [Crashlytics] をクリックします。
- Firebase プロジェクトに複数のアプリが登録されている場合は、コンソールの上部バーにある [Crashlytics] の横にあるプルダウンから追加したアプリを選択します。
- [Crashlytics を有効にする] をクリックします。
iOS用の設定
4番目の項目は任意なのでやらなくてもOK。
- From Xcode select Runner from the project navigation.
- Select the Build Phases tab, then click + > New Run Script Phase.
- Add ${PODS_ROOT}/FirebaseCrashlytics/run to the Type a script... text box.
- Optionally you can also provide your app's built Info.plist location to the build phase's Input Files field: For example:
$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
ポイント
- Crashlyticsの仕様を知る
- 意図的にクラッシュさせてテストする
- ログを仕込む
- Uncaught errorをハンドリングする
Crashlyticsの仕様
クラッシュしたとき
ではなく、クラッシュ後、再度アプリを起動したとき
にクラッシュレポートが送信される。
→クラッシュレポートが送信されるかのテストを行う際は注意が必要。
To send report data to Crashlytics, the application must be restarted. Crashlytics automatically sends any crash reports to Firebase the next time the application is launched.
意図的にクラッシュさせる
さーて、準備も終わったしクラッシュさせてみるか〜、と思ったが、クラッシュってどうやって発生させるの?という疑問が。。。
心配無用!!ちゃんとクラッシュを発生させるメソッドが用意されています・・・!
以下のコードをクラッシュさせたいところに差し込めばOK。
FirebaseCrashlytics.instance.crash()
今回は、完全にダミーボタンと化しているTwitterサインインボタンとGithubボタンを押下したときにクラッシュするようにしてみた。
SignInButton(
buttonType: ButtonType.twitter,
onPressed: () {
print('click');
FirebaseCrashlytics.instance.crash();
},
),
SignInButton(
buttonType: ButtonType.github,
onPressed: () {
print('click');
FirebaseCrashlytics.instance.crash();
},
),
いずれかのボタンを押下すると、アプリが落ちる。
アプリを再起動することでクラッシュレポートが送信されるので再起動を忘れずに。
Firebaseコンソールで確認してみる
コンソールを確認してみると、以下のエラーが発生。
dSYMが不足していると表示されてクラッシュレポートが反映されない
クラッシュ発生後、Firebaseのコンソールを確認すると、「dSYMが不足しているよ」と言われ、クラッシュレポートが反映されないといったことがあった。
dSYM自体はApp Store Connect経由でダウンロードできるらしいが、以下のようにXCodeの設定を変えればとりあえずざっくりとしたクラッシュレポートは見られるようになるっぽい。
XcodeのBuild Settings > Build Options > Debug Information Formatを DWARF with dSYM File にします。
引用元: [Firebase][iOS] Firebase CrashlyticsにdSYMをアップロードしたい! | Developers.IO
上記対処を実行後、再度クラッシュを発生→再起動してコンソールを確認すると、以下のような赤枠で囲ったクラッシュレポートが登録されているはず。(図はいろいろ試した後に撮ったものなので、他のレポートが含まれている)
ログを仕込む
参考記事: Firebase Crashlytics のログを活用する - Qiita
上記でクラッシュレポートを取得できることが確認できたが、ちょっと見てもどこでクラッシュが起きているかがわかりにくい。
例えば、以下のようにクラッシュしそうなところにそれぞれ異なるログを仕込んでおけば、どこでクラッシュが起こったのかがわかり易くなる。
SignInButton(
buttonType: ButtonType.twitter,
onPressed: () {
print('click');
FirebaseCrashlytics.instance.log("Twitter sign in");
FirebaseCrashlytics.instance.crash();
},
),
SignInButton(
buttonType: ButtonType.github,
onPressed: () {
print('click');
FirebaseCrashlytics.instance.log("Github sign in");
FirebaseCrashlytics.instance.crash();
},
),
このように、Githubのサインインボタンでクラッシュがおきたとわかる。
例外をキャッチする
これまでの設定で、アプリがクラッシュしてしまったときのレポートは送信されるようになったが、Crashlyticsでは プログラム内でキャッチできていないエラーが起きた際もレポートとして送信することができる。
FlutterError.onErrorをFirebaseCrashlytics.instance.recordFlutterErrorでオーバーライドすることにより、Flutterフレームワーク内でスローされたすべてのエラーを自動的にキャッチします。
By overriding FlutterError.onError with FirebaseCrashlytics.instance.recordFlutterError, it will automatically catch all errors that are thrown within the Flutter framework.
以下のように、runApp
する前にFlutterError.onError
をFirebaseCrashlytics.instance.recordFlutterError
でオーバーライドするだけ。
void main() {
// Pass all uncaught errors from the framework to Crashlytics.
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
runApp(MyApp());
}
例えば以下のような場合、hoge()
内で発生したFormatException
のレポートはCrashlyticsに送信されるが、hogeFuture()
で発生したFormatException
は送信されないことがわかった。
~~
SignInButton(
buttonType: ButtonType.github,
onPressed: hoge,
),
SignInButton(
buttonType: ButtonType.yahoo,
onPressed: hogeFuture,
)
],
),
),
),
);
}
// jsonTextがjson文字列ではない場合、FormatExceptionが投げられる
void parseToJson(String jsonText) {
json.decode(jsonText);
}
// こっちで発生するFormatExceptionはCrashlyticsに送信される
void hoge() {
FirebaseCrashlytics.instance.log("Not future");
parseToJson('aaaa');
}
// こっちは送信されない
Future<void> hogeFuture() async {
FirebaseCrashlytics.instance.log("Future");
parseToJson('bbbb');
}
}
非同期関数内で発生するエラーを捕捉したい場合は、以下のようにrunZonedGuarded
で囲めばOK。
Future<void> hogeFuture() async {
FirebaseCrashlytics.instance.log("Future");
await runZonedGuarded<Future<void>>(() async {
parseToJson('bbbb');
}, FirebaseCrashlytics.instance.recordError);
}
コードの修正
最後に、FirebaseCrashlytics.instance.crash()
をそのまま残しておくのは気持ち悪いので、クラッシュが起こりそうなところにログを仕込むように修正する。
今回はログイン、ログアウト処理が呼ばれた際にログが出力されるように修正した。
また、 Google以外のサインインボタンが押されたときにUnimplementedError
を発生させ、Crashlyticsにレポーティングするように修正した。
class AuthModel extends ChangeNotifier {
~~
Future<bool> login(ButtonType type) async {
// ログを追加
FirebaseCrashlytics.instance.log("Login: ${type.toString()}");
// エラーを追加
if (type != ButtonType.google) {
throw UnimplementedError(
'Unimplemented login button was tapped.: ${type.toString()}',
);
}
~~
}
Future<void> logout() async {
// ログを追加
FirebaseCrashlytics.instance.log("Logout");
~~
}
Future<UserCredential> _signInWithGoogle() async {
// ログを追加
FirebaseCrashlytics.instance.log("Login with google");
~~
}
Future<void> _signOutWithGoogle() async {
// ログを追加
FirebaseCrashlytics.instance.log("Logout with google");
await GoogleSignIn().signOut();
}
}
Future<bool> _login(BuildContext context, ButtonType type) async {
return await runZonedGuarded<Future<bool>>(() async {
bool loggedIn = false;
EasyLoading.show(status: 'loading...');
if (await context.read<AuthModel>().login(type)) {
loggedIn = true;
}
EasyLoading.dismiss();
return loggedIn;
}, FirebaseCrashlytics.instance.recordError);
}
Discussion