Article by: Sergiy Dybskiy
このブログは、最近実施されたライブワークショップに基づいています。YouTubeでフルのライブ配信を視聴できます。
Next.js は多くの機能を当初から提供しています。サーバーサイドレンダリング、ファイルベースルーティング、エッジランタイムなどです。しかし、実際に本番環境で何が起きているのかを明確に把握する手段は提供していません。フレームワークの3つのランタイム構成(クライアント、サーバー、エッジ)により、エラーがあるレイヤーで発生しているように見えても、実際には別のレイヤーに起因していることがあります。また、データベースクエリはORMの抽象化の背後に隠れ、サーバーアクションは有用なエラーメッセージをブラウザに到達する前に飲み込んでしまいます。
本記事では、Next.jsアプリにおけるいくつかの具体的なオブザーバビリティ(可観測性)のギャップ、それらが存在する理由、そしてSentryを用いてそれらをどのように解消するかについて説明します。
要約
- Next.js の本番ビルドでは、サーバーアクションからエラーの詳細が削除されます。クライアント側には「サーバーコンポーネントのレンダリング中にエラーが発生しました」とだけ表示され、コンテキストは一切提供されません。Sentryは完全なスタックトレース付きで元のサーバーサイド例外をキャプチャします。
- Hydrationエラーは、Reactにおいて最も一般的でありながら最も役に立たないエラーのひとつです。SentryはHTMLの差分ビューを提供し、サーバーとクライアントのレンダリング間でどのDOMノードが不一致だったのかを正確に示します。
- ログとメトリクスはトレースのようにサンプリングされません。tracesSampleRateの設定に関係なく、データは100%取得されます。データの欠落を避けたい場合はこれらを使用してください。
- サーバーアクションはOpenTelemetryのスパンを生成しないため、トレースに表示させるにはwithServerActionInstrumentationを使用した手動のインストルメンテーションが必要です。
- DrizzleのようなORMを通じたデータベースクエリは、デフォルトではトレーシングに表示されません。データベースクライアント(例:TursoのlibSQL)のインテグレーションを追加することで、すべてのクエリをスパンとして可視化できます。
- Vercel AI SDKとのインテグレーションによるAIエージェントのモニタリングでは、モデルごとのトークン使用量、コストの内訳、ツールコールのトレースを、Sentryから離れることなく確認できます。
3つのランタイム、3つの設定ファイル
Next.js は異なる環境でコードを実行します。Sentryのウィザードを実行することで、セットアップを開始できます。

ウィザードはそれぞれの環境に対して個別の初期化ファイルを作成します。ブラウザ用の instrumentation-client.ts、Node.js 用の sentry.server.config.ts、そしてエッジランタイム用の sentry.edge.config.ts です。
これにより、各ランタイム向けの設定ファイル、グローバルエラーバウンダリ(global-error.tsx)、そして withSentryConfig によってラップされた next.config.ts が生成されます。next.config.ts のラッパーは、可読性のあるスタックトレースのためのソースマップのアップロードを処理し、さらにトンネルルーティングを設定します。これは広告ブロッカーを回避するために、Sentryのデータを自分のサーバー経由で送信する仕組みです。
設定に関していくつか注意点があります。
- サンプルレートは重要です。開発環境では tracesSampleRate を 1.0 に設定し、本番環境では 10〜20% に設定します。これを高くしすぎると、クォータを急速に消費します。
- sendDefaultPii はリプレイやイベントにユーザーのIPアドレスを付与します。必須ではありませんが、セッションを実際のユーザーと関連付けるのに役立ちます。
- エッジの設定は異なる場合があります。ミドルウェアがリクエストの再ルーティングのみを行う場合、ノイズを減らすためにエッジ設定でトレーシングを無効にしても問題ありません。
セットアップに関してもう一点あります。認証後に一度 Sentry.setUser() を呼び出すことで、エラー、ログ、トレース、リプレイ全体にユーザーコンテキストを伝播させることができます。
Hydrationエラー:一般的だがあまり役に立たない
Hydrationとは、ReactがサーバーでレンダリングされたHTMLにイベントハンドラを紐づけ、インタラクティブにするプロセスです。Hydrationエラーは、クライアント側でReactがレンダリングしたマークアップが、サーバーで最初にレンダリングされたHTMLと一致しない場合、またはサーバーが無効なHTMLを返し、Reactがそれを修正できなかった場合に発生します。
典型的な原因は、localStorageから値を読み取るテーマ切り替えです。サーバーはライトテーマをレンダリングします(localStorageにアクセスできないため)が、クライアントは保存されたダークテーマの設定を読み込み、HTMLが一致しないためReactがHydrationエラーを投げます。
本番環境ではブラウザから得られる情報はほとんど役に立ちません。デコーダー用のURLを指す最小化されたReactエラーとチャンクファイルだらけのスタックトレースが表示されるだけです。
実際に役立つHTML差分
Hydrationエラーのデバッグを支援するために、SentryはクライアントでレンダリングされたHTMLとサーバーでレンダリングされたHTMLの差分を表示するdiffツールを提供します。Session Replayを有効にしている場合、SentryはHydrationエラーを検出し、それらをIssueストリームに取り込みます。
このdiffは、ReactによるHydrationの前(サーバー)と後(クライアント)を、GitHubのPRレビューのような形式で表示します。ページの差分を確認することで、エラーの原因となった要素や属性を特定しやすくなります。特に見つけやすいのは、テキスト内容の不一致、不正なHTMLのネスト、属性の変更です。
すでにSession Replayを使用している場合、Hydrationエラーは自動的にグループ化されたIssueとして取得できます。これらはReplayから生成されるため、エラークォータには影響しません。
テーマに関連するHydrationエラーの修正は通常シンプルです。テーマの読み取りをuseEffectに遅延させることで、初期のサーバーとクライアントのレンダリングを一致させ、その後Hydration完了後に保存された設定を適用します。
サーバーアクションはトレーシングの盲点
サーバーアクションは、Next.jsにおけるフォーム送信やミューテーションを処理するためのパターンで、実質的には型付きのPOSTリクエストです。Sentryは多くの操作を自動的にインストルメントしますが、サーバーアクションについては手動での設定が必要です。
その理由は、サーバーアクションがSentryがフックできるOpenTelemetryのスパンを生成しないためです。Turbopackによるバンドルの仕組みにより、自動インストルメンテーションは非常に難しく、かつエラーが発生しやすくなります。これを実現するにはNext.jsのサーバーアクション用コンパイラを構築する必要がありますが、それは現実的ではありません。
インストルメンテーションがない場合、サーバーアクションは匿名のHTTP POSTとして表示されます。これを追加すると、名前付きのスパン、タイミングデータ、そして(重要な点として)クライアントとサーバー間の分散トレースの連続性が得られます。
サーバーアクションのラップ方法
サーバーアクションを Sentry.withServerActionInstrumentation() でラップします。以下はその例です。

withServerActionInstrumentation のラッパーは、各アクションごとに名前付きのスパンを作成し、実行時間やエラーをキャプチャし、ヘッダーを通じてクライアントとサーバーのトレースを接続し、フォームデータをSentryのイベントに付加します。
headers パラメータは、分散トレーシングを機能させるための要素です。SentryはリクエストヘッダーからトレースIDとbaggageを読み取り、クライアントで開始されたトレースとサーバー側の実行を結びつけます。これがない場合、1つの連続したトレースではなく、2つの分断されたトレースとして表示されます。
本番環境のエラーメッセージは(意図的に)役に立たない
サーバーアクションのオブザーバビリティが重要である理由はもうひとつあります。本番ビルドでは、Next.jsはサーバー側の失敗に関するエラー詳細を、クライアントに届く前に意図的に削除します。ユーザーに表示されるのは次のようなメッセージです。「サーバーコンポーネントのレンダリング中にエラーが発生しました。本番ビルドでは、機密情報の漏洩を防ぐため、具体的なメッセージは省略されています」
これはセキュリティ上は正しい判断です。しかし、デバッグにはまったく役に立ちません。とはいえ、Sentryはサーバー側を直接インストルメントするため、「認証中にデータベース接続が失われました」のような完全な例外情報を取得できます。サニタイズされた空の情報ではありません。サーバーアクションを重要な用途で使用している場合、これだけでもセットアップのコストを正当化する理由になります。
ログとメトリクス:適切なシグナルの選択
エラー、ログ、メトリクスはそれぞれ異なる目的を持っており、この違いはNext.jsアプリをどのようにインストルメントするかにおいて重要です。
- エラー(Sentry.captureException):何かが壊れていて、修正が必要な状態です。Issueを作成し、アラートをトリガーし、Seerによる根本原因分析に利用されます。
- ログ(Sentry.logger):コンテキストを示す痕跡です。障害の前後や最中に何が起きたのかを示します。高カーディナリティで、クエリ可能かつトレースと紐づけられます。
- メトリクス(Sentry.metrics):カウンタ、期間、ゲージなどです。ダッシュボードや集約されたパターンに対するアラートに適しています。

有効化すると、Sentry.logger はアプリケーション内のあらゆる場所から構造化ログを送信できるようになります。

重要な違いがひとつあります。ログとメトリクスはサンプリングされません。tracesSampleRate が10%であっても、ログとメトリクスのデータポイントは100%取得されます。トレースは統計的サンプリングを使用し、Sentryは集計値を推定しますが、ログとメトリクスは正確な数値を提供します。
データベースクエリはORMの背後に隠れる
DrizzleのようなORMを、Tursoのようなデータベースと組み合わせて使用している場合、トレースにはサーバーアクションやAPIルートは表示されますが、その内部で実行されている実際のSQLクエリはデフォルトでは見えません。リクエストに850msかかったことは分かっても、その理由は分かりません。
これを解決するには、2つの対応が必要です。データベースクライアントのインテグレーションを設定し、それをSentryのサーバー設定に追加することです。
Turso(libSQL)データベースの場合、サーバー設定に libsqlIntegration を追加します。

また、next.config.ts の serverExternalPackages に @libsql/client を追加して、正しくバンドルされるようにする必要があります。
設定が完了すると、Drizzleのすべてのクエリがスパンとして表示され、実際のSQLも確認できるようになります。クエリ自体はDrizzleのTypeScript APIで記述していても、SentryはORMの呼び出しをトレースウォーターフォール内で対応するSQLに変換します。これにより、Query Insightsビューを使用して、1分あたりのオペレーション数や平均実行時間を確認でき、N+1クエリや遅いデータベース呼び出しに対する自動アラートを取得することもできます。
同様のパターンは他のデータベースにも適用されます。Postgres(Neonを含む)の場合、SentryのNode SDKにはデフォルトでPostgresのインストルメンテーションが含まれているため、追加の設定は不要な場合があります。Supabaseについては、専用のSupabaseインテグレーションが用意されています。
AIエージェントのモニタリング:トークン使用量をユーザーに紐づけて追跡する
Next.jsアプリにAI機能(チャットインターフェース、エージェントワークフロー、生成コンテンツなど)が含まれている場合、モデルプロバイダーからそれなりの請求が来ている可能性があります。一方で、そのコストがどの機能、どのユーザー、どのエージェントの経路に起因しているのかの内訳は把握できていないことが多いはずです。
vercelAIIntegration は、VercelのAI SDKに対するインストルメンテーションを追加し、AI SDKの組み込みテレメトリを使用してスパンをキャプチャします。このインテグレーションはNodeランタイムではデフォルトで有効になっていますが、Edgeランタイムでは有効になっていません。
各AI関数呼び出しごとに、詳細なテレメトリを有効にすることができます。

experimental_telemetry に functionId を設定すると、キャプチャされたスパンと関数呼び出しの対応付けが容易になります。複数のエージェントがある場合、たとえばルーターが検索エージェントや情報エージェントに処理を委譲し、それぞれが異なるモデルを使用しているようなケースでは、それぞれがトレース内で個別の名前付きスパンとして表示されます。
SentryのAgent Monitoringビューでは、以下の情報を確認できます。
- モデルごとのコスト内訳:使用しているモデル、それぞれの使用量、およびコスト
- トークン使用量:モデルごと、リクエストごとの入力トークンと出力トークン
- ツール呼び出しの可視化:すべてのツール呼び出し(エラーを含む)が、トリガーとなったトレースに紐づけられて表示される
- 完全なトレースコンテキスト:AIの呼び出しが、データベースクエリやAPIコールなど、そのリクエスト内の他のすべての処理と並んで表示される

最後のポイントが最も重要です。AIの応答に5秒かかっている場合、その原因はモデルが遅いからなのか、それともツール呼び出しによって遅いデータベースクエリが発生しているからなのか。トレースウォーターフォールでは、これらを同一のビューで確認でき、Anthropicのダッシュボードとアプリケーションログを突き合わせる必要はありません。
recordInputs と recordOutputs はどちらもデフォルトで true に設定されています。プロンプトやレスポンスにSentryへ送信したくない機密データが含まれる場合は、これらを false に設定してください。
ギャップを埋める
Next.jsにおける多くのオブザーバビリティの問題は、何を探すべきかを知らないうちは「データが欠けている」ように見えます。実際にはサーバーアクションである匿名のPOSTリクエスト、理由が分からないままの850msのレスポンス、最小化されたデコーダーURLを指すHydrationエラーなどを一度理解すれば、修正は比較的シンプルです。しかし、本番環境で初めて遭遇すると、何時間も費やしてしまいがちです。
- クライアント、サーバー、エッジという3つのランタイムにまたがる3つの設定。Next.jsはこれらに分割されています。すべてをインストルメントしつつ、それぞれに適した設定を行ってください。
- Hydrationエラーには視覚的な差分が必要です。本番環境ではブラウザのエラーメッセージは役に立ちません。Sentryのdiffツールは、実際のDOMの差異を示します。
- サーバーアクションには手動でのラップが必要です。OpenTelemetryのスパンがないため、自動インストルメンテーションは機能しません。withServerActionInstrumentation を使用し、分散トレーシングのために headers を渡してください。
- ログとメトリクスはサンプリングされません。トレースとは異なり、すべてのデータが取得されます。欠落が許されないデータにはこれらを使用してください。
- ORMのクエリはデフォルトでは不可視です。データベースインテグレーションを追加し、トレース内で実際のSQLを確認し、N+1クエリを自動的に検出してください。
- AIモニタリングはコストをコンテキストに結びつけます。どのユーザー、どの機能、どのコードパスがトークン消費を生み出したのかが分からなければ、そのコストには意味がありません。
Next.js SDKドキュメントから始めるか、YouTubeのNext.jsデバッグシリーズで同様の内容を確認してください。
Next.js 可観測性に関するFAQ
■ SentryはNext.jsアプリでAIのトークン使用量やコストを追跡できますか?
はい、Vercel AI SDKインテグレーションを通じて可能です。これはNodeランタイムではデフォルトで有効になっています。AI関数呼び出しに experimental_telemetry を設定すると、Sentryはモデルごとのトークン使用量、コストの内訳、ツール呼び出しのトレースをキャプチャします。異なるモデルを使用する複数のエージェントがある場合、それぞれが個別の名前付きスパンとして表示されます。これにより、応答の遅延がモデルによるものか、あるいは下流のデータベースクエリによるものかを確認できます。
■ SentryはNext.jsの3つのランタイムすべてで動作しますか?
はい、動作しますが、各ランタイムごとに設定ファイルが必要です。ブラウザ用の instrumentation-client.ts、Node.js用の sentry.server.config.ts、そしてエッジランタイム用の sentry.edge.config.ts です。npx @sentry/wizard@latest -i nextjs を実行すると、Sentryウィザードがこれら3つすべてを作成します。ミドルウェアがルーティングのみを処理している場合は、ノイズを減らすためにエッジ設定を簡略化することも可能です。
■ Sentryのログはトレースのようにサンプリングされますか?
いいえ、ログとメトリクスはサンプリングの対象ではありません。tracesSampleRate が10%であっても、ログデータは100%取得されます。トレースは統計的サンプリングを使用し、Sentryはそこから集計値を推定しますが、ログは正確な数値を提供します。有効化するには、各Sentry初期化ファイルに enableLogs: true を追加してください。
■ DrizzleなどのORMを使用している場合、データベースクエリはSentryのトレースに表示されますか?
デフォルトでは表示されません。ORMはSQLを抽象化するため、トレースにはサーバーアクションが850msかかったことは表示されても、その理由は分かりません。データベースクライアントのインテグレーション(Tursoの場合は libsqlIntegration など)を追加することで、各クエリが実際のSQL付きのスパンとして表示されます。これにより、N+1クエリや遅い呼び出しを自動的に検出するQuery Insightsも利用できるようになります。
■ なぜNext.jsのHydrationエラーは本番環境でデバッグしにくいのですか?
ブラウザのエラーメッセージは、最小化されたReactのデコーダーURLを指すだけで、ほとんど情報を提供しません。SentryのHTML diffツールは、サーバーでレンダリングされたDOMとクライアントでレンダリングされたDOMの差分を前後比較で表示し、どの要素や属性が不一致を引き起こしたのかを正確に示します。Session Replayを有効にしている場合、HydrationエラーのIssueは自動的に生成され、エラークォータにはカウントされません。
■ SentryはNext.jsのApp Routerやサーバーアクションに対応していますか?
はい、対応しています。SentryのNext.js SDKはApp Routerをサポートしていますが、サーバーアクションについては手動でのインストルメンテーションが必要です。サーバーアクションはOpenTelemetryのスパンを生成しないため、それぞれを Sentry.withServerActionInstrumentation() でラップする必要があります。また、クライアントとサーバーのトレースを接続するために headers() の値を渡してください。これを行わない場合、1つではなく2つの分断されたトレースとして表示されます。
Original Page: Next.js observability gaps and how to close them
IchizokuはSentryと提携し、日本でSentry製品の導入支援、テクニカルサポート、ベストプラクティスの共有を行なっています。Ichizokuが提供するSentryの日本語サイトについてはこちらをご覧ください。またご導入についての相談は「お問い合わせ」からお気軽にお問い合わせください。



