React Native 可观测性
问题陈述
无声失败是调试的噩梦。代码提前返回而没有记录日志,错误消息缺乏上下文,以及缺少可观测性使得生产问题无法诊断。在写代码时,就好像你会在凌晨3点仅凭日志进行调试一样。
模式:无静默提前返回
问题: 提前返回而没有记录日志,会创建看不见的失败路径。
示例(来自重拍错误):
// 错误 - 静默死亡
const saveAnswer = (questionId: string, value: number) => {
if (!retakeAreas.has(skillArea)) {
return; // ❌ 为什么返回?没人知道。
}
// ... 保存逻辑
};
// 正确 - 可观测
const saveAnswer = (questionId: string, value: number) => {
if (!retakeAreas.has(skillArea)) {
logger.warn('[saveAnswer] 丢弃答案 - 技能区域不在重拍集合中', {
questionId,
skillArea,
retakeAreas: Array.from(retakeAreas),
});
return;
}
// ... 保存逻辑
};
规则: 每个提前返回都应该记录返回的原因,并提供足够的上下文以进行诊断。
模式:错误消息设计
问题: 错误消息无助于诊断问题。
// 糟糕 - 没有上下文
throw new Error('未找到答案');
// 糟糕 - 稍微好一点,但在凌晨3点仍然无用
throw new Error('未找到答案。请至少完成一个问题。');
// 好 - 包含诊断上下文
throw new Error(
`未找到答案。已完成:${Object.keys(completedAnswers).length}, ` +
`新:${Object.keys(userAnswers).length}, ` +
`评估ID:${assessmentId}。这可能表明是一个时间问题。`
);
错误消息模板:
throw new Error(
`[${functionName}] ${whatFailed}. ` +
`上下文:${relevantState}. ` +
`可能的原因:${hypothesis}.`
);
应包含的内容:
| 元素 | 为什么 |
|---|---|
| 函数/位置 | 错误发生的位置 |
| 什么失败了 | 未满足的具体条件 |
| 相关状态 | 有助于诊断的值 |
| 可能的原因 | 你最好的猜测修复方法 |
模式:结构化日志记录
问题: 控制台.log语句难以解析和搜索。
// 糟糕 - 非结构化
console.log('保存答案', questionId, value);
console.log('当前状态', answers);
// 好 - 结构化,带上下文对象
logger.info('[saveAnswer] 保存答案', {
questionId,
value,
skillArea,
existingAnswerCount: Object.keys(answers).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 = __DEV__ ? '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'];
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 retakeFlow(assessmentId: string, skillArea: string) {
const flowId = `retake-${Date.now()}`;
logger.info(`[retakeFlow:${flowId}] 开始`, { assessmentId, skillArea });
try {
logger.debug(`[retakeFlow:${flowId}] 第1步:加载已完成的答案`);
await loadCompletedAssessmentAnswers(assessmentId);
logger.debug(`[retakeFlow:${flowId}] 第2步:启用重拍`);
await enableSkillAreaRetake(skillArea);
logger.debug(`[retakeFlow:${flowId}] 第3步:清除答案`);
await clearSkillAreaAnswers(skillArea);
logger.info(`[retakeFlow:${flowId}] 成功完成`);
} catch (error) {
logger.error(`[retakeFlow:${flowId}] 失败`, {
error: error.message,
stack: error.stack,
assessmentId,
skillArea,
});
throw error;
}
}
好处:
- 可以通过flowId搜索日志以查看整个流程
- 确切知道哪一步失败了
- 通过时间戳可见时间
模式:调试状态快照
问题: 在复杂流程中需要理解特定点的状态。
function snapshotState(label: string) {
const state = useStore.getState();
logger.debug(`[StateSnapshot] ${label}`, {
answers: Object.keys(state.answers).length,
retakeAreas: Array.from(state.retakeAreas),
completedAnswers: Object.keys(state.completedAssessmentAnswers).length,
loading: state.loading,
});
}
// 流程中的使用
async function retakeFlow() {
snapshotState('加载前');
await loadCompletedAnswers(id);
snapshotState('加载后');
await enableRetake(area);
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(assessment, `评估未找到:${assessmentId}`);
assertCondition(
retakeAreas.has(skillArea),
`技能区域不在重拍集合中`,
{ skillArea, retakeAreas: Array.from(retakeAreas) }
);
模式:生产错误报告
问题: 生产中的错误没有可见性。
// 与错误报告服务集成
import * as Sentry from '@sentry/react-native';
export function captureError(
error: Error,
context?: Record<string, unknown>
) {
logger.error(error.message, { ...context, stack: error.stack });
if (!__DEV__) {
Sentry.captureException(error, {
extra: context,
});
}
}
// 使用
try {
await riskyOperation();
} catch (error) {
captureError(error, {
assessmentId,
skillArea,
userAnswers: Object.keys(userAnswers),
});
throw error;
}
检查表:添加可观测性
当编写新代码时:
- [ ] 所有提前返回都有带上下文的日志记录
- [ ] 错误消息包含诊断信息
- [ ] 多步骤操作有流程追踪
- [ ] 敏感数据在记录之前被脱敏
- [ ] 复杂流程有状态快照可供调试
- [ ] 生产错误被捕获并带有上下文
当调试现有代码时:
- [ ] 为可疑的提前返回添加日志记录
- [ ] 在异步操作前后添加状态快照
- [ ] 检查是否有静默捕获错误
- [ ] 验证错误消息是否有足够的上下文
快速调试模板
在调试异步/状态问题时,暂时添加此模板:
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 });
在提交前删除,或通过标志控制。