mcpField
E
error-monitoring

error-monitoring

github.com/jmsktm開発1069 回インストール40 週間前に更新
リポジトリ

error-monitoring について

Next.js アプリ向けに、エラー処理、ログ記録、監視の実装方法を案内するスキルです。エラーバウンダリー、API エラー、Sentry、パフォーマンス監視、復旧手法を扱います。

主な機能

  1. 01React エラーバウンダリー
  2. 02カスタムエラークラス
  3. 03コンテキスト付き構造化ログ
  4. 04Sentry によるエラー追跡
  5. 05パフォーマンス監視
  6. 06リトライ戦略

活用シーン

  1. 01Next.js アプリへの境界の追加
  2. 02リクエスト情報付き API 障害の記録
  3. 03例外の Sentry への送信
  4. 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 エラーを処理するとき
  • カスタムエラークラスを作成するとき
  • 本番環境の問題をデバッグするとき
  • パフォーマンス監視を設定するとき
  • 再試行ロジックを実装するとき
  • ユーザーセッションを追跡するとき

コメント