Sentry だけに Production 環境のソースマップを表示させる

JavaScript

Production のように、Sentry だけにソースマップを読んでほしい環境がある時の備忘録。

環境、サンプル

  • Node v14.5.0
  • webpack 5.25.0
  • @sentry/webpack-plugin 1.14.2
  • @sentry/react 6.2.2
  • SaaS 版 Sentry

サンプルコードはこちら

Sentry ソースマップの扱い

ブラウザでソースマップを認識するには、バンドル内にリファレンス用のコメント(pragma)を含めるか、Sourcemap または X-SourceMap ヘッダーを追加する必要がある。
Use a source map - Firefox Developer Tools | MDN
SourceMap - HTTP | MDN

一方 Sentry ドキュメントの Source Maps for JavaScript によれば、スタックトレース内の URL をスクレイピングして、ソースマップを自動的に Fetch する、とある。
このソースマップは、 Sentry に Artifact としてアップロードするもの。

「ソースマップを出力しながら参照はさせない」という方法については、webpack の devtool オプションで指定するソースマップのタイプに、hidden- prefix をつけることで可能。

webpack.prod.config

// 〜〜省略〜〜
  devtool: 'hidden-source-map',
// 〜省略〜

これで、ソースマップを出力するが、バンドルファイルにはリファレンスを記載しなくなる。
だから hidden-sourcemap で出したソースマップを、Sentry にアップロードすれば終わり。
と思っていたがうまくいかない。

ソースマップのアップロード方法

まず、ソースマップを Sentry にアップロードする方法について書き残す。
あらかじめ、ここでトークンを発行しておく。
そして、ドキュメント を参考に、以下のような環境変数を ~/.sentryclirc~/.env に埋め込んでおく。

  • SENTRY_AUTH_TOKEN (発行したトークン)
  • SENTRY_ORG (organization)
  • SENTRY_PROJECT (project)

sentry-cli には、ソースマップのアップロード向けコマンドがある。
自動的に指定フォルダ内のソースマップを確認し、ソースマップ対象のバンドルファイルと一緒にアップロードしてくれる。

バンドル自体はアップロードしなくていいものの、Sentry 上で正確に表示されなくなるのでほぼ必須。
また、アップロード先の release の名前は Sentry.init() に渡しているものと揃える。

shell

# dist にある場合
# SENTRY_AUTH_TOKEN, SENTRY_ORG などの env が渡っている場合
# "my-project-バージョン" という release にアップデートする場合
sentry-cli releases files my-project-$npm_package_version upload-sourcemaps ./dist/

また、今回は webpack でバンドルするので、CLI をラップした sentry-webpack-plugin が使える。

webpack.prod.js

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

// 〜〜省略〜〜
plugins: [
  new SentryCliPlugin({
    authToken: process.env.SENTRY_AUTH_TOKEN,
    org: 'my-sentry-arg',
    project: 'my-project',
    release: `my-project-${process.env.npm_package_version}`,
    include: './dist',
  }),
],
// 〜〜省略〜〜

ソースマップがない時のトレース。何も見えない。
バンドルファイルがそのまま表示されているので、エラーを追うのが困難

ソースマップがある時のトレース。コードが見える。
バンドルファイルが難読化された前の状態で見えるので、エラーが追いやすい

これを hidden-source-map で実現しようとして、少し困ったという話。

hidden-source-map でうまくいかない時

A. ディレクトリ構造を一致させる

よく見るミスが、Artifact のディレクトリ構造が違うこと。
Sentry フォーラムの関連スレ によれば、Sentry の Artifact と、ページで読み込まれる JS のディレクトリ構造が一致していなければ、正常に動作しない。
sentry-cli のデフォルトでは、 Artifact のアップロード先が ~/ となる。
例えばバンドルファイルの出力先が ~/js/ にあるなら、sentry-cli 側でも --url-prefix オプションを付けて、~/js/ にアップロードする必要がある。

ただし今回の場合、Artifact と JS のパスは同じ ~/*.js になっているのでこれは当てはまらない。

B. 拡張子を min.js に変更する

Sentry や sentry-cli のテストケースを見ると、どうも min.js を期待しているようなので、雰囲気で拡張子を min.js にしたら読み込めるようになった。

webpack.common.js

// 〜〜省略〜〜
  output: {
    filename: '[name].min.js',
  },
// 〜〜省略〜〜

upload-sourcemap を見ても、確かに foo.min.js を指定するように書かれていて foo.js じゃなかった。
そして、このコメントこのコメントを見ても、.min.js にすることで正しく動いている様子。
これがバグか仕様か判断が付かず Issue を書いていたが、1回 min.js で通すと bundle.js でも .js でも通ってしまうし、再現できなくなって困っているところ。

C. hidden なしで出力して、コメントだけ消す

A がダメで B も難しそうな時のワークアラウンド。
hidden- をつけず、普通のソースマップを出力する。
そして、sed など使って、バンドルの末尾にあるソースマップのコメントを消す。
こちら にいい感じのコマンドがあったので参考にした。

shell

find 'dist' -type f \( -iname \*.js \) -exec sed -i -E 's/\/(\/|\*)# sourceMappingURL=[^ ]*\.js\.map(\*\/)?//' {} +

#※Mac(BSD)の場合はこう
find 'dist' -type f \( -iname \*.js \) -exec sed -i "" -E 's/\/(\/|\*)# sourceMappingURL=[^ ]*\.js\.map(\*\/)?//' {} +

おまけ1: ソースマップの削除

hidden-source-map は参照こそ消すものの、ファイル自体は残される。
Sentry にアップロード後は不要なので、rmrimraf で消しておく。

shell

rm -rf ./dist/*.js.map

おまけ2: 古い Artifacts の削除

[contenthash] などを付けてバンドルしている場合、ハッシュが変わるたびに新しい Artifact が生成される。
肥大化するので、アップロード前に初期化しておいた方が良さそう。
現状 sentry-webpack-plugin ではこの機能にアクセスできないので、sentry-cli を使う。

shell

# SENTRY_AUTH_TOKEN, SENTRY_ORG などの env が渡っている場合
# "my-project-バージョン" というリリース名にする場合
npx @sentry/cli releases files my-project-$npm_package_version delete --all

終わり

Sentry でソースマップをうまく読み込めない、という報告が無限にあるみたい。
ドキュメントにトラブルシューティングがあるので、困った時に読んでみると良さそう。
Troubleshooting for JavaScript | Sentry Documentation