Sentry JavaScript Heroku ECブログリレー

Sentryでソースマップを活用してHerokuから配信するSPAのエラー調査を楽にする

Sentry JavaScript Heroku ECブログリレー

カラーミーショップ DXチームのkymmtです。この記事では、webpackなどでビルドしてHerokuから配信するシングルページアプリケーション(SPA)でエラーが起きたとき、Sentryにエラーを送信しつつ、ソースマップを活用して元のソースコードのどこでエラーが起きたのかを特定する方法について説明します。

ソースマップ利用前 ソースマップ利用後

想定するアプリケーション

この記事では、実際の社内での作業に基づいて、次のようなアプリケーションを想定します。

一方で、上記の想定に当てはまらなくても、SPAをwebpackでビルドし、Sentryを使うのであれば、この記事はなんらかの知見を提供できると思います。

実現すること

この記事での目標は次の項目を実現することです。

  • HerokuにソースコードをデプロイするとSentryのリリースを自動で作成する
  • Herokuから配信するSPAでエラーが発生すれば、対応するリリースに紐づくソースマップを使ってSentryでわかりやすいエラーのスタックトレースが見られる

次の項目を実行することで、上記の目標を実現できます。

  • webpackでのビルド時にソースマップを生成する
  • SentryとHerokuを連携し、現在のリリースのトラッキングする
  • Herokuへのデプロイ時にSentryへソースマップとビルド前後のソースコード(アーティファクト)をアップロードし、リリースも作る

ソースマップとは

SPAをブラウザに配信するとき、事前にwebpackなどのツール(いわゆるバンドラー)を用いて、TypeScriptやReact.js/Vue.jsなどで書かれた複数のソースコードファイルをブラウザで実行できるファイルに変換(バンドル)します。その過程でトランスパイルやミニファイを実行します。

バンドル済みコードをブラウザで実行したときにエラーが発生すると、ブラウザの開発者ツールでエラーと発生したコードの位置を確認できます。しかし、開発者ツールはバンドル済みコードに基づいてエラーを表示するので、実際に確認してみても人間にはよくわからない見た目のコードになっています。これでは調査やデバッグに支障をきたします。そして、この問題はSentryでエラーのスタックトレースを見るときにも発生します。

そこで、バンドル前後のソースコードの名前や行/列の対応を格納したファイルであるソースマップを利用します。ソースマップを生成し、sourceMappingURLというディレクティブでバンドル済みファイルの中からソースマップのURLを参照すると、開発者ツールでバンドル前のコードに基づいたエラー情報を見ることができます。

とはいえ、この記事ではソースマップ自体についての詳しい説明はしません。他に詳しい記事があるので、そちらを参照してください:

Sentryでソースマップを使うための手順

先ほど図に示した構成を実現してSentryでソースマップを利用する方法について、順を追って説明します。

1. バンドラーでソースマップを生成する

webpackのdevtoolオプションを設定する

webpackにはdevtoolというオプションがあります。一見どのような機能を提供するのかわからない名前ですが、このオプションを使うとバンドル時のソースマップの生成方法を制御できます1

devtoolオプションの本番での利用方法についてのドキュメントを読むと、本番環境でソースマップを生成するときは次のいずれかの値を使うと書いてあります:

  • source-map
    • ソースマップを生成する。バンドルしたソースコードにsourceMappingURLでソースマップを配信するURLを含める。よって、ブラウザの開発者ツールでバンドル前のソースコードやファイル名が閲覧できる
  • hidden-source-map
    • ソースマップを生成する。しかし、バンドルしたソースコードにsourceMappingURLを含めない
  • nosources-source-map
    • ソースマップを生成するし、sourceMappingURLも含める。しかし、ソースマップ中に元のソースコードの情報は含めない

結論をいうと、ここではhidden-source-mapを使います。

本来、本番環境ではバンドル済みのソースコードだけを開発者ツールで閲覧させたいものです2。よってsource-mapnosources-source-mapは使えないということになります。

このあと説明しますが、今回はSentryにソースマップなど数種類のファイル(アーティファクト)をアップロードします。そして、SentryはそれらのアップロードされたファイルをSentry上でのエラーの表示に利用します。よって、開発者ツールでエラーを見るわけではないので、バンドルしたソースコードからsourceMappingURLを通じてソースマップを参照する必要はありません。したがって、ここではdevtoolの値としてhidden-source-mapを選びます。

ミニファイ用プラグインの設定値を確認する

2021年現在、webpackなどのバンドラーによるバンドル時に、プラグインを通じてソースコードをミニファイすることが多いです。webpack 5ではTerserPluginを使うことが多いようです。他にも、すでに非推奨ですがUglifyJSPluginというものもあります。今回扱ったアプリケーションはビルドに関する環境が少し古く、まだUglifyJSPluginを使っていました。

これらのミニファイ用プラグインもソースマップを生成するかどうかを決める設定を持ちます。たとえばTerserPluginの場合はwebpackのoptimizationオプションについてのドキュメントに次のような記述があります。

sourceMap: true, // Must be set to true if using source-maps in production

ミニファイ用プラグインを使っているときはこれらの設定も有効にしないとソースマップが生成されません。気をつけましょう。

2. HerokuへのデプロイをSentryでトラッキングするためにHerokuとSentryを連携する

バンドラー側でソースマップを生成する手筈が整ったので、次はSentryの準備をします。

Sentryでソースマップを使うときは、Sentryにおけるリリースというものを使う必要があります。リリースはある環境へデプロイしたバージョンを表す概念です。

後述のとおり、webpackでビルドするときにSentryへソースマップなどを含むアーティファクトをアップロードすると、ビルド中のコミットのハッシュ値を名前とするリリースをSentryに作り、ソースマップはそのリリースに紐づきます。そして、SPAでエラーが発生するときは、現在デプロイされているリリースと突き合わせることで、Sentryのエラー画面でバンドル前のソースコードの表示を実現しています。

現在デプロイされているリリースをトラッキングするためにSentryのHerokuアドオンを使います。さらに、HerokuアドオンはGitHubアドオンを設定することが前提になっているので、そちらも使います3

GitHubとSentryを連携する

まずGitHubアドオンを設定します。ペパボではGitHub Enterprise Server (GHES)を使っていますが、SentryはGHESにも対応しているので問題ありません。詳細はSentryのGitHubアドオンについてのドキュメントに書いてあるので割愛しますが、主に次のような作業をこなす必要がありました。

  1. GHESの該当organizationにSentryとGHESを連携させるためのGitHub Appを追加する
  2. SentryのGHESアドオンの画面からGitHub Appの画面へ遷移する
  3. Sentryとの連携が必要なリポジトリを選択する

HerokuとSentryを連携する

GitHubとの連携が終われば、Herokuアドオンを設定できるようになっています。こちらもHerokuアドオンのドキュメントのとおり設定します。

  1. SentryのHerokuアドオンを有効にして、GitHub連携時にSentryと連携させたリポジトリのいずれかを選ぶ
  2. HerokuのDeploy Hooksアドオンでデプロイ時にSentryのHerokuアドオンが発行したエンドポイントへ通知するよう設定する

これで、HerokuにアプリケーションをデプロイするとWebhookがSentryへ送信されます。この情報をもとに、Sentryは本番でのリリースをトラッキングできるようになります。よって、エラー発生時に本番でのリリースと紐づいたソースマップが利用できるようになります。

3. webpackでのビルド時にソースマップを生成してSentryへアップロードする

webpackプラグインの設定

Sentryへのソースマップなどアーティファクトのアップロードについて説明します。

まず、アップロードするソースマップはリリースに紐づける必要があります。リリースを作るにはいくつか方法がありますが、結論、webpack利用時はSentry自身が提供するSentry Webpack Plugin(npmのパッケージ名は@sentry/webpack-plugin)を使えばOKです。

このプラグインはwebpackのビルド中にSentry CLIの機能を使えるものです。次の設定で、バンドル前のコード、バンドル済みコード、ビルド中に生成したソースマップをアーティファクトとしてSentryにアップロードし、リリースを作り、アーティファクトとリリースを紐づけます。webpack.config.jsでの設定例を次に示します(実際のものとは異なります)。

const webpack = require('webpack')
const SentryWebpackPlugin = require('@sentry/webpack-plugin')

let plugins = []

// ...

if (process.env.NODE_ENV === 'production') {
  plugins.push(new SentryWebpackPlugin({
    authToken: process.env.SENTRY_API_KEY,
    org: process.env.SENTRY_ORG,
    project: process.env.SENTRY_PROJECT,
    include: '.',
    ignore: ['node_modules', 'build'],
  }))
}

module.exports = {
  mode: 'production',
  devtool: 'hidden-source-map',
  output: {
    path: path.resolve(__dirname, '../dist'),
    // ...
    // code splittingしているので次のような設定にしている
    sourceMapFilename: '[name].[chunkhash].js.map'
  },
  // resolve, module...
  plugins
}

プラグインには、まずSentryのWeb UIから得られるauthTokenやorganization名、project名を渡します。次に、includeでアーティファクトとしてSentryにアップロードするファイルを指定します。'.'を渡しているのでカレントディレクトリ配下すべてとなります。一方で、npmパッケージのコードやその他アップロードする必要のないファイルが存在するので、それらをignoreで除外しています。

Sentryへのアーティファクトのアップロード時に決まるリリース名の設定

この節はHeroku特有の事情の話です。

webpackを適切に設定すると、Herokuへgit pushでデプロイするとソースマップなどアーティファクトがSentryへアップロードされるようになります。このとき、デプロイ中のコミットのハッシュ値に基づいた名前でSentryのリリースが作られます。

SentryのHerokuアドオンのドキュメントを見ると、デプロイ中のコミットのハッシュ値を取得してリリース名を付与するために、HEROKU_SLUG_COMMITという環境変数を使うように書いてあります。HEROKU_SLUG_COMMITはHerokuのdyno metadataという機能を有効にしていると得られる環境変数で、現在Herokuで動いているアプリケーションのコミットのハッシュ値を持ちます。

しかし、Sentry CLIの2020年〜2021年時点での挙動に基づくと、この環境変数ではSentryにアップロードしたアーティファクトがSentry上の前回のリリースと紐づいてしまう現象が起きてしまいました。結論としてはSOURCE_VERSIONという環境変数を使えばいいのですが、この問題の詳細について次に説明します。

webpackプラグインとHerokuを使ってSentryのリリースを作るとき、リリース名は次のコードで決まります(Sentry CLI 2021年3月8日現在の最新であるv1.63.1)。

https://github.com/getsentry/sentry-cli/blob/1.63.1/src/utils/releases.rs#L93-L105

引用します。

    // try Heroku #1: https://docs.sentry.io/workflow/integrations/legacy-integrations/heroku/#configure-releases
    if let Ok(release) = env::var("HEROKU_SLUG_COMMIT") {
        if !release.is_empty() {
            return Ok(release);
        }
    }

    // try Heroku #2 https://devcenter.heroku.com/changelog-items/630
    if let Ok(release) = env::var("SOURCE_VERSION") {
        if !release.is_empty() {
            return Ok(release);
        }
    }

コードを見るとわかるように、デプロイ中のコミットのハッシュ値を取得するとき、HEROKU_SLUG_COMMITを先に見てからSOURCE_VERSIONを見るという順番で探索しています。

上述したようにSentryのドキュメントでHEROKU_SLUG_COMMITを使うよう書かれていた4ので、私も最初はdyno-metadataを有効にしてアーティファクトのアップロードを検証していました。しかし、そもそもwebpackによるビルド中、つまりHerokuへのデプロイ中はHEROKU_SLUG_COMMITの値は現在Heroku上で動いているコミットのハッシュ値のままでした。つまり、前回のビルドの値のままということです。なので、この環境変数を使うと、SentryにアップロードしたアーティファクトはSentry上の前回のリリースと紐づいてしまう現象が起きてしまいました。

この問題を解決するために、今回はHEROKU_SLUG_COMMITは使わず、Sentry CLIのリリース名探索時はSOURCE_VERSIONにフォールバックさせました。SOURCE_VERSIONもHerokuが設定済みである環境変数です。この環境変数はHerokuにpushされてビルド中であるソースコードのバージョンを表しており5HEROKU_SLUG_COMMITとは違って、pushされたコミットのハッシュ値が入っています。つまり、この環境変数を使えば、アップロードするアーティファクトと新規に作るリリースが正しく紐づきます。

4. SentryのSDKを使ってエラーをSentryへ送信する

ここまでの作業で、ついにSentry上でソースマップが利用できるようになっています。ためしに検証用環境でエラーを発生させてみます。

SentryのJavaScript SDKは以前はraven-jsという名前のパッケージでした。しかし、最近はnpmのネームスペース@sentry配下にSentry自身が作っているパッケージが集約されており、raven-jsはinactiveな状態となっています。@sentry/browserか、もしくは対応するフレームワーク用のパッケージを使いましょう。

raven-js: Our old stable JavaScript SDK, we still support and release bug fixes for the SDK but all new features will be implemented in @sentry/browser which is the successor.

今回はVue.jsアプリケーションだったので@sentry/vueを使いました。適当な画面に次のようにSentryへのメッセージ通知を仕込みます。

<template>
  <!-- ... -->
</template>

<script>
  import * as Sentry from '@sentry/vue'
  // ...

  export default {
    // ...
    mounted () {
      Sentry.captureException(new Error('test: something went wrong'))

      // ...
    }
  }
</script>

このコードを含むコミットをHerokuの検証環境へpushすると、次のようにアーティファクトがSentryへアップロードされている様子がビルドログへ出力されます。

       > Found 74 release files
       > Analyzing 74 sources
       > Rewriting sources
       > Adding source map references
       > Bundled 74 files for upload
       > Uploaded release files to Sentry
       > File upload complete

       Source Map Upload Report
         Scripts
           ~/foo.bar.js
           ...

この状態で該当の画面を開いてみると、バンドル前のソースコードの状態でエラーが発生した行を確認できます。目標達成です。

ちなみに、右上の"Minified"をクリックするとバンドル後のソースコードも閲覧できます。

おわりに

この記事では、SPAで発生するエラーをSentry上で見やすくするためにソースマップを使う方法について説明しました。Sentryをより活用したい方の参考になれば幸いです。


  1. つまり、ブラウザの開発者ツール(devtool)で元のソースコードを確認できるようにするかどうか、という観点で命名されたと思われる 

  2. バンドルを通じたミニファイでファイルを軽量化したいという目的に反するし、難読化という側面もある 

  3. SentryのHerokuアドオンでGitHub上のリポジトリを設定する必要があり、その情報を得るためにGitHubアドオンが必要なことによる 

  4. ふつうのサーバサイドアプリケーションのデプロイ時に自動でSentryのリリースを作りたいならこの方法で問題ないので、ドキュメントは間違っていない 

  5. Buildpack APIのドキュメントSOURCE_VERSIONの欄を参照