FlutterにDriftでSQLiteを組み込んだらSentryで監視しよう

今作っているFlutterアプリで [Drift](https://drift.simonbinder.eu/) を使っており、 [Sentry](https://sentry.io) でパフォーマンスモニタリングを入れてみました。

## Drift 

Flutterでスマホ内のストレージにデータを保存する方法としては [公式 Cookbook で紹介](https://docs.flutter.dev/cookbook/persistence) されている [sqflite](https://pub.dev/packages/sqflite) が代表的ですがAndroidのネイティブ開発の経験があれば、 [Drift](https://drift.simonbinder.eu/) も有力な選択肢です。過去にMoor（AndroidのRoomを逆順）という名前だったものからリネームされており、近い使用感で利用することができます。 `Stream` を使ってデータの更新のたびに再レンダリングできるため、 `onTap` では変更後のデータの反映を気にせずに、データの更新のみに集中することができます。

詳しい使い方は [DriftのGetting started with sql](https://drift.simonbinder.eu/docs/getting-started/starting_with_sql/) をご確認ください。

## Sentry

エラートラッキングツールの [Sentry](https://sentry.io) をアプリに仕込むと、捕捉しきれなかった例外を最も外側で収集してレポートすることができます。報告されたエラーはインターネットを経由してサーバーに収集され、表示・アラートを行うことができます。サーバーはSaaSを利用する以外にも、 [セルフホスト版](https://github.com/getsentry/self-hosted) を自分で運用することもできます。他に近いサービスとしてはDatadog/New Relic/Firebase Crashlyticsがあります。

詳しい使い方は [公式のGetting Started](https://docs.sentry.io/platforms/flutter/) をご確認ください。また、Sentry上でFlutterのプロジェクトを作成すると、導入手順に進んで使い始めることができます。

### Sentryのパフォーマンス監視

元々はエラートラッキング専門ツールというイメージでしたが、パフォーマンス監視に関する機能も備えています。

`startTransaction` から `transaction.finish` までの区間を作ると、その間の処理が監視対象になります。

```dart
final transaction = Sentry.startTransaction(
  'example-transaction',
  'db',
  bindToScope: true,
);
try {
  // TODO: 処理

  transaction.status = const SpanStatus.ok();
} catch (exception) {
  transaction.throwable = exception;
  transaction.status = const SpanStatus.internalError();
  rethrow;
} finally {
  await transaction.finish();
}
```

区間内は `transaction.startChild()` で子の区間 (Span) を開始して、段階的な構造を表現することができます。

`bindToScope: true` を指定すると、 `transaction` はスコープ（特になにもしていなければグローバル）にも保存され、 `Navigator` やネットワークライブラリの `dio` 、 `sqflite` などとの連携ではそれを使って記録されます。例えば、 [SentryNavigatorObserver](https://docs.sentry.io/platforms/flutter/performance/instrumentation/automatic-instrumentation/#routing-instrumentation) を使うと、デフォルトではページ遷移から3秒間のトランザクションが開始され、さらにその間に [設定されたdio](https://docs.sentry.io/platforms/dart/configuration/integrations/dio/) の通信が行われると、TransactionやSpanを自分で取り回さなくても自動で記録が行われます。

## DriftとSentry

:::note info
追記： [公式パッケージ](https://pub.dev/packages/sentry_drift) が出ました。今後はこちらをオススメします。
:::

ところが残念ながらDriftと連携するためのライブラリは用意されていません。 [sentry_sqflite](https://pub.dev/packages/sentry_sqflite) を参考に作ってみました。

https://pub.dev/packages/drift_sentry

https://github.com/utisam/drift_sentry

パッケージ名称を揃えれば `sentry_drift` なんですが、Sentry公式が作った時のためにとっておきました。

### 使い方

[DriftのGetting Started](https://drift.simonbinder.eu/docs/getting-started/starting_with_sql/) に習いつつ、 `_openConnection` で `.addSentry()` を呼び出すと `QueryExecutor` を加工します。

```dart
LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    final dbFolder = await getApplicationDocumentsDirectory();
    final file = File(p.join(dbFolder.path, 'db.sqlite'));
    // Use .addSentry() to wrap QueryExecutor
    return NativeDatabase.createInBackground(file).addSentry();
  });
}
```

あとは `bindToScope: true` で囲った間でDriftを使うだけです。あるいは `SentryNavigatorObserver` を使うと `Navigator` で遷移した直後が記録されます。

### 取得したTransaction

[drift_sentry](https://github.com/utisam/drift_sentry) のExampleをSaaSに送った様子のスクリーンショットです。起動して最初の実行で、データベースのOpenに時間がかかっていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15627/386a5550-8d6a-86e6-d83a-810d2aa52b23.png)