继上一篇前端监控体系与实践:从错误上报到内存与 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 示例:
/**
* 客户端全局监控:错误、未处理 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
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 插件封装(可选)
// 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:
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 的上报策略一并设计,避免同一异常多次上报。
上报接入
window.__FRONTEND_MONITOR_REPORT__ = function (payload) {
// sendBeacon / fetch
};
采集逻辑集中在 init 中实现;上报通过全局回调转发,变更采集端点或采样策略时,优先修改该回调或其封装层。