Edited at
Nuxt.jsDay 3

Sentry で Nuxt.js のエラー検知 + 環境変数の扱いに関する Tips


はじめに

Sentry は OSS のエラー検知プラットフォームです.様々な言語に対応した SDK が用意されており,例えば JavaScript モジュールを使うとブラウザ上で発生したエラーや Node 上のエラー通知が可能になります.

SPA でフロントエンドでの責務が多くなってきた昨今,エラーの検知は課題になりがちです.ユーザーの申告でしか気づけなかったバグや,特定のブラウザ・デバイスでしか発生しないエラーに気づけるようになります.

Nuxt.js アプリケーションと Sentry を組み合わせた本番運用で,考慮が必要になった Nuxt.js モジュールと環境変数の扱いに関して紹介したいと思います.


TL;DR;


  • 本番環境のエラー検知のため Sentry を使うときの DSN 管理に困った

  • DSN をサーバの環境変数として設定し,クライアント側には動的に渡したい


  • nuxt-env は,Nuxt 本家の Setnry モジュールと一緒に使うことができない

  • nuxt-env と一緒に扱える @tanakaworld/nuxt-sentry を実装した

動作確認用のサンプルプロジェクトはこちら


Sentry


登録

リアルタイムにエラー検知するだけなら無料で使うことができます.

Slack 連携や分析ができる有料プランもあります.



  • ここから新規登録

  • プロジェクトを新規作成
    image.png


通知に必要な URL を発行

DSN というアプリケーションに設定する URL を確認します.

https://xxx:yyy@sentry.io/zzz という形式の URL が DSNで,設定に必要なのはこれだけです.

image.png

image.png

※ 注意

configure-the-sdk に記載あるように,古いバージョンだと DSN に secret key を含んでいたようですが,最新の Sentry だとそれは Lagacy URL として deprecated 扱いになっています.本記事は最新バージョンの Sentry を前提として書きます.


Nuxt コミュニティ本家の Sentry モジュール

👉 nuxt-community/sentry-module

ブラウザ向けの @sentry/browser と,Node.js 向けの @sentry/node をラップしたモジュールです.

$ npm i -S @nuxtjs/sentry

nuxt.config.js に次のような記述で設定します.


nuxt.config.js

module.exports = {

// •••
modules: [ '@nuxtjs/sentry' ],
sentry: { dsn: 'https://xxx:yyy@sentry.io/zzz' }
}


エラー通知例

わざと例外が発生するページを用意してエラーを通知してみます.


pages/pageHasError.vue

<script>

export default {
data() {
// エラー
console.log(this.user.name)
return {}
}
}
</script>

ページ遷移でエラーページに到達したとき,及び SSR でエラーページを表示したときに次のようにエラー通知されます.

image.png

image.png


  • 例外が発生したときの Stack Trace

  • ユーザー環境の情報 (IP アドレス,UA,URL)

  • ユーザーのブラウザ上での動作

などが確認できます.

image.png

image.png

SDK 情報から sentry.javascript.browser で送信されたわかります.SSR 時に発生した場合は sentry.javascript.node で送信されます.

image.png

実際に本番運用するときは,DSN を nuxt.config.js にハードコーディングするのは DSN が git 管理されることになり望ましくないでしょう.そこで環境変数で DSN を設定する方法を考えてみます.


DSN を環境変数で設定する(ビルド時・実行時)


nuxt.config.js

module.exports = {

// •••
modules: [ '@nuxtjs/sentry' ],
sentry: { dsn: process.env.SENTRY_DSN }
}

// or

module.exports = {
// •••
modules: [ '@nuxtjs/sentry' ]
// "process.env.SENTRY_DSN" がデフォルトで設定されるので,明示的に記述しなくても OK
// https://github.com/nuxt-community/sentry-module#dsn
}


# ビルド

$ SENTRY_DSN=https://xxx:yyy@sentry.io/zzz npm run build
# 実行
$ SENTRY_DSN=https://xxx:yyy@sentry.io/zzz npm run start

これでハードコーディングするときと同じように動作させることができます.

npm run start 時にも SENTRY_DSN を設定する必要があるのは,ブラウザで実行されるコードはビルド時の環境変数を用いて値が直接バンドルに埋め込まれ,サーバー(node) 上で実行されるコードは実行時の環境変数が使われるためです.

仮に SENTRY_DSN を指定せずに npm run start と実行した場合,次のような info ログが表示され,SSR 時には Sentry に通知されません.

image.png

このようにビルドする環境・実行する環境それぞれに SENTRY_DSN を設定する必要があります.それぞれの環境の場合は DSN の二重管理や引き回しが発生し煩雑になりそうです.

管理しやすくするために,実行時にのみだけ設定する方法を考えます.


DSN を環境変数で設定する(実行時のみ)

# ビルド

$ npm run build
# 実行
$ SENTRY_DSN=https://xxx:yyy@sentry.io/zzz npm run start

実行時にのみ指定する場合は,ビルド時にその環境変数は参照できません.

その結果, SSR 時のエラーは Sentry に通知されるが,ブラウザ のエラーは Sentry に通知されないという状態になります.


コードが webpack でコンパイルされると、 process.env.your_var と記述されたすべての箇所が、定義した値に置き換えられます。


env プロパティ に解説がある通り,nuxt build はブラウザでも動くことを想定してビルドされた bundle を生成するので,当然ながらサーバのランタイム環境変数を参照できないためです.

この問題を解決するために,実行時の環境変数をブラウザに動的に渡す方法を考えます.


nuxt-env

👉 nuxt-env

これを使うと,実行時の環境変数をブラウザ上で参照できるようになります.

$ npm i -S nuxt-env


nuxt.config.js

modules: [

['nuxt-env', {
keys: ['SENTRY_DSN']
}]
]

export default {

computed: {
testValue () { return this.$env.SENTRY_DSN }
}
}
export default {
asyncData ({ app }) {
console.log(app.$env.SENTRY_DSN)
}
}

$env 経由で参照できるようになります.

仕組みは単純で,サーバーランタイムの process.env から nuxt.config.js で指定したキー の値をオブジェクトにマッピングして,$env として inject しているだけです.


@tanakaworld/nuxt-sentry

本家の sentry-module は nuxt-env と一緒に使うことを想定していないため,モジュールを自作しました.

👉 @tanakaworld/nuxt-sentry

$ npm i -S @tanakaworld/nuxt-sentry

nuxt.config.js に次のような記述で設定します.


nuxt.config.js

module.exports = {

// •••
modules: [ '@tanakaworld/nuxt-sentry' ]
}

このモジュールはサーバ上の環境変数で定義した SENTRY_DSN がブラウザでも使われます.

本家との違いは


  • plugin コードで $env.SENTRY_DSN を参照

  • Nuxt 旧バージョンの互換性を排除

  • オプションの生成方法を簡略化

でよりシンプルな構成にしています.


lib/module.js

const Sentry = require("@sentry/node");

const path = require("path");

const logger = require("consola").withScope("nuxt:sentry");

module.exports = function Module(moduleOptions) {
// •••

// Setup sentry
Sentry.init(options);

// Register the client plugin
this.addPlugin({
src: path.resolve(__dirname, "plugin.template.js"),
fileName: "nuxt-sentry-client.js",
ssr: false,
options
});

// NOTE: nuxt-sentry-client.js is depends on nuxt-env.
this.requireModule([
"nuxt-env",
{
keys: ["SENTRY_DISABLED", "SENTRY_DSN", "SENTRY_ENVIRONMENT", "SENTRY_RELEASE"]
}
]);

// •••
};



lib/plugin.template.js

import Vue from 'vue'

import * as Sentry from '@sentry/browser'

export default function (context, inject) {
// •••

// Set DSN via nuxt-env if it's exists
if (context.app.$env.SENTRY_DSN) {
opts.dsn = context.app.$env.SENTRY_DSN;
}

Sentry.init(opts);

// Inject Sentry to the context as $sentry
context.$sentry = Sentry;
inject('sentry', Sentry);
}



まとめ

@tanakaworld/nuxt-sentry によって SENTRY_DSN が一元管理できるようになりました.

nuxt-env は便利なモジュールですが,シークレットキーなどサーバー外には公開したくない値は扱わないように注意が必要です.