error-monitoring について
Next.js アプリ向けに、エラー処理、ログ記録、監視の実装方法を案内するスキルです。エラーバウンダリー、API エラー、Sentry、パフォーマンス監視、復旧手法を扱います。
主な機能
- 01React エラーバウンダリー
- 02カスタムエラークラス
- 03コンテキスト付き構造化ログ
- 04Sentry によるエラー追跡
- 05パフォーマンス監視
- 06リトライ戦略
活用シーン
- 01Next.js アプリへの境界の追加
- 02リクエスト情報付き API 障害の記録
- 03例外の Sentry への送信
- 04一時的な処理のリトライ
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ jmsktm/ claude-settings --skill error-monitoringインストール後、Claude Code で/error-monitoringを入力して読み込まれたか確認します。
詳細
エラー監視とロギングのスキル
概要
このスキルでは、Next.js アプリケーションに堅牢なエラー処理、ロギング、監視を実装する方法を説明します。ローカル開発から本番環境の監視まで、問題の検出と修正に必要なすべてを扱います。
エラーバウンダリ(React)
基本的なエラーバウンダリ
// app/error.tsx
'use client'
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log error to your monitoring service
console.error('Error caught:', error)
}, [error])
return (
<div className="flex flex-col items-center justify-center min-h-screen p-4">
<h2 className="text-2xl font-bold mb-4">Something went wrong!</h2>
<p className="text-gray-600 mb-4">{error.message}</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Try again
</button>
</div>
)
}
グローバルエラーバウンダリ
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Application Error</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</body>
</html>
)
}
ネストされたエラーバウンダリ
// app/dashboard/error.tsx
'use client'
export default function DashboardError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div className="p-4">
<h2>Dashboard Error</h2>
<p>{error.message}</p>
<button onClick={reset}>Reload Dashboard</button>
</div>
)
}
// app/dashboard/settings/error.tsx - More specific
'use client'
export default function SettingsError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div className="p-4">
<h2>Settings Error</h2>
<p>Failed to load settings: {error.message}</p>
<button onClick={reset}>Reload Settings</button>
</div>
)
}
カスタムエラークラス
// lib/errors.ts
export class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500,
public isOperational: boolean = true
) {
super(message)
this.name = this.constructor.name
Error.captureStackTrace(this, this.constructor)
}
}
export class ValidationError extends AppError {
constructor(message: string, public field?: string) {
super(message, 'VALIDATION_ERROR', 400)
}
}
export class AuthenticationError extends AppError {
constructor(message = 'Authentication required') {
super(message, 'AUTHENTICATION_ERROR', 401)
}
}
export class AuthorizationError extends AppError {
constructor(message = 'Insufficient permissions') {
super(message, 'AUTHORIZATION_ERROR', 403)
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 'NOT_FOUND', 404)
}
}
export class DatabaseError extends AppError {
constructor(message: string) {
super(message, 'DATABASE_ERROR', 500, false)
}
}
// Usage
throw new ValidationError('Invalid email format', 'email')
throw new NotFoundError('User')
throw new AuthenticationError()
エラーロガー
// lib/logger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
interface LogEntry {
level: LogLevel
message: string
timestamp: string
context?: Record<string, any>
error?: Error
}
class Logger {
private logs: LogEntry[] = []
private log(level: LogLevel, message: string, context?: Record<string, any>, error?: Error) {
const entry: LogEntry = {
level,
message,
timestamp: new Date().toISOString(),
context,
error
}
this.logs.push(entry)
// Log to console in development
if (process.env.NODE_ENV === 'development') {
const color = {
debug: '\x1b[36m',
info: '\x1b[32m',
warn: '\x1b[33m',
error: '\x1b[31m'
}[level]
console.log(
`${color}[${level.toUpperCase()}]\x1b[0m ${message}`,
context || '',
error || ''
)
}
// Send to monitoring service in production
if (process.env.NODE_ENV === 'production' && level === 'error') {
this.sendToMonitoring(entry)
}
}
debug(message: string, context?: Record<string, any>) {
this.log('debug', message, context)
}
info(message: string, context?: Record<string, any>) {
this.log('info', message, context)
}
warn(message: string, context?: Record<string, any>) {
this.log('warn', message, context)
}
error(message: string, error?: Error, context?: Record<string, any>) {
this.log('error', message, context, error)
}
private async sendToMonitoring(entry: LogEntry) {
// Send to Sentry, LogRocket, etc.
try {
await fetch('/api/logs', {
method: 'POST',
body: JSON.stringify(entry)
})
} catch (e) {
// Fallback: log to console
console.error('Failed to send log to monitoring:', e)
}
}
}
export const logger = new Logger()
// Usage
logger.info('User logged in', { userId: '123' })
logger.error('Payment failed', error, { orderId: '456' })
API エラー処理
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { AppError } from '@/lib/errors'
import { logger } from '@/lib/logger'
export async function GET(request: NextRequest) {
try {
const users = await db.users.findMany()
return NextResponse.json({ users })
} catch (error) {
return handleApiError(error)
}
}
function handleApiError(error: unknown): NextResponse {
// Log error
logger.error('API Error', error as Error)
// Handle known errors
if (error instanceof AppError) {
return NextResponse.json(
{
error: error.message,
code: error.code
},
{ status: error.statusCode }
)
}
// Handle Zod validation errors
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: 'Validation failed',
code: 'VALIDATION_ERROR',
issues: error.issues
},
{ status: 400 }
)
}
// Handle database errors
if (error.code === 'P2002') { // Prisma unique constraint
return NextResponse.json(
{
error: 'Resource already exists',
code: 'DUPLICATE_ERROR'
},
{ status: 409 }
)
}
// Handle unexpected errors
return NextResponse.json(
{
error: 'Internal server error',
code: 'INTERNAL_ERROR'
},
{ status: 500 }
)
}
Sentry の統合
セットアップ
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config')
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config')
}
}
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
debug: false,
replaysOnErrorSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
integrations: [
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
})
// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
debug: false,
})
Sentry の使用
import * as Sentry from '@sentry/nextjs'
// Capture exception
try {
await riskyOperation()
} catch (error) {
Sentry.captureException(error, {
tags: {
section: 'payment',
},
extra: {
userId: user.id,
orderId: order.id,
},
})
throw error
}
// Capture message
Sentry.captureMessage('Something went wrong', {
level: 'warning',
tags: { feature: 'checkout' },
})
// Set user context
Sentry.setUser({
id: user.id,
email: user.email,
username: user.name,
})
// Add breadcrumb
Sentry.addBreadcrumb({
category: 'auth',
message: 'User logged in',
level: 'info',
})
クライアント側のエラー追跡
// components/error-tracker.tsx
'use client'
import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'
export function ErrorTracker() {
useEffect(() => {
// Catch unhandled errors
window.addEventListener('error', (event) => {
Sentry.captureException(event.error)
})
// Catch unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
Sentry.captureException(event.reason)
})
}, [])
return null
}
// app/layout.tsx
import { ErrorTracker } from '@/components/error-tracker'
export default function RootLayout({ children }) {
return (
<html>
<body>
<ErrorTracker />
{children}
</body>
</html>
)
}
パフォーマンス監視
// lib/performance.ts
export class PerformanceMonitor {
private marks: Map<string, number> = new Map()
start(label: string) {
this.marks.set(label, performance.now())
}
end(label: string) {
const start = this.marks.get(label)
if (!start) return
const duration = performance.now() - start
this.marks.delete(label)
logger.info(`Performance: ${label}`, { duration: `${duration.toFixed(2)}ms` })
// Send to monitoring if slow
if (duration > 1000) {
Sentry.captureMessage(`Slow operation: ${label}`, {
level: 'warning',
extra: { duration },
})
}
return duration
}
}
export const perfMonitor = new PerformanceMonitor()
// Usage
perfMonitor.start('fetchUsers')
const users = await db.users.findMany()
perfMonitor.end('fetchUsers')
構造化ロギング
// lib/structured-logger.ts
type LogContext = {
userId?: string
requestId?: string
ip?: string
userAgent?: string
[key: string]: any
}
class StructuredLogger {
private context: LogContext = {}
setContext(context: LogContext) {
this.context = { ...this.context, ...context }
}
clearContext() {
this.context = {}
}
log(level: string, message: string, data?: any) {
const logEntry = {
timestamp: new Date().toISOString(),
level,
message,
...this.context,
...data,
}
// In production, send to log aggregation service
if (process.env.NODE_ENV === 'production') {
this.sendToLogService(logEntry)
} else {
console.log(JSON.stringify(logEntry, null, 2))
}
}
private async sendToLogService(entry: any) {
// Send to DataDog, Logtail, etc.
}
info(message: string, data?: any) {
this.log('info', message, data)
}
error(message: string, error?: Error, data?: any) {
this.log('error', message, {
...data,
error: {
message: error?.message,
stack: error?.stack,
name: error?.name,
},
})
}
}
export const structuredLogger = new StructuredLogger()
// Usage in API route
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID()
structuredLogger.setContext({
requestId,
ip: request.ip,
userAgent: request.headers.get('user-agent'),
})
structuredLogger.info('Processing payment', { amount: 100 })
try {
await processPayment()
structuredLogger.info('Payment successful')
} catch (error) {
structuredLogger.error('Payment failed', error)
throw error
} finally {
structuredLogger.clearContext()
}
}
エラー復旧戦略
// lib/retry.ts
export async function retry<T>(
fn: () => Promise<T>,
options = { maxAttempts: 3, delay: 1000 }
): Promise<T> {
let lastError: Error
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
try {
return await fn()
} catch (error) {
lastError = error as Error
logger.warn(`Attempt ${attempt} failed`, { error: lastError.message })
if (attempt < options.maxAttempts) {
await new Promise((resolve) =>
setTimeout(resolve, options.delay * attempt)
)
}
}
}
throw lastError!
}
// Usage
const data = await retry(
() => fetch('/api/data').then((r) => r.json()),
{ maxAttempts: 3, delay: 1000 }
)
ベストプラクティスのチェックリスト
- 重要な階層にエラーバウンダリを実装する
- 種類ごとにカスタムエラークラスを使用する
- コンテキスト(ユーザー、リクエスト ID など)とともにエラーを記録する
- エラーを監視サービス(Sentry)に送信する
- 想定されるエラーを適切に処理する
- ユーザーにわかりやすいエラーメッセージを表示する
- 一時的な障害に対する再試行ロジックを含める
- パフォーマンスのボトルネックを監視する
- 重大なエラーに対するアラートを設定する
- エラー率と傾向を追跡する
- デバッグ用にリクエスト ID を含める
- ログから機密データをサニタイズする
- エラーのシナリオをテストする
このスキルを使用するタイミング
次のような場合にこのスキルを呼び出します:
- エラーバウンダリを設定するとき
- エラーロギングを実装するとき
- Sentry や監視ツールを統合するとき
- API エラーを処理するとき
- カスタムエラークラスを作成するとき
- 本番環境の問題をデバッグするとき
- パフォーマンス監視を設定するとき
- 再試行ロジックを実装するとき
- ユーザーセッションを追跡するとき
コメント
Markdown 対応