フロントエンドのSentryを実運用に持ち込むためにやったことメモ

# 背景
弊社ではフロントエンドのエラートラッキングにSentryを導入していたのですが、ほとんど運用されていませんでした。
フロントエンドで重大なエラーが発生することは稀であり、エラー調査においてSentryが必須な場面がほとんどなかったためです。

ただし、小さなバグやユーザーからの問い合わせは定期的に発生しており、その調査を行う際にSentryがあった方が便利だと感じる場面は多々ありました。

そのため、Sentryを適切に活用しようと思ったのですが、現状を整理したところ以下の課題がありました。
- SourceMapsがなく、エラー箇所のコードが読みにくい
- エラーが解決されずに放置され、Issueに大量に溜まっている
- 頻繁に発生するエラーがクォーターを多く消費する
- ユーザーがら問い合わせがあった際に、エラーの再現が難しい(これは可能であれば改善したい課題)

上記の課題を解決するために調査・実施したことを紹介します。

# 動作環境
- [Nuxt(SPA)](https://nuxt.com/): v3.8.1
- [sentry/tracing](https://www.npmjs.com/package/@sentry/tracing): v7.38.0
- [sentry/vite-plugin](https://www.npmjs.com/package/@sentry/vite-plugin): v2.16.1
- [sentry/vue](https://www.npmjs.com/package/@sentry/vue): v7.108.0

# 1. SourceMapsの導入
`nuxi generate`時に、SourceMapsを生成するようにします
```ts:nuxt.config.ts
  sourcemap: {
    server: true,
    client: true
  }
```
`assets`にビルドしたコードと生成したSourceMapのディレクトリのパスを指定します。
`filesToDeleteAfterUpload`を指定することで、Sentry送信後にSourceMapsを削除します。これにより、SourceMapsがホスティングされて外部に公開されるのを防ぎます。
```ts:nuxt.config.ts
plugins: [
  sentryVitePlugin({
    sourcemaps: {
      assets: [
        '.nuxt/dist/client/_nuxt/*.js',
        '.nuxt/dist/client/_nuxt/*.js.map'
      ],
      filesToDeleteAfterUpload: [
        '.nuxt/dist/client/_nuxt/*.js.map',
      ],
    }
  })
]
```
ただ、弊社の環境では`filesToDeleteAfterUpload`を使用してもSourceMapsが削除されなかったので、CI上でホスティング前にSourceMapsを削除する処理を追加しました。

SourceMapsの導入することでコードの可読性が上がり、エラー調査がやりやすくなりました。

# エラーの整理
大量に溜まっているエラーを整理するために、以下を実施しました。
- エラー内容を確認し、恒久的に発生しないよう修正する
- エラーの修正が難しい場合
    - 重要度の低いエラーや発生頻度の低いエラーは[Priority](https://docs.sentry.io/product/issues/issue-priority/)を下げるか、一旦アーカイブする
    - ノイズになる場合や発生頻度が高くクォーターを大量消費する場合は、Sentryに送信しないようにする
        - ライブラリ内で発生した例外は原因の特定が難しく、かつノイズになるので送信しないようにしました
        - `4xx`系のエラーやネットワークエラーはノイズになるため、送信しないようにしました

エラーを恒久的に発生させないようにするのが最も良いので、1つ1つのバグを潰すような作業を1-2ヶ月かけて実施しました。
null(undefined)の値をメソッドチェーンで参照しようとして発生したエラーが多い印象でした。再現が難しく解決に時間を要するものもありました。

## エラーを送信しないようにする方法
Sentryでエラーを送信しないようにする方法は2つあります。
- [エラーの送信前にフィルタリング](https://docs.sentry.io/platforms/javascript/guides/vue/configuration/filtering/)するコードを書く(`beforeSend`)
- Business plan以上の場合、[Delete & Discard](https://docs.sentry.io/pricing/quotas/manage-event-stream-guide/#delete--discard)で画面上からエラーを送信しないようにできる

弊社はチームプランなので、エラー送信前にフィルタリングする方法を採用しました。

### 送信前のエラーに対して操作できる[beforeSend](https://docs.sentry.io/platforms/javascript/guides/vue/configuration/filtering/#using-before-send)関数
第一引数が`event`、第二引数が`hint`です。[eventとhintの2つの引数があり、それぞれエラーやイベントなどの情報が入っています。](https://docs.sentry.io/platforms/javascript/guides/vue/configuration/filtering/#event-hints)
`beforeSend`を使って、送信前にエラーオブジェクトに対して操作できます(以下のコードは[公式ドキュメント](https://docs.sentry.io/platforms/javascript/guides/vue/configuration/filtering/#event-hints)より引用

```js
Sentry.init({
  dsn: "https://1e30cec9a9124eb6a66f70a799cee948@o360365.ingest.us.sentry.io/3631418",

  // Called for message and error events
  beforeSend(event) {
    // Modify or drop the event here
    if (event.user) {
      // Don't send user's email address
      delete event.user.email;
    }
    return event;
  },
});
```

エラーを送信しないようにするためには、`beforeSend`の戻り値をnullにします。
```js
Sentry.init({
  dsn: "https://1e30cec9a9124eb6a66f70a799cee948@o360365.ingest.us.sentry.io/3631418",

  // Called for message and error events
  beforeSend(event) {
    // Modify or drop the event here
    if (event.user) {
      // Don't send user's email address
      delete event.user.email;
    }
    return null;
  },
});
```

`beforeSend`を使用して、特定のエラーのみ送信しないようにすることが可能です。
例えば、ノイズになるため`4xx`系のエラーやネットワークエラーは送信しないようにしました。

# Sentry管理画面から見ることのできる情報
[Issue一覧画面](https://docs.sentry.io/product/issues/)と[Issue詳細画面](https://docs.sentry.io/product/issues/issue-details/)をよく使用します

## Issue
エラーはIssueと呼ばれます。Sentry管理画面のIssues画面から確認できます。
ステータスについては、[Issueのステータス一覧](https://docs.sentry.io/product/issues/states-triage/)に詳しい説明が記載されています。

新しいエラーが発生すると`Unresolved`でIssueが作成されます。エラーを修正した場合は画面から`Resolved`にステータスを変更します。

緊急性が低いもの、またはノイズになるエラーは`Archived`に変更します。`Archived`にするとアラートを停止します。短時間に大量のエラーが発生すると`Escalating`になります。

Issueを完全に削除した場合は、同様のエラーが再発すると新しいエラー扱いになります。

## [Issue Grouping](https://docs.sentry.io/concepts/data-management/event-grouping/)
Sentryはエラーの類似性を評価し、同様のエラーを同じIssueにグルーピングしています。

`fingerprint`→`stack trace`→`exception`→`message`の順でグルーピングに使用されます。後者に行くほどグルーピング精度は下がります。

どの方ほグルーピングされたかは、Issue詳細ページの一番下で確認できます。

## Activity
Issueに対してコメント可能です。

## [Environments](https://docs.sentry.io/concepts/key-terms/environments/)
環境tagを設定し、どの環境で発生したエラーか区別できます。

[SDK初期化時に設定します。](https://docs.sentry.io/platforms/javascript/configuration/environments/)

## [Releases](https://docs.sentry.io/product/releases/)
ソースコードにバージョンをつけることができます。

どのバージョンのソースコードで発生したエラーか区別できます。

# [Session Replay](https://docs.sentry.io/product/explore/session-replay/)
Session Replayを導入すると、ユーザーの行動(画面録画、ネットワークリクエスト、DOMイベントやコンソールログ)を記録し再生することができます。

エラーを手元で再現しやすくしたり、ユーザーが操作に戸惑っている箇所を確認しUI/UX向上に役立てることができます。

ユーザーのエラーの再現が今回の目的なので、エラー発生時のみSession Replayを記録するようにしました。

## Set up

[こちら](https://docs.sentry.io/platforms/javascript/guides/vue/session-replay/#set-up)を参考に、以下のような設定にしました。
```js
import * as Sentry from "@sentry/vue";

Sentry.init({
  replaysSessionSampleRate: 0.0, // 正常時の記録は不要なので0.0

  replaysOnErrorSampleRate: 1.0, // エラーを記録したいので1.0

  integrations: [
    Sentry.replayIntegration({
      maskAllText: true, // テキストは個人情報を含むのでマスキング
      blockAllMedia: true, // img、svgやvideoなどは全て記録しない
      networkCaptureBodies: false // リクエスト・レスポンスボディは記録しない
    }),
  ],
});
```
細かい設定がいろいろできるので、詳細は以下を参照してください。
https://docs.sentry.io/platforms/javascript/session-replay/configuration/

## [privacy](https://docs.sentry.io/platforms/javascript/guides/vue/session-replay/privacy/)について
Session Replayでは、ユーザーが入力したパスワードや個人情報を記録しないようにできます。

Masking、Blocking、Ignoringの3種類あります。
### [Masking](https://docs.sentry.io/platforms/javascript/guides/vue/session-replay/privacy/#masking)
- テキストをマスキングする
    - ユーザーが入力したテキストに限らず、画面に表示されているテキストも対象
- defaultで`*`でマスキング

### [Blocking](https://docs.sentry.io/platforms/javascript/guides/vue/session-replay/privacy/#blocking)
- 指定した要素ごとマスキングする
- マスキングされると、空欄スペースになる

### [Ignoring](https://docs.sentry.io/platforms/javascript/guides/vue/session-replay/privacy/#ignoring)
- フォームのみ指定可能
- 再生した時に、inputフィールド内に文字が出てこなくなる

### class属性などを指定して、個別にマスキング対象を指定可能です
以下を参照ください。
https://docs.sentry.io/platforms/javascript/guides/vue/session-replay/privacy/#privacy-configuration

## [request・response body はデフォルトでは収集しません](https://docs.sentry.io/platforms/javascript/guides/vue/session-replay/privacy/#network-request-and-response-bodies-and-headers)
収集するのは、URL、request・response body size、method、status codeです。
[収集する項目をカスタマイズするには、`networkDetailAllowUrls` や`networkCaptureBodies` を設定します。](https://docs.sentry.io/platforms/javascript/session-replay/configuration/#network-details)
[request・response bodyを収集する場合でも、クレジットカード情報、社会保障番号、パスワードなどSentryがパターンマッチで検知してScrubしてくれます。](https://docs.sentry.io/platforms/javascript/guides/vue/session-replay/privacy/#network-request-and-response-bodies-and-headers)

## 送信前の録画に対して操作できる[beforeAddRecordingEvent](https://docs.sentry.io/platforms/javascript/session-replay/privacy/#custom-scrubbing)関数
beforeAddRecordingEventを使用することで、画面録画から個人情報を削除したり特定の録画イベントを無視することができます。


# 現在の弊社の運用ルール
フロントエンドのアラート内容は玉石混合で、全てを解決するのは現実的ではありません。発生期間、priorityやステータスなどで適切にラベリングし、優先順位をつけて対応するようにしました。

ノイズになるため、エラーが発生してもリアルタイム通知はしていません。ただし重要度の高いエラーを見逃してしまう可能性があるため、短時間に大量のエラーが発生した場合は通知する予定です。