🐶

ReactのエラーログをDataDogに送信する

2023/05/22に公開

やること

DataDogでReactの監視を導入する機会があったのでメモ。
まずは発生したエラーのログをDataDogに送信するところから始める。

前提条件

検証環境

  • React: v17.0.2
  • Next: v12.0.1

監視対象エラーについて

Reactで気にしたほうが良さそうなエラーは下記がありそう。

  1. レンダリング時に発生したエラー
  2. イベントハンドラで発生したエラー
  3. データフェッチやDB書き込みなどの非同期処理で発生したエラー
  4. 存在しないページにアクセスした時の404エラー
  5. SSR実行時の500エラー

上記の中で、今回フロントエンドの監視対象とするのは下記2つ。

  1. レンダリング時に発生したエラー
  2. イベントハンドラで発生したエラー

下記については、バックエンド側にAPMを導入して監視すれば良さそう。なので今回は対象外。

  1. データフェッチやDB書き込みなどの非同期処理で発生したエラー

下記も今回は監視対象外。Nextでのフォールバックについてはこちら参照。

  1. 存在しないページにアクセスした時の404エラー
  2. SSR実行時の500エラー

DataDogによるフロントエンド監視の基本

導入方法

導入は驚くほど簡単。rootに近い場所でdatadogLogs.initをするだけ。
デフォルトではブラウザで発生した全てのconsole.errorやthrowされたErrorをDataDogに送信する。

~/pages/_app.tsx
import { datadogLogs } from '@datadog/browser-logs';

datadogLogs.init({
  clientToken: <your-dataDog-clientToken>,
  site: <your-datadog-site>
});

ログのカスタマイズ方法

送信する内容を制限する。

datadogLogs.init({
  clientToken: <your-dataDog-clientToken>,
  site: <your-datadog-site>,
  forwardErrorsToLogs: false, // 明示的に出力指定したものだけ送信する
  forwardConsoleLogs: [], // console.logやerrorを送信しない
  beforeSend: (log) => {
    // 404 ネットワークエラーを破棄
    if (log.http && log.http.status_code === 404) {
      return false;
    }
  }
});

送信するログにタグをつける。

datadogLogs.addLoggerGlobalContext('env', 'dev')

ログにユーザデータを付加する。

datadogLogs.setUserProperty('name', user.name);
datadogLogs.setUserProperty('email', user.email);
datadogLogs.setUserProperty('organization', user.organization);

レンダリング時に発生したエラーをDataDogに出力する

ここからが本題。
react-error-boundaryを使うと、下記のようなコンポーネントのレンダリング中に発生したエラーを検知できる。

function RandomErrorComponent(){
  if(Math.random() < 0.1) throw Error('10%の確率でレンダリングエラーが起きる。')
  return <div>90%の確率でエラーが起きない。</div>
}

Error Boundaryの実装

rootコンポーネントに近い場所にError Boundaryを設置する。
今回はグローバルステートであるユーザ情報をError Boundaryで使いたいので、<RecoilRoot>の直下にError Boundaryを設置する。

~/pages/_app.tsx
import { ErrorBoundary } from '~/components/ErrorBoudary';
import { logger } from '~/logger';

const App = ...
...
const withContext = (Component: React.ComponentType<AppProps>) => {
  const WithContext = (props: AppProps) => (
    <RecoilRoot> // グローバルステートにユーザ情報を格納
      <ErrorBoundary logger={logger}>
        <Component {...props} />
      </ErrorBoundary>
    </RecoilRoot>
  );
  return WithContext;
};
export default withContext(App);
~/component/ErrorBounday.tsx
import type { ReactNode } from 'react';
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
import { ErrorFallback } from './ErrorFallback'; //フォールバック画面
import { Logger } from '~/logger'; //Loggerインターフェース(後述)を使用

type Props = {
  logger: Logger;
  children: ReactNode;
};

export function ErrorBoundary({ logger, children }: Props) {
  const { user } = useUser(); // グローバルステートからユーザ情報を取得。
  if (user) logger.setUserInfo(user); // ユーザ情報をloggerに格納する。シングルトンなのでアプリケーション全体で共有される。
  return (
    <ReactErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error) => logger.logError(error)} // 検知したエラーをDataDogに送信する
    >
      {children}
    </ReactErrorBoundary>
  );
}

loggerの実装

開発環境のログはコンソールに出力し、本番環境でのみログをDataDogに送信するようにする。
datadogLogsのcontextが1つなので、それに合わせてloggerシングルトンパターンで実装する。
これにより、アプリケーション内で唯一のloggerインスタンスを使うことが保証できるので、例えば複数のloggerインスタンスが別々にuser contextを書き換えてしまうことを防げる。

~/logger/index.ts
import { User } from '~/types';
import { DataDogLogger } from './dataDogLogger';
import { ConsoleLogger } from './consoleLogger';

export interface Logger {
  logError: (error: Error) => void;
  setUserInfo: (user: User) => void;
}

export const logger =
  process.env.NODE_ENV === 'development' ? ConsoleLogger.logger : DataDogLogger.logger;
~/logger/consoleLogger.ts
import { User } from '~/types';
import { Logger } from './logger';

export class ConsoleLogger implements Logger {
  private static _logger: ConsoleLogger;

  private constructor(
    private userName: string = '',
    private userEmail: string = '',
    private userOrganization: string = ''
  ) {}

  public static get logger(): ConsoleLogger {
    if (!this._logger) this._logger = new ConsoleLogger();
    return this._logger;
  }

  logError(error: Error) {
    console.error('Error => ', {
      message: error.message,
      userName: this.userName,
      userEmail: this.userEmail,
      userOrganization: this.userOrganization
    });
  }

  setUserInfo(user: User) {
    this.userName = user.name;
    this.userEmail = user.email;
    this.userOrganization = user.organization;
  }
}
~/logger/datadogLogger.ts
import { User } from '~/types';
import { datadogLogs } from '@datadog/browser-logs';
import { Logger } from './logger';

export class DataDogLogger implements Logger {
  private static _logger: DataDogLogger;
  private constructor() {
    datadogLogs.init({
      clientToken: <your-dataDog-clientToken>,
      site: <your-dataDog-site>,
    });
    datadogLogs.addLoggerGlobalContext('env', process.env.NODE_ENV);
  }

  public static get logger(): DataDogLogger {
    if (!this._logger) {
      this._logger = new DataDogLogger();
    }
    return this._logger;
  }

  logError(error: Error) {
    datadogLogs.logger.error(error.message);
  }

  setUserInfo(user: User) {
    datadogLogs.setUserProperty('name', user.name);
    datadogLogs.setUserProperty('email', user.email);
    datadogLogs.setUserProperty('organization', user.organization);
  }
}

イベントハンドラで発生したエラーをDataDogに出力する

イベントハンドラで起きたエラーはError Boundaryで検知してくれない。
(useErrorBoundaryを使えばイベントハンドラをErrorBoundaryで処理することも可能)

今回は素直にエラーをcatchして内容をDataDogに送信する。loggerはシングルトンなのでユーザ情報はすでに格納されている。

import { logger } from '~/logger';

function ErrorButton(){
  const onClick = () => {
    try {
      throw Error('')
    }
    catch (error){
      logger.logError(error) // これだけ
    }    
  }
  return <button onClick={onClick} >エラーが起きる</button>
}

参考

https://docs.datadoghq.com/logs/log_collection/javascript/#advanced-usage
https://zenn.dev/takepepe/articles/nextjs-error-handling
https://zenn.dev/kurosame/articles/482601fa0f422df9390d
https://zenn.dev/longbridge/articles/b7e76b31f993d9
https://zenn.dev/t_keshi/books/you-and-cleaner-react/viewer/invisible-https://zenn.dev/t_keshi/books/you-and-cleaner-react/viewer/invisible-hopeless-error

株式会社東京ファクトリー

Discussion