#日志采集
前端日志采集是线上问题排查和数据分析的基础。与性能指标不同,日志更关注业务层面的用户行为和异常信息。
#错误日志
#采集指标
| 类型 | 描述 |
|---|---|
| JavaScript 运行时错误 | SyntaxError、TypeError、ReferenceError 等 |
| 资源加载失败 | 图片、脚本、样式表等静态资源 404/500 |
| Promise 未捕获异常 | unhandledrejection 捕获的拒绝 |
| 框架层错误 | React ErrorBoundary、Vue errorCaptured |
#采集方案
// JS 运行时错误 + 资源加载失败
window.addEventListener(
'error',
(e) => {
if (e.target !== window) {
report({ type: 'resource-error', path: location.pathname, msg: e.target.src || e.target.href })
} else {
report({
path: location.pathname,
stack: e.error?.stack,
filename: e.filename,
lineno: e.lineno,
type: 'js-error',
colno: e.colno,
msg: e.message,
})
}
},
true
)
// Promise 未捕获异常
window.addEventListener('unhandledrejection', (e) => {
report({
msg: e.reason?.message || e.reason,
type: 'unhandledrejection',
path: location.pathname,
stack: e.reason?.stack,
})
})#行为日志
#采集指标
| 类型 | 描述 | 触发方式 |
|---|---|---|
| PV / UV | 页面浏览量、独立访客数 | 路由变化 |
| 停留时长 | 用户在每个页面的驻留时间 | 路由变化 |
| 点击热区 | 用户点击位置分布 | 事件代理 |
| 页面跳出率 | 仅访问一页就离开的比例 | 页面关闭 |
#采集方案
// 路由变化时(进入新页面 + 补发上一页的 page_hidden 以计算停留时长)
const prev = { path: location.pathname }
const reportPageView = (path) => {
report({ type: 'page_hidden', path: prev.path, time: Date.now() })
prev.path = path
report({ type: 'page_view', path, time: Date.now() })
}
;['pushState', 'replaceState'].forEach((m) => {
const orig = history[m]
history[m] = (...args) => {
orig.apply(history, args)
reportPageView(location.pathname)
}
})
window.addEventListener('popstate', () => reportPageView(location.pathname))
// 点击热区
document.addEventListener('click', (e) => {
const t = e.target.closest('[data-report]')
if (t) report({ type: 'click', path: location.pathname, time: Date.now(), element: t.dataset.report })
})
// 页面关闭时(用于计算跳出率)
window.addEventListener('beforeunload', () => report({ type: 'leave', path: location.pathname, time: Date.now() }))#指标计算
| 指标 | 计算方式 |
|---|---|
| PV | page_view 事件总数 |
| UV | 独立访客数,按 uid 去重 |
| 停留时长 | 配对的 page_hidden.time 减去 page_view.time,后求平均 |
| 点击热区 | 按 element 分组,统计每个元素的点击次数 |
| 页面跳出率 | 1 次 page_view 且有 leave 的 uid 数 / 有 leave 的 uid 数 |
#接口日志
#采集指标
| 类型 | 描述 |
|---|---|
| 接口成功率 | 成功请求 / 总请求 |
| 接口响应时间 | P50 / P95 / P99 分布 |
| 接口状态码 | 2xx / 4xx / 5xx 占比 |
#采集方案
axios.interceptors.request.use((config) => {
config.metadata = { startTime: Date.now() }
return config
})
axios.interceptors.response.use(
(response) => {
report({
duration: Date.now() - response.config.metadata.startTime,
url: response.config.url,
type: 'api_success',
})
return response
},
(error) => {
const config = error.config || {}
report({ type: 'api_error', url: config.url, status: error.response?.status })
return Promise.reject(error)
}
)#环境日志
#采集指标
| 类型 | 描述 | 采集方式 |
|---|---|---|
| 浏览器类型/版本 | Chrome、Firefox、Safari 等及版本号 | UA 解析 |
| 操作系统 | iOS、Android、Windows、macOS | UA 解析 |
| 屏幕分辨率 | DPR、屏幕尺寸 | window.screen |
#采集方案
const getEnvInfo = () => {
const ua = navigator.userAgent
const screen = window.screen
return {
ua,
browser: parseUA(ua).name + ' ' + parseUA(ua).version,
screen: `${screen.width}x${screen.height}`,
cookieEnabled: navigator.cookieEnabled,
language: navigator.language,
dpr: window.devicePixelRatio,
os: parseOS(ua),
}
}#日志上报策略
#批量上报
统一入口 report 负责缓存,阈值触发或定时批量发送:
const logBuffer = []
const report = (data) => {
logBuffer.push(data)
if (logBuffer.length >= 20) flush()
}
const flush = () => {
if (logBuffer.length === 0) return
navigator.sendBeacon('/analytics', JSON.stringify([...logBuffer]))
logBuffer.length = 0
}
setInterval(flush, 3000)
// 页面隐藏时立即上报,避免定时器被延迟
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush()
})#数据脱敏
上报前对敏感信息做处理:
const sanitize = (data) => {
const sensitive = /phone|pwd|token|cookie/gi
return JSON.stringify(data).replace(sensitive, '[filtered]')
}