《从零开始搭建前端监控平台》-学习笔记

/post/fe-monitor article cover image

前端性能优化的前提是获取前端性能数据的现状然后才能在此基础上进行性能优化所以前端性能监控平台是必不可少的

主要功能

一边了解用户的运行环境、更好的解决问题前端监控平台需要的主要功能能有:

  • 系统、设备统计
  • 点击量统计
  • 用户在线市场统计
  • 用户登陆状况、服务器页面加载状况、混合App内部报错检测和服务器接口返回报错统计
  • 预警提示(微信报警、邮件报警、短信报警)

上报数据

监控数据分为两部分自动上报手动上报所以需要提供标准、统一的上报数据方法集合,也称为数据SDK。

自动上报数据又分为错误类型数据性能相关数据环境相关数据:

  • 错误类型数据:通过对全局错误事件、全局异常处理的监听获取、主要是常见的JavaScript语法错误、运行错误、资源加载错误等
js
// 全局错误事件监听
window.addEventListener('error', function(event) {
  // 过滤target为window的异常,避免与onerror重复
  var errorTarget = event.target;
  var errorName = errorTarget.nodeName.toUpperCase();
  if (errorTarget !== window && errorTarget.nodeName && LOAD_ERROR_TYPE[errorName]) {
    handleError(formatLoadError(errorTarget))
  } else {
    // onerror会被覆盖,因此转为使用Listener进行监控
    let { message, filename, lineno, colno, error } = event;
    handleError(formatRunTimeError(message, filename, lineno, colno, error))
  }
}, true)

// 监听扑火到未处理的Promise错误
window.addEventListener('unhandledrejection', function(event) {
  handleError(event);
}, ture)

// 针对vue报错重写console.error
console.error = (function(origin) {
  return function(info) {
    var errorLog = {
      type: ERROR_CONSOLE,
      desc: info
    }
    handleError(errorLog)
    origin.call(console, info)
  }
})(console.error)
  • 性能相关数据:通常为performance API中的性能指标数据,包括但不限于timing.navigationStarttiming.fetchStart

    • DNS查询耗时 > DNS解析耗时 > domainLookupEnd - domainLookupStart: 对开发者的CDN服务器是否正确做出反馈
    • 请求响应耗时 > 网络请求耗时 > responseStart - requestStart: 对返回模版中同步数据的情况做出反馈
    • DOM解析耗时 > DOM解析耗时 > domInteractive - responseEnd: 从中可以得出DOM结构是否合理,以及是否有js阻塞页面的解析
    • 内容传输耗时 > TCP连接耗时 > responseStart - responseEnd: 从中可以检测处我们的网络同路是否正常,大多数情况是网络或者运营商本身的问题
    • 资源加载耗时 > 资源加载耗时 > loadEventStart - domContentLoadedEventEnd: 一般情况下是文档的下载时间,主要是观察一下文档流体积是否过大
    • DOM_READY耗时 > DOM阶段渲染时 > domContentLoadedEventEnd - fetchStart: DOM树解析完成后,网页内容资源加载完成的时间(如js脚本加载执行完成),这个阶段一般情况下可能会触发domContentLoaded事件
    • 首次渲染耗时 > 首次渲染时间 / 白屏时间 > responseEnd - fetchStart: 浏览器去加载文档到用户能看到第一帧非空图像的事件,也叫白屏时间
    • 首次可交互耗时 > 首次可交互时间 > domInterative - fetchSart: DOM树解析完成的时间,本阶段Document.readyState状态值为interactive,并且会抛出readyStateChange事件
    • 首包时间耗时 > 首包时间 > responseStart - domainLookupStart: 浏览器对文档发起查找DNS表的请求,到返回给浏览器第一个字节数据的事件。这个时间通常反馈的是DNS解析查找的时间
    • 页面完全加载耗时 > 页面完全加载时间 > loadEventStart - fetchStart: 下载整个页面的总时间,一般情况下指浏览器对一个URL发起请求到这个URL上的所需文档下载下来的时间。这个数据主要收到网络环境、文档大小的影响
    • SSL连接耗时 > SSL安全连接耗时 > connectEnd - secureConnectionStart: 反馈的是数据安全性、完整性建立耗时
    • TCP连接耗时 > TCP连接耗时 > connectEnd - connectStart: 建立连接过程的耗时,TCP协议主要是工作于传输层,是一种比UDP更为安全的传输协议
js
window.onload = () => {
  const isPerformanceFlagOn = _.get(commonConfig, ['record', 'performance'], _.get(DEFAULT_CONFIG, ['record', 'performance']));
  const isOldPerformanceFlagOn = _.get(commonConfig, ['performance'], false);
  const needRecordPerformance = isPerformanceFlagOn || isOldPerformanceFlagOn;

  if (needRecordPerformance === false) {
    debugLogger('config.recotd.performance值为false,跳过性能指标打点')
    return
  }

  const performance = window.performance
  if (!performance) {
    console.log('当前浏览器不支持performance接口')
    return
  }

  debugLogger('发送页面性能指标数据,上报内容 =>', {
    ...times,
    url: `${window.location.host}${window.location.pathname}`
  })

  log('perf', 20001, {
    ...times,
    url: `${window.location.host}${window.location.pathname}`
  })
}
  • 环境相关数据:通常为userAgent相关的环境数据和业务相关数据(uidsidmid),也称为公共数据即需要上报的数据(性能、报错)都会有的环境数据用以描述错误产生时间的用户状态

    • pid(projectId): 用以描述接入业务产品的id
    • uid(userId): 用以有任务数据去重方便追踪查询的唯一id
    • sid(sessionId): 用以登录相关操作下cookie种下的唯一标识
    • version: 用以记录版本的编号
    • ua(userAgent): 通过浏览器获取的默认ip、型号、操作系统、版本等通用环境信息的数据
    • 开关类数据: 检测从收集到上报链路的数据(是否采集错误数据、是否采集性能数据、数据是否为测试数据),但是不会入库
js
const DEFAULT_CONFIG = {
  pid: '',
  uid: '',
  sid: '',
  is_test: false,
  record: {
    time_on_page: true,       // 是否监控用户在线时长数据
    performance: true,        // 是否监控页面载入性能
    js_error: true,           // 是否监控页面报错信息
    js_error_report_config: { // 配置需要监控的页面报错类型,仅在js_error为true时生效
      ERROR_RUNTIME: true,    // JS运行时报错
      ERROR_SCRIPT: true,     // JS资源加载失败
      ERROR_STYLE: true,      // CSS资源加载失败
      ERROR_IMAGE: true,      // 图片资源加载失败
      ERROR_AUDIO: true,      // 音频资源加载失败
      ERROR_VIDEO: true,      // 视频资源加载失败
      ERROR_CONSOLE: true,    // Vue资源加载失败
      ERROR_TRY_CATCH: true,  // 未抓去错误
      checkErrorNeedReport: (desc = '', stack = '') => { // 自定义检测函数,判断是否报告该错误
        return true
      }
    }
  },
  version: '1.0.0'
}
js
/**
 *
 * @param {类型} type
 * @param {code码} code
 * @param {消费数据} detail
 * @param {展示数据} extra
 */
const log = (type = '', code, detail = {}, extra = {}) => {
  const errorMsg = validLog(type, code, detail, extra)
  if (errorMsg) {
    clog(errorMsg)
    return errorMsg
  }

  // 调用自定义函数, 计算pageType
  let getPageTypeFunc = _.get(
    commonConfig,
    ['getPageType'],
    _.get(DEFAULT_CONFIG, ['getPageType'])
  )
  let location = window.location
  let pageType = location.href
  try {
    pageType = '' + getPageTypeFunc(location)
  } catch (e) {
    debugLogger(`config.getPageType执行时发生异常, 请注意, 错误信息=>`, { e, location })
    pageType = `${location.host}${location.pathname}`
  }

  const logInfo = {
    type,
    code,
    detail: detailAdapter(code, detail),
    extra: extra,
    common: {
      ...commonConfig,
      timestamp: Date.now(),
      runtime_version: commonConfig.version,
      sdk_version: config.version,
      page_type: pageType
    }
  }
  // 图片打点
  const img = new window.Image()
  img.src = `${feeTarget}?d=${encodeURIComponent(JSON.stringify(logInfo))}`
}
js
export const Elog = log.error = (code, detail, extra) => {
  return log('error', code, detail, extra)
}
export const Plog = log.product = (code, detail, extra) => {
  return log('product', code, detail, extra)
}
export const Ilog = log.info = (code, detail, extra) => {
  return log('info', code, detail, extra)
}

<Callout type="info">自动上报数据的优势在于不破坏业务逻辑代码,只要在业务代码引入相关SDK即可,业务代码层面不需要改动</Callout>

手动上报数据主要分为用户行为数据流程错误数据用以根据拟定好的规则收集在匹配逻辑错误时手动触发数据上报

  • 用户行为数据:主要是用户平均在线时长、用户菜单点击量等
js
let lastTime = Date.now()
const SEND_MILL = 5 * 1000 // 每5s打点一次
const OFFLINE_MILL = 15 * 60 * 1000 // 15分钟不操作认为不在线
window.addEventListener('click', () => {
  // 检查是否监控用户在线时长
  const isTimeOnPageFlagOn = _.get(
    commonConfig,
    ['record', 'time_on_page'],
    _.get(DEFAULT_CONFIG, ['record', 'time_on_page'])
  )
  const isOldTimeOnPageFlagOn = _.get(commonConfig, ['online'], false)
  const needRecordTimeOnPage = isTimeOnPageFlagOn || isOldTimeOnPageFlagOn
  if (needRecordTimeOnPage === false) {
    debugLogger(`config.record.time_on_page值为false, 跳过停留时长打点`)
    return
  }

  const now = Date.now()
  const duration = now - lastTime
  if (duration > OFFLINE_MILL) {
    lastTime = Date.now()
  } else if (duration > SEND_MILL) {
    lastTime = Date.now()
    debugLogger('发送用户留存时间埋点, 埋点内容 => ', { duration_ms: duration })
    // 用户在线时长
    log.product(10001, { duration_ms: duration })
  }
}, false)
  • 流程错误数据:主要在全局错误事件不存在的情况下,或在全局异常处理中无法监听的错误数据
js
log('login_error', '', {
  errMsg: '登录失败',
  exVersion: `${pkg.version}`
})

上报形式

  • Http请求发送(get、head、post)
  • 加载资源发送(javascript srccss hrefImage src),相比http请求资源加载发送具有更好的跨域支持但是(scriptcss)存在阻塞也看渲染的问题

<Callout type="info">使用图片进行数据上报的优势:既不用考虑跨域的前提下在不支持javascript的浏览器中依然可以使用img标签进行发起。使用透明图片既不影响页面正常展示而且能够大大缩减体积,经比较BMPPNGGIF三者中GIF体积最小</Callout>

监控平台架构

监控平台架构由展示层服务层(Service)支撑层(Brace)组成

  • 展示层:数据展示逻辑基础框架设施及可视化
  • 服务层:业务逻辑处理不局限于数据的查询、聚合、拆分、变换、权限及api
  • 支撑层:在其他业务系统中也叫数据层,在监控业务中主要负责数据的日志处理、数据清洗、存储、加工、报警、任务调度等

根据不同职责的重要程度开发顺序也应该从支撑层(开发完成后不会怎么变动) > 服务层(相较于支撑层变动大一些) > 展示层(获取数据后才能进行展示)

数据处理

消息系统由生产者 > 代理 > 消费者组成,常见的是典型的Kafka集群中包含若干个消息生产者、若干个消息消费者和一个ZooKeeper集群在此业务中充当消息队列使用

  • 代理(Broker):会临时存储由生产者生产的新的消息,并且形成队列
  • 每条发布到Kafka集群的消息都有一个主题(Topic),这个主题也被称为Topic。消息生产者和消息消费者都在春时花的时候需要指定主题,相同主题的生产者的消息只能被相同主题的消费者消费
  • 生产者(Producer):负责生产指定主题的消息
  • 消费者(Consumer):负责消费指定主题的消息
  • 推送消息(Push):主要通知Broker有新消息,并且把消息发送给Broker
  • 拉取消息(Pull):每隔一段时间会拉取Broker上未被自己消费的新消息

数据可用性判断:数据合法性数据时间是否测试数据