React 可观测性
问题陈述
无声故障是调试的噩梦。代码提前返回而没有日志记录,错误消息缺乏上下文,以及缺失的可观测性使得生产问题无法诊断。编写代码时,就好像你会在凌晨3点仅凭日志进行调试一样。
模式:无静默提前返回
问题: 提前返回而没有日志记录会创建看不见的失败路径。
// 错误 - 静默死亡
const saveData = (id: string, value: number) => {
if (!validIds.has(id)) {
return; // ❌ 为什么我们返回?没人知道。
}
// ... 保存逻辑
};
// 正确 - 可观测
const saveData = (id: string, value: number) => {
if (!validIds.has(id)) {
logger.warn('[saveData] 丢弃保存 - 无效ID', {
id,
value,
validIds: Array.from(validIds),
});
return;
}
// ... 保存逻辑
};
规则: 每个提前返回都应该记录为什么返回,有足够的上下文来诊断。
模式:错误消息设计
问题: 错误消息无助于诊断问题。
// 坏 - 没有上下文
throw new Error('数据未找到');
// 坏 - 稍微好一点,但在凌晨3点仍然无用
throw new Error('数据未找到。请再试一次。');
// 好 - 包含诊断上下文
throw new Error(
`数据未找到。ID: ${id}, ` +
`可用: ${Object.keys(data).length} 项, ` +
`上次获取: ${lastFetchTime}。这可能表明缓存问题。`
);
错误消息模板:
throw new Error(
`[${functionName}] ${whatFailed}. ` +
`上下文: ${relevantState}. ` +
`可能的原因: ${hypothesis}.`
);
包含内容:
| 元素 | 为什么 |
|---|---|
| 函数/位置 | 错误发生的位置 |
| 什么失败了 | 未满足的具体条件 |
| 相关状态 | 有助于诊断的值 |
| 可能的原因 | 你最好的猜测修复方法 |
模式:结构化日志记录
问题: 控制台.log语句难以解析和搜索。
// 坏 - 非结构化
console.log('保存数据', id, value);
console.log('当前状态', data);
// 好 - 结构化,带上下文对象
logger.info('[saveData] 保存数据', {
id,
value,
existingCount: Object.keys(data).length,
});
日志级别:
| 级别 | 用途 |
|---|---|
error |
异常,需要立即关注的失败 |
warn |
未失败但可能表明问题的意外条件 |
info |
重要的业务事件(用户操作,流程里程碑) |
debug |
详细的诊断信息(状态转储,计时) |
一致日志记录的包装器:
// utils/logger.ts
const LOG_LEVELS = ['debug', 'info', 'warn', 'error'] as const;
type LogLevel = typeof LOG_LEVELS[number];
const currentLevel: LogLevel = process.env.NODE_ENV === 'development' ? 'debug' : 'warn';
function shouldLog(level: LogLevel): boolean {
return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(currentLevel);
}
export const logger = {
debug: (message: string, context?: object) => {
if (shouldLog('debug')) {
console.log(`[DEBUG] ${message}`, context ?? '');
}
},
info: (message: string, context?: object) => {
if (shouldLog('info')) {
console.log(`[INFO] ${message}`, context ?? '');
}
},
warn: (message: string, context?: object) => {
if (shouldLog('warn')) {
console.warn(`[WARN] ${message}`, context ?? '');
}
},
error: (message: string, context?: object) => {
if (shouldLog('error')) {
console.error(`[ERROR] ${message}`, context ?? '');
}
},
};
模式:敏感数据处理
问题: 在控制台或错误报告中记录敏感数据。
// utils/secureLogger.ts
const SENSITIVE_KEYS = ['password', 'token', 'ssn', 'creditCard', 'apiKey', 'secret'];
function redactSensitive(obj: object): object {
const redacted = { ...obj };
for (const key of Object.keys(redacted)) {
if (SENSITIVE_KEYS.some(s => key.toLowerCase().includes(s))) {
redacted[key] = '[REDACTED]';
} else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
redacted[key] = redactSensitive(redacted[key]);
}
}
return redacted;
}
export const secureLogger = {
info: (message: string, context?: object) => {
const safeContext = context ? redactSensitive(context) : undefined;
logger.info(message, safeContext);
},
// ... 其他级别
};
模式:流程追踪
问题: 多步骤操作中不清楚执行到了哪一步。
async function checkoutFlow(cartId: string) {
const flowId = `checkout-${Date.now()}`;
logger.info(`[checkoutFlow:${flowId}] 开始`, { cartId });
try {
logger.debug(`[checkoutFlow:${flowId}] 第1步:验证购物车`);
await validateCart(cartId);
logger.debug(`[checkoutFlow:${flowId}] 第2步:处理付款`);
await processPayment(cartId);
logger.debug(`[checkoutFlow:${flowId}] 第3步:确认订单`);
await confirmOrder(cartId);
logger.info(`[checkoutFlow:${flowId}] 成功完成`);
} catch (error) {
logger.error(`[checkoutFlow:${flowId}] 失败`, {
error: error.message,
stack: error.stack,
cartId,
});
throw error;
}
}
好处:
- 可以通过flowId搜索日志查看整个流程
- 确切知道哪一步失败了
- 通过时间戳可见的时机
模式:调试状态快照
问题: 在复杂流程中需要理解特定点的状态。
function snapshotState(label: string) {
const state = useStore.getState();
logger.debug(`[StateSnapshot] ${label}`, {
itemCount: Object.keys(state.items).length,
activeFeatures: Array.from(state.features),
loading: state.loading,
});
}
// 流程中的使用
async function complexFlow() {
snapshotState('加载前');
await loadData(id);
snapshotState('加载后');
await processData();
snapshotState('处理后');
}
模式:断言助手
问题: 条件是“永远不应该发生”的,但当它们确实发生时需要可见性。
// utils/assertions.ts
export function assertDefined<T>(
value: T | null | undefined,
context: string
): asserts value is T {
if (value === null || value === undefined) {
const message = `[Assertion Failed] 预期定义值:${context}`;
logger.error(message, { value });
throw new Error(message);
}
}
export function assertCondition(
condition: boolean,
context: string,
debugInfo?: object
): asserts condition {
if (!condition) {
const message = `[Assertion Failed] ${context}`;
logger.error(message, debugInfo);
throw new Error(message);
}
}
// 使用
assertDefined(user, `未找到用户:${userId}`);
assertCondition(
items.length > 0,
`未找到项目`,
{ searchQuery, filters }
);
模式:生产错误报告
问题: 生产中的错误没有可见性。
// 与错误报告服务集成(Sentry示例)
import * as Sentry from '@sentry/react';
export function captureError(
error: Error,
context?: Record<string, unknown>
) {
logger.error(error.message, { ...context, stack: error.stack });
if (process.env.NODE_ENV === 'production') {
Sentry.captureException(error, {
extra: context,
});
}
}
// 使用
try {
await riskyOperation();
} catch (error) {
captureError(error, {
userId,
action: 'checkout',
cartItems: cart.items.length,
});
throw error;
}
模式:React错误边界
问题: 未处理的错误会崩溃整个应用程序。
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
logger.error('[ErrorBoundary] 捕获错误', {
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
captureError(error, { componentStack: errorInfo.componentStack });
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <DefaultErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
检查表:添加可观测性
编写新代码时:
- [ ] 所有提前返回都有带有上下文的日志记录
- [ ] 错误消息包含诊断信息
- [ ] 多步骤操作有流程追踪
- [ ] 敏感数据在记录之前被删除
- [ ] 复杂流程的状态快照可用于调试
- [ ] 生产错误被捕获并带有上下文
调试现有代码时:
- [ ] 在可疑的提前返回处添加日志记录
- [ ] 在异步操作前后添加状态快照
- [ ] 检查静默捕获是否吞噬错误
- [ ] 验证错误消息是否有足够的上下文
快速调试模板
在调试异步/状态问题时临时添加:
const DEBUG = true;
function debugLog(label: string, data?: object) {
if (DEBUG) {
console.log(`[DEBUG ${Date.now()}] ${label}`, data ?? '');
}
}
// 在你的流程中
debugLog('流程开始', { inputs });
debugLog('第1步后', { state: getState() });
debugLog('第2步后', { state: getState() });
debugLog('流程结束', { result });
在提交前删除,或通过标志控制。