Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Nuxt3で@sentry/vueを使うとエラー発生時に500エラーが表示される時がある

※2024-10-11追記

@sentry/nuxt 8.32.0 のリリースにより本ブログの内容は発生しなくなりました。
Nuxt3で@sentry/vueを使っている場合は、@sentry/nuxtの利用をおすすめします。

www.npmjs.com


フロントエンドエンジニアのこん(@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はまだありません。絶賛開発中のようです。

github.com

代わりに、Vue向けのSentry SDK(@sentry/vue)を使うことができます。使い方はNuxtチームの方のブログがとても参考になります。

www.lichter.io

Nuxt3で@sentry/vueを使うとエラー発生時に500エラーが表示される

しかし、上記のブログのようにNuxtのプラグインで@sentry/vueを使うと、クライアントでエラー発生時にSentryにエラー通知されますが、同時に500エラー画面も表示されます😭

Nuxt3でエラーが発生すると500エラーが表示されるスクリーンショット
Nuxt3でエラーが発生すると500エラーが表示されるスクリーンショット

これは以下のデモ用リポジトリから確認できます。

github.com

この500エラーが表示される仕組みを説明するために、前提となる4つの知識をそれぞれ解説していきます。

前提知識1:Vueのapp.config.errorHandler

Vueのapp.config.errorHandlerは1つのVueアプリケーションにつき1つの関数を登録できます。複数の関数は登録できません。

ja.vuejs.org

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);
  }
}

https://github.com/getsentry/sentry-javascript/blob/9b5cf261700f774f3610e182f43f92edb20489d7/packages/vue/src/errorhandler.ts#L10-L41

前提知識3:Nuxtのapp.config.errorHandlerの使い方

Nuxtはapp.config.errorHandlerを以下のように使います。

  • Nuxtの初期化の最中だけapp.config.errorHandlerに関数を渡す。
  • Nuxtプラグインでapp.config.errorHandlerが上書きされたら、以降エラー発生時に上書きされた関数を使う。(=リセットしない)
  • Nuxtプラグインでapp.config.errorHandlerが上書きされなかったら、app.config.errorHandlerundefinedにする。(=リセットする)

実際のコードを見てみましょう。

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
}

https://github.com/nuxt/nuxt/blob/72a94881b6cdf39a14db2ad14bdf695368205596/packages/nuxt/src/app/entry.ts#L51-L85

このように、Nuxtの初期化中にapp.config.errorHandlerhandleVueErrorのままだったら、app.config.errorHandlerundefinedになります。

前提知識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)
}

https://github.com/nuxt/nuxt/blob/72a94881b6cdf39a14db2ad14bdf695368205596/packages/nuxt/src/app/entry.ts#L61-L64

重要なのはnuxt.payload.error = nuxt.payload.error || createError(error as any)です。

createErrorはエラーオブジェクトを生成するNuxtのユーティリティ関数です。

nuxt.com

ではnuxt.payload.errorが何なのかというと、これはuseErrorというNuxtのcomposables関数で参照されます。

nuxt.com

export const useError = () => toRef(useNuxtApp().payload, 'error')

useNuxtApp().payloadnuxt.payloadに当たります。

https://github.com/nuxt/nuxt/blob/72a94881b6cdf39a14db2ad14bdf695368205596/packages/nuxt/src/app/composables/error.ts#L10

さらに、useErrorはNuxtのルートコンポーネントで参照されています。 useErrorの返り値はRefなので、これで<ErrorComponent>をリアクティブに出し分けています。

<template>
  <!-- 省略 -->
  <ErrorComponent
    v-else-if="error"
    :error="error"
  />
  <!-- 省略 -->
</template>

<script setup lang="ts">
// 省略

// error handling
const error = useError()
</script>

つまり、Nuxtの初期化中のエラーハンドラーのhandlerVueErrorは、<ErrorComponent>を表示する副作用を持っています。
<ErrorComponent>が500エラーの正体のようですね。

500エラーが表示される事象に立ち向かう

では前提知識とともに、この事象を整理します。

まず、前提知識1のとおり、Vueではapp.config.errorHandlerは1つの関数しか代入できません。

また、前提知識2・3により、Nuxtプラグインで@sentry/vueを初期化すると、以下のような処理になります。

  1. Nuxtの初期化が始まり、Vueのapp.config.errorHandlerにNuxtのエラーハンドラーが登録される。(前提知識3
  2. プラグインの初期化が始まる。
  3. プラグインで@sentry/vueが初期化され、app.config.errorHandlerに@sentry/vueの関数が代入される。 この時、NuxtのエラーハンドラーもSentry SDKのハンドラーに保持される。(前提知識2
  4. プラグインの初期化が終わる。
  5. 3でapp.config.errorHandlerが上書きされたため、Nuxtはapp.config.errorHandlerundefinedにしない。(前提知識3

では、実際にエラーが発生するとどうなるかというと、以下の2つの関数が実行されます。

  1. @sentry/vueの関数
  2. Nuxtのエラーハンドラー

ここで、前提知識4の通り、Nuxtのエラーハンドラーによって500エラーが表示されます。

これがこの事象の裏側で起きている処理の流れです。

おわりに

Yappliでは本番環境とステージング環境のみSentryを有効化してエラー監視をしています。ただし、開発時に使う検証環境やローカル環境ではSentryを無効にしています。

このため、本番とステージングでは500エラーが表示されましたが、検証環境とローカル環境では500エラーが表示されなかったので、たまたまこの挙動に気づくことができました。

この事象に遭遇したことで、Nuxtの初期化や@sentry/vueの初期化の流れを実際のOSSのコードを追う機会ができ、とても良い勉強になりました。

ヤプリではただ開発するだけでなく、事象と向き合い知見を深める機会を重要視しています。もしご興味を持っていただけた方は、是非カジュアル面談でお話しましょう!

open.talentio.com