我给 Expo / React Native 做了个应用内调试面板,真机排障终于不用一直连电脑了 最近一段时间在做 Expo / React Native 项

1 阅读7分钟

我给 Expo / React Native 做了个应用内调试面板,真机排障终于不用一直连电脑了

最近一段时间在做 Expo / React Native 项目排障时,越来越觉得一个问题挺烦:很多 bug 只在真机、测试包,甚至某个用户环境里出现。

开发机上看着一切正常,一到手机上就开始玄学。让测试同学复现一下吧,往往只能得到一句:

我刚才点这里,然后它好像卡了一下,再然后接口就失败了。

这时候如果还得让对方接电脑、开 Metro、翻 Xcode / Android Studio 日志,基本就很难推进了。

所以我做了一个小工具:expo-inapp-debugger。它的目标很简单:把常用排障信息直接放进 App 里,让测试包、内部包、甚至受控的生产包都能自己看日志、错误和网络请求。

项目地址:

  • GitHub:https://github.com/Concur-max/expo-inapp-debugger
  • npm:expo-inapp-debugger

下面截图来自示例 Expo 项目,iOS / Android 都有。

它长什么样

接入之后,App 里会出现一个原生浮动入口。点开后可以看日志、网络请求、请求详情、App/runtime 信息,也可以临时打开原生日志或原生网络采集。

iOS 调试入口

日志面板大概是这样:

iOS 日志面板

网络请求列表:

iOS 网络面板

点进去可以看请求详情:

iOS 请求详情

Android 这边也做了同样的入口和面板:

Android 调试入口

Android 网络面板

为什么想做这个

React Native 项目排障时,很多信息其实都散在不同地方:

  • JS 日志在 Metro / DevTools 里
  • 原生日志在 Xcode 或 logcat 里
  • 网络请求可能在代理工具里
  • 崩溃和 React 错误又是另一套链路
  • 测试同学复现时,开发不一定在现场

这几个东西分开看时,最麻烦的不是“没有数据”,而是时间线很容易断

比如一个页面白屏了,到底是接口先 500,还是解析 response 时抛错,还是某个 native callback 比预期晚回来?如果只能靠口头描述和几段截断日志,很容易误判。

所以这个库一开始就不是按“做一个漂亮面板”的思路来的,而是按排障现场来设计:

尽量把日志、错误、请求这些事件放到同一个 App 内时间线里,优先保证真实发生顺序和可还原性。

目前能看哪些东西

现在主要支持这些能力:

  • JS 日志:console.logconsole.infoconsole.warnconsole.errorconsole.debug
  • JS 错误:全局 error、未处理 Promise rejection、React Error Boundary 错误
  • 网络请求:fetchXMLHttpRequestWebSocket
  • 手动写入:通过 inAppDebug.log()inAppDebug.captureError() 主动记录
  • 原生日志:iOS / Android 都支持,但默认不打开
  • 原生网络提示:部分 URLSession / OkHttp 信息,同样默认不打开
  • App 信息:基础 runtime、系统、应用信息

这里有个取舍:native logs 和 native network 会更重一点,所以我没有默认打开。一般是遇到需要查原生层问题时,再从 App Info 里临时开启。

快速接入

安装:

pnpm add expo-inapp-debugger

npm / yarn / bun 也都可以。

因为它包含 iOS / Android 原生代码,所以安装后需要重新构建 App。Expo Go 不支持,适合 Expo Dev Client、Expo prebuild 或 bare React Native。

npx expo prebuild
npx expo run:ios
# 或
npx expo run:android

最简单的接入方式是用 InAppDebugRoot

import { InAppDebugRoot } from 'expo-inapp-debugger';

export default function Root() {
  return (
    <InAppDebugRoot enabled={__DEV__} locale="zh-CN">
      <App />
    </InAppDebugRoot>
  );
}

InAppDebugRoot 里面包含了 Provider 和一个内置 React Error Boundary,适合开发阶段快速接入。

如果是准备放到正式包里,我更推荐只挂 Provider,继续使用你自己项目里的 Error Boundary:

import { InAppDebugProvider } from 'expo-inapp-debugger';

export default function Root() {
  return (
    <InAppDebugProvider enabled={__DEV__} locale="zh-CN">
      <App />
    </InAppDebugProvider>
  );
}

我比较在意的几个点

1. 关闭时尽量别打扰业务

调试工具最怕什么?不是功能少,而是它自己变成问题来源。

所以 enabled={false} 时,库不会安装 JS 日志和网络 hook,也不会启动 native 采集。也就是说,普通用户路径上它应该尽量接近“没存在过”。

如果你要把它随正式包发出去,建议不要对所有人默认开启,而是绑定内部账号、测试设备、远程开关或隐藏手势。

2. 生产包也可以按需开

有些问题只在正式包环境出现,这种场景下 Debug 包不一定复现。

我自己的建议是:正式包可以带着,但入口一定要收起来。例如:

  • 关于页连续点版本号 7 次
  • 内部账号才出现调试入口
  • 远程配置下发白名单
  • 临时口令 + 本地手势组合开启

如果你特别在意普通用户路径的开销,可以等开关命中后再 require

export default function Root() {
  const debuggerEnabled =
    isInternalUser && remoteConfig.inAppDebuggerEnabled;

  if (!debuggerEnabled) {
    return <App />;
  }

  const { InAppDebugProvider } = require('expo-inapp-debugger');

  return (
    <InAppDebugProvider enabled locale="zh-CN">
      <App />
    </InAppDebugProvider>
  );
}

这段代码不复杂,但对正式包接入会安心很多。

3. 原生能力默认克制

JS 层日志、错误、请求已经能解决一大部分问题。

原生日志和原生网络采集虽然有用,但也更容易带来额外成本。所以现在默认是关闭的,只在需要查 native 层问题时打开。

这点可能没有“默认全开”爽,但我觉得调试工具还是应该保守一点:平时别添乱,需要时再加码。

配置项

常用配置大概这些:

<InAppDebugProvider
  enabled={__DEV__}
  initialVisible
  enableNetworkTab
  enableNativeLogs={false}
  enableNativeNetwork={false}
  maxLogs={2000}
  maxErrors={100}
  maxRequests={100}
  locale="zh-CN"
>
  <App />
</InAppDebugProvider>

几个我觉得更常调的参数:

参数说明
enabled是否启用调试器 runtime 和采集器
initialVisible启用后是否显示原生浮动入口
enableNetworkTab是否启用网络面板和 JS 网络采集
enableNativeLogs是否启动时直接开启原生日志采集
enableNativeNetwork是否启动时直接开启原生网络采集
maxLogs最多保留多少条日志
maxErrors最多保留多少条错误
maxRequests最多保留多少条网络请求
localeUI 语言,支持 autoen-USzh-CNzh-TWja

手动写日志

除了自动采集,也可以在关键业务位置主动打点:

import { inAppDebug } from 'expo-inapp-debugger';

inAppDebug.log('checkout:start', {
  orderId,
  source: 'cart',
});

try {
  await submitOrder();
} catch (error) {
  inAppDebug.captureError(error, {
    scene: 'submitOrder',
    orderId,
  });
  throw error;
}

这类日志对复现偶发问题很有用。尤其是支付、登录、推送跳转这种链路,光看接口请求通常不够,业务上下文也得跟上。

适合什么场景

我觉得它比较适合这些情况:

  • 测试同学经常在真机上复现问题,但开发不一定在旁边
  • 有些 bug 只在 TestFlight、内测包或正式包出现
  • 想在 App 内快速看网络请求和错误信息
  • 项目里 Expo / React Native 混合了不少原生模块
  • 不想每次排障都先让对方接电脑、开代理、导日志

它不是用来替代 Sentry、Datadog 或 Charles 的。更准确地说,它补的是“现场排障”这一层:问题刚发生时,先在设备上把最关键的信息看一眼。

很多时候就这一眼,已经能决定下一步该查接口、查状态、查 native,还是查业务逻辑。

现在的状态

当前版本已经覆盖了 iOS / Android 的基础调试面板、JS 日志、错误、网络请求、WebSocket,以及部分原生采集能力。

后面我还想继续打磨几块:

  • 更准确的跨 JS / native 时间线合并
  • 更好用的过滤和搜索
  • 请求/日志导出
  • 更细的性能开销控制
  • 更多真实业务场景下的边界处理

如果你也在做 Expo / React Native 项目,并且经常被真机偶发问题折磨,可以试试看。

项目还比较新,欢迎提 issue,也欢迎直接拿去跑一跑。