ReactアプリにSentryを導入してエラー監視を行う

Sentryは、アプリケーションからエラーログを収集し、アプリで発生したエラーを一覧形式、または個別形式に可視化してくれるサービスです。Slackと連携することもでき、エラー監視ツールとして利用できます。

こういったツールはサーバーサイドに設置するケースが多いと思っていたのですが、今回フロントエンド側にも設置する機会があったので、導入方法についてまとめておきます。

## ReactアプリにSentryを導入する

Sentryの導入方法はめちゃくちゃ簡単で、アカウントを作成して[こちら](https://docs.sentry.io/platforms/javascript/react/)のドキュメントに従ってソースコードに変更を加えればOKです。

あとは、適当にエラーを発生させれば通知がSentryに飛び、こんな感じでエラーを表示してくれます。

<img width="842" alt="スクリーンショット 2019-11-20 19.08.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310577/e1d57f91-9034-0144-34f8-722cca0688e7.png">

ただ、デフォルト設定だとかゆいところに手が届かない感じがあるので、より運用しやすいようにアプリのほうに変更を加えていきます。

## 非同期通信のエラーをキャッチできるようにする

上記のドキュメントでは、componentDidCatchを使ってエラーのハンドリングを行なっています。（componentDidCatchに相当するHooksがまだ存在しないので、関数コンポーネント主体の設計でもこの部分だけはクラスコンポーネントにする必要がありそうです）

ただ、componentDidCatchがキャッチしてくれるのはReactのライフサイクルで発生したエラーのみで、たとえばajax通信のtry/catch節からthrowされたエラーはキャッチしてくれません。

axios通信時のエラーも監視したいというケースもあると思うので、非同期通信時のエラーもSentryに送信できるように少し手を加えます。

一番単純なのは、catch節でSentryへの送信処理を書くことですが、この方法だと各ファイルに同様の処理が散らばるので、個人的にあまりやりたくない方法です。

そこで、次のようにActionを定義して、エラーを一度Reduxに格納し、AppコンポーネントをラップするErrorBoundaryコンポーネントでまとめて送信処理を行うようにしました。

```typescript:ErrorBoundary/module.ts

export interface LogData {
  [key: string]: any; // 
}

interface SetErrorAction extends Action {
  type: ActionNames.SET_ERROR;
  payload: {
    error: Error;
    logData?: LogData;
  };
}

export const setError = (error: Error, logData?: LogData): SetErrorAction => ({
  type: ActionNames.SET_ERROR,
  payload: { error, logData }
});
```

Sentryへは任意のデータも合わせて送信できるので、logDataとしてそういったデータを付加できるようにしています。そして上記のActionを任意のContainerでdispatchし、ErrorBoundaryStateにエラー情報を格納し...

```tsx:ErrorBoundary/Container.tsx
public submitError(error: Error, errorInfo: ErrorInfo) {
  Sentry.configureScope(scope => {
    if (errorInfo) {
      // logDataを送信パラメータにセット
      scope.setExtras(errorInfo);
    }
  });

  // Sentryへの送信処理
  Sentry.captureException(error);
}
```

```tsx:ErrorBoundary/ErroBoundary.tsx
interface State {
  hasError: Boolean;
}

interface Props {
  value: ErrorBoundaryState;
  actions: ActionDispatcher;
}

export class ErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      hasError: false
    };
  }

  componentDidUpdate = () => {
    // ReduxのErrorBoundaryStateにエラー情報が格納されたらActionをdispatchする
    if (this.props.value.error) {
      this.props.actions.submitError(
        this.props.value.error,
        this.props.value.logData
      );
    }
  };

  // Reactのライフサイクル時エラーの場合はこちらが動作する
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    this.props.actions.submitError(error, errorInfo);
    // コンポーネントエラーの場合はエラー画面を表示させる
    this.setState({ hasError: true });
  }

  render() {
    if (this.state.hasError) {
      // エラー画面を表示
      return <div>Report feedback</div>;
    }

    return this.props.children;
  }
}
```

こんな感じでErrorBoundaryコンポーネントからSentryへの送信処理を行っています。

ReduxのErrorBoundaryStateにエラー情報が格納されると、ErrorBoundaryコンポーネントのcomponentDidUpdateが実行され、Sentryへエラー情報が送信されます。これで非同期通信時のエラーも捕捉することができます。

ReactライフサイクルのエラーはcomponentDidUpdateではキャッチできませんが、componentDidCatchが代わりにキャッチしてくれるので問題ありません。

## SentryへのSourceMapの送信

SentryはStackTraceを表示してくれるので、ソースコードのどの箇所でエラーが発生したのかを確認できます。

ただ、バンドルファイルがminify処理されていると、それがそのまま表示されるのでなんのこっちゃかわからなくなってしまいます。production環境ではminifyして配信するのが普通だと思うので、このままではSentryのメリットが一つ失われてしまうことに...。

こんなときに役立つのがSourceMap。SourceMapがあるとバンドルファイルがminifyされていても、元のソースコードのままStackTraceを出力できます。
SentryにあらかじめSourceMapを送信しておくと、production環境でも詳細なStackTrace情報を入手できるので設定しておきます。

SourceMapの生成自体はWebpackの設定に```devtool: "source-map"```を追加するだけでOK。これで、buildした際にバンドルファイルと一緒にbundle.js.mapのようなファイルも出力されるようになります。

次に、SentryへSourceMapを送信する方法ですが、やり方としてはいくつかあります。アプリがデプロイされるたびに手元のコマンドラインから手動で送信する方法もありますが、できれば自動化したいところです。

幸い、Netlifyから配信している場合は、netlify.tomlをルートディレクトリ直下に置くことでビルド時のオプションを設定できるので、その方法で進めます。

前準備として、```.sentryclirc```をルートディレクトリ直下に作成し、Sentry側に登録しているOrganization名、Project名を記述しておきます。

```.sentryclirc
[defaults]
org=XXXXXXXXXXXX
project=XXXXXXXXXXXX
```

また、コマンドラインからSentryにアクセスするには、認証トークンが必要です。Netlifyは環境変数を設定できるので、キー名をSENTRY_AUTH_TOKENとしてトークンを設定しておきましょう。トークンはSentryの管理画面から発行できます。

そして最後に、netlify.tomlは以下のように記述します。

```toml:netlify.toml
[context.production]
  command = """
yarn build:prod && 
npx sentry-cli releases files 1.0 upload-sourcemaps -i bundle --ext js --ext map ./dist --rewrite &&
rm ./dist/js/bundle.js.map
"""
```

context.productionとすることで、production環境（masterブランチ）へのデプロイ時にのみ上記のコマンドを実行することができます。sentry-cliライブラリをあらかじめインストールしておきましょう。コマンドの流れとしては、

・アプリをビルド
・SentryへSourceMapを送信
・生成されたSourceMapファイルを削除

といった感じ。SourceMapファイルを削除しているのは、クライアント側にSourceMapを配信したくないためです（配信ファイルのサイズや、セキュリティの観点から）

これでSentryへのSourceMap送信を自動化できました。

## まとめ

SentryへのSourceMap送信方法については情報が少なく、結構苦労しました。同じく困っている人の助けになれば幸いです。