expo-observe 实战指南:为 React Native 应用接入基于 OpenTelemetry 的性能监控
expo-observe 是 Expo 官方开源的 React Native 性能监控埋点库,负责从真实用户设备与真实网络上采集应用启动、按屏渲染、交互时间等指标,并通过 OpenTelemetry 协议(OTLP)上报。本指南以 packages/expo-observe/README.md 为核心,结合仓库源码说明其接入步骤、指标含义、运行时配置与自定义上报端点,帮助你在一小时内为自己的 Expo 应用建立可查询的性能观测体系。
一、整体架构:可分离的采集端与服务端
expo-observe 的核心理念是"两个部分,彼此可分离":
- expo-observe(本包):开源的采集端。它从生产环境的 App 中收集指标(metrics)、事件(events)与日志(logs),并通过 OpenTelemetry Protocol(OTLP)传输。
- EAS Observe(服务端):存储与分析这些数据的服务。它是唯一能把一条指标关联到对应 EAS build、OTA update 乃至背后的 commit 的目的地,因为这三者由同一套系统产生。
两者可以分开使用:你可以只引入库,把数据发往自己的 collector;默认端点则是 EAS Observe。保留默认端点的理由在于归因能力——只有 EAS 知道某个会话运行的是哪一个构建和哪一个更新。同时该包开源、说 OpenTelemetry 语言、端点可替换,不存在供应商锁定(lock-in)。
从当前仓库的 package.json 可以看到,本包版本为 57.0.9,其核心依赖为 expo-app-metrics 与 expo-eas-client 两个 workspace 包,而 expo-router 与 @react-navigation/native 被声明为可选 peer 依赖——这正是"按屏指标"集成可以按需启用的前提。
二、支持平台与前置要求
- 支持平台:Android、iOS 与 tvOS(expo-module.config.json 中声明了
platforms: ["apple", "android"],其中 apple 平台覆盖 iOS/tvOS)。 - 前置要求:
- Expo SDK 55 或更高版本;
- 一个 EAS 项目(在 app config 中配置
extra.eas.projectId,或运行eas init); - 开发或生产构建(development/production build)。
expo-observe不运行于 Expo Go,因为它需要原生模块随二进制一起编译进应用。
三、三步接入:从零到看到指标
第 1 步:安装
npx expo install expo-observe
使用 npx expo install 而不是直接 npm install,可以让 Expo CLI 自动匹配合适的版本。
第 2 步:包装根布局
import { ObserveRoot } from 'expo-observe';
function RootLayout() {
// your app
}
export default ObserveRoot.wrap(RootLayout);
ObserveRoot.wrap 的实现位于 ObserveRoot.tsx:它生成一个高阶组件,内部用 AppMetricsRoot 包裹 ObserveProvider,再渲染你的根组件。从源码注释可以确认:
- 在 SDK 55 上,导出名为
AppMetricsRoot.wrap(当前版本在 index.ts 中仍以@deprecated形式保留AppMetrics旧名,用于向后兼容)。
第 3 步:标记"应用真正可用"的时刻
import { useObserve } from 'expo-observe';
import { useEffect } from 'react';
function HomeScreen() {
const { markInteractive } = useObserve();
useEffect(() => {
// after your initialization work finishes
markInteractive();
}, [markInteractive]);
return <Feed />;
}
useObserve 钩子(见 useObserve.ts)会检查当前是否启用了 expo-router 或 react-navigation 集成,并优先使用集成提供的 markInteractive(它会自动补充当前路由名),否则回退到 AppMetrics.markInteractive。调用时请在每个入口屏幕(entry screen)都执行一次——同一会话中只有第一次调用会被记录。在 SDK 55 上,对应写法是 AppMetrics.markInteractive()。
README 对这套设计有一段精准的总结:"Launch, bundle load and render metrics are automatic. The one explicit call exists because only your code knows when your app is genuinely ready for input."(启动、包加载与渲染指标都是自动的;唯一显式调用是因为只有你的代码才知道应用何时真正可交互。)——能自动的地方自动,需要精确的地方显式。
最后运行 eas build,埋点会随二进制一起分发,指标即可在 EAS 控制台的 Observe 标签页中看到。
如果你更喜欢声明式写法,包内还提供了 ObserveInteractiveMarker 组件(见 ObserveInteractiveMarker.tsx):它不渲染任何 UI,只在挂载时调用一次 markInteractive,适合"数据加载完成后再渲染"的场景:
import { ObserveInteractiveMarker } from 'expo-observe';
function Feed({ items }) {
if (!items) return <Spinner />;
return (
<>
<FeedList items={items} />
<ObserveInteractiveMarker params={{ cacheHit: true }} />
</>
);
}
注意它是 fire-once 语义:挂载后 params 变化会被忽略并在开发模式下告警,需要后续追加属性时应改用命令式的 useObserve().markInteractive(...)。
四、启动指标:自动采集的五个维度
以下指标均为自动采集,无需任何埋点代码:
- 冷启动(cold launch) 与 热启动(warm launch) 时长;
- 包加载时长(bundle load);
- 首帧渲染时间(time to first render,TTR);
- 可交互时间(time to interactive,TTI)——来自你的
markInteractive()调用。
每一个 TTI 事件还会携带帧率、热状态、电量、网络参数,以及启动期间发出的请求摘要,从而帮你区分"设备慢"还是"网络慢"。README 特别指出:为浏览器设计的工具不会采集这些字段,因为它们在 Web 上并不存在。
同时,每条事件都会携带 app、release、device 与 route 上下文,因此你可以按版本、机型或屏幕维度对结果分组。
从原生侧源码可以印证这套自动采集的实现路径:Android 端由 ObservabilityManager.kt 管理指标生命周期,iOS 端由 Observability.swift 实现同样的逻辑;而 module.ts 中的 markInteractive、markFirstRender 等调用经由 JSI 直通原生模块 ExpoObserve(见 types.ts 中对应方法的注释)。
五、按屏指标:Expo Router 与 React Navigation 集成
在 SDK 56 及更高版本上,Expo Router 与 React Navigation 集成会记录按路由(per-route)的渲染与可交互时间,两者均为可选启用(opt-in)。
启用方式是在运行时配置中声明:
import { Observe } from 'expo-observe';
Observe.configure({
integrations: {
'expo-router': true,
},
});
对应的类型定义见 types.ts:
'expo-router'?: boolean | ObserveNavigationIntegrationConfig——记录路由状态变化产生的cold_ttr、warm_ttr、tti指标,要求安装expo-router;'react-navigation'?: boolean | ObserveNavigationIntegrationConfig——同样记录cold_ttr、warm_ttr、tti,要求安装@react-navigation/native,并且必须用<ObserveNavigationContainer>替换原生<NavigationContainer>(相关实现位于 integrations/react-navigation/ObserveNavigationContainer.tsx)。
配置项可以传对象,用于过滤导出到指标中的路由/查询参数:
type ObserveNavigationIntegrationConfig = {
/** 从导出的 navigation metric `routeParams` 中移除的参数键。 */
filteredParams?: string[];
};
(见 types.ts)。当任一被配置的参数被移除时,导出的解析 URL/路径会用 urlHidden: true 替换,但 routeName 不受影响。
configure 中对集成的处理逻辑位于 module.ts,有几个值得注意的边界行为:
- 若启用了
'expo-router'但未安装 expo-router,会打印警告且集成不初始化; - 若同时启用了两个集成,只有 expo-router 会初始化,react-navigation 会被忽略并给出警告;
- 集成应在启动时、任何屏幕挂载前一次性配置(
useObserve会通过useAssertValueDoesNotChange断言集成状态不能在屏幕生命周期中切换)。
六、更新下载与用户自定义事件
EAS Update 下载性能
使用 EAS Update 的应用会自动上报每个更新包(update bundle)的下载耗时。这完全不需要插桩——只要应用在使用 EAS Update,该指标就会出现在 Observe 中。
用户自定义事件
用 Observe.logEvent 记录你自己的命名事件:
import { Observe } from 'expo-observe';
Observe.logEvent('checkout_completed', {
attributes: { orderValue: 129.9, currency: 'USD' },
});
它们与启动指标落在同一条时间线上,因此一个业务事件会紧挨着它附近发生的掉帧被一起呈现。源码层面的细节:logEvent 事件会先持久化到本地,在下次 dispatchEvents() 冲刷时统一发送(见 types.ts);在 module.ts 中,凡是原生模块 ExpoObserve 未实现的属性(如 logEvent),都会通过 Proxy 自动转发给 expo-app-metrics 模块——Android 端 JSI host object 没有 has 钩子,因此用 Object.keys 判断真实成员并决定转发。
七、错误上报:自动 + 边界 + 手动三层
在 SDK 57 及更高版本上,未处理的 JavaScript 错误会自动记录(该功能当前处于 preview 状态),并配套两个显式手段:
import { Observe, ObserveErrorBoundary } from 'expo-observe';
// 1. 用错误边界捕获渲染期错误
function App() {
return (
<ObserveErrorBoundary fallback={<ErrorScreen />}>
<HomeScreen />
</ObserveErrorBoundary>
);
}
// 2. 对自己捕获并处理的错误,手动上报
try {
await syncCart();
} catch (error) {
Observe.reportError(error);
}
要点(来自 types.ts 与 ObserveErrorBoundary.tsx):
- 未处理错误记录为
exception日志事件,由errorHandlingEnabled控制(默认true);关闭后,React Native 自身的处理(开发模式红框、生产模式致命终止)不受影响; - 全局错误处理器在包首次 import 时就已安装,早于任何
configure调用,因此在configure之前抛出的错误也会被记录; Observe.reportError会把捕获值规范化:Error对象提取name、message、stack;其他值(字符串、普通对象)会被字符串化作为 message;ObserveErrorBoundary是AppMetricsErrorBoundary的别名导出,用于捕获子树渲染错误。
八、自定义端点:摆脱锁定的 OTLP 上报
expo-observe 通过 OTLP over HTTP(JSON payload)传输数据。要把数据发往自己的 collector,只需在 app config 中设置 endpointUrl:
// app.json / app.config.js
{
"expo": {
"extra": {
"eas": {
"observe": {
"endpointUrl": "https://your-collector.example.com/v1/metrics"
}
}
}
}
}
Android 端从 manifest 读取该配置的实现见 ExpoManifest.kt(读取 extra.eas.observe.endpointUrl 属性),实际 POST 请求由 EventDispatcher.kt 发送。
底层协议细节:OTLP 重试语义
值得说明的是,传输层严格遵循 OTLP 规范。Android 端 DispatchUtils.kt 与 iOS 端 DispatchUtils.swift 都把一次 dispatch 的结果划分为五种情形(对齐 OTLP HTTP 响应指引):
- 成功(success);
- 可重试失败(如 TLS、超时、连接重置);
- 部分成功(
partial_success,服务端接受了批次但拒绝了部分记录); - 明确拒绝(明确的非 2xx 状态码,不重试);
- 不可重试错误。
同时,ObservabilityManager.kt 注释说明内存中的重试门控(retry-gate)状态是按 OTLP 端点独立维护的——/v1/metrics 与日志端点互不干扰;仓库中 DispatchUtilsBackoffTest、DispatchUtilsRetryGateTest、DispatchUtilsTest 等测试(见 android/src/test)将这些规范规则与真实网络调用解耦,纯单元测试验证。此外,OTAnyValueSerializer.kt 按 OTLP 规范把 64 位整数编码为 JSON 字符串以避免精度丢失。
九、运行时配置:Observe.configure 全参数详解
Observe.configure() 在运行时设置环境标签、采样率、派发行为与集成。完整配置类型见 types.ts:
Observe.configure({
environment: 'production', // 事件的环境标签,默认 process.env.NODE_ENV
dispatchingEnabled: true, // 是否向服务器派发事件,默认 true
dispatchInDebug: false, // 是否派发 debug 构建采集的指标,默认 false,对 release 构建无影响
sampleRate: 0.5, // 参与派发的安装比例 [0,1],默认 undefined(全量)
errorHandlingEnabled: true, // 是否记录未处理 JS 错误,默认 true
integrations: { 'expo-router': true }, // 按屏指标集成,默认全部关闭
});
各参数的底层行为要点:
dispatchingEnabled:设为false时,待处理指标会被标记为"已发送"而不实际派发,直到重新设为true;dispatchInDebug:为false时 debug 构建产生的指标被标记为已发送而不派发;为true时与 release 指标一起派发。注意:如果dispatchingEnabled为false,或该设备在sampleRate采样之外,则无论dispatchInDebug如何都不会派发;sampleRate:超出[0,1]会被钳制。决策按安装确定性——一台设备对给定采样率要么永久在样本内、要么永久在样本外,跨启动保持稳定。样本外的设备会丢弃待处理指标而不是累积;environment:默认值来自 index.ts 中模块导入时自动调用的Observe.setBundleDefaults({ environment: process.env.NODE_ENV ?? 'production', isJsDev: !!__DEV__ })。
手动冲刷:Observe.dispatchEvents()
Observe.dispatchEvents() 立即冲刷待发送事件。自动派发时机因平台而异:
- 应用进入后台时自动派发;
- Android 上还有一个后台 worker,在网络连通后派发事件;
- iOS 上在应用 resign active 或即将终止时派发。
因此手动冲刷主要用于测试或"在某个节点前确保事件已发出":
await Observe.dispatchEvents();
其他实用 API
Observe.clientId:EAS client id,一个随机的、假名化的安装标识符,所有 EAS 客户端库共享;Observe 以expo.eas_client.id属性记录它,可用于与其他服务的数据关联。它存储在原生偏好中,跨启动与更新保持稳定,清除数据或重装会变化(备份恢复可能带旧 id 过来);标识的是安装而非用户或设备;在 Web 上为null(见 types.ts)。Observe.setGlobalAttributes({...}):设置合并进此后每条指标与日志事件的全局属性;按条目的键在冲突时优先;传null/undefined/空对象可清除。Observe.registerIntegration(name, callback):在指定集成配置可用时回调一次(实现见 module.ts),供第三方包集成自己的行为。
十、命令行与 Agent:把观测结果接入脚本
控制台里能看到的一切,同样可以通过终端获取。所有 eas observe: 命令都支持 --json --non-interactive 参数,因此可以直接管道给脚本,或交给编码 Agent 调用。仓库 README 还提到 eas-observe Expo Skill 可以教 Agent 完成"配置库 + 查询结果"的完整流程。
典型用法(示意):
eas observe:metrics --json --non-interactive
十一、总结:一套"真实用户视角"的观测体系
expo-observe 的设计哲学贯穿始终:自动采集所有能自动的东西(启动、包加载、渲染、TTI 附带的环境参数、EAS Update 下载耗时),只把无法推断的时刻(应用真正可交互)留给开发者显式标记,并为归因、扩展与迁移留足空间——OTLP 标准协议保证可替换端点,集成注册机制保证可扩展第三方采集,eas observe: CLI 与 Skill 保证可被脚本与 Agent 消费。
想在当前仓库继续深入,建议按以下路径阅读:
- 包入口与导出:packages/expo-observe/src/index.ts
- 原生模块接口与配置类型:packages/expo-observe/src/types.ts
- 集成初始化与 Proxy 转发逻辑:packages/expo-observe/src/module.ts
- Expo Router 集成:packages/expo-observe/src/integrations/expo-router/
- React Navigation 集成:packages/expo-observe/src/integrations/react-navigation/
- Android 原生实现:packages/expo-observe/android/src/main/java/expo/modules/observe/
- iOS 原生实现与测试:packages/expo-observe/ios/
- 完整变更记录:packages/expo-observe/CHANGELOG.md
前提说明:本文涉及的 SDK 版本行为(55 的
AppMetrics*旧名、56 的按屏指标、57 的错误上报预览)均以当前仓库 README.md 与源码为准;接入前请确认你的 Expo SDK 版本与仓库中57.0.9版本的对应关系。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00