フロントエンドエンジニアのこん(@k0n_karin)です!今回はNuxt3でSentryを扱ったときに遭遇した事象について投稿します。
※NuxtとSentryそのものの説明は割愛します。
※本記事執筆時点のバージョンは以下のとおりです。
- nuxt: 3.12.2
- @sentry/vue: 8.13.0
解決策
先に解決策です。事象の解説も見たい方は次の章以降を参考にしてください。
Nuxt3で@sentry/vueを使う際、エラー発生時に500エラーの画面を表示したくない場合は、Sentry.init()
の前にapp.config.errorHandler
に任意の関数を代入しましょう。
(エラーハンドリングが不要ならundefined
でも可)
// plugins/sentry.ts import * as Sentry from '@sentry/vue' export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.config.errorHandler = (error) => { // 必要に応じてここでエラーハンドリングする } Sentry.init({ app: nuxtApp.vueApp, dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' }) })
Nuxt3とSentry
本ブログの執筆時点(2024-07-10)では、Nuxt3用のSentry SDKはまだありません。絶賛開発中のようです。
代わりに、Vue向けのSentry SDK(@sentry/vue)を使うことができます。使い方はNuxtチームの方のブログがとても参考になります。
Nuxt3で@sentry/vueを使うとエラー発生時に500エラーが表示される
しかし、上記のブログのようにNuxtのプラグインで@sentry/vueを使うと、クライアントでエラー発生時にSentryにエラー通知されますが、同時に500エラー画面も表示されます😭
これは以下のデモ用リポジトリから確認できます。
この500エラーが表示される仕組みを説明するために、前提となる4つの知識をそれぞれ解説していきます。
前提知識1:Vueのapp.config.errorHandler
Vueのapp.config.errorHandler
は1つのVueアプリケーションにつき1つの関数を登録できます。複数の関数は登録できません。
app.config.errorHandler
はアプリケーションでハンドリングされなかった全てのエラーをハンドリングできます。
app.config.errorHandler = (err, instance, info) => { // エラー処理(例 サービスに報告) }
前提知識2:@sentry/vueのapp.config.errorHandler
の使い方
@sentry/vueは、エラー発生時にあらかじめ指定されたDSNにエラーの内容を送る関数をapp.config.errorHandler
に代入します。
ただし、前提知識1の通り、1つのVueアプリケーションにつき1つの関数しか登録できません。
そのため、@sentry/vueが提供する関数は、もともと定義されていたapp.config.errorHandler
の関数を保持し、@sentry/vueの関数内でその関数も実行してくれます。
const { errorHandler, warnHandler, silent } = app.config; app.config.errorHandler = (error: Error, vm: ViewModel, lifecycleHook: string): void => { // 省略 if (typeof errorHandler === 'function') { (errorHandler as UnknownFunc).call(app, error, vm, lifecycleHook); } }
前提知識3:Nuxtのapp.config.errorHandler
の使い方
Nuxtはapp.config.errorHandler
を以下のように使います。
- Nuxtの初期化の最中だけ
app.config.errorHandler
に関数を渡す。 - Nuxtプラグインで
app.config.errorHandler
が上書きされたら、以降エラー発生時に上書きされた関数を使う。(=リセットしない) - Nuxtプラグインで
app.config.errorHandler
が上書きされなかったら、app.config.errorHandler
をundefined
にする。(=リセットする)
実際のコードを見てみましょう。
vueApp.config.errorHandler = handleVueError // 省略 // If the errorHandler is not overridden by the user, we unset it if (vueApp.config.errorHandler === handleVueError) { vueApp.config.errorHandler = undefined }
このように、Nuxtの初期化中にapp.config.errorHandler
がhandleVueError
のままだったら、app.config.errorHandler
はundefined
になります。
前提知識4:Nuxtの初期化中のエラーハンドラーの副作用
最後に、前提知識3のコードで登場したhandleVueError
関数が何をしているか見ましょう。
async function handleVueError (error: any) { await nuxt.callHook('app:error', error) nuxt.payload.error = nuxt.payload.error || createError(error as any) }
重要なのはnuxt.payload.error = nuxt.payload.error || createError(error as any)
です。
createError
はエラーオブジェクトを生成するNuxtのユーティリティ関数です。
ではnuxt.payload.error
が何なのかというと、これはuseError
というNuxtのcomposables関数で参照されます。
export const useError = () => toRef(useNuxtApp().payload, 'error')
useNuxtApp().payload
がnuxt.payload
に当たります。
さらに、useError
はNuxtのルートコンポーネントで参照されています。
useError
の返り値はRef
なので、これで<ErrorComponent>
をリアクティブに出し分けています。
<template> <!-- 省略 --> <ErrorComponent v-else-if="error" :error="error" /> <!-- 省略 --> </template> <script setup lang="ts"> // 省略 // error handling const error = useError() </script>
- https://github.com/nuxt/nuxt/blob/72a94881b6cdf39a14db2ad14bdf695368205596/packages/nuxt/src/app/components/nuxt-root.vue#L4-L7
- https://github.com/nuxt/nuxt/blob/72a94881b6cdf39a14db2ad14bdf695368205596/packages/nuxt/src/app/components/nuxt-root.vue#L56
つまり、Nuxtの初期化中のエラーハンドラーのhandlerVueError
は、<ErrorComponent>
を表示する副作用を持っています。
<ErrorComponent>
が500エラーの正体のようですね。
500エラーが表示される事象に立ち向かう
では前提知識とともに、この事象を整理します。
まず、前提知識1のとおり、Vueではapp.config.errorHandler
は1つの関数しか代入できません。
また、前提知識2・3により、Nuxtプラグインで@sentry/vueを初期化すると、以下のような処理になります。
- Nuxtの初期化が始まり、Vueの
app.config.errorHandler
にNuxtのエラーハンドラーが登録される。(前提知識3) - プラグインの初期化が始まる。
- プラグインで@sentry/vueが初期化され、
app.config.errorHandler
に@sentry/vueの関数が代入される。 この時、NuxtのエラーハンドラーもSentry SDKのハンドラーに保持される。(前提知識2) - プラグインの初期化が終わる。
- 3で
app.config.errorHandler
が上書きされたため、Nuxtはapp.config.errorHandler
をundefined
にしない。(前提知識3)
では、実際にエラーが発生するとどうなるかというと、以下の2つの関数が実行されます。
- @sentry/vueの関数
- Nuxtのエラーハンドラー
ここで、前提知識4の通り、Nuxtのエラーハンドラーによって500エラーが表示されます。
これがこの事象の裏側で起きている処理の流れです。
おわりに
Yappliでは本番環境とステージング環境のみSentryを有効化してエラー監視をしています。ただし、開発時に使う検証環境やローカル環境ではSentryを無効にしています。
このため、本番とステージングでは500エラーが表示されましたが、検証環境とローカル環境では500エラーが表示されなかったので、たまたまこの挙動に気づくことができました。
この事象に遭遇したことで、Nuxtの初期化や@sentry/vueの初期化の流れを実際のOSSのコードを追う機会ができ、とても良い勉強になりました。
ヤプリではただ開発するだけでなく、事象と向き合い知見を深める機会を重要視しています。もしご興味を持っていただけた方は、是非カジュアル面談でお話しましょう!