ReactのエラーログをDataDogに送信する
やること
DataDogでReactの監視を導入する機会があったのでメモ。
まずは発生したエラーのログをDataDogに送信するところから始める。
前提条件
検証環境
- React: v17.0.2
- Next: v12.0.1
監視対象エラーについて
Reactで気にしたほうが良さそうなエラーは下記がありそう。
- レンダリング時に発生したエラー
- イベントハンドラで発生したエラー
- データフェッチやDB書き込みなどの非同期処理で発生したエラー
- 存在しないページにアクセスした時の404エラー
- SSR実行時の500エラー
上記の中で、今回フロントエンドの監視対象とするのは下記2つ。
- レンダリング時に発生したエラー
- イベントハンドラで発生したエラー
下記については、バックエンド側にAPMを導入して監視すれば良さそう。なので今回は対象外。
- データフェッチやDB書き込みなどの非同期処理で発生したエラー
下記も今回は監視対象外。Nextでのフォールバックについてはこちら参照。
- 存在しないページにアクセスした時の404エラー
- SSR実行時の500エラー
DataDogによるフロントエンド監視の基本
導入方法
導入は驚くほど簡単。rootに近い場所でdatadogLogs.init
をするだけ。
デフォルトではブラウザで発生した全てのconsole.error
やthrowされたError
をDataDogに送信する。
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
を設置する。
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);
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
を書き換えてしまうことを防げる。
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;
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;
}
}
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>
}
参考
Discussion