hacomono TECH BLOG

フィットネスクラブ・スクールなど施設・店舗のための会員管理・予約・決済システム「hacomono」 開発チームの技術ブログ

Nuxt3 で Sentry を使う (SSR, server/api, sentry tunnel 対応)

どうも。フロントエンドのテックリードをやってるっぽい、みゅーとん(@_mew_ton)です。

nuxt で sentry を使う方法を調べると、 @nuxt/sentry (https://sentry.nuxtjs.org/) に行き着くことが多いかと思われます。

しかし、このライブラリは nuxt2 のみに対応しており、nuxt3 への対応は一向に進んでいない様子が見られます。

そこで、独自に対応しました。以下にその方法をまとめていきます。

TL;DR

  • クライアントサイドは nuxt plugin、サーバサイド(SSR / api) は nitro plugin で実装する
  • アクセス制限を導入している環境を想定すると sentry tunnel api を nitro に作ると良い

この記事で扱わないこと

  • sentry とはなにか / 入門
  • sentry に mapfile を publish する方法

要件

sentry 導入に際し、以下の要件がありました。

  • レンダリング方法は基本的に SSR
  • nitro サーバ を BFF としても利用しており、 server/api に API エンドポイントを実装している
  • toB 向けアプリを作っており、ネットワーク制限がかかっている環境が存在しうる

クライアントサイド

以下のコードを src/plugins/sentry.client.ts として実装すればOKです。

import type { NuxtApp } from '#app'

export default defineNuxtPlugin(async ({ vueApp }) => {
  // dynamic import することで、バンドルサイズを軽減する
  const sentry = await import('@sentry/vue')

  sentry.init({
    dsn: '', // DSN
    app: vueApp,
    /** ここに各種設定値を記載 */
  })
})

上記コードでは、@sentry/vue を動的 import しています。build した際にバンドルされる entry.(hash).js が肥大化してしまうのを、これで防いでいます。

また、ファイル名を sentry.client.ts とすることで、クライアント側のみの plugin として作用するようになっています。

サーバサイド (SSR / nitro server)

nuxt の SSR は nitro 上で動作するため、サーバサイドのロジックに sentry を仕込みたい場合は、nitro の plugin を使用する必要があります。

以下のコードを src/server/plugin/sentry.ts として実装すればOKです。

import * as Sentry from '@sentry/node'

export default defineNitroPlugin(async (nitro) => {
  Sentry.init({
    dsn: '', // DSN
    /** ここに各種設定値を記載 */.
  })
})

@sentry/vue ではなく @sentry/node をインポートしている点に注意してください。ブラウザではなく、 nodejs 上で動作するため、このようになっています。

また、バンドルサイズを考慮しなくて良いため、動的 import は行っていません。

tunnel API

クライアントサイドでエラーを sentry 検知したとき、それをそのまま sentry のサーバに post しようとします。これが、仮に「ネットワークアクセス制限を有効にしている」環境であれば、sentry に post できず、エラーを集積することができなくなります。

このケースでは、server/api に sentry tunnel api を生やし、BFF経由で sentry に通知させる対応方法があります。

例として、 /api/__sentry に post する前提で対応する場合を紹介します。

クライアントサイドの書き換え

src/plugins/sentry.client.ts にて、sentry の初期化に以下を追記します

  sentry.init({
    dsn: '', // DSN (省略しない!!)
    app: vueApp,
+   tunnel: '/api/__sentry',
  })

この設定をすると、ブラウザ側でエラーを検知した際に /api/__sentry に対して post するようになります。

tunnel api を追加

src/server/api/__sentryindex.post.ts を追加します

import { type H3Event, defineEventHandler, readRawBody } from 'h3'

async function sendToSentry(event: H3Event): Promise<void> {
  const body = await readRawBody(event)

  try {
    await $fetch("" /** Sentry Envelope URL */, {
      method: 'POST',
      body
    })
  } catch (error) {
    // レスポンスエラーは sentry にハンドリングさせるため、 console.error に出力する
    // エラーレスポンスとして返さず、ここでエラーを握りつぶす
    console.error(error)
  }
}

export default defineEventHandler<null>((event) => {
  // 非同期的に処理を行うため、レスポンスを待たない
  sendToSentry(event)
  return null
})

sentry DSN が https://hogehoge@oxxxxxx.ingest.sentry.io/12345678 だとすると、envelope url は https://oxxxxxx.ingest.sentry.io/api/12345678/envelope/ になります。

ここでは、 post に含まれるデータをそのまま envelope url に送るだけの処理をしています。

また、クライアントサイドにレスポンスを待たせたくないため、sendToSentry をあえて await しないロジックにしています。(効果があるかはわかりませんが・・)

まとめ

nuxt3 で sentry を利用する方法をまとめました。

エラー検知の一助となれば幸いです。

また、うまく知見がたまってきたら、nuxt module として汎用化も考えています。


株式会社hacomonoでは一緒に働く仲間を募集しています!
採用情報や採用ウィッシュリストも是非ご覧ください!