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