Open7

【NestJS】Sentryの使い方

TKDTKD

Sentryのライブラリ

選択肢は3つある。

  • @sentry/node
    • Sentry公式のNode用ライブラリ
  • @sentry/nestjs
    • Sentry公式のライブラリ
    • 2024/07/10にリリースされたばかりで、2024/07/30現在だとまだアルファ版
  • @ntegral/nestjs-sentry
    • サードパーティ製のライブラリ

おそらく今後は@sentry/nestjsが主流になっていくと思うが、2024/07/30現在だとまだアルファ版なので@sentry/nodeを使うことにする。

TKDTKD

@sentry/nodeをNestJSにインストール

インストール

npm install @sentry/node

sentry用の設定ファイル

src/sentry/instrument.ts
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  // ...
});

グローバルフィルターを作成

src/filters/all-exception.filter.ts
import { ArgumentsHost, Catch } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import * as Sentry from '@sentry/node';

@Catch()
export class AllExceptionFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    // Sentryにエラー通知する
    Sentry.captureException(exception);
    super.catch(exception, host);
  }
}

グローバルフィルターを適用

src/main.ts
// sentryは一番最初にimportする
import './sentry/instrument.js';

import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { AllExceptionFilter } from './filters/all-exception.filter.js';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new AllExceptionFilter(httpAdapter));

  await app.listen(3000);
}

bootstrap();

Sentryにエラー通知されることを確認

以下のようにエラーを発生させるとSentryに通知される。

src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service.js';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    // 意図的にエラーを発生させる
    throw new Error('sample error');
    return this.appService.getHello();
  }
}

TKDTKD

4xxエラーのときはSentry通知したくない

このままだと存在しないパスにアクセスがあったときの404エラーなどもSentry通知されてしまうので、5xxエラーのときだけSentry通知するように設定する。

src/filters/all-exception.filter.ts
import { ArgumentsHost, Catch } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import * as Sentry from '@sentry/node';

@Catch()
export class AllExceptionFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    // HttpExceptionが発生した場合、statusCodeが設定される
    const statusCode = (exception as { status?: number }).status;

    // HttpException以外の例外、もしくは500系エラーの場合はSentryに通知する
    if (statusCode === undefined || statusCode >= 500) {
      Sentry.captureException(exception);
    }
    super.catch(exception, host);
  }
}

ちなみに@sentry/nestjsを使う場合はデフォルトで4xxエラーは通知されないように設定されてある。

https://github.com/getsentry/sentry-javascript/blob/7dc6a25eea53ebdf895ba4384fcb1abe7800d298/packages/nestjs/src/setup.ts#L66-L71

TKDTKD

Sentryに通知されるタイトルとメッセージを変える

それぞれ、errorのnameとmessageが表示されている。

CustomErrorを発生させた場合

class CustomError extends Error {}

throw new CustomError('sample error');

クラスを変えただけではSentry側のタイトルは変わらない。

nameを変更したCustomErrorを発生させた場合

class CustomError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'CustomError';
  }
}

throw new CustomError('sample error');

タイトルが変わる。

messageを変えたとき

throw new CustomError('errrrrrrrrrrr');

メッセージが変わる。

TKDTKD

どのようにエラーがグルーピングされるのか?

似たようなエラーはグルーピングされるが、ほぼ同じ内容なのに違うエラーとして認識されることもある。

グルーピングはスタックトレースやエラーのタイプ、メッセージによって行われる。
参考:https://docs.sentry.io/product/issues/grouping-and-fingerprints/

What is a fingerprint?
A fingerprint is a way to uniquely identify an event. Sentry sets a fingerprint for you by default, using built-in grouping algorithms based on information such as a stack trace, exception type, and message. Events with the same fingerprint are grouped together.

フィンガープリントとは何か?
フィンガープリントはイベントを一意に識別する方法です。 Sentry はデフォルトでフィンガープリントを設定し、スタックトレース、例外タイプ、メッセージなどの情報に基づいた組み込みのグルーピングアルゴリズムを使用します。 同じフィンガープリントを持つイベントは一緒にグループ化されます。

つまり発生箇所(スタックトレース)が変わればエラーメッセージが同じでも別のエラーとして認識される。

TKDTKD

エラー発生時のリクエストボディやヘッダーについて

どこで設定してくれているのか詳しく分かっていないが、自動でSentry側でデータが取れている。

TKDTKD

Contextの付与

https://docs.sentry.io/platforms/javascript/guides/node/enriching-events/context/

src/filters/all-exception.filter.ts
Sentry.setContext('character', {
  name: 'Mighty Fighter',
  age: 19,
  attack_type: 'melee',
});
Sentry.captureException(exception);

Additional Dataの付与

https://docs.sentry.io/platforms/javascript/guides/node/enriching-events/context/#additional-data

Additional Dataは非推奨で、Contextを使ってほしいとのこと。
以下のように付与することは可能。

src/filters/all-exception.filter.ts
Sentry.setExtras({
  'my-additional-info': 'this is additional info',
  'my-additional-info2': 'this is additional info2',
});
Sentry.captureException(exception);

タグの付与

https://docs.sentry.io/platforms/javascript/guides/node/enriching-events/tags/

src/filters/all-exception.filter.ts
Sentry.setTags({
  'my-tag': 'my-tag-value',
  'my-tag2': 'my-tag-value2',
});
Sentry.captureException(exception);

ユーザー情報の付与

https://docs.sentry.io/platforms/javascript/guides/node/enriching-events/identify-user/

src/filters/all-exception.filter.ts
Sentry.setUser({
  id: 123,
  username: 'sample-username',
  email: 'test@example.com',
});
Sentry.captureException(exception);

ユーザー数もカウントされる。