上一篇:上报——信封怎么打包,怎样发出去
相关:无埋点——重写原生 API · 行为栈系列:前端数据埋点· 第 5 篇
下一篇:web-vitals——三个数怎么算出来前四篇把浏览器原生层走完了:采集 → 整形 → 行为栈 → 上报。本篇是加餐:Vue / React 在原生 API上面又接了一层。组件里抛的错、路由名字,不一定会落到第 2 篇包过的
window.onerror/pushState上。代码取自 Sentry JavaScript SDK 的@sentry/vue、@sentry/react,讲机制,不是产品接入。
先说结论
第 2 篇的无埋点听的是浏览器出厂接口。Vue 3 把渲染期、生命周期、事件处理函数里没接住的错,交给app.config.errorHandler。钩子一旦接住,默认不会再冒泡成window.onerror。只包原生层,结算页组件里抛的TypeError会从稳定性报表里消失。
React 同类缺口在 Error Boundary:componentDidCatch接住之后,同样到不了window.onerror。
| 原生无埋点(第 2 篇) | 框架钩子(本篇) | |
|---|---|---|
| 听谁 | window.onerror、Promise、XHR、pushState | VueerrorHandler、ReactcomponentDidCatch、Router 守卫 |
组件里throw | 常常听不到 | 能听到,还能带上组件名 |
| 路由 | 只有 URL 变化,进行为栈 | 有路由name,才对得上track('PAGE') |
一句话:框架接住的错,要在框架的钩子上再挂一份采集;不要假定
window.onerror已经够了。
发出去之后仍走第 4 篇那套闸和信封,本篇不重复。
1. 缺口在哪
Vue 渲染PayButton,点击处理函数里user.balance.toFixed(),user是null。框架会:
- 调用
app.config.errorHandler(err, vm, info) - 控制台打出组件追踪
- 不再当成「页面级未捕获错误」交给浏览器
第 2 篇包的window.onerror此时是静的。你在 Sentry / 自建收集里只能看到脚本加载失败、个别unhandledrejection,看不到「支付按钮组件炸了」。
errorHandler的第三个参数info是生命周期名,例如mounted、event handler。这比一句Cannot read property of undefined更接近「在哪一步炸的」。
React 把子树错误收到最近的 Error Boundary。没有 Boundary 才会冲到根、再可能变成全局错误。很多业务会故意加 Boundary 以免白屏,结果稳定性采集被挡在门外。
路由是另一条:第 2 篇pushState只能记from/to两个 URL。产品要的是「进入了结算页」,对应路由表里的name: 'checkout',不是/checkout?coupon=xx。URL 进栈;页面曝光仍要track('PAGE', { trackId: 'checkout' })或等价的路由 name。
2. 概念词典
| 词 | 是什么 | 解决什么 |
|---|---|---|
| errorHandler | Vue 应用级错误钩子。app.config.errorHandler = (err, vm, info) => {} | 组件树里没 catch 的错 |
| vm | 出错时所在组件实例 | 拼组件名、往上爬父组件 |
| 组件追踪 | 从当前组件爬到根,类似PayButton ... Checkout ... App | 只有 JS stack 对不上.vue文件时,靠这个定位 |
| Error Boundary | React 用componentDidCatch接住子树错误的组件 | 白屏降级;同时把错交出去采集 |
| 路由 name | 路由表里的名字,和 path 不是一回事 | PAGE要对产品页,不要对带 query 的 URL |
3. Vue:包一层 errorHandler,不要盖掉业务的
做法和包装fetch同一类:先记下原来的钩子,自己的跑完再调用原来的。
摘自packages/vue/src/errorhandler.ts:
export const attachErrorHandler = (app: Vue, options?: Partial<VueOptions>): void => { const { errorHandler: originalErrorHandler } = app.config; app.config.errorHandler = (error: Error, vm: ViewModel, lifecycleHook: string): void => { const componentName = formatComponentName(vm, false); const trace = vm ? generateComponentTrace(vm) : ''; const metadata: Record<string, unknown> = { componentName, lifecycleHook, trace, }; if (options?.attachProps !== false && vm) { // Vue2 - $options.propsData // Vue3 - $props if (vm.$options?.propsData) { metadata.propsData = vm.$options.propsData; } else if (vm.$props) { metadata.propsData = vm.$props; } } // Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time. setTimeout(() => { captureException(error, { captureContext: { contexts: { vue: metadata } }, mechanism: { handled: !!originalErrorHandler, type: 'auto.function.vue.error_handler' }, }); }); // Check if the current `app.config.errorHandler` is explicitly set by the user before calling it. if (typeof originalErrorHandler === 'function' && app.config.errorHandler) { (originalErrorHandler as UnknownFunc).call(app, error, vm, lifecycleHook); } else { throw error; } }; };几点是机制,不是 Vue 语法课:
| 点 | 为什么 |
|---|---|
| 组件名 + 生命周期 + 组件树 | 信封里除了 JS stack,还能看到「PayButton 在 click 里炸了,父级是 Checkout」 |
setTimeout(0) | 同一轮里的点击可能还没写进行为栈;挪到下一轮再captureException,第 3 篇那列面包屑才带得上「刚点了支付」 |
再调用原来的errorHandler | 业务可能已经用来弹 Toast、打日志;采集不能把它盖掉 |
mechanism.type | 标明来源是 Vue 钩子,方便和window.onerror来的错区分 |
formatComponentName优先用组件name/__name,没有就从.vue文件名猜,根实例写成<Root>。组件追踪从$parent往上爬,和 DOM 路径body > form > button#pay是同一类「往上拼」,只是拼的是组件,不是 HTML 标签。
attachProps默认是开的。埋点场景建议关掉。第 1 篇说过:Vue props 默认不要进信封。订单号、手机号、折扣码经常在 props 里。Sentry Vue 集成默认attachProps: true,是为了排障方便,不是隐私默认。自建或接入时写成attachProps: false,需要某个字段再白名单。
还要先init/ 挂 errorHandler,再app.mount()。已经挂上的应用,这份包装进不去实例。源码里若发现已经 mounted 会打警告。
captureException之后走第 4 篇:beforeSend、采样、信封、原生fetch。框架钩子只补采集入口,不另开一条 HTTP。
4. 路由:URL 进栈,页面名走 PAGE
无埋点已经在听pushState。Vue Router 再包beforeEach/onError,多出来的是路由表信息:
to.name:产品页名(checkout)to.matched里的 path 模板:/order/:id,而不是/order/8821to.params/query:动态段。query 里常有 token,不要默认整包进信封
Sentry 的instrumentVueRouter主业是 Tracing 的 navigation span,顺手router.onError里captureException。对埋点来说,更贴近产品的是:
router.afterEach((to)=>{consttrackId=typeofto.name==='string'?to.name:to.pathtrack('PAGE',{trackId})})pushState那条行为栈仍然要:出错时能看到「从/cart到了/checkout?x=1」。PAGE回答「结算页曝光了多少次」。两条不要互相冒充——和第 2 篇「history 默认只进栈」是同一句话,这里只是多了路由 name 这个更干净的trackId。
onError接的是导航失败(守卫里 throw、异步组件加载失败),不是组件渲染错误。渲染错误仍走上一节的errorHandler。
5. React:Error Boundary 是同一类缺口
React 没有应用级errorHandler,缺口在 Boundary:
public componentDidCatch(error: unknown, errorInfo: React.ErrorInfo): void { const { componentStack } = errorInfo; const { beforeCapture, onError, showDialog, dialogOptions } = this.props; withScope(scope => { if (beforeCapture) { beforeCapture(scope, error, componentStack); } const handled = this.props.handled != null ? this.props.handled : !!this.props.fallback; const eventId = captureReactException(error, errorInfo, { mechanism: { handled, type: 'auto.function.react.error_boundary' }, });errorInfo.componentStack是组件树字符串,作用约等于 Vue 那份trace。captureReactException会把这份栈挂到error.cause上,信封里就能同时看到 JS stack 和组件树。
有fallbackUI 时,这条错对用户是「已处理」(页面没整页白),对采集仍要发——否则你只优化了体验,稳定性还是盲区。handled字段就是这个意思:人接住了,报表仍要记。
React Router 同样:history 无埋点记 URL;PAGE用 route id / path 模板。不要把?utm=当成页面名。
6. 和 Tracing 的边界
Vue 集成里还有mixin去听mounted/updated,给每个组件打 span。那是性能时间线,不是PAGE/EVENT。
| 埋点要的 | Tracing 要的 | |
|---|---|---|
| 进结算页 | track('PAGE', { trackId: 'checkout' }) | 一次 navigation span |
| 组件 render 慢 | 一般不要每条都当 EVENT | ui.updatespan |
| 组件 throw | captureException,带组件名 | 同一条错误事件上挂 trace id |
到这里,错误入口挂到框架钩子上;页面名用路由 name。组件级 span 默认不要全开。体感快慢(LCP / INP / CLS)下一篇用 Performance 观察,不靠再包fetch。
下一篇:前端数据埋点(6):web-vitals——三个数怎么算出来
和前作怎么接
| 篇 | 补哪一段 |
|---|---|
| (2)无埋点 | 包原生 API;框架内部接住的错不经过这些 API |
| (3)行为栈 | 点击仍进栈;setTimeout(0)是为了让栈先写完 |
| (4)上报 | captureException之后的闸和信封 |
| 本篇 | VueerrorHandler/ React Boundary / 路由 name |
你可以从这里带走什么?
- 框架接住的组件错误,默认到不了
window.onerror。要在errorHandler或 Error Boundary 上再采集一次。 - 包装钩子时先保住业务原来的 handler,采集完再转交。
- 信封里带组件名和组件树,比只有 JS stack 容易定位
.vue/ JSX;props 默认不要带。 - 同一轮里的点击可能还没进栈,错误上报挪到下一轮(
setTimeout(0))更稳。 - URL 变化进行为栈;
PAGE用路由 name(或 path 模板)。组件生命周期 span 是 Tracing,不要当成点击量。
仓库与相关文档
- 机制例子:sentry-javascript(
packages/vue/src/errorhandler.ts、packages/vue/src/router.ts、packages/react/src/errorboundary.tsx) - 相关前作:(2)无埋点 · (4)上报
本文是「前端数据埋点」第 5 篇:框架在原生层之上接住的错误和路由名。不展开 Pinia/Redux 插件和完整 Tracing。