2020-05-29

Sentryをちゃんとセットアップしたら、想像以上にできるやつだった話(フロントエンドのエラー監視)

1010real / okamotti

はじめに

こんにちは。エンジニアリング事業本部の@1010realです。 今年初めからスマートショッピングに入社して、既存サービスの管理画面のリプレースや運用・新規機能開発におけるフロントエンド開発を全般的に行っております。

今回はWebフロントエンド開発における本番環境のエラートラッキング及びそのバグフィックスについて、Sentryを用いて効率的に行う方法を紹介しようと思います。 内容としては、Webフロントエンドに限らず、バックエンドやアプリなど、Sentryが対応しているPlatformであれば、参考にできる内容となっています。

目次

  • はじめに
  • 目次
  • Sentryについて
  • なぜこの記事を書こうと思ったか
  • ちゃんとセットアップってどういうこと?
  • 導入手順
    • 最初のエラーをトラッキングするまで
    • ユーザ情報をログに付与する
    • リリース(リビジョン)管理とソースマップ連携
    • アラート設定
    • その他
  • 運用に当たって
  • 開発時のTips
  • 最後に

Sentryについて

Sentryはオープンソースのエラートラッキングソフトウェアです。 ユーザはランタイムエラーが発生したタイミングでSentryに適切にログを送ることで、エラーの可視化と管理を行うことができます。 スマートショッピングではSaaS利用しています。

なぜこの記事を書こうと思ったか

Sentry自体はかなり昔から存在していて、オープンソースと言うこともあり、試してみた系の記事はたくさん見つかります。事実、エラーをトラッキングするだけなら、ほんの数行のコードを設定するだけで可能です。 ただし、実際にサービスを運用していく中での活用事例はあまり見つけることができず、効率的にエラーのモニタリングと解決を行うためには、もう一歩踏み込んだ設定が必要だと感じている今日この頃です。 なので今回は、現時点での自分なりのベストプラクティスを紹介しようと思います。

ちゃんとセットアップってどういうこと?

Sentryにはじめてログインすると、管理画面のサイドメニューに「Setup Sentry」と言うメニューが表示され、Todoがリストアップされています。これを終わらせる=「ちゃんとセットアップできた」と定義しています。 setup-sentry 導入手順もこれに沿って紹介していこうと思います(細かい部分は割愛します)

導入手順

最初のエラーをトラッキングするまで

  1. Sentryを利用するためにまずはアカウントを登録します。 https://sentry.io/welcome/ ※ちなみに今回の記事は、Teams以上のプランでの活用を前提としています。Freeプランで利用する場合には適宜設定をスキップしてください。

  2. 管理画面が表示できたら、「Settings」 -> 「Members」から、チームメンバーを招待しましょう。 その後、「Settings」 -> 「Teams」で新しいTeamを作り、メンバーを紐つけます。

  3. 「Projects」メニューからプロジェクトを追加します。 プラットフォームを選択し、プロジェクト名と先程のTeamを指定するだけです。 プロジェクトができたら、「Settings」 -> 「Projects」から該当のプロジェクトを選択し、「Client Keys (DSN)」を確認しておきます。 これがSDKがエラーイベントをこのプロジェクトに対して、送信する際のエンドポイントとなります。

  4. クライアントにSDKをセットアップし、先ほどのDSNに対してエラーを送信するように設定します。 Nuxt.jsを使用している場合は、@nuxtjs/sentryのインストールとnuxt.config.jsへの設定のみです。

    $ yarn add @nuxtjs/sentry

nuxt.config.js

    ...
    modules: [
      ...
      '@nuxtjs/sentry' // 追加
    ],
    sentry: {
      dsn: process.env.ENV === 'production'
        ? 'https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@sentry.io/xxxxxxx'
        : false  // DSNを設定
    },

他の主要なプラットフォームを使用している場合には、「Settings」 -> 「Projects」から該当のプロジェクトを選択し、「Error Tracking」を確認すると導入方法が見つけられると思います。 存在しない場合は、このサイトからも検索できます。

この設定を反映後、数分あるいは数時間後に、「Issues」内に、新たなIssueが追加されるはずです。 もし、1日経っても追加されない場合は、設定が間違っているか、あなたのサイトが全くエラーを起こさない完璧なサイト(あるいは握り潰している)かのいずれかでしょう。 Issues - smartshopping - Sentry 2020-05-28 03-29-20 ここまでで、Sentry-Setup内、以下項目は完了となります。

  • Create a Project
  • Send your first event
  • Invite team members

ユーザ情報をログに付与する

Sentryはこれまでの設定だけでも非常に多くの情報を開発者にもたらしてくれます。 OS, Browser, IP Address, Stack Trace, User Activity... ただ、それぞれのプラットフォームで管理しているユーザ情報は、別途ログに付与して送信してあげないとSentryは検知することができません。(勝手に収集されたら困りますよね。。) どんなユーザがどの程度影響を受けているかどうかも、Issueの重要度に関与してきますので、問題のない情報は追加してあげると良いと思います。

Sentry.configureScopeを使用して、エラーログにユーザ情報を付与することができます。 Nuxt.jsの場合は、以下のようになります。

// ユーザ認証直後の処理に追加
const user = { id: "xxx", email: "yyyyy@zzzzz.com" }
this.$sentry.configureScope((scope) => {
  scope.setUser({ id: user.id, email: user.email })
})

※@nuxtjs/sentryを利用すると、this.$sentryでSentryインスタンスにアクセスできます。

これで、Sentry-Setupの項目は完了となります。

  • Add user context

リリース(リビジョン)管理とソースマップ連携

エラーの原因調査において、そのエラーがどのリリースに起因して起きているかを特定することも解決への近道です。 事前に、Sentryにリリースバージョンとその内容を登録しておいて、Sentryへのエラーログ送信時にリリースバージョンを付与してあげることで、どのバージョンでどんなエラーが起こったかを追跡できるようにすることができます。

また、リリースの内容にソースマップを紐づけることで、StackTraceがMinifyされたコードではなく、ソースマップ上の行数で表示できるようになります。 Minifyされたコードだと、結局どこが問題かわからないため、開発環境で再現するのに苦労すると思いますが、その手間がなくなり、ぱっと見で原因のコードが特定できて爆速で修正できるIssueが増えるので、それも行っておくと良いと思います。

Sentryへのリリース内容の登録は sentry-cli を利用して行うことができます。 コマンドラインからも実行できますが、CIに組み込むのが良いと思います。 スマートショッピングでは、github workflowを使ってCIを行っていますので、その前提で説明します。

※ちなみに、Nuxt単体であれば、sentry-cliを使わなくても、リリース管理は可能みたい(詳しく調べてない)ですが、他プラットフォームでも再利用できるように、今回はsentry-cliを利用してCIに組み込んでいます。

  1. sentry-cliを叩くためのApiKeyを以下で発行します。 https://sentry.io/settings/account/api/auth-tokens/

  2. Sentryをビジネスプラン以上で利用している場合は、「Setting」 -> 「General Setting」から組織名も確認しておきます。

  3. 1,2で確認した値をCI実行時の環境変数として定義します。 SENTRY_AUTH=[先程発行したApiKey] SENTRY_ORG=[組織名]

  4. CI時にソースマップをSentryにアップロードしたいので、必要に応じてci用のビルドを定義します。

nuxt.config.js

...
build: {
    ...
    extend(config, ctx) {
      if (process.env.MODE === 'ci' && ctx.isClient) {
        config.devtool = 'source-map'
      }
    }
  },
...

package.json

  "scripts": {
    ...
    "build:ci": "VERSION=$VERSION ENV=production MODE=ci nuxt build",
    ...

※$VERSIONについては、追って説明します

  1. エラーログとリリースバージョンを紐づけるために、ClientSDKからの送信情報にReleaseVersionを追加します。 sentryに登録したリリースバージョンと一致させる必要があるので、実行時に外部から渡す形にしてあげると良いかと思います。

nuxt.config.js

  ...
  sentry: {
    dsn: process.env.ENV === 'production'
        ? 'https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@sentry.io/xxxxxxx'
        : false
    config: {
      release: `xxxxxx@${process.env.VERSION}` // 追加。xxxxxxにはプロジェクト名を指定
    }
  },
  ...

package.json

  ...
  "scripts": {
    ...
    "start:prd": "VERSION=$VERSION ENV=production nuxt start",
    ...
  1. リリース内容にコミット情報を付与するために、Sentryと開発対象のリポジトリを連携します。 「Organization Settings」 > 「Integrations」 から、Githubを連携します。 インストール後、「Configure」ボタンから、開発対象のリポジトリを追加します。

  2. 下準備はできたので、デプロイ時にリリース情報をSentryへ登録するジョブを作成していきます。

.github/workflows/xxxx.yml (本番環境へのデプロイ用定義)

...
sentry-releases-new:
    needs: build
    name: Create new releases
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: 12.x
      - name: Create new releases by sentry-cli
        env:
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
        run: |
          SENTRY_PROJECT_NAME="xxxxxx"
          GIT_REF=$(git describe --always)
          SENTRY_RELEASE_VERSION="${SENTRY_PROJECT_NAME}@${GIT_REF}"
          # create sourcemaps
          yarn install
          yarn build:ci
          # sentry releases integration
          curl -sL https://sentry.io/get-cli/ | bash
          sentry-cli releases new -p $SENTRY_PROJECT_NAME $SENTRY_RELEASE_VERSION
          sentry-cli releases -p $SENTRY_PROJECT_NAME files $SENTRY_RELEASE_VERSION upload-sourcemaps .nuxt/dist/client --rewrite --url-prefix '~/_nuxt/'
          sentry-cli releases set-commits --auto $SENTRY_RELEASE_VERSION
          sentry-cli releases finalize $SENTRY_RELEASE_VERSION
          sentry-cli releases deploys $SENTRY_RELEASE_VERSION new -e production
  ...

  deploy:
    needs: [build, sentry-releases-new]

内容について補足しておきます。

  • GIT_REF=$(git describe --always) コミットハッシュをバージョンの一部としています。(タグでも良いと思います)

  • curl -sL https://sentry.io/get-cli/ | bash sentry-cliのインストール
  • sentry-cli releases new -p $SENTRY_PROJECT_NAME $SENTRY_RELEASE_VERSION 指定したプロジェクトに新たなリリース定義を作成します
  • `sentry-cli releases -p $SENTRYPROJECTNAME files $SENTRYRELEASEVERSION upload-sourcemaps .nuxt/dist/client --rewrite --url-prefix '~/_nuxt/' リリースにソースマップを紐付けます。また、StackTrace上のパスに合わせて、プレフィックスを指定します。これに寄りSentryがエラーを検知した際に、StackTraceの内容をソースマップ上で展開して表示することが可能になります。
  • sentry-cli releases set-commits --auto $SENTRYRELEASEVERSION 指定したリリースにコミット履歴を紐付けます。(これには、手順6のリポジトリ設定が必須です) ちなみに--autoオプションを指定すると、前回のリリースからの差分を自動で紐付けてくれます。(初回は最新20件のコミットを勝手に紐付けます)
  • sentry-cli releases finalize $SENTRYRELEASEVERSION リリース内容を確定させます。
  • sentry-cli releases deploys $SENTRYRELEASEVERSION new -e production 確定させたリリースが、正式にデプロイされたことを通知します。

  • あとは本番環境での実行時に、作成したリリース定義と同じVERSIONを渡して、起動してあげるようにしてください。

以上で設定は終わりです。設定が正常であれば、以下のような情報が確認できるようになります。

Releases リリース毎にデプロイした日付やこのリリースに関連するIssueを確認できます。

Release commit-ffd9fb5 詳細を見ると、リリース後どのくらいのエラーがトラッキングされているかをグラフで確認することができます。 また、含まれるコミットやコード差分、リソース及びソースマップを確認できます。

TypeError: Cannot read property 'location' 5 Issueについても、releaseタグが追加され、どのバージョンのソースで起きたエラーかがわかります。

TypeError: Cannot read property 'location' 6 また、StackTraceの表示も、OriginalとMinifiedを選択できるようになり、ダイレクトに問題が起こっている行数が確認できます。 また、同様のエラーがリリースバージョン毎にどのくらいの割合で起こっているかも表示されます。

エラーの起こっている行数が判明すれば、対応は容易です。また、そこまでわからなかったとしても、どのリリース以降起きているかを絞り込めれば、疑うべき差分が明確になり、作業時間は大幅に短縮できます。

これで、Sentry-Setupの項目は完了となります。

  • Set up release tracking
  • Upload source maps

アラート設定

まずは、自分のよく見るツールに通知を送れるようにしましょう。Slackでも、DataDogでも良いと思います。 「Organization Settings」 > 「Integrations」 から連携しましょう。

次にアラートを送るルールを追加しましょう。 プロジェクトの「Settings」 > 「Alerts」 から任意のアラート設定を追加することができます。 新しいエラーの場合、一定期間に何回/何ユーザで発生したかなど、様々なルールが設定できます。

Sentry alert 2

通知は多すぎると形骸化して見なくなりますし、とはいえクリティカルなものはすぐに確認したいものです。 適切なルールを設定するためには、何度か試行錯誤が必要になると思います。 個人的なおすすめは、まず下記を設定し、あとは微調整していくと良いと思います。 - An issue is first seen 初めて検知したエラー - An issue changes state from resolved to unresolved 修正したはずのエラーを再検知 - An issue is seen by more than {value} users in {interval} 期間内に一定以上のユーザに影響が出ているエラー (valueとintervalはサイトのPVやユーザ数による)

これで、Sentry-Setupの項目は完了となります。

  • Configure alerting rules

その他

Sentry-Setupには含まれていませんが、以下は有効にしておくと良いと思います。

  • 一定期間起こっていないエラーに対するIssueは、勝手に解決にする Issueの中には、他の修正や依存パッケージのアップデートにより勝手に修正される場合もあります。 そのようなIssueに時間を割いていてはもったいないですし、そもそも長期間起こっていないエラーを今直す理由がありません。 自動で解決済みにして、よりクリティカルなエラーに向き合いましょう。

プロジェクトの「Setting」 > 「General Setting」から有効にできます。 Project Settings - smartmat-admin - Sentry 2020-05-28 03-40-40

運用に当たって

以下2点を心がけています。 - Issueのトリアージを意識する どんなエラーに対しても全て100%で向き合っていたら、おそらくいつまでたっても新規開発が進められません。 Issueを確認する際には必ず問題の重要性と影響範囲(ユーザ数・対象ブラウザ等)を把握し、対応の優先順位付けを行うようにします。 一人当たりにアサインするIssueは常時2つ以下としています。

  • 積極的にIssueを除外する 依存モジュール内のバグなどもトラッキングされてくるのですが、致命的でない物も多いです。 より緊急性・重要性の高いIssueを見えやすくするため、積極的に除外すると良いと思います。 未来永劫、同じエラーを表示しないようにするには、ゴミ箱アイコン横のプルダウンから設定できます。 Issueが1ページに治らなくなってくると、もうカオスになってくるので、そうなる前に除外していきましょう。 Delete and discard event

開発時のTips

  • エラーを握り潰さないでSentryに送信する try-catchでcatch句に入った時の処理ってどうしてますか? 例外が発生したとユーザに表示して処理を終えるかもしれませんし、もしかしたらconsoleに出力して終わりにしている方も居るのではないでしょうか?

実装にも寄りますが、あくまで例外処理なので外部環境に依存するApiリクエストのような処理はTry-Catchしておくべきだというのが一般的だと思います。 ただ、どんな理由で例外に入ったかはトラッキングしておくと、予期しないエラーが起きていることを知る手立てになるかもしれません。 ※特に400 Bad Requestなどは、フロントでのリクエストパラメータ生成が間違っており、ユーザが何かしらの操作をできなくなっている可能性が高いので、要注意です。

もちろんエラーをThrowしてあげれば、Sentryにログが送信されますが、ユーザに見せたくはないと思います。その場合は、Sentry.captureExceptionを使うと良いです。

async asyncData ({ params, $sentry }) {
  try {
    let { data } = await axios.get(`https://my-api/posts/${params.id}`)
    return { title: data.title }
  } catch (error) {
    this.$sentry.captureException(error)
  }
}

最後に

最近別のプロジェクトにも新たにSentryを導入したのですが、Issueがガンガン作られているので、より一層エラーのトリアージが大事だなと感じています。限られた人数の中でも、効率よく進めていきたいです。 また、Sentryに限らず、エラーのトラッキングがしっかりできていると、開発者が物怖じせずガンガンコードをリファクタしていけるので、こういう仕組みは大事だなと感じています。

最新の記事