@arms/rum-browser-react
v0.1.2
Published
React plugin for @arms/rum-browser SDK (route view.name convergence + React error boundary + hooks)
Readme
@arms/rum-browser-react
@arms/rum-browser SDK 的 React 插件 —— 提供路由驱动的 view.name 收敛(startView 手动 view 模式)、React ErrorBoundary 集成,以及用于自定义事件上报的 useRum Hook。
安装
本插件不独立工作 —— 需与主包 @arms/rum-browser(必需 peer dependency)同时安装,并完成主包初始化(见「快速开始」):
npm install @arms/rum-browser @arms/rum-browser-reactPeer dependencies
| Package | Version | Required |
| ------------------- | ------------------------ | -------- |
| @arms/rum-core | >=0.1.11 | Yes |
| @arms/rum-browser | >=0.1.16 | Yes |
| react | >=16.8 <20 | Yes |
| react-router | 5 \|\| 6 \|\| 7 \|\| 8 | Optional |
| react-router-dom | 5 \|\| 6 \|\| 7 | Optional |
react-router/react-router-dom为可选 peer —— 仅在使用路由集成功能时需要。请安装与你所使用的 router 版本对应的子路径入口(v5/v6/v7/v8 各有独立入口)。注意:react-router v8 已移除
react-router-dom包,DOM 专用 API(如RouterProvider)改从react-router/dom导入;本插件的react-router-v8入口导出的路由包装 API 与 v7 入口完全一致(re-export v7 实现)。
history@^4.9.0为本包的直接依赖(dependencies,随包安装):仅react-router-v5入口使用(包装 Router 系列需自建 history 实例)。以直接依赖而非 optional peer 安装,可保证在 pnpm 严格隔离、或宿主应用自带 history@5(react-router v6 data router 场景)等环境下,v5 入口始终解析到 history@4;npm/yarn hoist 场景下与 react-router-dom@5 自带的 history@4 传递依赖等价。webpack4 兼容:四个子路径入口(
react-router-v5/-v6/-v7/-v8)均在包根提供同名 CJS 存根(react-router-v5.js等,包含于files),不读取exports字段的旧构建链(webpack 4、部分 CDN 拼接工具)可直接require('@arms/rum-browser-react/react-router-v5')解析;同时提供同名根级.d.ts类型存根(react-router-v5.d.ts等,export *转发es/entries/入口全部具名导出,包含于files),moduleResolution: "node"(经典模式,不读exports的types条件)的 TS 工程亦有完整类型,子路径 API 不会回落any。
快速开始
1. 安装主包与插件
npm install @arms/rum-browser @arms/rum-browser-react2. 初始化主包 @arms/rum-browser
view、错误、资源等全部上报能力均由主包提供 —— 插件不替代主包,必须完成主包初始化,且 endpoint 为必填项(其余配置项与 @arms/rum-browser 一致,完整可运行示例见 examples/browser-react/src/monitor.ts):
import armsRum from '@arms/rum-browser';
// 建议放在应用入口模块执行(如 index.tsx 首行 import './monitor')
armsRum.init({
endpoint: 'https://<你的 RUM 上报端点>', // 必填
sessionConfig: {},
collectors: {
pv: { enable: true },
jsError: { enable: true },
// ... 其余采集器按需开启
},
});3. 初始化插件
initReactPlugin 可在主包 init 前后任意时序同步调用 —— 不依赖 armsRum.init() 返回的 Promise resolve,先后顺序可互换:
import { initReactPlugin } from '@arms/rum-browser-react';
import armsRum from '@arms/rum-browser';
armsRum.init({
endpoint: 'https://<你的 RUM 上报端点>', // 必填
// ... 其余主包配置
});
// 插件接入不再依赖 init 完成,可在 init 前后任意时序同步调用
initReactPlugin(armsRum, {
/* router options,见下文「路由集成」 */
});时序说明:插件接入(
initReactPlugin)不再依赖主包 init 完成,可在 init 前后任意时序同步调用;init 与插件接入的先后顺序可互换,两者等价安全。路由首次导航会被内部 pending 机制暂存、init 完成后补发,不会丢失。注意:init 完成前触发的错误与自定义事件不会被采集(默认配置下 init 几乎瞬间完成;但 React 等同步渲染框架的首屏渲染期错误可能落入此窗口)。若需捕获首屏渲染期错误,可将应用挂载(mount/render)置于 init 完成后(仅挂载时序保留.then/await,插件接入不再需要)。
4. 路由集成
插件初始化时会自动开启 trackViewsManually 手动 view 模式:view 的创建、命名、计时完全由路由变化驱动(内部调用 shell.startView),不再依赖 history 拦截。这从根本上解决了 popstate 前进/后退、首屏时序、懒加载路由、memory router 等全部时序问题。
警告:非 react-router 应用(如 Next.js)接入后 PV 不会自动采集
本插件面向 react-router(v5~v8)系应用 —— 手动 view 模式下 view/PV 完全由包装版路由 API 驱动。Next.js 等使用自带路由方案的应用接入
initReactPlugin后:主包 PvCollector 因trackViewsManually: true跳过自动 PV(首屏与 history 变化均不采集),而应用内没有任何包装版路由 API 会触发startView—— view/PV 将完全静默丢失(无报错、无告警)。此类应用请勿直接接入本插件的路由集成,可二选一:
- 不启用路由集成:不调用
initReactPlugin、仅使用主包@arms/rum-browser—— 未注入trackViewsManually时,主包的自动 PV 采集正常工作(ErrorBoundary/useRum等插件能力随之一并不可用);- 手动驱动 view:接入插件后,在首屏与页面切换时机手动调用主包实例的
startView上报 view ——armsRum.startView('/order/:id', { loading_type: 'initial_load' }),签名startView(name, { url?, referrer?, loading_type? })。Next.js 专用入口当前未提供。
方式一 — 使用包装后的 router API(推荐)
用包装版本替换 react-router-dom 的导入:
import { createBrowserRouter } from '@arms/rum-browser-react/react-router-v6';
// For react-router v7: import from '.../react-router-v7'
// For react-router v8: import from '.../react-router-v8'
// For react-router v5: import from '.../react-router-v5'
const router = createBrowserRouter(routes, { basename: '/app' });
initReactPlugin(armsRum);
// No need to pass routes — the wrapped APIs drive view lifecycle automatically可用的包装 API(v6/v7/v8):createBrowserRouter、createHashRouter、createMemoryRouter、useRoutes、Routes。
react-router v5 无 data router API(createBrowserRouter 等)与 useRoutes / Routes,包装版 API 为 BrowserRouter / HashRouter / MemoryRouter / Router / Switch —— 将 react-router-dom 中这些组件的导入替换为 @arms/rum-browser-react/react-router-v5 即可,Route / Link 等仍从 react-router-dom 导入,无需替换。
view 生命周期:
- 首次渲染 / router 创建:
loading_type: 'initial_load' - 后续路由变化(pushState / replaceState / popstate):
loading_type: 'route_change'
URL /app/user/42 → view.name 将收敛为 /user/:id(而非原始路径)。
方式二 — 传入 routes(兼容保留,不推荐)
createRouterConfig 仍可传入(API 签名不变):
import { initReactPlugin } from '@arms/rum-browser-react';
import { createRouterConfig } from '@arms/rum-browser-react/react-router-v6';
const routes = [
{ path: '/', element: <Home /> },
{ path: '/user/:id', element: <User /> },
{ path: '/files/*', element: <Files /> },
];
initReactPlugin(armsRum, {
router: createRouterConfig({ routes, basename: '/app' }),
});注意:手动 view 模式下 view 上报由包装版路由 API 驱动。若仅使用方式二(未替换路由 API 导入),view 将不会上报 —— 请迁移到方式一:用
@arms/rum-browser-react/react-router-v6的包装版替换react-router-dom的导入即可,initReactPlugin无需传入options.router。自检告警:插件初始化后若检测到传入了
options.router但未使用包装版路由 API,会在 5 秒后打印一条logger.warn迁移提示(一次性,不拦截任何功能)。
react-router v5 支持
react-router v5([email protected]~5.3)经 react-router-v5 子路径入口支持,示例工程见 examples/browser-react-v5。包装组件为类组件实现(不依赖 5.1 引入的 hooks),兼容 react-router 5.0~5.3 全线;useRumRouteV5 除外(仅 react-router >= 5.1 可用)。
行为要点:
- path 归一化:
:id?→:id、:id(\d+)→:id(对齐 v6 view.name 风格);splat 末段展开实际值,与 v6/v7/v8 行为一致(/files/*匹配/files/a/b→ view.name 为/files/a/b); - 嵌套 Switch:一次导航仅产生一条 view —— 延迟提交合并在 microtask 窗口内合并父/子 Switch 的候选,取最深模板(如
/user/:id内嵌 Switch 声明/user/:id/profile,仅上报/user/:id/profile); - 首屏 Redirect:首屏 Redirect(REPLACE)不干扰首屏 view —— 首屏 view 收敛为 Redirect 目标模板,仅一条;
- 懒加载(已知降级):懒加载子路由跨窗口迟到时会先提交父模板、子模板作为第二条 view(
route_change)到达;同名场景由 view 同名去重兜底; - ErrorBoundary:在 v5 应用中行为与 v6 一致 —— React 18 下同一渲染错误出现
source='react'与source='onError'双通道为 React re-throw 固有行为,非重复上报。
ErrorBoundary
包裹你的组件树,以自动将 React 渲染错误上报至 RUM:
import { ErrorBoundary } from '@arms/rum-browser-react';
function App() {
return (
<ErrorBoundary
fallback={({ error, resetError }) => (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={resetError}>Retry</button>
</div>
)}
>
<UserProfile />
</ErrorBoundary>
);
}addReactError
对于自定义错误边界(或 react-error-boundary 等),可手动上报错误:
import { addReactError } from '@arms/rum-browser-react';
class MyErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
addReactError(error, errorInfo);
}
// ...
}上报的错误携带 source: 'react',并包含 snapshots(JSON 字符串,内部 component_stack 为 errorInfo.componentStack 组件栈、handling: 'handled')。
error 参数支持 unknown 类型(React 19 createRoot 回调中 error 可为任意值),内部自动 normalize 为 Error。
createErrorBoundary
创建自定义 ErrorBoundary 组件(工厂函数),允许接入方自定义错误上报逻辑:
import { createErrorBoundary, addReactError } from '@arms/rum-browser-react';
const MyErrorBoundary = createErrorBoundary((error, errorInfo) => {
// 可同时上报到接入方自有的错误上报通道(可选)
myErrorReporter.report(error);
addReactError(error, errorInfo);
}, 'MyErrorBoundary');
<MyErrorBoundary
fallback={({ error, resetError }) => (
<div>
<p>渲染出错:{error.message}</p>
<button onClick={resetError}>重试</button>
</div>
)}
>
<App />
</MyErrorBoundary>;useRum hook
在任意组件中访问 RUM 上报能力:
import { useRum } from '@arms/rum-browser-react';
function CheckoutButton() {
const rum = useRum();
const onClick = () => {
try {
submit();
rum.sendCustom({ type: 'biz', name: 'checkout_click', value: 1 });
} catch (e) {
rum.addReactError(e);
}
};
return <button onClick={onClick}>Checkout</button>;
}该 hook 返回一个稳定引用,包含以下方法:
addReactError(error, errorInfo?)—— 上报 React 渲染错误(error支持unknown,内部 normalize)sendCustom(payload)—— 发送自定义事件sendEvent(payload, options?)—— 发送原始事件getConfig()—— 读取当前 browser 配置
API Reference
initReactPlugin(shell, options?)
shell—— ArmsRum 实例options.router?—— 由createRouterConfig()生成的RouterConfig(方式二,兼容保留,见“路由集成”)
初始化时注入 trackViewsManually: true:view 生命周期改由路由驱动(包装版路由 API 内部调用 shell.startView),PvCollector 跳过首屏 PV 与 history 拦截。
幂等:可安全地多次调用(HMR 场景)。若 react collector 已注册,将跳过重复注册;仅刷新 shell 引用与 router 配置。
包装版路由 API(方式一,推荐)
从版本子路径入口导入(@arms/rum-browser-react/react-router-v6 / -v7 / -v8),签名与对应版本 react-router API 完全一致:createBrowserRouter、createHashRouter、createMemoryRouter、useRoutes、Routes,以及方式二的 createRouterConfig({ routes, basename? })。
react-router v5 专属导出(@arms/rum-browser-react/react-router-v5)
包装组件 BrowserRouter / HashRouter / MemoryRouter / Router / Switch 的签名与 react-router-dom (v5) 完全一致(用法见「路由集成」与「react-router v5 支持」),另有:
createRouterConfigV5({ routes, basename? })—— 方式二等价物(方案 C):react-router-config 风格路由表现算,供initReactPlugin的options.router传入;感知复用包装版 Router(BrowserRouter等需从本入口导入);useRumRouteV5(options?)—— 路由监控 hook(方案 B):在根组件调用,仅 react-router >= 5.1 可用;传入routes时现算模板收敛 view.name,不传时回退 URL path(无收敛能力)。推荐优先使用包装版组件。
ErrorBoundary
Props:fallback(接收 error 与 resetError 的组件)、children。
addReactError(error, errorInfo?)
手动上报一个 React 错误。error 支持 unknown(React 19 createRoot 兼容),内部自动 normalize 为 Error。errorInfo 为 componentDidCatch 的第二个参数。
createErrorBoundary(reportError, displayName?)
创建自定义 ErrorBoundary 组件。reportError 为自定义上报函数,displayName 可选(React DevTools 组件名)。返回 ComponentType<ErrorBoundaryProps>。
useRum()
返回 UseRumResult,包含 addReactError、sendCustom、sendEvent、getConfig。
computeViewName(routeMatches)
由路由匹配结果计算视图名(路由模板)—— 插件内部即用该函数将 /user/42 这类原始路径收敛为 /user/:id 模板;需要与插件口径一致地自行计算 view.name 时可直接复用。
routeMatches—— react-routermatchRoutes()返回的匹配数组(版本无关结构{ route, params },v5~v8 均兼容;运行时对空值/空数组安全)- 返回
string—— 路由模板名称,如/user/:id、/order/:orderId/detail;无匹配时返回''
计算规则::param 动态段原样保留(收敛的关键);嵌套路由逐段拼接(相对 path 追加,绝对 path 以 / 开头则替换已拼结果);末段 * splat 用 params['*'] 实际值展开,中间段 * 连同前导 / 移除,params['*'] 缺失时保留 *;无 path 的 layout/index 路由段跳过。
import { computeViewName } from '@arms/rum-browser-react';
import { matchRoutes } from 'react-router-dom'; // 按所用 react-router 版本导入
const matches = matchRoutes([{ path: '/user/:id' }], '/user/42');
computeViewName(matches ?? []); // => '/user/:id'已知限制
以下限制是当前 MVP 设计所固有的,后续版本计划改进。
react-router v5:已有 view 时的 REPLACE 导航会产生新 view
v5 入口下,当会话已有 view 时执行 replace 导航(history.replace、或 v5 <Redirect> 默认 REPLACE),会产生一条新的 view(route_change)而非静默吞掉。这与“REPLACE 不应产生新 view”的直觉语义不同,用于防御首屏 Redirect 后再次 replace 场景的丢 view 问题(hasExistingViews() 时 replace 同样提交)。首屏 Redirect(尚无 view)不受影响,仍仅一条 initial_load view。
react-router v5:懒加载子路由的降级行为
懒加载子路由跨延迟合并窗口迟到时,会先提交父模板,子模板作为第二条 view(route_change)到达;同名场景由 view 同名去重兜底(见「react-router v5 支持」行为要点)。
单实例约束
该插件使用模块级单例来持有 shell/ctx 引用。在存在多个 RUM 实例或重复调用 initReactPlugin 的情况下,最后一次注册生效 —— ErrorBoundary 的错误上报可能会被路由到与预期不同的实例。未来版本计划采用 RumProvider / React Context 方案以支持多实例场景。
手动 view 模式对 PV 的影响
插件注入 trackViewsManually: true 后,browser SDK 的 PvCollector 不再自动采集 PV(首屏与 history 变化均跳过)。view 上报完全由包装版路由 API 触发(shell.startView)。这意味着:
- 仅使用方式二(未替换路由 API 导入)时,view 不会上报 —— 请迁移到方式一;
- 未使用本插件的其他页面/应用不受影响(
trackViewsManually由插件注入,默认关闭)。
source='react' 错误不受 collectors.jsError 采样管控
通过 ErrorBoundary / addReactError 上报的 React 错误携带 source: 'react'。browser SDK 的 SessionProcessor 采样键映射当前未包含 'react' source,这意味着这些错误会绕过 collectors.jsError 采样配置。如需禁用 React 错误上报,请使用 collectors.exception(设置为 false)。后续版本可能会在 browser session-processor 中新增 'react' → 'js' 的 source 映射。
初始化时序
initReactPlugin 可在 armsRum.init() 前后任意时序同步调用 —— init 与插件接入的先后顺序可互换(路由首次导航经内部 pending 机制暂存、init 完成后补发,见「快速开始」的时序说明)。在初始化完成之前调用 useRum / sendCustom / addReactError 将安全降级(打印告警并跳过操作),init 完成前触发的错误与自定义事件不会被采集。useRum hook 本身可在任意时刻调用 —— 它返回一个稳定引用,在初始化完成后即可正常使用。
License
ISC
