ReactNativeObservabilitySkill rn-observability

React Native 可观测性指南,包含日志记录、错误消息设计、调试模式等,旨在提高代码的可观测性和调试效率。

移动开发 0 次安装 7 次浏览 更新于 3/3/2026

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 });

在提交前删除,或通过标志控制。