前端监控体系与实践(二):全局监控

继上一篇前端监控体系与实践:从错误上报到内存与 GC 观测,当需要全局监控时该如何实施呢?

监控采集集中为 initClientMonitoring(),在应用入口调用一次。常见做法是在 main.js 中、创建根实例之前调用;若需复用,可封装为 Vue 插件,在 install 中调用同一函数。

init 中通常注册:error、unhandledrejection;对 history.pushState / replaceState 做包装并监听 popstate 以记录路由变化;可按需通过 PerformanceObserver 采集 LCP。若另有 GC 相关探针,可通过自定义事件 frontend-monitor:gc-suspect 由业务侧 dispatchEvent,非必需。


采集模块示例

环境变量命名需与构建工具一致(Vue CLI 常用 VUE_APP_*)。以下为 clientMonitor.js 示例:

js 复制代码
/**
 * 客户端全局监控:错误、未处理 Promise、路由变化、基础 Web Vitals(LCP)。
 * 上报走 window.__FRONTEND_MONITOR_REPORT__(payload)。
 *
 * payload.type 约定:error | unhandledrejection | navigation | web-vital | gc-suspect
 */

function isMonitorEnabled() {
  if (typeof window === "undefined") return false;
  if (process.env.VUE_APP_FRONTEND_MONITOR === "1") return true;
  return process.env.NODE_ENV === "development";
}

function isGcReportEnabled() {
  if (typeof window === "undefined") return false;
  if (process.env.VUE_APP_FRONTEND_MONITOR_GC === "1") return true;
  return process.env.NODE_ENV === "development";
}

function report(payload) {
  const fn = window.__FRONTEND_MONITOR_REPORT__;
  if (typeof fn === "function") {
    try {
      fn(payload);
    } catch (e) {
      /* 上报回调异常不应影响主流程 */
    }
  }
  if (process.env.NODE_ENV === "development") {
    console.debug("[frontend-monitor]", payload);
  }
}

let installed = false;

export function initClientMonitoring() {
  if (typeof window === "undefined" || installed) return;
  if (!isMonitorEnabled()) return;
  installed = true;

  window.addEventListener("error", (ev) => {
    const err = ev.error;
    report({
      type: "error",
      message: ev.message || String(err != null ? err : "unknown"),
      source: ev.filename,
      lineno: ev.lineno,
      colno: ev.colno,
      stack: err instanceof Error ? err.stack : undefined,
    });
  });

  window.addEventListener("unhandledrejection", (ev) => {
    const r = ev.reason;
    const reason =
      r instanceof Error
        ? r.message + "\n" + (r.stack || "")
        : String(r);
    report({ type: "unhandledrejection", reason });
  });

  const path = () =>
    window.location.pathname + window.location.search + window.location.hash;

  report({ type: "navigation", kind: "initial", path: path() });

  window.addEventListener("popstate", () => {
    report({ type: "navigation", kind: "popstate", path: path() });
  });

  const origPush = history.pushState.bind(history);
  history.pushState = function () {
    origPush.apply(history, arguments);
    report({ type: "navigation", kind: "pushState", path: path() });
  };
  const origReplace = history.replaceState.bind(history);
  history.replaceState = function () {
    origReplace.apply(history, arguments);
    report({ type: "navigation", kind: "replaceState", path: path() });
  };

  if (isGcReportEnabled()) {
    window.addEventListener("frontend-monitor:gc-suspect", (ev) => {
      const d = ev.detail;
      if (d) report({ type: "gc-suspect", id: d.id, aliveMs: d.aliveMs });
    });
  }

  try {
    const po = new PerformanceObserver((list) => {
      for (const e of list.getEntries()) {
        if (e.entryType === "largest-contentful-paint") {
          report({
            type: "web-vital",
            name: "LCP",
            value: Math.round(e.startTime),
          });
        }
      }
    });
    po.observe({ type: "largest-contentful-paint", buffered: true });
  } catch (e) {
    /* 浏览器不支持 LCP observer */
  }
}

installed 用于防止重复初始化。上述监听绑定在 window 与 history 上,与具体页面组件无关,不宜分散到各页面的 mounted 中重复注册。


main.js

js 复制代码
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import { initClientMonitoring } from "@/lib/monitoring/clientMonitor";

initClientMonitoring();

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");

若初始化依赖远程配置(例如先请求 /config 再决定是否开启监控),应注意:error 与 unhandledrejection 注册过晚时,可能在脚本加载初期遗漏部分异常。


Vue 插件封装(可选)

js 复制代码
// plugins/clientMonitoring.js
import { initClientMonitoring } from "@/lib/monitoring/clientMonitor";

export default {
  install() {
    initClientMonitoring();
  },
};

// main.js
import ClientMonitoring from "./plugins/clientMonitoring";
Vue.use(ClientMonitoring);

同一插件多次 Vue.use 只会执行一次 install,与模块内 installed 标志可并存,择一即可。


router.afterEach 是否必要

在已包装 history 的前提下,vue-router 使用 History 模式时,路由切换通常会触发 pushState / replaceState,仅按 URL 做埋点或 RUM 时,一般不必再写 afterEach,否则易与历史 API 包装产生重复上报,需在服务端或协议层约定去重。

当需要 路由名称、meta 等无法从 URL 直接还原的信息 (例如实验分组、业务归属)时,可在 router.afterEach 中调用 __FRONTEND_MONITOR_REPORT__ 单独上报;字段需与网关或数据模型一致。


Vue.config.errorHandler

组件渲染与生命周期中的错误未必冒泡至 window 的 error 事件,建议在 main.js 中配置全局 errorHandler:

js 复制代码
Vue.config.errorHandler = (err, vm, info) => {
  const fn = window.__FRONTEND_MONITOR_REPORT__;
  if (typeof fn === "function") {
    try {
      fn({
        type: "error",
        message: err && err.message ? err.message : String(err),
        stack: err instanceof Error ? err.stack : undefined,
        source: info,
      });
    } catch (_) {}
  }
  if (process.env.NODE_ENV === "development") {
    console.error("[vue-error]", err, info);
  }
};

若在祖先组件中使用 errorCaptured 拦截子树错误,应与 errorHandler 的上报策略一并设计,避免同一异常多次上报。


上报接入

js 复制代码
window.__FRONTEND_MONITOR_REPORT__ = function (payload) {
  // sendBeacon / fetch
};

采集逻辑集中在 init 中实现;上报通过全局回调转发,变更采集端点或采样策略时,优先修改该回调或其封装层。

上一篇: 前端监控体系与实践:从错误上报到内存与 GC 观测

相关推荐
前端 贾公子2 分钟前
第 5 章:Codex 从可运行程序到可交付工具
java·前端·数据库
用户3600555790025 分钟前
Kestrel_VLLM - 26 · 发射空档三步曲:事件计时 → nsys → 判别实验
前端
echome88829 分钟前
JavaScript async/await 错误处理:别让 Promise 静默失败
开发语言·前端·javascript·promise
PedroQue991 小时前
v0.2.0 架构升级:class + reactive 重塑状态管理
前端·uni-app
鶴哥只手遮天1 小时前
Angular 22 源码级深度解析-第 02 章 编译链路总览:从模板字符串到 `ɵɵ` 指令
前端·开源·angular.js
尘世中一位迷途小书童1 小时前
别再堆万字 Prompt:用 Agent Skills 给团队写一个「前端 Review」Skill
前端·ai编程·cursor
aixingpan1 小时前
aixingpan.cn API开发文档:api_docs_bichart_common2接口指南
前端·php
super_Omni1 小时前
text/PDF/Word → 解析成纯文本 → 分词 → 词表匹配 → 加权打分 → 低于阈值
前端·chrome
阶进遐1 小时前
浏览器工具自制指南·对话导出(3):消息池设计
前端
SameX1 小时前
HarmonyOS 用 relationalStore 做阅读统计 App —— 5 张表背后的 5 个 schema 决策
前端