Sentry を Laravel と Vue.js に導入してみた

Sentry を導入したので導入手順をまとめました。
PHP (Laravel) と Vue.js の場合について記載しています。

## Sentry？

https://sentry.io/

Web アプリケーションで起きたエラー等をロギングできるサービスです。
Slack やメールで通知を送れます。

## 導入

### Laravel 5.x (PHP)

Laravel 用のパッケージがあるのでこれを利用します。記事作成時点での最新バージョンは `0.10.0` です。

[getsentry/sentry\-laravel: Laravel integration for Sentry](https://github.com/getsentry/sentry-laravel)

導入方法: [Docs \- Laravel](https://docs.sentry.io/clients/php/integrations/laravel/)

#### 基本

パッケージをインストールします。

```sh
composer require sentry/sentry-laravel
```

**Laravel 5.4 以下** の場合は `config/app.php` に設定を追加する必要があるようです。

```php:config/app.php
'providers' => array(
    // ...
    Sentry\SentryLaravel\SentryLaravelServiceProvider::class,
)

'aliases' => array(
    // ...
    'Sentry' => Sentry\SentryLaravel\SentryFacade::class,
)
```

Sentry へのロギングを実施させる仕組みとして、 `app/Exceptions/Handler.php` の `report()` に手を加えます。

```php:app/Exceptions/Handler.php
public function report(Exception $exception)
{
    // ここから
    if (app()->bound('sentry') && $this->shouldReport($exception)) {
        app('sentry')->captureException($exception);
    }
    // ここまで

    parent::report($exception);
}
```

`config/sentry.php` を生成するため以下のコマンドを実行します。

```sh
php artisan vendor:publish --provider="Sentry\SentryLaravel\SentryLaravelServiceProvider"
```

Sentry の DSN を設定します。
`config/sentry.php` にべた書きするなり `.env` に書き込むなりしましょう。

```php:config/sentry.php
return array(
    'dsn' => env('SENTRY_LARAVEL_DSN', 'https://foobar@sentry.io/1234567890'),
    ...
```

仕上げに Laravel の 500 エラーページを用意します。
サーバエラーが起きた際に、ロギングだけでなくユーザーに詳細な報告を入力してもらうためのフォームを表示できます。

```php:resources/views/errors/500.blade.php

<div class="content">
    <div class="title">予期しないエラーが発生しました。</div>

    @if( app()->bound('sentry') && !empty(Sentry::getLastEventID()) )
    <div class="subtitle">Error ID: {{ Sentry::getLastEventID() }}</div>
    <script src="https://cdn.ravenjs.com/3.3.0/raven.min.js"></script>
    <script>
        Raven.showReportDialog({
            eventId: '{{ Sentry::getLastEventID() }}',
            dsn: '{{ config("sentry.dsn") }}',
            user: {
                'name': '{{ Auth::User() ? Auth::User()->name : "" }}',
                'email': '{{ Auth::User() ? Auth::User()->email : "" }}'
            }
        })
    </script>
    @endif
</div>
```


#### エラー発生時の認証ユーザー情報をログに含める

Middleware を作成します。
ユーザーの操作中にエラーが発生した時に、どのユーザーでエラーが発生したのかをログと一緒に Sentry に送信できます。

```php:app/Http/Middleware/SentryContext.php
<?php

namespace App\Http\Middleware;

use Closure;

class SentryContext
{
    public function handle($request, Closure $next)
    {
        // ここから
        if (app()->bound('sentry')) {
            $sentry = app('sentry');

            if (auth()->check()) {
                $sentry->user_context([
                    'id' => auth()->user()->id,
                    'email' => auth()->user()->email
                ]);
            } else {
                $sentry->user_context([
                    'id' => null,
                    'email' => null
                ]);
            }
        }
        // ここまで
        return $next($request);
    }
}
```

ファイルを作成したら `app/Kernel.php` の `$routeMiddleware` に登録します。

```php:app/Kernel.php
        ...
        'sentry.context' => \App\Http\Middleware\SentryContext::class
    ];
```

ロギング時にユーザー情報を含めたいルートに対してこの Middleware を適用します。

```php:routes/web.php
Route::middleware(['auth', 'sentry.context'])->group(function() {
  ...
}
```

### JavaScript (+Vue.js)

`4.0.0` からパッケージが新しくなり、以前の `raven-js` ではなくなってるっぽい。
同時に導入方法も変わっていたのでこちらもまとめ。

[getsentry/sentry\-javascript: Official Sentry SDKs for Javascript](https://github.com/getsentry/sentry-javascript)

いくつかのパッケージがありますが、ブラウザ上で動作する React や Vue 製の Web アプリケーション であれば `@sentry/browser` と `@sentry/integrations` をインストールすれば良いようです。
型定義 (`@sentry/types`) も提供されているので TypeScript でも利用できます。

#### 基本

パッケージをインストールします。ここでインストールされるバージョンは `5.x` の想定です。

```sh
npm i @sentry/browser @sentry/integrations
```

アプリケーションの出来るだけ早いタイミングで読み込み、 `init()` を実行します。

```js:path/to/my/app.js
import { init } from '@sentry/browser'
import { Dedupe, ExtraErrorData } from '@sentry/integrations'

init({
  dsn: 'https://foobar@sentry.io/1234567890',
  integrations: [
    new Dedupe(),
    new ExtraErrorData({ depth: 3 })
  ]
})
```

`init()` が受け付ける引数は以下で定義されていますが、最低限必要なのは `dsn` だけなのかなと思います。

- https://github.com/getsentry/sentry-javascript/blob/5.0.0/packages/browser/src/backend.ts#L16
- https://github.com/getsentry/sentry-javascript/blob/5.0.0/packages/types/src/options.ts#L8

`integrations` に渡している `Dedupe`, `ExtraErrorData` はオプショナルになります。これらは `@sentry/browser@4.x` ではデフォルトで適用されていた物でしたが、 `5.x` からは明示的に登録してあげないと適用されません。
使い方や役割は [こちらの公式ドキュメント](https://docs.sentry.io/platforms/node/pluggable-integrations/) を参照してください。



基本的には以上で完了ですが、利用している JS フレームワークによってはそれぞれ異なる `integrations` が必要になります。
下記は Vue.js の場合です。

```js:path/to/my/app.js
import Vue from 'vue'
import * as Sentry from '@sentry/browser'
import * as Integrations from '@sentry/integrations'

Sentry.init({
  dsn: 'https://foobar@sentry.io/1234567890',
  integrations: [
    new Dedupe(),
    new ExtraErrorData({ depth: 3 }),
    new Integrations.Vue({ Vue })
  ]
})
```

他のフレームワークの場合については公式ドキュメントから参照してください。
[Docs \- JavaScript](https://docs.sentry.io/platforms/javascript/)


#### エラー発生時の認証ユーザー情報をログに含める

`init()` の実行よりも後に、 `configureScope()` のコールバック内で `scope.setUser()` を実行します。
(`scope` の定義はこちら: https://github.com/getsentry/sentry-javascript/blob/4.0.6/packages/hub/src/scope.ts)


```js
import * as Sentry from '@sentry/browser'

const user = {
  id: '1',
  email: 'foo@bar.baz',
  username: 'FooBar'
}

Sentry.init(...)

Sentry.configureScope(scope => {
  scope.setUser( user )
})
```

この `scope` を使ってユーザー情報や Sentry のタグなどを設定できます。
例えば `environment` の値を設定する場合は次のようになります。

```js
Sentry.configureScope(scope => {
  scope.setTag('environment', 'production')
})
```

削除もできます。それまで `configureScope()` で設定された値が丸ごと消されるようです。

```js
Sentry.configureScope(scope => {
  // SPA でログアウト処理したら実行…とか?
  scope.clear()

  // 続けて次のスコープを設定したりも可能
  scope.setUser(user2)
})
```

これはいい感じのラッパーを作って操作するのが良さそうですね。