Next.jsにSentryを導入した際の課題と解決策について
はじめまして、2021年11月に食べログFE(フロントエンド)チームにジョインした遠藤です。
Next.jsを採用した新規プロジェクトに参画し、Sentryの導入を行いました。本記事ではSentryを導入した際の課題と解決策について記載していきます。
1. はじめに
「Sentryとは何か?」、「食べログでSentryを選定した理由」などにご興味がある方はまず下記の記事を読んでみてください。
Sentryは便利ですが以前はアプリケーションに導入するにはいくつかのファイルを作成して、エラーやパフォーマンスをトラッキングするのに様々な設定を行う必要がありました。
そこでSentryが簡単にセットアップができるように@sentry/nextjsでwizardを提供してくれています。
wizardはコマンドを実行するだけでSentryに必要なファイルを自動で生成し、設定までしてくれる便利な代物です。
# @sentry/nextjs をインストール
npm install --save @sentry/nextjs
# @sentry.wizard を実行
npx @sentry/wizard -i nextjs
# 必要なファイルが自動生成される!!
自動生成されるファイル
sentry.client.config.js
sentry.server.config.js
next.config.js
sentry.properties
.sentryclirc
基本的にはwizardでファイルを自動生成した後に、next buildを実行するだけでSentryにソースマップをアップロードし、アプリケーション内のエラーを送信できるようになります。
とても親切ですね!wizardを使用すれば簡単にセットアップはできるのですが、wizardだけでは解決できない課題がいくつかあったので共有します。
本記事では7つの課題と解決策を紹介します。
2. 課題と解決策
課題① 開発環境以外でAUTH_TOKENを使用できない
wizardのコマンドを実行すると.sentryclircファイルが自動生成され、ファイル内にAUTH_TOKENの値が設定されます。
また、AUTH_TOKENは秘匿情報なので.gitignoreに.sentryclircが自動で追加されます。
ただ、このままでは開発環境以外ではAUTH_TOKENを読み込むことができませんし、AUTH_TOKENはSentryのOrganizationと接続するための認証情報なのでリポジトリにアップロードすることもできません。
解決策① 環境変数でAUTH_TOKENを注入する
デプロイ時に環境変数として値を注入することで、リポジトリへのアップロードは避けました
SentryではSENTRY_AUTH_TOKENという名前で環境変数を注入すると自動で読み込んでくれます。
【Tips】 本番環境のAUTH_TOKENはInternal Integrationで作成する
本番環境のAUTH_TOKENはDeveloper Settings のInternal Integrationで作成した値を使用しましょう。
個人のAUTH_TOKENではその個人がOrganizationから削除されるとAUTH_TOKENも無効になってしまうからです。
【Tips】 DSNは秘匿情報として扱わなくて良い
「DSNも秘匿情報として扱ったほうがいいのか?」と思った方もいるかと思います。
最新のDSNの形式は SECRET_KEY が含まれていないので秘匿情報として扱う必要はないようです。
# {PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}{PATH}/{PROJECT_ID}
https://public@sentry.example.com/1
DSNはエラーやtransactionを送信することのみを許可し、情報を読み取ることは許可していません。
仮にDSNが漏洩するとエラーやtransactionを外部から送信されるリスクはありますが、IPでブロックしたりDSNの値を変更する機能が提供されているので対処することができます。
課題② パフォーマンスのトラッキング上限を超える可能性がある
sentry.server.config.ts/sentry.client.config.tsファイルのSenry.initに定義されているtraceSampleRateがパフォーマンスを測定するためのtransactionをSentryに送る割合を決定しています。
デフォルトでは1.0(100%)になっているので、画面表示や画面遷移で必ずtransactionを送られるようになっています。
#sentry.server.config.ts/sentry.client.config.ts
Sentry.init({
dsn: SENTRY_DSN || "https://examplePublicKey@o0.ingest.sentry.io/0",
// デフォルトでは1.0(100%) トランザクションが送られる
tracesSampleRate: 1.0,
});
エラーだけではなくtransactionも契約プラン毎に受け取れる上限が決まっています。デフォルトの設定のままではすぐにトラッキング上限を超えてしまいパフォーマンス測定ができなくなる可能性があります。
解決策② traceSampleRateの設定を変更する
traceSampleRateの値を低くすればtransactionが送られる割合が低くなるので、単純に値を低くすればするだけトラッキング上限を超える可能性を低くできます。
ただ、値を低くしすぎて有用なパフォーマンス測定ができなければ意味がないので、traceSampleRateをどのくらいの値に設定すればいいのか悩むと思います。
traceSampleRateをいくつに設定すればいいかわからない場合は低い値から設定し、徐々に値を上げて調整していくのをSentryが推奨しています。
課題③ ソースマップのアップロードに関するログが表示されない
デフォルトではソースマップのアップロードに関するログは表示されないようになっているので、next build をしてもソースマップのアップロードに関するログは表示されません。
デバッグするときにログが表示されないと不便です。
解決策③ silentオプションをfalseにする
next.config.jsで設定できるsentryWebpackPluginOptionsのsilentをfalseにするとログが表示されるようになります。デフォルトではsilentがtrueになっています。
# next.config.js
const { withSentryConfig } = require("@sentry/nextjs");
const moduleExports = {
// your existing module.exports
};
const sentryWebpackPluginOptions = {
// デフォルトではソースマップのアップロードのログが表示されない
silent: true,
};
module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions)
送信先のOrganizationやProject、Release、アップロードされたファイルなどをログから確認できます。
また、対応するソースマップが存在しなければwarningが表示されるので、基本的にはログを表示したほうが良いでしょう。
課題④ イベンドハンドラ以外のエラーをトラッキングできない
onClickなどのイベントハンドラ内のエラーに関しては、何も設定せずにエラーをSentryに送信することができます。
ただ、デフォルトではNext.jsのpages配下などのコンポーネントで発生したエラーに関してはエラーが送信されません。
解決策④ pages配下に _error ファイルを作成する
pages/_errorファイルを作成する必要があります(Sentry導入前に作成してある場合はコードを追加)。
# pages/_error.js
import NextErrorComponent from "next/error";
import * as Sentry from "@sentry/nextjs";
const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => {
if (!hasGetInitialPropsRun && err) {
Sentry.captureException(err);
}
return <NextErrorComponent statusCode={statusCode} />;
};
MyError.getInitialProps = async ({ res, err, asPath }) => {
const errorInitialProps = await NextErrorComponent.getInitialProps({
res,
err,
});
errorInitialProps.hasGetInitialPropsRun = true;
if (err) {
// エラーをSentryに送信する
Sentry.captureException(err);
await Sentry.flush(2000);
return errorInitialProps;
}
Sentry.captureException(
new Error(`_error.js getInitialProps missing data at path: ${asPath}`)
);
await Sentry.flush(2000);
return errorInitialProps;
};
export default MyError;
Next.jsはpages/_errorファイルを作成するとコンポーネントのエラーはpages/_errorがキャッチする仕様があるので、それを利用してSentryにエラーの送信ができるようになっています。
課題⑤ エラーのスタックトレースが取得できない
next.config.jsでbasePathを設定しているとデフォルトではソースマップからエラーのスタックトレースを取得できません。
エラーをトラッキングできてもコードのどこでエラーが起きているかわかなければデバッグに時間がかかってしまいます。
解決策⑤ urlPrefixとincludeオプションを設定する
next.config.jsのsentryWebpackPluginOptionsにあるurlPrefixに_next、includeに.nextを設定しましょう。
urlPrefixはアップロードするソースマップのファイルパスにある接頭辞を指定できます。
Next.jsのデフォルトは_next/xxxとなっているので指定する必要はないのですが、basePathを設定しているとbasePath/_next/xxxになってしまうので、urlPrefixに_nextを指定する必要があります。
includeはアップロードするディレクトリを指定できます。
Next.jsのデフォルトはビルドとソースマップは.nextディレクトリに出力されるので.next以下のファイルをアップロードします。
ただ、basePathを設定している場合は明示的にicludeに.nextを指定しないとurlPrefixが反映されないので、includeに.nextを指定しましょう。
basePathを指定しているとurlPrefixとincludeオプションの2つが必要になるので注意が必要です。
# next.config.js
const { withSentryConfig } = require("@sentry/nextjs");
const moduleExports = {
// your existing module.exports
};
const sentryWebpackPluginOptions = {
// アップロードするソースマップのファイルパスにある接頭辞を指定する
urlPrefix: "_next",
// アップロードするディレクトリを指定する
include: ".next",
basePath: "base",
};
module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions);
課題⑥ 本番環境でもソースマップがアップロードされてしまう
本番環境でソースマップをアップロードしたままにすると下記の問題が発生します。
compile, minifyする前のソースコードを見られてしまう
ファイル容量を圧迫する
解決策⑥ ソースマップと参照URLを削除する
本番環境ではソースマップをSentryにアップロードすれば不要なので.map拡張子のファイルを削除しましょう。
next build && next export && find ./out -name *.map -delete
次にnext.config.jsのhideSourceMapsをtrueにしてビルドしたファイルからソースマップの参照URLを削除します。
# next.config.js
const moduleExports = {
sentry: {
hideSourceMaps: true,
},
};
ソースマップを削除してもビルドしたファイルにソースマップの参照URLが残っていると、devtoolでエラーがでるので削除しましょう。
課題⑦ 同一プロジェクトを使用するとソースマップの上書きとエラー上限値を超過するリスクがある
開発環境と本番環境で同じSentryのプロジェクトを使用すると2つのリスクがあります。
①仮に本番環境以外で本番環境と同じrelease名を付けてソースマップをアップロードしてしまうと、本番環境のソースマップを上書きしてしまうリスクがあります。
②本番環境以外で大量のエラーが発生してSentryでエラーを受け取れる上限値を超過すると、本番環境でもエラーを受け取ることができなくなってしまいます。
理由は同一のSentry Organizationに属するプロジェクトでは、エラーやtransactionを受け取れる上限値が各プロジェクトで共有になっているためです。
[補足]
開発環境ではSentryにエラーを送る必要はないと思うかもしれません。食べログでも開発環境ではログを送っていませんでした。
しかし、Sentryへのエラー送信時のみ発生するバグを本番環境にリリースされるまでエラーを検知できなかった経験から、本番環境以外の開発環境やステージング環境でもSentryにエラーを送ることにしました。
解決策⑦ Sentryのプロジェクトを分けて、Rate Limitingを設定する
①のソースマップを上書きしてしまうリスクに対しては、本番環境と本番環境以外で使用するSentryのプロジェクトを分けるようにして解決しました。
②のエラーを受け取れる上限値を超過すると本番環境でもエラーを受け取ることができなくなるリスクに対しては、SentryのRate Limitingを使用することで解決しました。
Rate Limitingを設定することで時間単位でエラーを受け取れる数を制限できます。
下の画像のケースでは1分間にエラーを受け取れる数が200回に制限されています。
開発環境でもエラーを送信する目的は前述したように本番と環境差異をなくすことなので、1日に1回までエラーを受け取るようにRate Limitingで制限しています。
仮に開発環境で大量のエラーが発生しても、エラーの受け取れる上限に与える影響を最小限にすることができます。
パフォーマンスのtransactionについてはRate Limitingによる受け取る数の制限ができないので注意が必要です。
3. まとめ
wizardはとても便利で簡単にセットアップができます。ただ、そのまま使うにはいくつか課題がありました。
予想していたよりも導入に時間がかかり、ハマったポイントや注意するべきポイントが多くありました。
本記事を読んでくださった皆様のSentry導入に少しでもお役に立てたら幸いです。
また、記事に対してご指摘などありましたらコメントをいただけると助かります。
4. 最後に
現在、食べログではフロントエンドに関わるポジションとして以下の2つを募集しています。
気になった方は是非チェックしてみてください!
・フロントエンド統括チームに所属するフロントエンドエンジニア
・フロントエンドをメインにサービス開発を担当していくWEBエンジニア
どれかに当てはまった方は以下のリンクも是非御覧ください!