ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

前端数据埋点(5):框架钩子——Vue 接住的错,window.onerror 听不到

前端数据埋点(5):框架钩子——Vue 接住的错,window.onerror 听不到 上一篇上报——信封怎么打包怎样发出去相关无埋点——重写原生 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 BoundarycomponentDidCatch接住之后同样到不了window.onerror。原生无埋点第 2 篇框架钩子本篇听谁window.onerror、Promise、XHR、pushStateVueerrorHandler、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?couponxx。URL 进栈页面曝光仍要track(PAGE, { trackId: checkout })或等价的路由 name。2. 概念词典词是什么解决什么errorHandlerVue 应用级错误钩子。app.config.errorHandler (err, vm, info) {}组件树里没 catch 的错vm出错时所在组件实例拼组件名、往上爬父组件组件追踪从当前组件爬到根类似PayButton ... Checkout ... App只有 JS stack 对不上.vue文件时靠这个定位Error BoundaryReact 用componentDidCatch接住子树错误的组件白屏降级同时把错交出去采集路由 name路由表里的名字和 path 不是一回事PAGE要对产品页不要对带 query 的 URL3. Vue包一层 errorHandler不要盖掉业务的做法和包装fetch同一类先记下原来的钩子自己的跑完再调用原来的。摘自packages/vue/src/errorhandler.tsexport const attachErrorHandler (app: Vue, options?: PartialVueOptions): 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: Recordstring, 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产品页名checkoutto.matched里的 path 模板/order/:id而不是/order/8821to.params/query动态段。query 里常有 token不要默认整包进信封Sentry 的instrumentVueRouter主业是 Tracing 的 navigation span顺手router.onError里captureException。对埋点来说更贴近产品的是router.afterEach((to){consttrackIdtypeofto.namestring?to.name:to.pathtrack(PAGE,{trackId})})pushState那条行为栈仍然要出错时能看到「从/cart到了/checkout?x1」。PAGE回答「结算页曝光了多少次」。两条不要互相冒充——和第 2 篇「history 默认只进栈」是同一句话这里只是多了路由 name 这个更干净的trackId。onError接的是导航失败守卫里 throw、异步组件加载失败不是组件渲染错误。渲染错误仍走上一节的errorHandler。5. ReactError Boundary 是同一类缺口React 没有应用级errorHandler缺口在 Boundarypublic 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 无埋点记 URLPAGE用 route id / path 模板。不要把?utm当成页面名。6. 和 Tracing 的边界Vue 集成里还有mixin去听mounted/updated给每个组件打 span。那是性能时间线不是PAGE/EVENT。埋点要的Tracing 要的进结算页track(PAGE, { trackId: checkout })一次 navigation span组件 render 慢一般不要每条都当 EVENTui.updatespan组件 throwcaptureException带组件名同一条错误事件上挂 trace id到这里错误入口挂到框架钩子上页面名用路由 name。组件级 span 默认不要全开。体感快慢LCP / INP / CLS下一篇用 Performance 观察不靠再包fetch。下一篇前端数据埋点6web-vitals——三个数怎么算出来和前作怎么接篇补哪一段2无埋点包原生 API框架内部接住的错不经过这些 API3行为栈点击仍进栈setTimeout(0)是为了让栈先写完4上报captureException之后的闸和信封本篇VueerrorHandler/ React Boundary / 路由 name你可以从这里带走什么框架接住的组件错误默认到不了window.onerror。要在errorHandler或 Error Boundary 上再采集一次。包装钩子时先保住业务原来的 handler采集完再转交。信封里带组件名和组件树比只有 JS stack 容易定位.vue/ JSXprops 默认不要带。同一轮里的点击可能还没进栈错误上报挪到下一轮setTimeout(0)更稳。URL 变化进行为栈PAGE用路由 name或 path 模板。组件生命周期 span 是 Tracing不要当成点击量。仓库与相关文档机制例子sentry-javascriptpackages/vue/src/errorhandler.ts、packages/vue/src/router.ts、packages/react/src/errorboundary.tsx相关前作2无埋点 · 4上报本文是「前端数据埋点」第 5 篇框架在原生层之上接住的错误和路由名。不展开 Pinia/Redux 插件和完整 Tracing。
返回列表