FlutterアプリでSentryとFirebase Crashlyticsにエラーレポートを送りアプリの品質改善に役立てる

by

@wapa5pow

Flutterアプリとしてエラーは大きく2種類あります。

  1. Flutter/Dart側で起きるエラー
  2. ネイティブ(Android/iOS)側で起きるエラー

今回は、1をSentryで、2をFirebase Crashlyticsで収集します。本当はどちらともFirebase Crashlyticsで収集したかったのですが、Crashlyticsのほうは、iOSで同じエラーでも以下のように別々のエラーとして扱われてしまったのでSentryとCrashlyticsを使っています。同じように扱える場合は、Crashlyticsで統一したいですね(ここ解決する方法しっていたら教えてください)。

crashlytics_01

Sentryの導入

まずあらかじめSentryでアカウントを作っておきます。Sentryには、Organization, Projectという概念があり、Organizationが複数のProjectをもっているという感じです。初めてアカウントを作ったときに、まずOrganizationを作り、その後に環境(production, developmentなど)に応じて複数Projectを作るといいと思います。

Organizationは、ページの左上のSwitch organization=>Create a new organizationから作成でき、Projectは、OrganizationページのSettings=>Projectから作成できます。

SentryはReport errors to a serviceで公式にエラーレポートの導入の仕方を説明しているので、この通りに導入すれば大丈夫です。今回は、2.2.0のバージョンを入れました。

ドキュメントではrunZonedを使って、runAppをラップしています。Zoneは、runApp実行中に起きたハンドリングできなかったエラーをハンドルして、Sentryなどにエラー情報を送ることができます。try-catchとは違って、runAppを終了せずにそのまま実行できます。

なにか適当なボタンのタップイベントなどにthrow FlutterError('err');とかいてエラーを起こさせます。そうするとSentryで以下のようなIssueが生成されます。

sentry_01

ドキュメントのままでは、エラーログとしの情報が足りないので以下の情報をつけて送れるようにします。

  • OSの種類とバージョン
  • アプリのバージョン
  • productionやdevelopmentなどの環境名
  • ユーザの情報

SentryはSentryClientを初期化するときにEventというイベントを送るときに付属させる情報を定義できるので以下のように上記の情報をつけます。

final version

final sentry = new SentryClient(
  dsn: 'SentryのDNS',
  environmentAttributes: Event(
    release: version,
    environment: 'production',
    tags: {
      'os': deviceInfo.os,
      'osVersion': deviceInfo.osVersion,
      'model': deviceInfo.model,
    },
  ),
);

versionpackage_infoのPackageInfoを'${packageInfo.version} (${packageInfo.buildNumber})';ようにして取得しています。

deviceInfoは、device_infoから以下のように定義して_deviceName()から取得しています。

class _DeviceInfo {
  var os = '';
  var osVersion = '';
  var model = '';

  _DeviceInfo({
    this.os,
    this.osVersion,
    this.model,
  });
}

Future<_DeviceInfo> _deviceName() async {
  final deviceInfo = _DeviceInfo(
    os: Platform.operatingSystem,
  );
  final deviceInfoPlugin = DeviceInfoPlugin();
  if (Platform.isAndroid) {
    final androidInfo = await deviceInfoPlugin.androidInfo;
    deviceInfo.model = androidInfo.model;
    // ignore: cascade_invocations
    deviceInfo.osVersion = androidInfo.version.release;
  } else if (Platform.isIOS) {
    final iosInfo = await deviceInfoPlugin.iosInfo;
    deviceInfo.model = iosInfo.name;
    // ignore: cascade_invocations
    deviceInfo.osVersion = iosInfo.systemVersion;
  }

  return deviceInfo;
}

ちなみに、device_infoはAndroid/iOS各以下のように端末の情報がとれます。

device_infoのAndroidの端末情報
device_info android

device_infoのiOSの端末情報
device_info ios

ユーザの情報に関しては以下のようにSentryで定義されているUseruserContextに代入します。

sentry.userContext = User(id: 123);

このように設定してからSentryにエラーを送ると、以下のように各種付属した情報がSentryにでます。これで端末情報などでしぼりこめるので、エラーの原因解明がはかどります。

sentry attributes
(注意: 実際のアプリで実装したコードなので、ソースコードの値とは若干ことなります)

これでFlutter/Dart側で起きたエラーはSentryでエラーレポートがみれるようになりました。

Firebase Crashlyticsの導入

ネイティブ(Android/iOS)のコードがクラッシュしたときのエラーレポートはCrashlyticsで取得することにします。まずはFirebase Crashlyticsを設定します。FirebaseのコンソールのQualityにあるCrashlyticsを選んでから以下を選んでおきます。

crashlytics_02

Flutterのパッケージとして、flutter_crashlyticsを使用します。このパッケージはFirebase Crashlyticsの場合と、Fabricから移行したCrashlyticsの場合で、設定が若干ことなるのですが、今回はFirebase Crashlyticsを使った場合を説明します。

pubspec.yamldependenciesに以下を追加します。今回、kiwi-bop/flutter_crashlyticsをforkして、このコミットだけを行ったものをパッケージとして使ってます。fatal error: 'Fabric.h' file not foundというエラーがでたので、Issueに記載されている修正をしました。

title=pubspec.yaml
flutter_crashlytics:
  git:
    url: https://github.com/wapa5pow/flutter_crashlytics.git
    ref: f0b5f56d1bcaae4e352991437a29ebc65ae6ef50

パッケージを導入したら、Crashlyticsにエラーレポートを送れるように以下のように設定します。

Android側

title=android/app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'io.fabric' // <= 追記
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
title=android/build.gradle
repositories {
    ...
    maven { url 'https://maven.fabric.io/public' } // <= 追記
}

dependencies {
    ...
    classpath 'io.fabric.tools:gradle:1.+' // <= 追記
    ...
}

iOS側

Run Scriptを追加する必要があるので以下のように左上の+ボタンから追加します。READMEにはFabricのAPIキーを引数として設定してますが、Firebase Crashlyticsの場合はいりません。

run script

Flutter側

main.dartに以下のように記載してCrashlyticsを初期化します。環境を設定している部分は必要なら記載してください。setInfoでSentryのタグみたいに独自情報を付属できます。

title=main.dart
await FlutterCrashlytics().initialize();
await FlutterCrashlytics().setInfo('environment', 'production';

終わったらネイティブ連携など適当な部分でクラッシュさせてログがでているか確かめます。

まとめ

SentryとFirebase Crashlyticsを使いFlutterアプリのクラッシュしたときやハンドルしていない例外がでたときにエラーレポートを送ることができるようになりました。アプリのバージョンやOSなど細かい情報も入れられるので、ユーザからお問い合わせがあったときも対応しやすくなりアプリの品質が高められますのでまだエラーレポートを入れていないFlutterアプリがある場合はぜひためしてみてください。