ExpoアプリにSentryを導入して検証してみる

エラー監視サービスのSentryをExpoを使用して開発しているReact Nativeアプリに導入し、ある程度動作検証してみます。

[Sentry](https://sentry.io/)
<img width="1438" alt="Application_Monitoring_and_Error_Tracking_Software___Sentry.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/fbea33c2-66e2-837f-0a47-ecd2812e58f5.png">

画面に沿ってアカウント作成後、

<img width="1439" alt="Get_Started_on_Sentry.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/8b3d4877-6fd3-5bb3-4dfa-ac94eaa3f345.png">

最初はこのようにチュートリアルのような形でプロジェクトを作成できますが、ダッシュボード（トップページ）の`Projects`→`Create Project`からプロジェクト名、プラットフォーム（React-Native）を指定して作成します。

![Get_Started_on_Sentry2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/376db6f8-0443-27b8-6308-1c31c6b0a7f1.png)

ここからはExpoのドキュメントに従って進めます。
https://docs.expo.io/versions/latest/guides/using-sentry/

必要な情報は

* DSN  
  プロジェクト作成時に案内が出ているかもしれませんが、`Settings`→`Projects`→プロジェクトを選択→`Client Keys(DSN)`からも取得できます。
* 組織名（Organization）  
  個人で登録した際は個人名を指定してるかと思います。ケバブケース化されています。
* プロジェクト名  
  ケバブケース化されています。
* Authトークン  
  ダッシュボードの`Settings`→パンくずリストの`Settings`で設定のトップへ→`API Keys`の[`Auth Tokens`ページ](https://sentry.io/settings/account/api/auth-tokens/)で作成します。

Authトークンの作成時には、Expoのドキュメントに従って`project:write`をチェックしておきます。
![Create_API_Token_-_Sentry.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/81066b06-c39e-bae0-15a7-fff5c3298068.png)

Expoプロジェクト側で設定・初期化する部分は、`app.json`と最初に呼ばれるJSファイルのみです。
まずは`sentry-expo`をインストールします。`publish`ごとに生成されるIDを取得するため、`expo-constants`もインストールしておきます。

```console
$ npm install sentry-expo expo-constants
```

`app.json`の`expo.hooks.postPublish`にこのように設定を追加します。
`publish`時にSentryにソースマップファイルをアップロードしているようです。

```json:app.json
{
  "expo": {
    "hooks": {
      "postPublish": [
        {
          "file": "sentry-expo/upload-sourcemaps",
          "config": {
            "organization": "組織名",
            "project": "プロジェクト名",
            "authToken": "Authトークン"
          }
        }
      ]
    }
  }
}
```

JS側でSentrySDKをセットアップします。

```js:App.js
import * as Sentry from "sentry-expo";
import Constants from 'expo-constants';

Sentry.setRelease(Constants.manifest.revisionId || "DEV");
Sentry.init({
  dsn: 'DSNをここに指定',
  enableInExpoDevelopment: true,
  debug: __DEV__
});
```

`init`メソッドに[オプション](https://docs.sentry.io/error-reporting/configuration/?platform=javascript)を指定して初期化します。
一応デバッグモードは開発時のみ`true`になるようにしておきますが、[それほど気にしなくていい](https://docs.sentry.io/error-reporting/configuration/?platform=javascript#debug)みたいです。
先ほどインストールした`expo-constants`からリビジョンIDをSentryにセットしてあげると、どのバージョンでエラーが発生したかが特定できます。

とりあえず`captureException`メソッドを使って、Expo Clientの開発環境でエラーを発生させてみます。
`setUser`メソッドでユーザーを特定できるようにしたりできるようなので、これも使ってみます。

```js:App.js
Sentry.setUser({"email": "john.doe@example.com"});
Sentry.captureException(new Error('error!'));
```

Expo画面にはエラーが表示されました。
<img width="375" alt="スクリーンショット 2019-12-23 11.13.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/4a514768-8b5d-7cb3-68c0-9d664731828b.png">

`The stack is null`とあります。Sentryがうまくスタックトレースにアクセスできてないような感じです（？）。

しかしSentryの`issues`を見ると、ちゃんとエラーイベントが取得できているようです。ユーザーも問題なくセットできています。
<img width="612" alt="Error__error_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/a71132f5-16a0-0792-2954-67e2383fce54.png">

今度は明示的に`captureException`を使うのではなくエラーを発生させてみます。

```js:App.js
Sentry.setUser({"email": "john.doe@example.com"});
global.foo(); // 存在しないfunction
```

こちらは当然Expo Clientの画面では通常のエラー表示になります。

<img width="612" alt="TypeError__global_foo_is_not_a_function.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/c8da9492-be30-5af2-6ba4-7c365df8fa6d.png">

Sentryの方でも問題なくエラーが取得できていました。

<img width="624" alt="ReferenceError__foo_is_not_defined.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/3312f51a-4856-8b77-7ae9-5d5cf91648f8.png">

ちなみに、先ほど`setRelease`に`DEV`が入るようにしているので、`RELEASE`欄にはちゃんと`DEV`と表示されています。

では、リリース後のアプリでも確認してみます。
以前作ったQRコード生成のアプリにこっそりとエラーを発生させる仕組みを仕込んで、Expoで`publish`してみます。

```jsx:App.js
<TouchableWithoutFeedback
  onPress={() => {
    throw new Error('error test!');
  }}
>
  <View
    style={{
      position: "absolute",
      top: 0,
      left: 0,
      width: 70,
      height: 70
    }}
  />
</TouchableWithoutFeedback>
```

タップするとエラーを発生させる隠しボタンを追加して`publish`でOTAアップデートし、
アプリを起動してエラーを発生してみます。

<img width="607" alt="Error__error_test_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/df6e82f3-650a-5568-9a8c-1b00a14e84a3.png">

Sentryの方でも問題なくエラーが取得できています。

<img width="435" alt="Error__error_test_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/cf819a64-298e-173d-8979-59fc02724cca.png">

[追記]
Androidでも確認しました。
<img width="486" alt="Error__error_test_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/5384acdb-3af7-9821-697a-8c606c57d3c7.png">

`setRelease`で設定しているので、`RELEASE`欄にはExpoが自動で生成しているユニークな値が入っています。

<img width="841" alt="Banners_and_Alerts_と_Error__error_test_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/65593712-89ac-9504-59c6-25f59a04a512.png">

エラーの詳細を見ると、元ソースもエラーが発生した位置もちゃんと閲覧できます。
ユーザーのいないしょぼいアプリとはいえ、そのままにしていたら普通にまずいのですぐ消しました。

今回試してみたのは`RELEASE`や`USER`のみですが、他にも色々とコンテキストを設定できるようです。
詳しくは[ドキュメント](https://docs.sentry.io/platforms/javascript/#adding-context)を参照してください。
ただし、一部React Nativeでは対応していないものがあるようです。例えばコンソールの内容を送信するための`Sentry.Integrations.CaptureConsole`などは使用できませんでした。