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. 01为 Next.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 错误
  • 创建自定义错误类
  • 调试生产环境问题
  • 设置性能监控
  • 实现重试逻辑
  • 跟踪用户会话

评论

支持 Markdown