@arms/rum-browser-nextjs
v0.1.1
Published
Next.js plugin for @arms/rum-browser SDK (App Router / Pages Router view tracking + Next.js error capture with digest)
Readme
@arms/rum-browser-nextjs
Next.js 插件 for @arms/rum-browser RUM SDK —— 同时支持 App Router(Next.js 13+)与 Pages Router。
Overview
本包为 Next.js 应用提供框架级可观测适配:自动检测 App Router 与 Pages Router 路由变化、将动态路由收敛为模板化 view 名称、采集 Next.js 错误边界异常并携带 digest 关联键。结合 @arms/rum-browser 基座能力,可追踪 Core Web Vitals、资源加载、用户操作与会话回放,定位性能瓶颈与用户旅程中的问题。
能力范围:
- 路由视图追踪(view.name 模板收敛):开启手动 view 模式后,view 的创建、命名与计时完全由路由变化驱动(内部调用
shell.startView)。view.name 自动收敛为路由模板 —— App Router 经「具体 pathname + 参数」反推(/user/42→/user/[id],catch-all/docs/a/b/c→/docs/[...slug]);Pages Router 直接采用router.pathname(本身就是模板) - Next.js 错误采集:
addNextjsError面向app/error.tsx/app/global-error.tsx接收的错误参数上报,snapshots自动携带digest(生产模式下关联服务端日志的唯一键)与framework: 'nextjs'标识;附ErrorBoundary组件 - 纯客户端集成档:本包为轻量客户端适配层,不含服务端监控、next.config 包装、sourcemap 上传(见「已知限制」与演进说明)
安装
本插件不独立工作 —— 需与主包 @arms/rum-browser(必需 peer dependency)同时安装,并先完成主包初始化(见「App Router 接入」):
npm install @arms/rum-browser @arms/rum-browser-nextjs@arms/rum-browser-react 是本包的内部依赖(非 peer),npm 会自动安装,用户无需单独安装或关心。
版本要求
| 依赖 | 版本要求 | 必选 | 说明 |
| ------------------- | ---------- | ---- | ---------------------------------------------------------------------------------------------------------------------- |
| @arms/rum-core | >=0.1.11 | 是 | 含 Shell.startView 与 IConfiguration.trackViewsManually 的发版(已发布) |
| @arms/rum-browser | >=0.1.16 | 是 | 含 PvCollector 手动模式防护(trackViewsManually 早退)的发版(已发布) |
| react | >=18 | 否 | optional:组件本质是 React 组件,但 peer 标记为 optional 以兼容边缘消费场景(npm 7+ 自动解析传递 peer) |
| next | >=13.0.0 | 否 | optional:仅 ./app-router(next/navigation)与 ./pages-router(next/router)两个子入口使用;主入口零 next 依赖 |
选择你的 Router
入口决策:App Router(项目含
app/目录)→@arms/rum-browser-nextjs/app-router;Pages Router(项目含pages/目录)→@arms/rum-browser-nextjs/pages-router;两者并存时 App Router 优先。不确定项目用的是哪种 Router?看项目根目录:有app/即为 App Router,有pages/且无app/即为 Pages Router。
快速开始(5 分钟最短接入)
适用 Next.js 15.3+ App Router 项目的最短路径(三步)。每步仅列必填项,详细字段见下方「App Router 接入」。
一键生成(可选):在项目根目录执行
npx -p @arms/rum-browser-nextjs rum-nextjs-init,自动探测 Router 类型(App/Pages、src/布局)与 Next 版本(15.3+ / 13~14 降级),生成instrumentation-client.ts、app/error.tsx并输出layout.tsx/_app.tsx待合并片段;已存在的文件默认跳过(--dry-run预览、--force覆盖)。以下三步为手工接入的等价说明。
1. 安装(2 个包)
npm install @arms/rum-browser @arms/rum-browser-nextjs2. 初始化并重导出路由钩子 —— instrumentation-client.ts(项目根)
import armsRum from '@arms/rum-browser';
import { initNextjsPlugin } from '@arms/rum-browser-nextjs';
armsRum.init({ endpoint: 'https://<你的 RUM 上报端点>' });
initNextjsPlugin(armsRum);
export { onRouterTransitionStart } from '@arms/rum-browser-nextjs';3. 根布局挂载检测组件 + error.tsx 上报 —— app/layout.tsx、app/error.tsx
// app/layout.tsx
import { ArmsAppRouter } from '@arms/rum-browser-nextjs/app-router';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<ArmsAppRouter />
{children}
</body>
</html>
);
}// app/error.tsx
'use client';
import { useEffect } from 'react';
import { addNextjsError } from '@arms/rum-browser-nextjs';
export default function Error({
error,
}: {
error: Error & { digest?: string };
}) {
useEffect(() => {
addNextjsError(error);
}, [error]);
return <button onClick={() => location.reload()}>重试</button>;
}完成后启动
next dev,访问任一路由即可在平台看到首条view事件。Pages Router 或 Next.js 13/14 项目见下文对应章节。
App Router 接入(推荐路径,Next.js 15.3+)
1. 初始化插件并重导出路由过渡钩子 —— instrumentation-client.ts
在项目根(或 src/)目录新建 instrumentation-client.ts(Next.js 15.3+ 的客户端监测入口,hydration 前执行,不阻塞首屏渲染):
// instrumentation-client.ts
import armsRum from '@arms/rum-browser';
import { initNextjsPlugin } from '@arms/rum-browser-nextjs';
armsRum.init({
endpoint: 'https://<你的 RUM 上报端点>', // 必填
sessionConfig: {},
collectors: {
pv: { enable: true },
jsError: { enable: true },
// ... 其余采集器按需开启(与 @arms/rum-browser 配置一致)
},
});
// 插件接入不再依赖 init 完成,可在 init 前后任意时序同步调用
initNextjsPlugin(armsRum);
// 如需自定义路由集成(方式 a,可选):
// import { createRouterConfig } from '@arms/rum-browser-react/react-router-v6';
// initNextjsPlugin(armsRum, { router: createRouterConfig({ routes }) });
// 重导出 Next.js 路由过渡钩子(必须):
// App Router 导航时 React 渲染先于 pushState 执行,渲染期读取 window.location
// 拿到的是上一个路由的旧值(url 滞后一拍)。Next.js 在路由过渡开始(渲染前)
// 回调该钩子,插件捕获导航目标 URL 并在下一次 view 上报中一次性消费 ——
// 不重导出则 view 事件的 url 将滞后一次导航。
export { onRouterTransitionStart } from '@arms/rum-browser-nextjs';2. 根布局挂载 view 检测组件 —— app/layout.tsx
// app/layout.tsx
import { ArmsAppRouter } from '@arms/rum-browser-nextjs/app-router';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body>
<ArmsAppRouter />
{children}
</body>
</html>
);
}ArmsAppRouter 在渲染期通过 usePathname + useParams 检测路由变化,反推路由模板后调用 startView,组件返回 null(无渲染输出、无 hydration mismatch)。
3. 错误组件上报 —— app/error.tsx / app/global-error.tsx
// app/error.tsx(app/global-error.tsx 同理)
'use client';
import { useEffect } from 'react';
import { addNextjsError } from '@arms/rum-browser-nextjs';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// 生产模式下 Server Component 抛出的错误 message 会被 generic 化
// (如 "An error occurred in the Server Components render"),
// digest 是关联服务端日志的唯一稳定键 —— addNextjsError 自动携带
addNextjsError(error);
}, [error]);
return (
<div>
<h2>页面出错了</h2>
<button onClick={() => reset()}>重试</button>
</div>
);
}验证提醒:digest 仅在生产构建(
next build && next start)下是可预期的错误关联形态(dev 下 message 保留原文、digest 可能缺失),联调验证请以 prod 模式为准。
Pages Router 接入
在 pages/_app.tsx 挂载 ArmsPagesRouter;初始化方式与 App Router 相同(Next.js 15.3+ 推荐仍经 instrumentation-client.ts,13/14 移入客户端组件 useEffect):
// pages/_app.tsx
import { useEffect } from 'react';
import type { AppProps } from 'next/app';
import armsRum from '@arms/rum-browser';
import { initNextjsPlugin } from '@arms/rum-browser-nextjs';
import { ArmsPagesRouter } from '@arms/rum-browser-nextjs/pages-router';
export default function App({ Component, pageProps }: AppProps) {
useEffect(() => {
armsRum.init({
endpoint: 'https://<你的 RUM 上报端点>', // 必填
});
initNextjsPlugin(armsRum);
}, []);
return (
<>
<ArmsPagesRouter />
<Component {...pageProps} />
</>
);
}Pages Router 下 onRouterTransitionStart 重导出非必需:ArmsPagesRouter 检测到导航时以 router.asPath 作为显式 URL 传入(优先级高于预捕获值),url 无滞后问题。
hydration 前提:
ArmsPagesRouter为客户端组件,view 检测以 React hydration(客户端接管)启动为前提 —— hydration 启动后渲染期路由比较才会执行。个别环境(部分 Electron / WebView 壳浏览器)不启动 Pages Router 的 hydration,此时ArmsPagesRouter不渲染、view 不会上报 —— 这是环境限制而非插件缺陷(App Router 不受影响)。接入前如需确认目标环境行为,可先在页面组件的useEffect中设置标记(如window.__HYDRATED__ = true)验证 hydration 是否启动。
Next.js 13 / 14 接入(退化指引)
Next.js 13/14 无 instrumentation-client.ts 入口,初始化移入任意客户端组件的 useEffect:
// app/rum-init.tsx(在根 layout 中挂载:<RumInit />)
'use client';
import { useEffect } from 'react';
import armsRum from '@arms/rum-browser';
import { initNextjsPlugin } from '@arms/rum-browser-nextjs';
export function RumInit() {
useEffect(() => {
armsRum.init({
endpoint: 'https://<你的 RUM 上报端点>', // 必填
});
initNextjsPlugin(armsRum);
}, []);
return null;
}行为差异:
- view.name 收敛不受影响 —— 渲染期路由比较机制全版本可用;
- 仅丢失 url 精确捕获:无路由过渡钩子可重导出,路由切换时 view 的
url退化为渲染期的window.location.href(可能滞后一次导航); - 插件以行为信号检测该场景(路由切换未捕获到预捕获 URL,无论成因是 Next < 15.3 还是 15.3+ 忘记 re-export),打印一次性的
logger.warn提示 —— 属预期降级提示,非错误。
路由追踪说明
警告:接入本插件后 view/PV 完全由路由组件驱动,主包自动 PV 被禁用
插件(经转发的
initReactPlugin)注入trackViewsManually: true:主包 PvCollector 跳过首屏 PV 与 history 拦截(避免双发),view 的创建、命名、计时完全依赖ArmsAppRouter/ArmsPagesRouter在渲染期检测路由变化驱动(内部调用shell.startView)。这意味着 view 上报以路由检测组件真实挂载为前提 —— 未挂载ArmsAppRouter/ArmsPagesRouter时 view 将不会上报(无报错、无告警)。
view 上报行为:
- view.name 收敛:App Router 经
computeViewNameFromParams由「具体 pathname + 参数」反推模板(算法见下文 API 参考);Pages Router 直接采用router.pathname(本身就是路由模板) - url 优先级:Pages Router 显式
asPath>onRouterTransitionStart预捕获值(一次性消费,防串用)> 渲染期window.location.href兜底;最终统一绝对化后上报 - loading_type:首个 view 为
initial_load,此后为route_change - 同名动态路由的不同具体 URL(
/user/42→/user/43)经 pathname/asPath 变化检测正常触发上报调用,view 是否新建遵循Shell.startView同名去重语义(模板相同不新建,与 react-router 场景一致)
// 访问 /user/42 时上报的 view 事件:
// { event_type: 'view', type: 'pv', name: '/user/[id]', loading_type: 'initial_load', url: 'https://...', ... }错误采集说明
addNextjsError(error, errorInfo?) 上报的事件结构与 @arms/rum-browser-react 的 addReactError 对齐(source: 'react',复用既有存储/查询链路),差异仅在 snapshots 内附加 digest 与 framework: 'nextjs':
{
event_type: 'exception',
type: 'error',
source: 'react',
name: 'Error', // error.name(非 Error 值自动 normalize)
message: "An error occurred in the Server Components render...",
stack: '...',
// 扩展信息收敛为单一 JSON 字符串字段(值为 undefined 的内部键不会出现)
snapshots: JSON.stringify({
component_stack: 'at UserProfile\nat ...', // errorInfo.componentStack(传入时)
handling: 'handled',
digest: '3669057531', // error.digest(存在时)
framework: 'nextjs',
}),
times: 1,
}ErrorBoundary 组件包裹组件树时自动走 addNextjsError 上报(用法与 @arms/rum-browser-react 的 ErrorBoundary 一致,fallback 接收 error 与 resetError)。
错误聚合注意事项
core 的 reporter 对 exception 类事件按 getErrorID(message + stack) 聚合,聚合 key 不包含 source 字段。当同一错误同时被全局异常采集通道(window.onError)与 addNextjsError 捕获时,addNextjsError 上报的 source='react' 富上下文事件(含 digest / framework: 'nextjs')可能被先到的全局事件通过 times++ 合并吞掉,无法独立可见。典型场景是 app/global-error.tsx:错误在客户端被重放时会先触发 window.onError 入队。
规避方法:通过 init 配置的 filters.exception 过滤对应错误 —— filters 仅作用于 collector 自动采集通道,不影响 addNextjsError(sendEvent 直发)上报;过滤全局通道后 source='react' 事件即可独立上报(times=1)。参考示例:examples/browser-nextjs/instrumentation-client.ts 中有实际用法。
API 参考
主入口 @arms/rum-browser-nextjs
| 导出 | 说明 |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| initNextjsPlugin(shell: Shell, options?: NextjsPluginOptions): void | 初始化插件:缓存 shell + 转发 initReactPlugin(继承 trackViewsManually 注入/首屏防御/幂等守卫)。options.router 透传给 initReactPlugin(方式 a,可选) |
| onRouterTransitionStart(url: string): void | 路由过渡钩子 —— 须在 instrumentation-client.ts 中 re-export;捕获导航目标 URL,解决渲染期 url 滞后一拍 |
| addNextjsError(error: unknown, errorInfo?: ErrorInfo): void | 手动上报 Next.js 渲染异常(app/error.tsx / global-error.tsx 接收的 error 参数);snapshots 附 digest 与 framework: 'nextjs' |
| ErrorBoundary: ComponentType<ErrorBoundaryProps> | Next.js 场景错误边界(createErrorBoundary(addNextjsError) 工厂组装),上报自动携带 digest |
| computeViewNameFromParams(pathname: string, params: RouteParams): string | 路由模板反推工具(高级用法/测试可用) |
类型导出:NextjsError(Error & { digest?: string })、RouteParams(Record<string, string | string[] | undefined>)、ErrorBoundaryProps / ErrorBoundaryFallback(自 @arms/rum-browser-react type-only re-export)。
刻意不导出 useRum 等通用 React 能力 —— 需要者直接从 @arms/rum-browser-react 导入(最小 API 暴露原则)。
子入口 @arms/rum-browser-nextjs/app-router
| 导出 | 说明 |
| ----------------------- | ----------------------------------------------------------------- |
| ArmsAppRouter(): null | App Router view 检测组件,挂载于根 layout;返回 null 无渲染输出 |
子入口 @arms/rum-browser-nextjs/pages-router
| 导出 | 说明 |
| ------------------------- | ------------------------------------------------------------------------------- |
| ArmsPagesRouter(): null | Pages Router view 检测组件,挂载于 pages/_app.tsx;内置 router.isReady 守卫 |
computeViewNameFromParams(pathname, params)
两阶段反推算法(App Router 无客户端路由配置,只能由实际 pathname + 参数反推):
- catch-all(数组)参数优先:
[...slug]展开的连续段整体定位并替换回[...slug](防止值重叠时被普通参数替换吞掉); - 普通(字符串)参数贪婪左到右:参数值在段中首次出现的位置替换为
[paramName];多参数同值时按迭代顺序从左到右命名。
编码对称处理:usePathname() 与 useParams() 的编码形态并无稳定契约 —— Next 文档宣称 params 为 decoded,实测 next 15.5.23 两者均返回 URL 编码形态(如 /user/%E5%BC%A0%E4%B8%89 与 { id: '%E5%BC%A0%E4%B8%89' }),且历史版本间行为亦有摇摆 —— 直接以原段匹配参数值在「path 与 params 形态不一致」的场景下必然失配,中文等非 ASCII 动态段会退化为具体路径、模板不收敛。本包以「统一解码域匹配」覆盖全部形态组合(匹配双方均经 decodeURIComponent 归一,替换同步写入原段):中文动态段在任一形态组合下正常收敛,返回值中静态段保留原始编码形态(与 Next 自身 pathname 形态一致)。
| pathname | params | 返回值 |
| -------------------------- | -------------------------------- | --------------------------------------- |
| /blog/hello-world | {} | /blog/hello-world(原样) |
| /user/42 | { id: '42' } | /user/[id] |
| /user/%E5%BC%A0%E4%B8%89 | { id: '张三' } | /user/[id](编码对称) |
| /docs/a/b/c | { slug: ['a', 'b', 'c'] } | /docs/[...slug] |
| /shop/item/1/review/1 | { itemId: '1', reviewId: '1' } | /shop/item/[itemId]/review/[reviewId] |
| /user/42/posts/42 | { id: '42' } | /user/[id]/posts/42(仅首次出现替换) |
反推是启发式算法,极端值重叠场景存在理论误差,实际业务影响可忽略。
版本兼容矩阵
| Next.js 版本 | 支持情况 | 说明 |
| ------------ | ----------- | ----------------------------------------------------------------------------------------------------------- |
| 15.3+ | ✅ 完整能力 | instrumentation-client.ts 初始化 + onRouterTransitionStart URL 预捕获(无滞后) |
| 13 ~ 14 | ⚠️ 退化支持 | 初始化移入客户端组件 useEffect(见「Next.js 13 / 14 接入」);view.name 收敛不受影响,仅丢失 url 精确捕获 |
| Turbopack | ✅ 支持 | exports 全条件带 .js 扩展名(Next 16 起 Turbopack 默认打包器) |
| webpack 5 | ✅ 支持 | Next.js 内置构建链 |
本包仅经 bundler(npm 工程)消费,不出 UMD dist 产物;目标环境为 Next.js 工程(内置 webpack 5 / Turbopack),不涉及 webpack 4(未提供根级存根,区别于
@arms/rum-browser-react的 v4 兼容存根)。
React 兼容性
| React 版本 | 支持情况 | 说明 |
| ---------- | --------- | ----------------------------------------------------------------------------------------------------------------- |
| 18.x | ✅ 已验证 | devDeps 锁定 ^18.3.1,单测与 E2E 全量通过 |
| 19.x | ✅ 已验证 | peer 放开为 >=18;本地以 React 19.0.0 一次性运行全量单测(38/38 通过),view/error/ErrorBoundary 行为与 18 一致 |
产物压缩说明:
es/lib产物已经过 terser 压缩与混淆,且不附带 sourcemap。排查线上堆栈时请对照仓库源码与 CHANGELOG 版本定位对应发布版本。
已知限制
以下限制是当前设计所固有的,后续版本计划改进。
query-only 导航不触发新 view
/list?page=1 → /list?page=2 仅 query 变化:App Router 的 usePathname 与 Pages Router 的 asPath path 段检测均不视为导航 —— 不产生新 view(预期行为)。query 维度变化如需观测请通过自定义事件补齐。
parallel / interception routes 不触发新 view
并行路由与拦截路由不改变 primary pathname,渲染期比较无法感知(预期行为)。Next.js 16.3+ 的 fromRoutes(experimental)是官方给出的未来解法,列入演进路径。
CDN 场景指引
本包仅经 bundler 消费(无 UMD 产物),CDN(<script> 标签)接入场景不适用本插件 —— 请直接使用主包 @arms/rum-browser(自动 PV + 全局异常采集)。若以 CDN 方式加载主包,请在 App Router 下用 next/script 的 afterInteractive 策略加载;勿用 beforeInteractive(App Router 下不支持且阻塞首屏渲染)。
单实例约束
插件为模块级单例(导航状态持有 shell 引用,与 @arms/rum-browser-react 一致)。多 RUM 实例场景下最后一次初始化生效;微前端多子应用各自持有独立实例的场景暂不支持,列入后续演进。
source='react' 错误不受 collectors.jsError 采样管控
addNextjsError / ErrorBoundary 上报的错误携带 source: 'react'。browser SDK 的 SessionProcessor 采样键映射当前未包含 'react' source,这些错误会绕过 collectors.jsError 采样配置。如需禁用,请使用 collectors.exception(设置为 false)。后续版本可能会新增 'react' → 'js' 的 source 映射。
初始化时序
initNextjsPlugin 可在 armsRum.init() 前后任意时序同步调用 —— useCollectors 的双时序注册机制保证 collector 在 init 完成后统一 setup。初始化完成之前路由组件或 addNextjsError 触发的上报会安全降级(打印告警并跳过,不抛错)。
推荐写法是同步调用(initNextjsPlugin(armsRum) 紧跟 armsRum.init({...}) 之后),这也是本包示例工程的写法。若使用 remote-config 远端优先模式(init 晚于 hydration),早到的 view 会因 client 未就绪被丢弃并告警 —— 此场景下可改用 armsRum.init({...}).then(() => initNextjsPlugin(armsRum)) 确保首屏 view 就绪率。
