🛎

Sentry を活用したい!

2021/09/30に公開

Agenda

  • はじめに
  • Sentry によるエラー検知
    • Sentry の初期設定
    • フロントでのエラー検知
    • フロントから Sentry へのエラー通知
    • リリース、環境と紐付けたエラートラッキング
  • Source Map のアップロード
  • Slack への通知
  • まとめ

はじめに

プロダクトを運用していく上で、エラーの監視は避けては通れませんよね。
バックエンドにエラー監視ツールを導入しているプロダクトは多いと思いますが、フロントエンドのエラー監視については、ツールを導入していないことも多いのではないでしょうか。
Magic Moment では フロントエンドのエラー監視として Sentry を、バックエンドのエラー監視として Cloud Logging を採用しています。
本記事ではフロントエンドのエラー監視にフォーカスし、 Magic Moment における Sentry の活用事例を紹介します。

Sentry によるエラー検知

Sentry の初期設定

Magic Moment のプロダクト「Magic Moment Playbook」は、フロントに TypeScript + React を用いた SPA (Single Page Application) になっています。
まずは React で Sentry のセットアップを行います。
どこに書いても良いですが、設定系のファイルは config ディレクトリなどにまとめておいた方が捗ります。
Magic Moment における Sentry の設定は下記の通りです。

config/sentry.ts

import * as Sentry from '@sentry/browser';
import Axios, { AxiosError } from 'axios';

import { isProd, env } from '~/config';
import { CustomError } from '~/constants';
import { axiosCancellationPrefix } from '~/plugins';
import { isAxiosError } from '~/utils';

const SENTRY_DSN = env.SENTRY_DSN;
const RELEASE_TAG = env.RELEASE_TAG;
const ENVIRONMENT = env.ENVIRONMENT;

let isSentryEnabled = false;

export function setupSentry() {
  if (isProd && SENTRY_DSN) {
    isSentryEnabled = true;
    Sentry.init({
      dsn: SENTRY_DSN,
      release: RELEASE_TAG,
      environment: ENVIRONMENT,
      normalizeDepth: 10,
      ignoreErrors: [
        'Request failed with status code 40',
        'Request failed with status code 50',
        'Network Error',
        'AutosizeInput, Select', // react-select
        'MenuPlacer', // react-select
        'DraggableCore', // react-draggable
        'NodeResolver',
        'Non-Error promise rejection captured with keys',
        axiosCancellationPrefix,
      ],
    });
  }
}

export function sentrySetUser(userId: string, teamId: string) {
  if (isSentryEnabled) {
    Sentry.configureScope((scope) => {
      scope.setUser({ userId, teamId, host: window.location.host });
    });
  }
}

export function sentryLog(err: Error | AxiosError) {
  console.error(err);

  const skipErrorTypes: string[] = [
    CustomError.ValidationError.toString(),
    CustomError.PermissionError.toString(),
  ];
  if (skipErrorTypes.includes(err.name)) {
    return;
  }

  if (!isSentryEnabled) return;

  if (Axios.isCancel(err)) return;

  if (isAxiosError(err)) {
    let contexts = {};
    const response = err.response;
    const endpoint = response?.config.url || '';
    const status = response?.status;
    const method = response?.config.method || '';

    contexts = { response };

    Sentry.withScope((scope) => {
      scope.setFingerprint(['{{ default }}', endpoint, String(status), method]);
      Sentry.captureException(err, {
        contexts,
      });
    });
  } else {
    Sentry.captureException(err);
  }
}

sentry.ts は大きく3つの関数に分かれています。

  • setupSentry ... Sentry の初期設定を行います。React 最上位の index.tsx から呼ばれます。
  • sentrySetUser ... ログにユーザ情報を付与します。認証直後にこの関数を呼び出してユーザ情報をセットしています。
  • sentryLog ... Sentry へエラーを通知します。App.tsx の componentDidCatch 関数で呼び出します。

ここでは setupSentry について詳しく説明します。

最初の条件分岐は、本番環境についてのエラーのみ Sentry に通知することを表しています。
isProd はビルド&リリースされる環境が本番環境かどうかを表す真理値が返されます。
SENTRY_DSN は .env で定義した環境変数を呼び出したもので、Sentry の Data Source Name を表す文字列が返されます。

次に isSentryEnabled ですが、これは Sentry への通知を有効化するかどうかを示すフラグです。
デフォルトで false だったものをここで true にしています。

そしてお待ちかねの Sentry.init です。
normalizeDepth は、Sentry のエラー確認画面でデータ (Array や Object) の階層をどこまで深堀りして表示するかについて設定するためのものです。
ignoreErrors では、無視したい (Sentry に通知しない) エラーを設定しています。
メンテナンスモード時の通信エラーや、利用しているモジュール側で発生するエラーなど、開発側で想定済みのエラーを ignore の対象にしています。
release や environment については後述します。

これで Sentry の初期設定が出来ました。

フロントでのエラー検知

フロントならではのエラーといえば、なんと言っても UI に関するものでしょう。
UI の一部に不具合があった際、React 16 以降では error boundary という機構により、子コンポーネントのエラーを親コンポーネントでキャッチ出来ます。
これを使って UI のエラーを Sentry に通知させます。

まず、error boundary によって UI のエラーを検知し、Sentry に通知されるまでのイメージを以下に示します。

React で UI エラーを検知して Sentry に通知する流れ

client-sentry.png

これを実現していくことになるわけですが... 安心して下さい。そんなに難しくありません。
前節でも少し触れましたが、上位の App.tsx に componentDidCatch を書いておくことで、下位の各コンポーネントのエラーをキャッチし、取り扱うことが出来ます。
キャッチしたエラーは sentryLog 関数を使って Sentry に通知します。
(sentryLog 関数の詳細は後述します)

App.tsx

// 前略

class App extends React.Component<Props, State> {
  // 中略

  componentDidCatch(error: Error, _: React.ErrorInfo) {
    sentryLog(error);
  }

  // 中略
}
	
// 後略

尚、 componentDidCatch が使えるのは Class Component の場合のみです。
「うちの App.tsx は Functional Component だよ」という方は、react-error-boundary のようなモジュールを使うと幸せになれると思います。
便利ですね。

ただし、error boundary はイベントハンドラ内で発生したエラーや非同期コードのエラーはキャッチしないことに注意して下さい。
例えば axios を使って通信を行っている場合、axios の通信エラー、すなわち非同期コードのエラーは error boundary ではキャッチ出来ません。
こうしたエラーは、お馴染みの try / catch 文を使ってキャッチして Sentry に通知する必要があります。
catch ブロックの中で直接 sentryLog を呼び出せば OK です。

もし、通信エラーが発生した際に必ず呼び出されるコンポーネントがあるのであれば、sentryLog を参照するのはそのコンポーネントに一任してしまうというのも手です。
Magic Moment Playbook 上で通信エラーが起きた際は、スナックバーの UI コンポーネント AlertSnackbar.tsx を表示させるようにしていますので、try / catch で拾ったエラーの大半はこのコンポーネントから Sentry に通知させています。

AlertSnackbar.tsx

// 前略

const AlertSnackbar = ({ error, handleClose }: AlertSnackbarProps) => {
  // 中略

  React.useEffect(() => {
    if (error) {
      sentryLog(error);
    }
  }, [error]);

  // 中略
};

// 後略

フロントから Sentry へのエラー通知

さて、AlertSnackbar.tsx で参照している sentryLog は、前節で紹介した config/sentry.ts 内で定義されているものです。
流れるように sentryLog の説明をします。

結論から書くと、最終的に Sentry.captureException 関数を使って Sentry にエラーを通知させています。
通知する際は、isAxiosError によって axios のエラーかどうか —— すなわち通信系のエラーかどうかで条件分岐させています。
通信系のエラーの場合は、エラーメッセージだけではなく通信の内容も送っているのがミソです。
こういったところからエラー分析・改善の糸口が見つかりますからね。

Sentry に通知したくないエラーがある場合はスキップさせて下さい。
sentryLog の前半部分がこれにあたります。
Sentry.captureException の前に空の return を挟むことで、関数が途中終了し、Sentry に通知しないように出来ます。

リリース、環境と紐付けたエラートラッキング

Magic Moment では、エラーとリリースを紐づけることで、どのリリースで発生したエラーなのか、追跡出来る仕込みを構築しています。
具体的な手段として、config/sentry.ts にて定義した環境変数を Sentry へのエラー情報送信時のパラメータに埋め込むことで判別しています。

Sentry でエラーと環境を紐付けた結果1

error-release-mapping-1.png

Sentry でエラーと環境を紐付けた結果2

error-release-mapping-2.png

これにより、リリース前のテスト環境でエラーを検知、対策が出来たり、また、どのリリースに起因して発生したエラーなのかを特定することにも活用出来ます。

Source Map のアップロード

TypeScript + React で書かれたソースコードは、トランスパイラ (Babel など) とモジュールバンドラー (Webpack など) によって変換した上で、ブラウザから実行されます。
このため、ブラウザが実行する JavaScript は、エンジニアが実際に書いた React の元のソースコードとはかけ離れたものになっています。
Sentry では、エラーが起きた際に該当するソースコードを示してくれますが、ビルド後の JavaScript からではエラー箇所の特定が困難です。
そこで Source Map を利用します。
Sentry にソースマップファイルをアップロードすることで、エラーが起きた際に、元の React のソースコードでエラー箇所を示してくれるようになります。

Sentry のドキュメントを参考に、Sentry 向けにソースマップの設定を行っていきます。
sentry-cli を使って手動でアップロードする方法と、Webpack の設定として書いておきビルドのたびに自動でアップデートする方法があります。
Magic Moment では後者の方法を取っていますので、Webpack の設定方法を紹介します。
Sentry Webpack plugin を利用するため、あらかじめ npm などでインストールしておいて下さい。

設定ですが、通常は webpack.config.js などに書いておきます。
ですが Create React App を使って React プロジェクトを作成した場合、webpack.config.js は見えなくなっています。
(eject すれば設定ファイルが全て展開されますが、元に戻せなくなるのでオススメ出来ません)

そのため Magic Moment では react-app-rewired を使って設定をオーバーライドしています。
config-override.js に以下を書くことで、ソースマップの設定が出来ます。

config-override.js

// ...その他モジュールのインポート
const SentryPlugin = require("@sentry/webpack-plugin");

const SENTRY_ORG = process.env.SENTRY_ORG;
const SENTRY_PROJECT = process.env.SENTRY_PROJECT;
const SENTRY_AUTH_TOKEN = process.env.SENTRY_AUTH_TOKEN;
const REACT_APP_RELEASE_TAG = process.env.REACT_APP_RELEASE_TAG;

module.exports = (config) => {
	// ...その他の設定

  if (
    SENTRY_ORG &&
    SENTRY_PROJECT &&
    SENTRY_AUTH_TOKEN &&
    REACT_APP_RELEASE_TAG
  ) {
    const sentryPlugin = new SentryPlugin({
      org: SENTRY_ORG,
      project: SENTRY_PROJECT,
      authToken: SENTRY_AUTH_TOKEN,
      release: REACT_APP_RELEASE_TAG,
      include: "./build",
    });
    config.plugins.push(sentryPlugin);
  }

  return config;
};

.env

# その他の環境変数

# Sentry
SENTRY_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
SENTRY_ORG="magic-moment"
SENTRY_PROJECT="magic-playbook-client"
REACT_APP_SENTRY_DSN="https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@xxxxxxx.ingest.sentry.io/xxxxxxx"
REACT_APP_RELEASE_TAG=""
REACT_APP_ENVIRONMENT="local"

詳細は割愛しますが、一部の環境変数については、ビルドおよびリリースされる環境によって動的に変化するようにしています。
本番環境でのみ設定したいもの、逆に開発環境でのみ設定して動いて欲しいものなどがある、というのが理由です。
REACT_APP_RELEASE_TAG が空になっているのはそのためです。(REACT_APP_ENVIRONMENT が production のときのみリリースタグが動的に入るようになっています)

ソースマップ設定適用後の Sentry の管理画面を以下に示します。

ソースマップファイルアップロード後の Sentry の管理画面

sentry-source-map.png

ご覧の通り、ソースマップファイルがアップロードされたことで、元のソースコードが表示されています。
これでエラー箇所を特定しやすくなりました。

Slack への通知

Magic Moment では、Sentry で検知したエラーを Slack に通知することで、エラーに対し迅速に対応する仕組みを構築しています。
この仕組みを導入した背景として、「プロダクトの現状を正確に把握し、あるべき理想と現状のギャップを常に埋めようとしていること」を テックチームの Vision の一つとして掲げており、ユーザが負を感じる部分の可視化・改善を常日頃から意識しているところにあります。

Magic Moment では、3種類の通知を設定しています。

  1. 本番環境で発生したエラーの通知
  2. テスト環境で発生したエラーの通知
  3. SLI/SLO に基づいた通知

Sentry から Slack への通知設定

sentry-slack.png

Slack に通知されると、即座に調査を開始し、緊急度・重要度の観点で優先順位をつけ、対策方針の検討を行うことが出来ます。
Magic Moment のテックチームは、品質に対する意識が高いため、全員が積極的に調査に参加・フォローすることが特徴です。

まとめ

Magic Moment での Sentry 活用事例について紹介しました。

まだまだ Sentry の機能を熟知・使い倒している訳ではなく、試行錯誤しながら、地道に機能導入・活用に取り組む毎日です。
ツールは闇雲に導入しても成果が上がる訳ではありません。
私達もプロダクトの課題、機能導入の目的を明確にし仮説検証するなど、時間が掛かり苦労することも多いですが、その分、プロダクトが日々改良されることを目の当たりにすると、やりがいを感じます。

弊社 Magic Moment では、プロダクトの成長を一緒に喜べるエンジニアを募集しています!

Magic Momentに少しでも興味を持っていただけたら是非エントリーください!

8/30にはFindy様主催のイベントにMagic Momentから石田さんが登壇されます!
こちらの記事の内容と関連した発表となっています!よろしければぜひご視聴ください!

さらに、こちらのイベントも8/29開催予定です!こちらはオンラインイベントです。Magic Momentの開発がどんなものか興味を持っていただいた方は是非ご参加ください!

この記事は私達が書きました

jiei-suganuma.png

makoto-mori.png

Discussion