フロントエンドエラーモニタリング完全攻略:Sentry、SourceMapとユーザーに気づかれない例外キャプチャ
前端工程(更新: 2026年6月2日)
なぜフロントエンドエラーモニタリングが不可欠か?
本番環境でユーザーが白画面、ボタン無反応、APIエラーに遭遇しても—知らなければ修正できません。フロントエンドエラーモニタリングはユーザーの実際の体験と開発者の認識をつなぐ架け橋です。
| 監視しない場合の代償 | 監視する場合の利益 |
|---|---|
| ユーザーからの苦情で初めてバグを発見 | リアルタイム異常検知、分単位の対応 |
| 再現不可能な散発的問題 | 完全なスタックトレース + ユーザー行動リプレイ |
| 本番障害が数時間継続 | 自動アラート、MTTRの大幅短縮 |
| 推測による修正 | データ駆動、正確な特定 |
フロントエンドエラーの5つの発生源
1. 構文およびランタイムエラー
// ReferenceError: x is not defined
console.log(x);
// TypeError: Cannot read properties of undefined
const user = undefined;
user.name;
2. リソース読み込みエラー
// 画像、スクリプト、スタイルシートの読み込み失敗
const img = new Image();
img.src = '/broken-image.png';
img.onerror = () => {
trackError({ type: 'resource', tag: 'img', src: img.src });
};
3. 未捕捉のPromise例外
// 最も見落とされやすいエラータイプ
fetch('/api/data')
.then(res => res.json())
// .then内で例外が発生するが.catchがない
.then(data => data.results.map(r => r.name));
4. API例外
// HTTPエラーコード + ビジネスエラーコード
fetch('/api/order')
.then(res => {
if (!res.ok) {
trackError({ type: 'http', status: res.status, url: res.url });
}
return res.json();
});
5. Web Worker / iframeエラー
// Worker内部の例外はメインスレッドにバブルしない
const worker = new Worker('/worker.js');
worker.onerror = (e) => {
trackError({
type: 'worker',
message: e.message,
filename: e.filename,
lineno: e.lineno,
});
};
グローバルエラーキャプチャ:4つの防衛線
防衛線1:window.onerror
window.onerror = (message, source, lineno, colno, error) => {
trackError({
type: 'runtime',
message,
source,
lineno,
colno,
stack: error?.stack,
});
return false; // デフォルトのコンソール出力を抑制しない
};
制限:リソース読み込みエラーとPromise例外をキャプチャできません。
防衛線2:window.addEventListener('error')
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLElement) {
// リソース読み込みエラー
trackError({
type: 'resource',
tagName: event.target.tagName,
src: event.target.src || event.target.href,
});
} else {
// ランタイムエラー(onerrorと重複する可能性あり、重複除去が必要)
trackError({
type: 'runtime',
message: event.message,
stack: event.error?.stack,
});
}
}, true); // キャプチャフェーズを使用
防衛線3:window.addEventListener('unhandledrejection')
window.addEventListener('unhandledrejection', (event) => {
trackError({
type: 'unhandledRejection',
reason: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
});
});
防衛線4:React Error Boundary
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
trackError({
type: 'react',
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
}
render() {
if (this.state.hasError) {
return <FallbackUI onRetry={() => this.setState({ hasError: false })} />;
}
return this.props.children;
}
}
SourceMapデオブフスケーション:圧縮コードから真のスタックトレースを復元
本番コードは圧縮・難読化されており、エラースタックトレースは全く読めません。SourceMapデオブフスケーションは本番トラブルシューティングの中核能力です。
圧縮後のスタックトレース例
TypeError: Cannot read properties of undefined (reading 'name')
at a (main.abc123.js:1:2345)
at o (main.abc123.js:1:5678)
at t (main.abc123.js:1:9012)
デオブフスケーション後の真のスタックトレース
TypeError: Cannot read properties of undefined (reading 'name')
at getUserInfo (src/services/user.ts:42:15)
at fetchDashboard (src/pages/dashboard.tsx:128:22)
at useEffectCallback (src/pages/dashboard.tsx:115:5)
SourceMapセキュリティ戦略
| 戦略 | セキュリティ | 複雑度 | 適したシナリオ |
|---|---|---|---|
| プライベートNPM + CIデオブフスケーション | 高 | 中 | ほとんどのチーム |
| Sentryサーバーサイドデオブフスケーション | 中 | 低 | 迅速な統合 |
| Base64インラインSourceMap | 低 | 低 | 開発環境のみ |
| 独立した.mapファイルの本番公開 | 極低 | 低 | 非推奨 |
// source-mapライブラリでデオブフスケーション
import { SourceMapConsumer } from 'source-map';
async function applySourceMap(position, mapContent) {
const consumer = await new SourceMapConsumer(mapContent);
const original = consumer.originalPositionFor({
line: position.line,
column: position.column,
});
return original;
}
Sentry統合実践
インストールと初期化
npm install @sentry/react @sentry/tracing
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: 'https://xxx@o123456.ingest.sentry.io/789',
environment: process.env.NODE_ENV,
release: process.env.APP_VERSION,
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.01,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
],
beforeSend(event, hint) {
// 無意味なエラーをフィルタリング
if (event.exception?.values?.[0]?.type === 'ResizeObserver loop limit exceeded') {
return null;
}
return event;
},
});
Reactルーター統合
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
function App() {
return (
<SentryRoutes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</SentryRoutes>
);
}
手動レポートとパンくずリスト
// 手動ビジネス例外レポート
Sentry.captureException(new Error('注文支払いタイムアウト'), {
tags: { orderId: 'ORD-12345', paymentMethod: 'alipay' },
extra: { retryCount: 3, lastAttempt: Date.now() },
});
// パンくずリストの追加(ユーザー行動軌跡)
Sentry.addBreadcrumb({
category: 'ui.click',
message: '送信ボタンをクリック',
level: 'info',
});
軽量モニタリングシステムの自社構築
サードパーティサービスに依存したくない場合は、自社構築できます:
データ収集SDK
class ErrorMonitor {
constructor(options) {
this.endpoint = options.endpoint;
this.queue = [];
this.timer = null;
this.install();
}
install() {
window.addEventListener('error', this.handleError.bind(this), true);
window.addEventListener('unhandledrejection', this.handleRejection.bind(this));
}
handleError(event) {
this.report({
type: event.target instanceof HTMLElement ? 'resource' : 'runtime',
message: event.message || event.target?.outerHTML,
stack: event.error?.stack,
timestamp: Date.now(),
url: location.href,
ua: navigator.userAgent,
});
}
handleRejection(event) {
this.report({
type: 'unhandledRejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: Date.now(),
});
}
report(data) {
this.queue.push(data);
if (!this.timer) {
this.timer = setTimeout(() => this.flush(), 5000);
}
}
flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0);
navigator.sendBeacon(this.endpoint, JSON.stringify(batch));
this.timer = null;
}
}
主要指標ダッシュボード
| 指標 | 計算方法 | アラート閾値 |
|---|---|---|
| JSエラー率 | エラーPV / 総PV | > 0.1% |
| API失敗率 | 5xx数 / 総リクエスト数 | > 1% |
| ファーストビュークラッシュ率 | 白画面PV / 総PV | > 0.01% |
| 平均修復時間 | 最初のアラートからデプロイまでの時間 | > 30分 |
ベストプラクティスチェックリスト
- 4つの防衛線を完全カバー:onerror + errorイベント + unhandledrejection + Error Boundary
- SourceMapは本番公開しない:ビルド成果物を分離、CI環境でデオブフスケーション
- エラーサンプリングとフィルタリング:高頻度の無意味なエラーが実際の問題を埋もれさせないようにする
- ユーザー行動リプレイ:パンくずリスト + セッションリプレイで特定を加速
- アラートの階層化:P0即時通知、P1チケット追跡、P2週次レポート集計
- バージョン関連付け:各リリースにバージョン番号を付与、エラーがコード変更に自動関連付け
- プライバシーコンプライアンス:レポートデータを匿名化、ユーザー個人情報を含めない
- パフォーマンス関連付け:エラーモニタリングとパフォーマンスモニタリングを連携し、パフォーマンス劣化の根本原因を発見
ブラウザローカルツールを無料で試す →
#错误监控#Sentry#SourceMap#异常捕获#DevOps