Vue 项目怎么做用户行为全链路监控?轻量插件方案详解
线上用户反馈"按钮点了没反应",后台接口日志一片空白------这种场景每个前端都头疼。没有截图、没有复现路径,只能靠猜:是重复点击?路由跳转过早?弹窗遮挡?还是接口悄悄超时?
很多团队选择在业务组件里零散埋点,但随项目膨胀,埋点代码很快变成没人敢删的历史包袱。更合理的做法是封装一个 Vue 全局插件,在入口处挂载一次,统一采集用户行为、异常与接口状态,让排障时有据可循。
一、我们要采集哪些数据?
一个实用的前端行为监控应覆盖:
- 路由变化:页面进入 / 离开及停留轨迹
- 用户交互:点击事件,关键操作需有明确标识
- JS 异常:运行时错误 + 未捕获 Promise rejection
- 接口监控:请求耗时过长或响应异常
- 用户最后 N 步操作(Breadcrumb):排障时比口头描述有用得多
不需要一上来就引入重型 RUM SDK,自己写一个轻量插件完全够用。
二、监控插件核心实现
js
// user-behavior-monitor.js
export function createBehaviorMonitor(options = {}) {
const queue = []
const maxCache = options.maxCache || 30
const endpoint = options.endpoint || '/api/client/track'
let currentPath = ''
// 会话级追踪 ID
function getTraceId() {
let id = sessionStorage.getItem('__trace_id__')
if (!id) {
id = `${Date.now()}-${Math.random().toString(16).slice(2)}`
sessionStorage.setItem('__trace_id__', id)
}
return id
}
// 入队,错误立即上报
function push(event) {
queue.push({
traceId: getTraceId(),
page: currentPath || location.pathname,
ts: Date.now(),
ua: navigator.userAgent,
...event
})
if (queue.length > maxCache) queue.shift()
if (event.level === 'error') flush('error')
}
// 上报:优先 sendBeacon,降级 fetch(keepalive)
function flush(reason = 'normal') {
if (!queue.length) return
const body = JSON.stringify({ reason, events: queue.splice(0, queue.length) })
if (navigator.sendBeacon) {
navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' }))
return
}
fetch(endpoint, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body,
keepalive: true
}).catch(() => {})
}
// 从点击元素向上查找 data-track 或截取 innerText
function resolveClickName(el) {
let node = el, depth = 0
while (node && depth < 4) {
if (node.dataset?.track) return node.dataset.track
if (node.innerText?.trim()) return node.innerText.trim().slice(0, 40)
node = node.parentElement
depth++
}
return el.tagName
}
return {
install(app, { router } = {}) {
app.config.globalProperties.$track = push
// 全局点击采集
document.addEventListener('click', e => {
if (!e.target) return
push({
type: 'click',
target: resolveClickName(e.target),
x: e.clientX,
y: e.clientY
})
}, true)
// JS 错误
window.addEventListener('error', e => {
push({
type: 'js_error', level: 'error',
msg: e.message, file: e.filename,
line: e.lineno, col: e.colno
})
})
// Promise 未捕获异常
window.addEventListener('unhandledrejection', e => {
push({
type: 'promise_error', level: 'error',
msg: String(e.reason?.message ?? e.reason)
})
})
// 页面关闭补发
window.addEventListener('beforeunload', () => flush('leave'))
// Vue Router 埋点
if (router) {
router.beforeEach((to, from, next) => {
push({ type: 'route_leave', from: from.fullPath, to: to.fullPath })
currentPath = to.fullPath
next()
})
router.afterEach(to => {
push({ type: 'route_enter', path: to.fullPath, title: document.title })
})
}
// 定时批量上报
setInterval(() => flush('timer'), options.interval || 8000)
}
}
}
三、在 main.js 中挂载
js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createBehaviorMonitor } from './user-behavior-monitor'
const app = createApp(App)
app.use(router)
app.use(
createBehaviorMonitor({
endpoint: '/track/front',
maxCache: 40,
interval: 10000
}),
{ router }
)
app.mount('#app')
四、关键按钮显式声明埋点标识
自动采集 innerText 对通用按钮够用,但表格行内"删除""退款"等操作必须加 data-track,否则只知道"点了删除"而不知道删的哪条数据:
html
<button data-track="order.pay.submit" @click="pay">立即支付</button>
<button :data-track="`order.refund.${orderId}`" @click="refund">退款</button>
规范:普通交互可自动采集,核心业务链路必须显式命名。
五、接入 Axios 监控请求慢与异常
js
// request.js
import axios from 'axios'
export function createTrackedHttp(app) {
const http = axios.create({ timeout: 12000 })
http.interceptors.request.use(cfg => {
cfg.metadata = { start: Date.now() }
return cfg
})
http.interceptors.response.use(
res => {
const cost = Date.now() - res.config.metadata.start
if (cost > 3000) {
app.config.globalProperties.$track({
type: 'api_slow',
url: res.config.url,
method: res.config.method,
cost
})
}
return res
},
err => {
const cfg = err.config || {}
const cost = cfg.metadata ? Date.now() - cfg.metadata.start : -1
app.config.globalProperties.$track({
type: 'api_error', level: 'error',
url: cfg.url, method: cfg.method, cost,
status: err.response?.status,
msg: err.message
})
return Promise.reject(err)
}
)
return http
}
后端收到带同一 traceId 的 click + api_error 日志后,就能完整还原:
用户进入
/order/submit→ 点击order.pay.submit→ 调/api/pay/create超时 504 → 耗时 12s
前端不用背锅,后端拿 traceId 去网关和应用日志串起来排查即可。
六、几个生产落地的注意点
- 不上报输入框值 :最多记字段名(如
field: 'phone'),绝不采集用户输入内容,避免隐私合规风险。 - 页面白名单 :只监控核心流程页面(如
/order、/pay),低频管理页跳过,减少噪音。 - 批量 + 错误即时上报:平时攒队列定时发,遇 error 或 beforeunload 立即 flush,兼顾性能与完整性。
- 不过度点击上报:高频区域可做采样,避免埋点请求反压页面性能,尤其移动端弱网环境。
好的用户行为监控,核心价值不是"全量埋点",而是能还原用户操作现场------点了哪、跳去哪、接口慢在哪、错误断在哪。能做到这点,它才是排障利器,而不只是漂亮的日志垃圾。
用户点击一次页面就一次记录,会不会太频繁了
「每次 click 都往队列里 push 一条」确实偏多,尤其在大表单页、移动端弱网或列表页疯狂点的情况下,会产生大量低价值日志。生产环境一般会做几层"降频"。
下面给你几种常用、低成本的控制方式,可以按项目敏感度组合用。
✅ 方案一:高频区域采样(最推荐)
对非核心交互,随机或按比例丢弃:
js
// 在 click 监听里
if (Math.random() > (options.sampleRate ?? 0.3)) return // 只采 30%
普通浏览/滚动区点击 → 采样
关键按钮(
data-track存在)→ 必采
js
document.addEventListener('click', e => {
const hasTrack = !!e.target.closest('[data-track]')
if (!hasTrack && Math.random() > 0.3) return
push({ type: 'click', target: resolveClickName(e.target) })
}, true)
✅ 保留排障价值
❌ 不会淹没后端
✅ 方案二:同目标防抖(避免连点暴击)
同一按钮短时间多次点击只记一次:
js
let lastClickKey = ''
let lastClickTime = 0
document.addEventListener('click', e => {
const key = resolveClickName(e.target)
const now = Date.now()
if (key === lastClickKey && now - lastClickTime < 800) return
lastClickKey = key
lastClickTime = now
push({ type: 'click', target: key })
}, true)
适合:
- 提交按钮
- 分页切换
- Tab 切换
✅ 方案三:只采集「有关键标识」的点击(极简方案)
如果你只关心业务链路,可以彻底放弃自动采集 innerText:
js
document.addEventListener('click', e => {
const el = e.target.closest('[data-track]')
if (!el) return
push({
type: 'click',
track: el.dataset.track
})
}, true)
✅ 日志极少、语义清晰
❌ 无法还原"用户乱点哪了"
很多中后台系统最终都会走到这一步。
✅ 方案四:页面级白名单(控制范围)
只在关键流程页面开启点击采集:
js
const enablePages = ['/order', '/pay', '/checkout']
if (!enablePages.some(p => location.pathname.startsWith(p))) return
避免:
- 管理后台列表页
- 长滚动配置页
📌 推荐组合(实战常用)
| 场景 | 策略 |
|---|---|
| 核心按钮 | data-track + 必采 |
| 普通点击 | 采样 20~30% |
| 同按钮 | 800ms 防抖 |
| 页面 | 仅核心流程开启 |
| 错误 / 慢接口 / JS Error | 100% 立即上报 |
一句话总结
不是"点一次记一次",而是:普通点击可丢、可采样;关键操作必记;异常必记。