日志采集

前端日志采集是线上问题排查和数据分析的基础。与性能指标不同,日志更关注业务层面的用户行为和异常信息

错误日志

采集指标

类型描述
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() }))

指标计算

指标计算方式
PVpage_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、macOSUA 解析
屏幕分辨率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]')
}