[Flutter] Firebase Crashlyticsを導入した
Crashlyticsとは
Crash Reporting Serviceの一種。
アプリ内で発生したエラーやクラッシュをFirebaseに送信し、Firebaseコンソール内のダッシュボードでそれらを確認することができる。
類似サービスにはSentryがある。公式ではSentryを利用したError Reportingを紹介している。
導入背景
現状、リリースされたアプリで確認できる不具合は、API通信時のエラーのみである。つまり、Flutterアプリ内のエラーやクラッシュに関しては、ユーザからのバグ報告のみでしか知ることができない。したがって、それを解決するために、Flutterアプリ内のエラーやクラッシュを監視し、補足したエラー等をCrashlyticsでFirebaseに送信することによって、中央集権的に常にFlutterアプリの状態を確認できるようにしたい。
導入手順
基本的に公式ドキュメントを参考にした。
Flutterアプリ内のエラーやクラッシュを監視し
→ Handling errors in Flutter: https://docs.flutter.dev/testing/errors
補足したエラー等をCrashlyticsでFirebaseに送信する
→ FlutterFire Crashlytics: https://firebase.flutter.dev/docs/crashlytics/overview
まず、Flutterでのエラーハンドリングについて知る必要がある。
その種類と解決法は以下である。
- Flutterによってキャッチされるエラー
-
FlutterError.onError
に上書きする
-
- ビルドフェーズ中に発生したエラー
-
ErrorWidget.builder
に上書きする
-
- Flutterによってキャッチされないエラー
-
runZonedGuarded
でラップする
-
よって、それぞれの場合において発生したエラーをCrashlyticsで送信すればよい。
具体的な方法に関しては公式に任せることにする。
注意する点
クラッシュレポートがFirebaseに送信されるタイミングは、次回のアプリケーションの起動時である。したがって、アプリケーションの再起動が必要である。
- debugビルドの場合は、一度アプリケーションをstopさせてから再びrunさせる必要がある。
- releaseビルドの場合は、アプリケーション終了させて、再起動させればよい。
実装方針
Firebase Crashlyticsを直接使用せず、インターフェースを切ることで、異なるCrash Reporting Serviceを用いたくなった場合に切り替えが容易になる。また、ユニットテスト時においても簡単にmockに切り替えることができる。
ユースケースは以下2つ。
- Widget構築前の初期化やオーバーライド
- シングルトン化したクラスのインスタンスを静的ゲッターで取得し、各々メソッドコール
- ログイン後のidentifyの設定
- Riverpodでシングルトン化したクラスのインスタンスをDIし、WidgetRefから参照
コード
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final crashReporter = Provider<ICrashReporter>((ref) => CrashReporter.instance);
abstract class ICrashReporter {
Future<void> initialize();
Future<void> setIdentify(String id);
Future<void> report(dynamic exception, StackTrace? stack);
}
class CrashReporter implements ICrashReporter {
CrashReporter._(this._reporter);
final FirebaseCrashlytics _reporter;
static ICrashReporter? _instance;
static ICrashReporter get instance {
_instance ??= CrashReporter._(FirebaseCrashlytics.instance);
return _instance!;
}
Future<void> initialize() async {
await _reporter.setCrashlyticsCollectionEnabled(kReleaseMode);
}
Future<void> setIdentify(String id) async {
await _reporter.setUserIdentifier(id);
}
Future<void> report(dynamic exception, StackTrace? stack) async {
await _reporter.recordError(exception, stack);
}
}
void main() {
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
await CrashReporter.instance.initialize();
FlutterError.onError = (FlutterErrorDetails details) {
CrashReporter.instance.report(details.exceptionAsString(), details.stack);
};
runApp(
ProviderScope(
child: const App('user_identify'),
),
);
}, (error, stack) {
CrashReporter.instance.report(error, stack);
});
}
class App extends HookConsumerWidget {
const App(this.id, {Key? key}) : super(key: key);
final String id;
Widget build(BuildContext context, WidgetRef ref) {
useEffect(() {
ref.read(crashReporter).setIdentify(id);
return null;
}, []);
return MaterialApp(
builder: (context, widget) {
Widget error = const Text('...rendering error...');
if (widget is Scaffold || widget is Navigator) {
error = Scaffold(body: Center(child: error));
}
ErrorWidget.builder = (FlutterErrorDetails details) {
ref
.read(crashReporter)
.report(details.exceptionAsString(), details.stack);
return error;
};
return widget;
},
);
}
}
よくわかっていない点
- 以下の使い所: https://firebase.flutter.dev/docs/crashlytics/usage#errors-outside-of-flutter
Isolateを用いたバックグラウンド処理中のエラーを補足するのか?
Isolate.current.addErrorListener(RawReceivePort((pair) async {
final List<dynamic> errorAndStacktrace = pair;
await FirebaseCrashlytics.instance.recordError(
errorAndStacktrace.first,
errorAndStacktrace.last,
);
}).sendPort);
-
オプトインレポーティングの使い所
「クラッシュしたエラー内容を送信してよいですか?」などの確認ダイアログが出現し、それを許可したアプリケーションのみfirebaseに送信するといったユースケースか? -
プラットフォーム統合の設定用途: https://firebase.flutter.dev/docs/crashlytics/overview#2-optional-platform-integration
Add to app用か? -
dSYMについて
debugビルド時にはデフォルトでアプリのbinary fileにdebug symbolが含まれているが、release build時では配布するアプリのサイズを減らすためにアプリのbinaryとは別にDebug Symbol fileを生成する。
このDebug Symbol fileがdSYM と呼ばれている。
参照: https://plum-plus.jp/2021/02/08/dsym-bitcodeについて/
デフォルトでは、Firebase Crashlytics はデバッグ シンボル(dSYM)ファイルを自動的に処理して、難読化解除された(人が読める形式の)クラッシュ レポートを生成します。この動作は、Crashlytics を初期化する実行スクリプトをアプリのビルドフェーズに追加するときに設定されます。
参照: https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports?hl=ja&platform=ios
今のところ、Crashlyticsのダッシュボードを見る限り、『不足している dSYM はありません』と表示されているので、問題ないという認識。
もし不足しているdSYMがあれば、ダッシュボードの手順通りにdSYMをアップロードすればよさそう。
追記
Firebase コンソールに「不足している dSYM」というアラートが表示されたので、それを見つけ、アップロードする手順を追記する。
-
Firebase コンソールの存在しない dSYM ファイルから警告が出ているUUIDを見つける
-
上記UUIDより不足しているdSYMファイルを見つける
mdfind -name .dSYM | while read -r line; do dwarfdump -u "$line"; done
- dSYMファイルをアップロードする
/path/to/pods/directory/FirebaseCrashlytics/upload-symbols -gsp /path/to/GoogleService-Info.plist -p ios /path/to/dSYMs
upload-symbolsはFinderで検索をかけて発見した。
Successfully uploaded Crashlytics symbols
上記メッセージが表示されれば成功。Firebaseコンソールに無事反映された。
Discussion