欢迎光临
我们一直在努力

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

上一篇:上报——信封怎么打包,怎样发出去 相关:无埋点——重写原生 API · 行为栈

系列:前端数据埋点 · 第 5 篇

前四篇把浏览器原生层走完了:采集 → 整形 → 行为栈 → 上报。本篇是加餐: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 Vue errorHandler、React componentDidCatch、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/8821
    • to.params / query:动态段。query 里常有 token,不要默认整包进信封

    Sentry 的 instrumentVueRouter 主业是 Tracing 的 navigation span,顺手 router.onError 里 captureException。对埋点来说,更贴近产品的是:

    router.afterEach((to) => {
    const trackId = typeof to.name === 'string' ? to.name : to.path
    track('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 和组件树。

    有 fallback UI 时,这条错对用户是「已处理」(页面没整页白),对采集仍要发——否则你只优化了体验,稳定性还是盲区。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.update span
    组件 throw captureException,带组件名 同一条错误事件上挂 trace id

    本篇只要求:错误入口挂到框架钩子上;页面名用路由 name。组件级 span 默认不要全开(Sentry 里 trackComponents 默认就是 false)。


    和前作怎么接

    篇补哪一段
    (2)无埋点 包原生 API;框架内部接住的错不经过这些 API
    (3)行为栈 点击仍进栈;setTimeout(0) 是为了让栈先写完
    (4)上报 captureException 之后的闸和信封
    本篇 Vue errorHandler / 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)上报
    赞(0)
    未经允许不得转载:171主机测评 » 前端数据埋点(5):框架钩子——Vue 接住的错,window.onerror 听不到
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址