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

# はじめに

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

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

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

# TL;DR;

- 本番環境のエラー検知のため Sentry を使うときの DSN 管理に困った
- DSN をサーバの環境変数として設定し，クライアント側には動的に渡したい
- [nuxt-env](https://github.com/samtgarson/nuxt-env) は，[Nuxt 本家の Setnry モジュール](https://github.com/nuxt-community/sentry-module)と一緒に使うことができない
- nuxt-env と一緒に扱える [@tanakaworld/nuxt-sentry](https://www.npmjs.com/package/@tanakaworld/nuxt-sentry) を実装した

動作確認用のサンプルプロジェクトは[こちら](https://github.com/tanakaworld/nuxt-sentry-example)

# Sentry

## 登録

リアルタイムにエラー検知するだけなら無料で使うことができます．
Slack 連携や分析ができる[有料プラン](https://sentry.io/pricing/)もあります．

- [ここ](https://sentry.io/signup/)から新規登録
- プロジェクトを新規作成
![image.png](https://qiita-image-store.s3.amazonaws.com/0/31862/8d304683-b52f-5e4d-857c-4a18400b6ac2.png)


## 通知に必要な URL を発行

DSN というアプリケーションに設定する URL を確認します．
`https://xxx:yyy@sentry.io/zzz` という形式の URL が DSNで，設定に必要なのはこれだけです．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/31862/346c8eab-3ecd-df03-98a9-6c066b4efe31.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/31862/b33d92f8-ca6b-6560-390e-742f957b547c.png)

**※ 注意**
[configure-the-sdk](https://docs.sentry.io/error-reporting/quickstart/?platform=node#configure-the-sdk) に記載あるように，古いバージョンだと DSN に secret key を含んでいたようですが，最新の Sentry だとそれは Lagacy URL として deprecated 扱いになっています．本記事は最新バージョンの Sentry を前提として書きます．

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

👉 [nuxt-community/sentry-module](https://github.com/nuxt-community/sentry-module)

ブラウザ向けの [@sentry/browser](https://github.com/getsentry/sentry-javascript/tree/master/packages/browser) と，Node.js 向けの [@sentry/node](https://github.com/getsentry/sentry-javascript/tree/master/packages/node) をラップしたモジュールです．

```bash
$ npm i -S @nuxtjs/sentry
```

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


```javascript:nuxt.config.js
module.exports = {
  // •••
  modules: [ '@nuxtjs/sentry' ],
  sentry: { dsn: 'https://xxx:yyy@sentry.io/zzz' }
}
```

## エラー通知例

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

```vuejs:pages/pageHasError.vue
<script>
export default {
  data() {
    // エラー
    console.log(this.user.name)
    return {}
  }
}
</script>
```

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/31862/fab2655c-b0a1-ec34-7a3c-c888bb09008d.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/31862/a13bed13-471a-c961-1fff-c0bd54451d6f.png)

- 例外が発生したときの Stack Trace
- ユーザー環境の情報 (IP アドレス，UA，URL)
- ユーザーのブラウザ上での動作

などが確認できます．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/31862/dffea208-a295-3f0e-1ec2-52bb8073f77e.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/31862/1cb2e247-8b51-b165-70f2-3dc6b46adb1e.png)


SDK 情報から `sentry.javascript.browser` で送信されたわかります．SSR 時に発生した場合は `sentry.javascript.node` で送信されます．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/31862/d4e5a307-cbe1-af82-fae5-1de416af4a39.png)

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


# DSN を環境変数で設定する（ビルド時・実行時）

```javascript: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
}
```

```bash
# ビルド
$ 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` を設定する必要があるのは，[ブラウザで実行されるコード](https://github.com/nuxt-community/sentry-module/blob/master/lib/templates/sentry-client.js#L20)はビルド時の環境変数を用いて値が直接バンドルに埋め込まれ，[サーバー（node） 上で実行されるコード](https://github.com/nuxt-community/sentry-module/blob/master/lib/sentry.js#L11)は実行時の環境変数が使われるためです．

仮に  `SENTRY_DSN` を指定せずに `npm run start` と実行した場合，次のような info ログが表示され，SSR 時には Sentry に通知されません．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/31862/1a55b86f-6604-8b62-9286-7aba8c8d72af.png)

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

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

# DSN を環境変数で設定する（実行時のみ）

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

実行時にのみ指定する場合は，ビルド時にその環境変数は参照できません．
その結果， SSR 時のエラーは Sentry に通知されるが，ブラウザ のエラーは Sentry に通知されないという状態になります．

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

と [env プロパティ](https://ja.nuxtjs.org/api/configuration-env/) に解説がある通り，`nuxt build` はブラウザでも動くことを想定してビルドされた bundle を生成するので，当然ながらサーバのランタイム環境変数を参照できないためです．

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

# nuxt-env

👉 [nuxt-env](https://github.com/samtgarson/nuxt-env) 

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

```bash
$ npm i -S nuxt-env
```

```javascript:nuxt.config.js
modules: [
  ['nuxt-env', {
    keys: ['SENTRY_DSN']
  }]
]
```

```javascript
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](https://www.npmjs.com/package/@tanakaworld/nuxt-sentry)



```bash
$ npm i -S @tanakaworld/nuxt-sentry
```

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


```javascript:nuxt.config.js
module.exports = {
  // •••
  modules: [ '@tanakaworld/nuxt-sentry' ]
}
```

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

本家との違いは

- plugin コードで `$env.SENTRY_DSN` を参照
- Nuxt 旧バージョンの互換性を排除
- オプションの生成方法を簡略化

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

```javascript: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"]
    }
  ]);

  // •••
};
```

```javascript: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](https://www.npmjs.com/package/@tanakaworld/nuxt-sentry) によって `SENTRY_DSN` が一元管理できるようになりました．
[nuxt-env](https://github.com/samtgarson/nuxt-env) は便利なモジュールですが，シークレットキーなどサーバー外には公開したくない値は扱わないように注意が必要です．