目录

浅谈前端统计

监控系统在现在的项目中是必不可少的,它具有性能监控、错误监控以及数据上报等功能。通过对的错误监控、用户正常行为的收集,从而指定对应的优化方案、营销策略以及项目的迭代。 试想,如果没有这些监控系统,那么线上的运行的项目出问题了,定位是比较困难的,这时候修复起来就比较劳神了。我记录下我所了解的,常见的方式。

1.错误监控

我们对于错误的处理,在js中很常见的错误捕获try catchpromise中的catch等等。在window对象上有一个方法onerror,可以拿到当前报错的信息,我们就可以将这个信息拿到然后提交到我们自己的监控系统,还有个很重要的原因,不处理这些错误,导致页面挂了停止加载,那损失可就大了。对于抛出的错误,它有几个属性

1
2
3
4
5
6
7
8
// 定义一个错误对象
const defaults = {
  msg: '', // 错误的具体信息
  url: '', // 错误所在的url
  line: '', // 错误所在的行
  col: '', // 错误所在的列
  nowTime: '' // 时间
}

然后实现window.onerror方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
window.onerror = function (msg, url, line, col, error) {
  col = col || (window.event && window.event.errorCharacter) || 0

  defaults.url = url
  defaults.line = line
  defaults.col = col
  defaults.nowTime = new Date().getTime()

  if (error && error.stack) {
    // 如果浏览器有错误堆栈信息,直接使用
    defaults.msg = error.stack.toString()
  } else if (arguments.callee) {
    // 通过callee拿堆栈信息
    let ext = []
    let fn = arguments.callee.caller
    // 最多拿到三层
    let floor = 3
    while (fn && (--floor > 0)) {
      ext.push(fn.toString())
      if (fn === fn.caller) {
        break
      }
      fn = fn.caller
    }
    ext = ext.join(',')
    defaults.msg = error && error.stack && error.stack.toString()
  }
  let str = ''
  // 格式化这些错误信息
  for (const i in defaults) {
    if (!defaults[i]) {
      defaults[i] = 'null'
    }
    str += '&' + i + '=' + defaults[i].toString()
  }
  // 确定错误是由哪位用户引起的,这一步有些时候可以不用
  let userinfo = // 拿到用户信息
  if (typeof userinfo === 'object') {
    userinfo = JSON.stringify(userinfo)
  }
  if (userinfo) userinfo = encodeURIComponent(userinfo)
  str = encodeURIComponent(str.replace('&', '').replace('\n', '').replace(/\s/g, ''))
	
  // 避免出现跨域错误
  new Image().src = 'api地址?msg=' + str + '&userinfo=' + userinfo
}

这种方式的缺陷在于,没办法处理promise中未处理的错误,对于promise,推荐使用unhandledrejection事情处理

1
2
3
4
5
6
7
8
9
window.addEventListener('unhandledrejection', event => {
  let error = event.reason && event.reason.message
  if (!error) {
    error = typeof event.reason === 'object' ? JSON.stringify(event.reason) : event.reason
  }

  (new Image()).src = 'api地址?msg=' + encodeURIComponent(error)
})

2.埋点

埋点是一种常见的统计用户行为的方式或工具。通过在页面设置埋点,来统计用户对于某个页面、广告、功能的使用频率、喜好,然后通过对埋点的数据统计,确定好页面或功能的迭代。简单的做法,设置一个全局方法用来提交埋点点击的行为,在需要的地方调用这个方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 定义一个全局方法
// 传入的point为埋点数据
function clickPoint(point = {}) {
  const { id, name } = point
  cosnt userInfo = // 通过对应的操作拿到用户信息
  // 基础参数
  const basicParams = {
    id: id || '',
    name: name || ''
  }
  // 用户信息
 	const userParams = {
  	name: userInfo.name || ''
    ......
 	}
  // 合并参数
  const params = {
    ...basicParams,
    ...userParams
  }
  let str = ''
  for (const i in params) {
    if (!params[i]) {
      params[i] = 'null'
    }
    str += '&' + i + '=' + params[i].toString()
  }
  new Image().src = 'api地址?msg=' + str
}

通常这样来统计页面上某个功能、某个广告的点击量,获取对应的统计数据。还有一种比较常见的方式,为需要添加埋点的html标签上加上某个约定好的自定义属性,如<div data-link="111"></div>,这个111就是就是实现生成好或者规划好的埋点。然后给这种属性添加全局的点击事件,点击后同样的将对应的数据提交到自己的后台系统去。有些时候为了方便查阅或者直观的观看统计数据,还可以在页面渲染的时候,为含有data-link属性的标签生成对应的一些图标或者文字标签,使用absolute定位显示在对应的元素上,当然这种方式在你添加买点埋点属性的时候,需要给这个元素设置相应的position css属性;当然也可能设置一些热力图啊等其它形式,反正都是实现同一个效果的。

3.白屏时间

这个之前说过,就不说废话了,白屏时间的统计是非常重要的。比如有些时候,只有某个用户的某个设备出现了页面加载过长,页面没内容,发现这个问题后,通过判断错误日志和白屏时间才能更快的定位问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
window.logInfo = {}
window.logInfo.openTime = window.performance && window.performance.timing.navigationStart || 0
window.logInfo.whiteScreenTime = +new Date() - window.logInfo.openTime
window.logInfo.mobile = mobileType()

// 使用 DOMContentLoaded 统计页面有内容的时间
document.addEventListener('DOMContentLoaded', function () {
  window.logInfo.readyTime = +new Date() - window.logInfo.openTime
})
window.onload = function () {
  window.logInfo.allloadTime = +new Date() - window.logInfo.openTime
  window.logInfo.nowTime = new Date().getTime()
  let timname = {
    whiteScreenTime: '白屏时间',
    readyTime: '用户可操作时间',
    allloadTime: '总下载时间',
    mobile: '使用设备',
    nowTime: '时间',
  }
  let logStr = ''
  for (const i in timname) {
    if (i === 'mobile') {
      logStr += '&' + i + '=' + window.logInfo[i]
    } else {
      logStr += '&' + i + '=' + window.logInfo[i]
    }
  }
  // 这里如果需要的话,还可以把用户的一些信息收集出来
  (new Image()).src = 'api地址?msg=' + logStr
}

function mobileType() {
  const u = navigator.userAgent
  // 移动终端浏览器版本信息
  const type = {
    ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), // ios终端
    iPad: u.indexOf('iPad') > -1, // 是否iPad
    android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1, // android终端或者uc浏览器
    iPhone: u.indexOf('iPhone') > -1 || u.indexOf('Mac') > -1, // 是否为iPhone或者QQHD浏览器
    trident: u.indexOf('Trident') > -1, // IE内核
    presto: u.indexOf('Presto') > -1, // opera内核
    webKit: u.indexOf('AppleWebKit') > -1, // 苹果、谷歌内核
    gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') === -1, // 火狐内核
    mobile: !!u.match(/AppleWebKit.*Mobile/i) || !!u.match(/MIDP|SymbianOS|NOKIA|SAMSUNG|LG|NEC|TCL|Alcatel|BIRD|DBTEL|Dopod|PHILIPS|HAIER|LENOVO|MOT-|Nokia|SonyEricsson|SIE-|Amoi|ZTE/), // 是否为移动终端
    webApp: u.indexOf('Safari') === -1 // 是否web应该程序,没有头部与底部
  }
  const lists = Object.keys(type)
  for (const i = 0; i < lists.length; i++) {
    if (type[lists[i]]) {
      return lists[i]
    }
  }
}