NestJS アプリケーションに Sentry によるエラートラッキングを導入する

この記事は NestJS Advent Calendar 2019 17日目の記事です。昨日は @odanado さんの [NestJS の @nestjs/swagger でコントローラーから Open API(Swagger) の定義書を生成する](https://qiita.com/odanado/items/60456ab3388f834dc9ca) でした。

本日は実務向け tips の一つとして、エラー監視・ログ収集基盤の Sentry の NestJS への導入方法を紹介します。

## Sentry について

<img width="1680" alt="スクリーンショット 2019-12-17 14.10.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75633/cf09735d-bfac-98be-8c88-73bf8837b1cc.png">

https://sentry.io/

Functional Software, Inc. により運営されているエラー監視とログ収集の基盤を提供する SaaS です。単純にエラーが発生したときに通知したりログを継続的に出力するだけなら他にも多くのサービスがありますが、Sentry はエラーをより詳細にトラッキングできる、以下のような機能が揃っています。

- エラー発生時の Slack や Email での障害通知
- 収集したエラーの自動チケット化と対応状況・担当者の管理
- エラーが発生した環境や、クライアントサイドの場合はデバイス情報を含めてのレポーティング
- 発生した頻度およびエラー詳細の可視化

例えば、実際に私が運用しているサービスのフロントエンドであれば、このような情報が表示されています。

<img width="1326" alt="スクリーンショット 2019-12-17 22.49.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75633/bde4b5c3-f58c-d613-3e1a-ed9e807e070e.png">

エラーの数や頻度、対応状況が可視化されることにより、よりプロダクトの改善を行いやすい土壌が Sentry にはあります。今回は、そんな Sentry を NestJS アプリケーションに導入してみます。

## サンプルコード

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day17-sentry

## NestJS での Sentry 導入について

早速導入を進めます。事前に Sentry のアカウントとプロジェクトを作成しておいてください。Sentry をテスト規模で利用する分には、課金は発生しないため、安心して登録ください。

今回も CLI で初期化したプロジェクトを前提とします。 `nest new day17-sentry` を実行ののち、次に進んでください。

### nest-raven の導入

NestJS から Sentry を利用するときは、 nest-raven を利用します。Raven は Sentry SDK の過去のの名前であり、パッケージ名はそれが踏襲されています。

NPM または Yarn からインストールしてください。

```terminal
$ yarn add nest-raven
```

なお、 nest-raven はサードパーティのパッケージであることを留意ください。ただ作りは非常にシンプルなため、試しにパッケージのコードを読んでみても良いでしょう。単一のモジュールに intercepter のコードで明快です。

https://github.com/mentos1386/nest-raven

### NestJS アプリケーションへの設定追加

nest-raven をインストールしたら、 main.ts および app.module.ts の設定を書き換えていきます。まずは main.ts で、 Sentry の初期化コードを実装します。

開発環境では Sentry が導入されていてもされていなくても良いですが、本番環境ではされていない場合はエラー落ちさせておくとより安全かと思います。

これでサーバーが異常終了した場合についても、エラーログを収集することができるようになりました。

```main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as Sentry from '@sentry/node';

async function bootstrap() {
  if (process.env.NODE_ENV === 'production' && !process.env.SENTRY_DSN) {
    // tslint:disable-next-line:no-console
    console.error('Missing required environment variable \'SENTRY_DSN\'');
    return process.exit(1);
  }
  if (process.env.SENTRY_DSN) {
    Sentry.init({
      dsn: process.env.SENTRY_DSN,
    });
  }
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();
```

続いて NestJS の範囲で返せるエラーについても正しくトラッキングしていきます。NestJS は、 Controller のメイン処理の中でエラーが発生したとしても、 catch した上で 500 エラーを返却するように作られています。

これは非常に便利な一方で、ただ Sentry を初期化しただけでは支配的であろうメイン処理内のエラーログを収集できません。

以下のように、 RavenIntercepter を APP_INTERCEPTOR というグローバルフック領域に対して登録してやることで、処理を横取りして Sentry の情報を流すことができます。

app.module.ts

```app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { RavenModule, RavenInterceptor } from 'nest-raven';

@Module({
  imports: [
    RavenModule,
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_INTERCEPTOR,
      useValue: new RavenInterceptor(),
    },
  ],
})
export class AppModule {}
```

これで準備は完了です。

### 実際のリクエストでエラーを発生してみる

実際にエラーを発生させてみましょう。TypeScript だと雑に実行時エラーを生み出すのは難しいので、適当に以下のようなコードを書いてみます。

期待通りに動作した場合、 `a.foobar is not function` によって 500 となってくれるはずです。

```app.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller()
export class AppController {
  @Get()
  getHello(): string {
    const a = {};
    (a as any).foobar();
    return 'Hello, world'
  }
}
```

### 結果の確認

実際にやってみます。 http://localhost:3000 へとアクセスすると、以下のようにエラーが発生することが確認できます。また、それと同時に Sentry へとエラーログが飛ぶはずです。

<img width="1200" alt="スクリーンショット 2019-12-17 14.33.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75633/51f8e816-5340-3a65-a06b-03e2eb720178.png">

Sentry のダッシュボードからエラーを見て、正しくトラッキングされているかを確認します。

<img width="1272" alt="スクリーンショット 2019-12-17 14.26.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75633/afb89724-fde7-a338-67ee-97fe52b232ad.png">

問題なければ導入は完了です。

### おわりに

Sentry は、うまく使うことでエラー検知基盤、収集基盤、改善基盤などの複数の役割を持つことができます。ときにはクリティカルでないエラーレポートが来ることもありますが、割れ窓を可能な限り作らないよう、堅実に潰していくことがプロダクト品質の向上に繋がります。

もし利用したことがない人は、試しに NestJS とともに導入してみてください。