本章目标:建立一套完整的错误处理与监控体系——理解 Next.js App Router 中错误边界的层次结构,掌握 Server Component / Client Component / Server Action / Route Handler 各自的错误处理策略,并集成 Sentry 实现生产环境的实时监控与告警。
20.1 错误类型全景
Next.js 中的错误来源
错误类型 │ 出现位置 │ 处理策略
──────────────────────┼─────────────────────────┼─────────────────
渲染错误 │ Server / Client Component │ error.tsx
路由错误 │ 全局(未捕获) │ global-error.tsx
404 错误 │ 找不到页面 │ notFound.tsx
Server Action 错误 │ 表单提交 / 函数调用 │ try/catch + state
Route Handler 错误 │ API 端点 │ try/catch + NextResponse
Middleware 错误 │ 请求拦截层 │ try/catch + redirect
第三方 API 错误 │ fetch 调用 │ try/catch + fallback
数据库错误 │ Prisma 调用 │ try/catch + 友好提示
错误边界层次结构
Root Layout
└── global-error.tsx ← 捕获根布局错误
└── app/[locale]/layout.tsx
└── app/[locale]/articles/error.tsx ← 捕获文章路由段错误
└── app/[locale]/articles/[slug]/error.tsx ← 捕获单篇文章错误
└── 具体页面
20.2 Error Boundary(error.tsx)
基础错误边界
// app/articles/error.tsx
'use client';
import { useEffect } from 'react';
import { Button } from '@/components/ui/button';
type ErrorProps = {
error: Error & { digest?: string };
reset: () => void;
};
export default function ArticlesError({ error, reset }: ErrorProps) {
useEffect(() => {
// 上报到错误监控服务
console.error('Articles page error:', error);
// Sentry.captureException(error);
}, [error]);
return (
<div className="max-w-2xl mx-auto py-16 px-4 text-center">
<div className="text-6xl mb-6">😵</div>
<h2 className="text-2xl font-bold mb-4">哎呀,出了点问题</h2>
<p className="text-muted-foreground mb-8">
加载文章列表时遇到了问题,请稍后再试。
</p>
{error.digest && (
<p className="text-xs text-muted-foreground mb-4">
错误代码:{error.digest}
</p>
)}
<div className="flex items-center justify-center gap-4">
<Button onClick={reset}>重试</Button>
<Button variant="outline" onClick={() => window.location.href = '/'}>
返回首页
</Button>
</div>
</div>
);
}
带分类处理的错误边界
// app/dashboard/error.tsx
'use client';
import { useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { useRouter } from 'next/navigation';
// 自定义错误类
class AuthError extends Error {
constructor(message: string) {
super(message);
this.name = 'AuthError';
}
}
class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = 'NetworkError';
}
}
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const router = useRouter();
useEffect(() => {
console.error('Dashboard error:', {
name: error.name,
message: error.message,
digest: error.digest,
stack: error.stack,
});
}, [error]);
// 根据错误类型显示不同 UI
if (error.name === 'AuthError' || error.message.includes('Unauthorized')) {
return (
<div className="max-w-md mx-auto py-16 text-center">
<div className="text-6xl mb-6">🔐</div>
<h2 className="text-xl font-bold mb-4">需要登录</h2>
<p className="text-muted-foreground mb-6">
请先登录后再访问仪表盘。
</p>
<Button onClick={() => router.push('/login?callbackUrl=/dashboard')}>
去登录
</Button>
</div>
);
}
if (error.name === 'NetworkError' || error.message.includes('fetch')) {
return (
<div className="max-w-md mx-auto py-16 text-center">
<div className="text-6xl mb-6">📡</div>
<h2 className="text-xl font-bold mb-4">网络连接失败</h2>
<p className="text-muted-foreground mb-6">
请检查你的网络连接,然后重试。
</p>
<Button onClick={reset}>重试</Button>
</div>
);
}
// 通用错误 UI
return (
<div className="max-w-md mx-auto py-16 text-center">
<div className="text-6xl mb-6">⚠️</div>
<h2 className="text-xl font-bold mb-4">出了点问题</h2>
<p className="text-muted-foreground mb-6">
{error.message || '应用遇到了意外错误。'}
</p>
<div className="flex items-center justify-center gap-4">
<Button onClick={reset}>重试</Button>
<Button variant="outline" onClick={() => router.push('/')}>
返回首页
</Button>
</div>
</div>
);
}
错误边界的限制
❌ error.tsx 不能捕获的错误:
- 根布局(root layout)中的错误 → 使用 global-error.tsx
- 静态渲染中的错误(build time)
- 自身组件内的错误(需要外层 error.tsx)
✅ error.tsx 能捕获的错误:
- 路由段内 Server / Client Component 的渲染错误
- 数据获取错误(fetch、数据库查询)
- useEffect 抛出的错误
- 事件处理器抛出的错误(如果未 catch)
20.3 全局错误页(global-error.tsx)
根级错误边界
// app/global-error.tsx
'use client';
import { useEffect } from 'react';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// 上报到 Sentry
// Sentry.captureException(error);
console.error('Global error:', error);
}, [error]);
// global-error.tsx 必须包含完整的 <html> 和 <body> 标签
return (
<html lang="zh-CN">
<body>
<div
style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f8fafc',
fontFamily: 'system-ui, -apple-system, sans-serif',
}}
>
<div style={{ textAlign: 'center', padding: '2rem' }}>
<h1 style={{ fontSize: '6rem', margin: 0 }}>💥</h1>
<h2
style={{
fontSize: '1.5rem',
fontWeight: 'bold',
color: '#0f172a',
marginTop: '1rem',
}}
>
应用出现严重错误
</h2>
<p style={{ color: '#64748b', marginTop: '1rem', maxWidth: '400px' }}>
抱歉,应用遇到了无法恢复的错误。我们的团队已被通知,正在处理中。
</p>
{error.digest && (
<p style={{ fontSize: '0.75rem', color: '#94a3b8', marginTop: '1rem' }}>
错误 ID:{error.digest}
</p>
)}
<div
style={{
marginTop: '2rem',
display: 'flex',
gap: '1rem',
justifyContent: 'center',
}}
>
<button
onClick={reset}
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
fontSize: '1rem',
}}
>
重试
</button>
<a
href="/"
style={{
padding: '0.75rem 1.5rem',
backgroundColor: 'white',
color: '#0f172a',
border: '1px solid #e2e8f0',
borderRadius: '0.5rem',
textDecoration: 'none',
fontSize: '1rem',
}}
>
返回首页
</a>
</div>
</div>
</div>
</body>
</html>
);
}
20.4 404 页面(notFound.tsx)
自定义 404 页面
// app/not-found.tsx
import Link from 'next/link';
import { Button } from '@/components/ui/button';
export default function NotFound() {
return (
<div className="min-h-[70vh] flex items-center justify-center px-4">
<div className="text-center max-w-lg">
<div className="relative mb-8">
<h1 className="text-9xl font-bold text-muted-foreground/10 select-none">
404
</h1>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-6xl">🔍</span>
</div>
</div>
<h2 className="text-2xl font-bold mb-4">页面走丢了</h2>
<p className="text-muted-foreground mb-8">
你访问的页面不存在,或者已经被移动到其他位置。
</p>
<div className="flex items-center justify-center gap-4">
<Link href="/">
<Button>返回首页</Button>
</Link>
<Link href="/articles">
<Button variant="outline">浏览文章</Button>
</Link>
</div>
<div className="mt-12 text-sm text-muted-foreground">
<p>如果你认为这是一个错误,请</p>
<Link href="/contact" className="text-primary hover:underline">
联系我们
</Link>
</div>
</div>
</div>
);
}
在 Server Component 中触发 404
// app/articles/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { getArticle } from '@/lib/services/article';
export default async function ArticlePage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const article = await getArticle(slug);
if (!article) {
// 触发 not-found.tsx
notFound();
}
return <article>{/* ... */}</article>;
}
20.5 Server Action 错误处理
标准错误处理模式
// app/actions/article.ts
'use server';
import { prisma } from '@/lib/prisma';
import { requireAuth } from '@/lib/auth-utils';
// 错误类型定义
export type ActionError = {
error: string;
fieldErrors?: Record<string, string>;
code?: 'UNAUTHORIZED' | 'NOT_FOUND' | 'CONFLICT' | 'VALIDATION' | 'INTERNAL';
};
export type ActionResult<T> =
| { success: true; data: T }
| { success: false } & ActionError;
// Server Action 实现
export async function updateArticle(
id: string,
data: UpdateArticleInput
): Promise<ActionResult<Article>> {
try {
// 1. 认证检查
const user = await requireAuth();
// 2. 输入验证
const validation = updateArticleSchema.safeParse(data);
if (!validation.success) {
return {
success: false,
error: '输入验证失败',
fieldErrors: Object.fromEntries(
validation.error.issues.map((i) => [i.path[0], i.message])
),
code: 'VALIDATION',
};
}
// 3. 权限检查
const article = await prisma.article.findUnique({
where: { id },
select: { authorId: true },
});
if (!article) {
return {
success: false,
error: '文章不存在',
code: 'NOT_FOUND',
};
}
if (article.authorId !== user.id && user.role !== 'admin') {
return {
success: false,
error: '没有权限修改此文章',
code: 'UNAUTHORIZED',
};
}
// 4. 业务逻辑
const updated = await prisma.article.update({
where: { id },
data: validation.data,
});
revalidatePath(`/articles/${updated.slug}`);
return { success: true, data: updated };
} catch (error) {
// 5. 未知错误兜底
console.error('updateArticle failed:', error);
// 上报到 Sentry
// Sentry.captureException(error);
return {
success: false,
error: '操作失败,请稍后重试',
code: 'INTERNAL',
};
}
}
在组件中处理错误
// app/components/article-form.tsx
'use client';
import { useActionState } from 'react';
import { updateArticle, ActionResult } from '@/app/actions/article';
import { toast } from 'sonner';
export function ArticleForm({ article }: { article: Article }) {
const [state, formAction] = useActionState<ActionResult<Article>, FormData>(
async (prevState, formData) => {
const data = {
title: formData.get('title') as string,
content: formData.get('content') as string,
};
const result = await updateArticle(article.id, data);
if (result.success) {
toast.success('文章已更新');
} else {
// 根据错误代码显示不同提示
switch (result.code) {
case 'VALIDATION':
// 字段级错误由 FormMessage 显示
break;
case 'UNAUTHORIZED':
toast.error('权限不足,请重新登录');
break;
case 'NOT_FOUND':
toast.error('文章不存在');
break;
default:
toast.error(result.error);
}
}
return result;
},
{ success: true, data: article }
);
return (
<form action={formAction}>
{/* ... 表单字段 */}
</form>
);
}
20.6 Route Handler 错误处理
统一错误响应
// lib/api-errors.ts
import { NextResponse } from 'next/server';
export class ApiError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
export class BadRequestError extends ApiError {
constructor(message = '请求参数错误') {
super(400, message, 'BAD_REQUEST');
}
}
export class UnauthorizedError extends ApiError {
constructor(message = '未登录或登录已过期') {
super(401, message, 'UNAUTHORIZED');
}
}
export class ForbiddenError extends ApiError {
constructor(message = '没有权限执行此操作') {
super(403, message, 'FORBIDDEN');
}
}
export class NotFoundError extends ApiError {
constructor(message = '资源不存在') {
super(404, message, 'NOT_FOUND');
}
}
export class ConflictError extends ApiError {
constructor(message = '资源冲突') {
super(409, message, 'CONFLICT');
}
}
export class InternalError extends ApiError {
constructor(message = '服务器内部错误') {
super(500, message, 'INTERNAL_ERROR');
}
}
// 错误处理装饰器
export function withErrorHandler(
handler: (request: NextRequest, ...args: any[]) => Promise<NextResponse>
) {
return async (request: NextRequest, ...args: any[]) => {
try {
return await handler(request, ...args);
} catch (error) {
if (error instanceof ApiError) {
return NextResponse.json(
{
error: error.message,
code: error.code,
},
{ status: error.statusCode }
);
}
// 未知错误
console.error('Unhandled API error:', error);
// Sentry.captureException(error);
return NextResponse.json(
{
error: '服务器内部错误',
code: 'INTERNAL_ERROR',
},
{ status: 500 }
);
}
};
}
在 Route Handler 中使用
// app/api/v1/articles/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import {
NotFoundError,
ForbiddenError,
BadRequestError,
withErrorHandler,
} from '@/lib/api-errors';
import { authenticate } from '@/lib/auth-middleware';
type Params = { params: Promise<{ id: string }> };
export const GET = withErrorHandler(async (
request: NextRequest,
{ params }: Params
) => {
const { id } = await params;
const article = await prisma.article.findUnique({
where: { id },
});
if (!article) {
throw new NotFoundError('文章不存在');
}
return NextResponse.json(article);
});
export const PUT = withErrorHandler(async (
request: NextRequest,
{ params }: Params
) => {
const { id } = await params;
// 认证
const user = await authenticate(request);
if (!user) {
throw new UnauthorizedError();
}
// 检查存在
const article = await prisma.article.findUnique({ where: { id } });
if (!article) {
throw new NotFoundError('文章不存在');
}
// 权限检查
if (article.authorId !== user.id && user.role !== 'admin') {
throw new ForbiddenError();
}
// 解析 body
const body = await request.json();
// 验证
if (!body.title || body.title.length < 1) {
throw new BadRequestError('标题不能为空');
}
const updated = await prisma.article.update({
where: { id },
data: body,
});
return NextResponse.json(updated);
});
20.7 结构化日志
日志系统封装
// lib/logger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
type LogContext = {
requestId?: string;
userId?: string;
path?: string;
method?: string;
duration?: number;
[key: string]: unknown;
};
class Logger {
private level: LogLevel;
private levels: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
constructor() {
this.level = (process.env.LOG_LEVEL as LogLevel) || 'info';
}
private shouldLog(level: LogLevel): boolean {
return this.levels[level] >= this.levels[this.level];
}
private format(level: LogLevel, message: string, context?: LogContext) {
return {
timestamp: new Date().toISOString(),
level,
message,
...context,
};
}
private write(level: LogLevel, message: string, context?: LogContext) {
if (!this.shouldLog(level)) return;
const logEntry = this.format(level, message, context);
const output = JSON.stringify(logEntry);
switch (level) {
case 'debug':
console.debug(output);
break;
case 'info':
console.info(output);
break;
case 'warn':
console.warn(output);
break;
case 'error':
console.error(output);
break;
}
}
debug(message: string, context?: LogContext) {
this.write('debug', message, context);
}
info(message: string, context?: LogContext) {
this.write('info', message, context);
}
warn(message: string, context?: LogContext) {
this.write('warn', message, context);
}
error(message: string, error?: Error, context?: LogContext) {
this.write('error', message, {
...context,
errorName: error?.name,
errorMessage: error?.message,
errorStack: process.env.NODE_ENV === 'development' ? error?.stack : undefined,
});
}
}
export const logger = new Logger();
在 Middleware 中使用
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { logger } from '@/lib/logger';
export async function middleware(request: NextRequest) {
const requestId = crypto.randomUUID();
const startTime = Date.now();
const requestHeaders = new Headers(request.headers);
requestHeaders.set('X-Request-Id', requestId);
logger.info('Request started', {
requestId,
method: request.method,
path: request.nextUrl.pathname,
userAgent: request.headers.get('user-agent') || 'unknown',
ip: request.ip || 'unknown',
});
try {
const response = NextResponse.next({
request: { headers: requestHeaders },
});
const duration = Date.now() - startTime;
response.headers.set('X-Request-Id', requestId);
response.headers.set('X-Response-Time', `${duration}ms`);
logger.info('Request completed', {
requestId,
method: request.method,
path: request.nextUrl.pathname,
duration,
});
return response;
} catch (error) {
logger.error('Request failed', error as Error, {
requestId,
method: request.method,
path: request.nextUrl.pathname,
});
throw error;
}
}
在 Server Action 中使用
// app/actions/article.ts
'use server';
import { logger } from '@/lib/logger';
export async function createArticle(formData: FormData) {
const user = await requireAuth();
logger.info('Article creation started', {
userId: user.id,
title: formData.get('title') as string,
});
const startTime = Date.now();
try {
const article = await prisma.article.create({ /* ... */ });
logger.info('Article created', {
userId: user.id,
articleId: article.id,
duration: Date.now() - startTime,
});
return { success: true, data: article };
} catch (error) {
logger.error('Article creation failed', error as Error, {
userId: user.id,
duration: Date.now() - startTime,
});
return { success: false, error: '创建失败' };
}
}
20.8 Sentry 集成
安装
npm install @sentry/nextjs
# 或使用向导
npx @sentry/wizard@latest -i nextjs
客户端配置
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// 性能监控采样率
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
// Session Replay(用户操作回放)
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
Sentry.feedbackIntegration({
colorScheme: 'system',
}),
],
// 忽略已知无害错误
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'Non-Error promise rejection captured',
'NetworkError',
'Failed to fetch',
],
// 环境标识
environment: process.env.NODE_ENV,
// 敏感数据过滤
beforeSend(event) {
if (event.request?.headers) {
delete event.request.headers['Authorization'];
delete event.request.headers['Cookie'];
}
return event;
},
});
服务端配置
// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
environment: process.env.NODE_ENV,
// 不捕获 4xx 错误(通常是用户行为导致)
beforeSend(event, hint) {
const error = hint.originalException as any;
if (error?.statusCode && error.statusCode < 500) {
return null;
}
return event;
},
});
Edge Runtime 配置
// sentry.edge.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1,
environment: process.env.NODE_ENV,
});
全局错误处理
// app/global-error.tsx
'use client';
import * as Sentry from '@sentry/nextjs';
import { useEffect } from 'react';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html lang="zh-CN">
<body>
{/* ... */}
<button onClick={() => Sentry.showReportDialog()}>
反馈问题
</button>
</body>
</html>
);
}
手动上报错误
// lib/error-reporting.ts
import * as Sentry from '@sentry/nextjs';
export function reportError(
error: Error,
context?: Record<string, unknown>
) {
Sentry.captureException(error, {
extra: context,
tags: {
source: 'manual',
},
});
}
export function setUser(user: { id: string; email: string; name?: string }) {
Sentry.setUser(user);
}
export function clearUser() {
Sentry.setUser(null);
}
// 性能标记
export function measurePerformance<T>(
name: string,
fn: () => Promise<T>
): Promise<T> {
return Sentry.startSpan({ name, op: 'function' }, async () => {
return fn();
});
}
在 Server Action 中使用
// app/actions/payment.ts
'use server';
import * as Sentry from '@sentry/nextjs';
export async function processPayment(amount: number) {
const user = await requireAuth();
// 设置用户上下文
Sentry.setUser({ id: user.id, email: user.email });
try {
const result = await chargeUser(user.id, amount);
return { success: true, data: result };
} catch (error) {
// 上报支付错误,附带上下文
Sentry.captureException(error, {
extra: {
userId: user.id,
amount,
paymentMethod: 'stripe',
},
tags: {
feature: 'payment',
},
level: 'error',
});
return { success: false, error: '支付失败' };
} finally {
Sentry.setUser(null);
}
}
next.config.js 配置
// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');
/** @type {import('next').NextConfig} */
const nextConfig = {
// ... 你的配置
};
module.exports = withSentryConfig(nextConfig, {
// Sentry Webpack 插件选项
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
// 静默模式(生产构建)
silent: process.env.NODE_ENV === 'production',
// 上传 source maps
sourcemaps: {
disable: process.env.NODE_ENV !== 'production',
},
// 自动注入 Sentry 代码
autoInstrumentServerFunctions: true,
autoInstrumentClientComponents: false,
// 隐藏 source maps(安全)
hideSourceMaps: true,
// 调整 bundle size 警告阈值
tunnelRoute: '/monitoring',
});
20.9 错误监控仪表盘
健康检查端点
// app/api/health/route.ts
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export const dynamic = 'force-dynamic';
export async function GET() {
const checks: Record<string, { status: 'ok' | 'error'; latency?: number }> = {};
// 数据库检查
try {
const start = Date.now();
await prisma.$queryRaw`SELECT 1`;
checks.database = {
status: 'ok',
latency: Date.now() - start,
};
} catch {
checks.database = { status: 'error' };
}
// 应用状态
const overallStatus = Object.values(checks).every((c) => c.status === 'ok')
? 'healthy'
: 'unhealthy';
return NextResponse.json(
{
status: overallStatus,
timestamp: new Date().toISOString(),
version: process.env.NEXT_PUBLIC_APP_VERSION || 'unknown',
environment: process.env.NODE_ENV,
checks,
},
{
status: overallStatus === 'healthy' ? 200 : 503,
}
);
}
错误统计 API
// app/api/admin/error-stats/route.ts
import { NextResponse } from 'next/server';
import { requireRole } from '@/lib/auth-utils';
export async function GET() {
try {
await requireRole('admin');
} catch {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// 这里可以对接你的错误日志存储(如 Loki、ClickHouse 等)
return NextResponse.json({
last24h: {
totalErrors: 42,
uniqueErrors: 15,
affectedUsers: 28,
topErrors: [
{ message: 'Failed to fetch articles', count: 12 },
{ message: 'Payment processing failed', count: 8 },
{ message: 'Image upload timeout', count: 5 },
],
},
});
}
20.10 生产监控清单
## 生产环境监控清单
### 错误监控
- [ ] Sentry 已集成(客户端 + 服务端 + Edge)
- [ ] error.tsx 在每个关键路由段配置
- [ ] global-error.tsx 已配置
- [ ] not-found.tsx 已自定义
- [ ] 错误上报过滤了 4xx 和无害错误
### 性能监控
- [ ] Sentry Performance 或 DataDog APM 已启用
- [ ] Core Web Vitals 已上报(web-vitals 库)
- [ ] 慢查询告警已配置
- [ ] API 响应时间监控已配置
### 健康检查
- [ ] /api/health 端点可访问
- [ ] 数据库连接检查已实现
- [ ] 外部服务依赖检查已实现
- [ ] Uptime 监控(如 UptimeRobot、Better Uptime)
### 日志系统
- [ ] 结构化日志(JSON 格式)
- [ ] 日志级别可通过环境变量控制
- [ ] 敏感信息(密码、Token)已过滤
- [ ] 请求追踪(X-Request-Id)已实现
### 告警
- [ ] 5xx 错误率告警(> 1%)
- [ ] P99 延迟告警(> 2s)
- [ ] 数据库连接失败告警
- [ ] 磁盘 / 内存使用率告警
### 安全
- [ ] 登录失败率监控
- [ ] API 限流监控
- [ ] 敏感数据泄露检测
- [ ] OWASP 漏洞扫描
### 业务指标
- [ ] 注册转化率
- [ ] 页面访问量
- [ ] API 调用量
- [ ] 服务器成本
本章小结
Key Takeaways
- 错误边界层次化:error.tsx 处理路由段错误,global-error.tsx 处理根布局错误,not-found.tsx 处理 404
- Server Action 必须返回结构化错误:不要 throw,而是返回
{ success, error, code } - Route Handler 用错误类封装:
ApiError继承体系 +withErrorHandler装饰器 - 结构化日志是生产环境的必需品:JSON 格式、统一字段、可查询
- Sentry 是错误监控的标准方案:自动捕获、Session Replay、性能监控
- 监控是一个持续过程:健康检查、告警、仪表盘缺一不可
下一步
下一章我们将深入 Docker 部署与 CI/CD——构建 Next.js 的完整部署流水线,包括 Docker 多阶段构建、GitHub Actions 自动化、Vercel / Docker / Cloudflare 多种部署方案。
参考资料
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。