フロントエンドのエラー検知を見直す

## フロントエンドのエラー検知、後回しになっていませんか？
バックエンドでは、エラー検知や監視の仕組みを整えるのがもはや前提になっています。APIの障害やシステムの異常にすばやく対応できるよう、モニタリングツールや通知設定をしっかり導入しているチームも多いでしょう。

一方で、フロントエンドについてはどうでしょうか。
テストコードの文化は浸透してきたものの、本番環境でのエラー監視については、まだ手が回っていないケースも少なくありません。私自身も、フロント側のエラー検知の対応にはなかなか目が向いていませんでした。

テストだけではカバーしきれない本番特有のエラー。ネットワーク状況やブラウザ環境、ユーザーの操作状況など、実運用の中では予期しづらい問題が発生することもあります。

そこで重要になるのが、「実際にユーザーが体験している問題を検知できる仕組み」、つまりフロントエンドのエラーけんちです。

今回は、そうしたフロントエンドエラーの検知をどう始めるか、そして無理なく導入するための方法を紹介します。

## フロントエンドのエラー検知が後回しにされる理由
- ユーザーからのフィードバックがメインのエラー検出手段になっているケースも多く、フィードバックがない限り問題に気づけないことがある
- 端末・ブラウザ・通信環境などの多様性により、再現が困難
- SaaS導入の初期コスト（設定・SDK・通知設定など）がかかる
- 何もしないとノイズ（古いブラウザの警告、CORSエラーなど）が多すぎる

他の開発タスクとの兼ね合いで、後回しにされやすく導入のタイミングを見失いやすい領域でもあります。

## 検知すべきフロントエンドのエラー例
|種別|内容|
|-|-|
|JavaScriptエラー|	window.onerror, unhandledrejection でキャッチ可能|
|API通信エラー|	CORS失敗、認証エラー（401/403）など|
|リソース読み込みエラー|	画像・CSS・スクリプトの読み込み失敗|
|UI操作エラー|	ボタンが動作しない、遷移しないなど|
|パフォーマンス指標|	LCP/FCPなどのパフォーマンス悪化傾向|

## なぜフロントエンドのエラー検知が必要なのか？
ユーザーが利用する環境は非常に多様です。どれほど丁寧にテストしても、全ての組み合わせをカバーするのは不可能です。
そのため、本番環境での実際のユーザーのエラーを可視化することが非常に重要になります。

### よく使われるエラー検知サービス
|ツール名|特徴|
|-|-|
|Sentry|	OSS 由来で導入実績が多く、初学者にも比較的扱いやすい。無料枠もあるため小規模サービスでも使いやすい。|
|Bugsnag|	UI やアラート機能が分かりやすく、マネージャーや非エンジニアもログを確認しやすい設計になっている。|
|Datadog RUM|	大規模サービスでの活用に強く、バックエンドとの統合にも向いている反面、コスト面は注意が必要。|
|CloudWatch RUM|	AWS にインフラを統一している場合に導入コストが低く、IAMやログ設計と連携しやすい。|

### クラウドサービス導入の課題
受託開発や他社サービスの開発では、以下のような導入障壁があります。
- ユーザー情報を外部に出せない
- 契約・合意が必要
- 外部サービス利用のコストがネックになることがある

このようなケースでは、内製でエラー検知を仕組み化する方法が現実的です。


## 内製でフロントエンドエラーを収集する方法
ここでは、APIサーバーにエラーを送信するシンプルな方法をご紹介します。

###  同期エラー、スクリプト読み込みエラーの検知
```ts
window.onerror = function (message, source, lineno, colno, error) {
  reportError({
    type: "onerror",
    message,
    source,
    lineno,
    colno,
    stack: error?.stack,
  });
};
```

###  Promise の unhandled rejection（非同期エラー）の検知
```ts
window.addEventListener('unhandledrejection', (event) => {
  reportError({
    type: "unhandledrejection",
    message: event.reason?.message || String(event.reason),
    stack: event.reason?.stack,
  });
});
```

###  API 通信など try/catch による明示的なエラーの検知

```ts
try {
  const res = await fetch("/api/data");
  if (!res.ok) throw new Error(`Status: ${res.status}`);
  const data = await res.json();
} catch (err) {
  reportError({
    type: "api",
    message: err.message,
    stack: err.stack,
  });
}
```

###  エラーレポート関数（共通）

```ts
function reportError(info: {
  type: string;
  message: string;
  stack?: string;
  [key: string]: any;
}) {
  const payload = {
    ...info,
    url: window.location.href,
    userAgent: navigator.userAgent,
    timestamp: new Date().toISOString(),
    // 必要ならログインユーザーやクリック履歴も追加
  };

  fetch("/log-client-error", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  }).catch(console.error); // 二次エラー対策
}
```

なお、内製でエラー監視の仕組みを構築する場合は、運用・保守コストや機能のスケーラビリティにも留意が必要です。
ログの保管・可視化・通知フローなどをすべて自前で整えるには、一定の工数が発生します。
サービスの規模やチームのリソースに応じて、「どこまでを内製するか」の線引きを検討することが大切です。

## フレームワークごとの対応

### React
UI コンポーネントのレンダリング中に発生する例外を補足するために ErrorBoundary を使用する。

```ts
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error: any) {
    return { hasError: true };
  }

  componentDidCatch(error: any, info: any) {
    reportError({
      type: "react-error",
      message: error.message,
      stack: error.stack,
    });
  }

  render() {
    if (this.state.hasError) return <h1>Something went wrong.</h1>;
    return this.props.children;
  }
}

// 使用法
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>;
```

※イベントハンドラ内の例外は try/catch を併用してください。

### Vue
グローバルもしくはコンポーネント単位でエラーハンドラを設定する。

```ts
// グローバルハンドラ
app.config.errorHandler = (err, instance, info) => {
  reportError({
    type: "vue-global",
    message: err.message,
    stack: err.stack,
    info,
  });
};

// コンポーネント単位
defineComponent({
  errorCaptured(err, instance, info) {
    reportError({
      type: "vue-component",
      message: err.message,
      stack: err.stack,
      info,
    });
    return false;
  },
});
```

## ネットワークの負荷を抑えたい場合

reportError()関数でサーバー側にエラー情報を送信しているため、エラーが頻発する場合はネットワークの負荷が高まる恐れがあります。<br>
その場合は一定時間ごとや一定量を超えた場合にまとめて送信するなどの方法を検討してください。

```ts
const errorBuffer: any[] = [];

// エラーをバッファリング
function bufferError(info: any) {
  errorBuffer.push({
    ...info,
    timestamp: new Date().toISOString(),
  });
}

// 定期的に送信
setInterval(() => {
  if (errorBuffer.length > 0) {
    fetch("/log-client-error", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(errorBuffer),
    })
      .then(() => (errorBuffer.length = 0)) // 成功したらクリア
      .catch(console.error);
  }
}, 5000); // 5秒ごとに送信
```

## エラーを送信した「その後」は？
本記事では、フロントエンドで発生したエラーをAPI経由で送信するシンプルな方法を紹介しました。ただし、送信したエラー情報をどう扱うか（保存・可視化・通知など）は、システム構成や目的に応じて設計が必要です。

例えば以下のような対応が考えられます。
- 送信されたログをデータベースに保存して後で分析する
- SlackやTeamsなどのチャットツールに通知し、迅速な対応を促す
- CloudWatch Logsなどでログ検索やフィルタリングを可能にする

## まとめ
フロントエンドのエラーは、ユーザー体験に直結します。しかしその特性上、再現の難しさや多様な環境の影響により、把握が難しいこともあります。
だからこそ、本番環境でのエラーを適切に検知し、記録し、改善につなげる仕組みが重要です。
専用のモニタリングサービスを活用するもよし、プロジェクトに合わせて軽量な仕組みを内製するもよし。やり方はいろいろありますが、「ユーザーの見えないところで起きている問題に目を向ける」ことが、プロダクトの質を高める第一歩です。
チームやプロジェクトのフェーズに合わせて、できるところから少しずつ取り入れていけると良いですね。