1. 当你的应用频繁报错找不到原因的时候。
  2. 需要分析用户兴趣爱好、购买习惯。
  3. 需要优化程序的时候,可以做监控收集数据,做针对性的优化。
  4. 需要保证服务可靠性稳定性。

事前预警:提前设置一个阈值,当监控的数据达到阈值时,通过短信或者邮件通知管理员。例如 API 请求数量突然间暴涨,就得进行报警,否则可能会造成服务器宕机。

事后分析:通过监控日志文件,分析故障原因和故障发生点。从而做出修改,防止这种情况再次发生。

本章内容分为前端监控原理分析和如何对项目实行监控两个部分。第一部分有三个小节:数据采集、数据上报、扩展;第二部分只有一个小节:如何使用 实现项目监控。

好了,下面让我们开始进入正文吧。

数据采集

性能数据采集需要使用 window.performance API。

从 MDN 的文档可以看出, 包含了页面加载各个阶段的起始及结束时间。

这些属性需要结合下图一起看,更好理解:

在这里插入图片描述

为了方便大家理解 timing 各个属性的意义,我在知乎找到一位网友对于 timing 写的简介,在此转载一下。

通过以上数据,我们可以得到几个有用的时间:

  1. // 重定向耗时
  2. redirect: timing.redirectEnd - timing.redirectStart,
  3. // DOM 渲染耗时
  4. dom: timing.domComplete - timing.domLoading,
  5. // 页面加载耗时
  6. load: timing.loadEventEnd - timing.navigationStart,
  7. // 页面卸载耗时
  8. unload: timing.unloadEventEnd - timing.unloadEventStart,
  9. // 请求耗时
  10. request: timing.responseEnd - timing.requestStart,
  11. // 获取性能信息时当前时间
  12. time: new Date().getTime(),

还有一个比较重要的时间就是白屏时间,它指从输入网址,到页面开始显示内容的时间。

将以下脚本放在 </head> 前面就能获取白屏时间。

  1. <script>
  2. whiteScreen = new Date() - performance.timing.navigationStart
  3. // 通过 domLoading 和 navigationStart 也可以
  4. whiteScreen = performance.timing.domLoading - performance.timing.navigationStart
  5. </script>

通过这几个时间,就可以得知页面首屏加载性能如何了。

另外,通过 window.performance.getEntriesByType('resource') 这个方法,我们还可以获取相关资源(js、css、img…)的加载时间,它会返回页面当前所加载的所有资源。

它一般包括以下几个类型:

  • sciprt
  • link
  • img
  • css
  • fetch
  • other
  • xmlhttprequest

我们只需用到以下几个信息:

  1. // 资源的名称
  2. name: item.name,
  3. // 资源加载耗时
  4. duration: item.duration.toFixed(2),
  5. // 资源大小
  6. size: item.transferSize,
  7. // 资源所用协议
  8. protocol: item.nextHopProtocol,

现在,写几行代码来收集这些数据。

  1. // 收集性能信息
  2. const getPerformance = () => {
  3. if (!window.performance) return
  4. const timing = window.performance.timing
  5. const performance = {
  6. // 重定向耗时
  7. redirect: timing.redirectEnd - timing.redirectStart,
  8. // 白屏时间
  9. whiteScreen: whiteScreen,
  10. // DOM 渲染耗时
  11. dom: timing.domComplete - timing.domLoading,
  12. // 页面加载耗时
  13. load: timing.loadEventEnd - timing.navigationStart,
  14. // 页面卸载耗时
  15. unload: timing.unloadEventEnd - timing.unloadEventStart,
  16. // 请求耗时
  17. request: timing.responseEnd - timing.requestStart,
  18. // 获取性能信息时当前时间
  19. time: new Date().getTime(),
  20. }
  21. return performance
  22. }
  23. // 获取资源信息
  24. const getResources = () => {
  25. if (!window.performance) return
  26. const data = window.performance.getEntriesByType('resource')
  27. const resource = {
  28. xmlhttprequest: [],
  29. css: [],
  30. other: [],
  31. script: [],
  32. img: [],
  33. link: [],
  34. fetch: [],
  35. // 获取资源信息时当前时间
  36. time: new Date().getTime(),
  37. }
  38. data.forEach(item => {
  39. const arry = resource[item.initiatorType]
  40. arry && arry.push({
  41. // 资源的名称
  42. name: item.name,
  43. // 资源加载耗时
  44. duration: item.duration.toFixed(2),
  45. // 资源大小
  46. size: item.transferSize,
  47. // 资源所用协议
  48. protocol: item.nextHopProtocol,
  49. })
  50. })
  51. return resource
  52. }

小结

通过对性能及资源信息的解读,我们可以判断出页面加载慢有以下几个原因:

  1. 资源过多、过大
  2. 网速过慢
  3. DOM 元素过多

除了用户网速过慢,我们没办法之外,其他两个原因都是有办法解决的,关于如何做性能优化我们将在下一章学习。

PS:其实页面加载慢还有其他原因,例如没有使用按需加载、没有使用 CDN 等等。不过在这里我们强调的是仅通过对性能和资源信息的解读来得知原因。

错误数据采集

目前所能捕捉的错误有三种:

  1. 资源加载错误,通过 addEventListener('error', callback, true) 在捕获阶段捕捉资源加载失败错误。
  2. js 执行错误,通过 window.onerror 捕捉 js 错误。
  3. promise 错误,通过 addEventListener('unhandledrejection', callback)捕捉 promise 错误,但是没有发生错误的行数,列数等信息,只能手动抛出相关错误信息。

我们可以建一个错误数组变量 errors 在错误发生时,将错误的相关信息添加到数组,然后在某个阶段统一上报,具体如何操作请看下面的代码:

小结

通过错误收集,可以了解到网站发生错误的类型及数量,从而做出相应的调整,以减少错误发生。

性能数据上报

性能数据可以在页面加载完之后上报,尽量不要对页面性能造成影响。

  1. window.onload = () => {
  2. // 在浏览器空闲时间获取性能及资源信息
  3. // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
  4. if (window.requestIdleCallback) {
  5. window.requestIdleCallback(() => {
  6. monitor.performance = getPerformance()
  7. monitor.resources = getResources()
  8. })
  9. } else {
  10. setTimeout(() => {
  11. monitor.performance = getPerformance()
  12. monitor.resources = getResources()
  13. }, 0)
  14. }
  15. }

当然,你也可以设一个定时器,循环上报。不过每次上报最好做一下对比去重再上报,避免同样的数据重复上报。

错误数据上报

我在 DEMO(在小节末尾) 里提供的代码,是用一个 errors 数组收集所有的错误,再在某一阶段统一上报(延时上报)。

其实,也可以改成在错误发生时上报(即时上报)。这样可以避免“收集完错误,但延时上报还没触发,用户却已经关掉网页导致错误数据丢失”的问题。

  1. window.onerror = function(msg, url, row, col, error) {
  2. const data = {
  3. type: 'javascript',
  4. row: row,
  5. col: col,
  6. msg: error && error.stack? error.stack : msg,
  7. url: url,
  8. // 错误发生的时间
  9. time: new Date().getTime(),
  10. }
  11. // 即时上报
  12. axios.post({ url: 'xxx', data, })
  13. }

另外,还可以使用 来进行上报。

  1. window.addEventListener('unload', logData, false);
  2. function logData() {
  3. navigator.sendBeacon("/log", analyticsData);
  4. }

它的技术特点是:

使用 sendBeacon() 方法会使用户代理(浏览器)在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。

DEMO 代码

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <script>
  8. function monitorInit() {
  9. const monitor = {
  10. // 数据上传地址
  11. url: '',
  12. // 性能信息
  13. performance: {},
  14. // 资源信息
  15. // 错误信息
  16. errors: [],
  17. // 用户信息
  18. user: {
  19. // 屏幕宽度
  20. screen: screen.width,
  21. // 屏幕高度
  22. height: screen.height,
  23. // 浏览器平台
  24. platform: navigator.platform,
  25. // 浏览器的用户代理信息
  26. userAgent: navigator.userAgent,
  27. // 浏览器用户界面的语言
  28. language: navigator.language,
  29. },
  30. // 手动添加错误
  31. addError(error) {
  32. const obj = {}
  33. const { type, msg, url, row, col } = error
  34. if (type) obj.type = type
  35. if (msg) obj.msg = msg
  36. if (url) obj.url = url
  37. if (row) obj.row = row
  38. if (col) obj.col = col
  39. obj.time = new Date().getTime()
  40. monitor.errors.push(obj)
  41. },
  42. // 重置 monitor 对象
  43. reset() {
  44. window.performance && window.performance.clearResourceTimings()
  45. monitor.performance = getPerformance()
  46. monitor.resources = getResources()
  47. monitor.errors = []
  48. },
  49. // 清空 error 信息
  50. clearError() {
  51. monitor.errors = []
  52. },
  53. // 上传监控数据
  54. upload() {
  55. // 自定义上传
  56. // axios.post({
  57. // url: monitor.url,
  58. // data: {
  59. // performance,
  60. // resources,
  61. // errors,
  62. // user,
  63. // }
  64. // })
  65. },
  66. // 设置数据上传地址
  67. setURL(url) {
  68. monitor.url = url
  69. },
  70. }
  71. // 获取性能信息
  72. const getPerformance = () => {
  73. if (!window.performance) return
  74. const timing = window.performance.timing
  75. const performance = {
  76. // 重定向耗时
  77. redirect: timing.redirectEnd - timing.redirectStart,
  78. // 白屏时间
  79. whiteScreen: whiteScreen,
  80. // DOM 渲染耗时
  81. dom: timing.domComplete - timing.domLoading,
  82. // 页面加载耗时
  83. load: timing.loadEventEnd - timing.navigationStart,
  84. // 页面卸载耗时
  85. unload: timing.unloadEventEnd - timing.unloadEventStart,
  86. // 请求耗时
  87. request: timing.responseEnd - timing.requestStart,
  88. // 获取性能信息时当前时间
  89. time: new Date().getTime(),
  90. }
  91. return performance
  92. }
  93. // 获取资源信息
  94. const getResources = () => {
  95. if (!window.performance) return
  96. const data = window.performance.getEntriesByType('resource')
  97. const resource = {
  98. xmlhttprequest: [],
  99. css: [],
  100. other: [],
  101. script: [],
  102. img: [],
  103. link: [],
  104. fetch: [],
  105. // 获取资源信息时当前时间
  106. time: new Date().getTime(),
  107. }
  108. data.forEach(item => {
  109. const arry = resource[item.initiatorType]
  110. arry && arry.push({
  111. // 资源的名称
  112. name: item.name,
  113. // 资源加载耗时
  114. duration: item.duration.toFixed(2),
  115. // 资源大小
  116. size: item.transferSize,
  117. // 资源所用协议
  118. protocol: item.nextHopProtocol,
  119. })
  120. return resource
  121. }
  122. window.onload = () => {
  123. // 在浏览器空闲时间获取性能及资源信息 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
  124. if (window.requestIdleCallback) {
  125. window.requestIdleCallback(() => {
  126. monitor.performance = getPerformance()
  127. monitor.resources = getResources()
  128. console.log('页面性能信息')
  129. console.log(monitor.performance)
  130. console.log('页面资源信息')
  131. console.log(monitor.resources)
  132. })
  133. } else {
  134. setTimeout(() => {
  135. monitor.performance = getPerformance()
  136. monitor.resources = getResources()
  137. console.log('页面性能信息')
  138. console.log(monitor.performance)
  139. console.log('页面资源信息')
  140. console.log(monitor.resources)
  141. }, 0)
  142. }
  143. }
  144. // 捕获资源加载失败错误 js css img...
  145. addEventListener('error', e => {
  146. if (target != window) {
  147. monitor.errors.push({
  148. type: target.localName,
  149. url: target.src || target.href,
  150. msg: (target.src || target.href) + ' is load error',
  151. // 错误发生的时间
  152. time: new Date().getTime(),
  153. })
  154. console.log('所有的错误信息')
  155. console.log(monitor.errors)
  156. }
  157. }, true)
  158. // 监听 js 错误
  159. window.onerror = function(msg, url, row, col, error) {
  160. monitor.errors.push({
  161. type: 'javascript', // 错误类型
  162. row: row, // 发生错误时的代码行数
  163. col: col, // 发生错误时的代码列数
  164. msg: error && error.stack? error.stack : msg, // 错误信息
  165. url: url, // 错误文件
  166. time: new Date().getTime(), // 错误发生的时间
  167. })
  168. console.log('所有的错误信息')
  169. console.log(monitor.errors)
  170. }
  171. // 监听 promise 错误 缺点是获取不到行数数据
  172. addEventListener('unhandledrejection', e => {
  173. monitor.errors.push({
  174. type: 'promise',
  175. msg: (e.reason && e.reason.msg) || e.reason || '',
  176. // 错误发生的时间
  177. time: new Date().getTime(),
  178. })
  179. console.log('所有的错误信息')
  180. console.log(monitor.errors)
  181. })
  182. return monitor
  183. }
  184. const monitor = monitorInit()
  185. </script>
  186. <link rel="stylesheet" href="test.css">
  187. <title>Document</title>
  188. </head>
  189. <body>
  190. <button class="btn1">错误测试按钮1</button>
  191. <button class="btn2">错误测试按钮2</button>
  192. <button class="btn3">错误测试按钮3</button>
  193. <img src="https://avatars3.githubusercontent.com/u/22117876?s=460&v=4" alt="">
  194. <img src="test.png" alt="">
  195. <script src="192.168.10.15/test.js"></script>
  196. <script>
  197. document.querySelector('.btn1').onclick = () => {
  198. setTimeout(() => {
  199. console.log(button)
  200. }, 0)
  201. }
  202. document.querySelector('.btn2').onclick = () => {
  203. new Promise((resolve, reject) => {
  204. reject({
  205. msg: 'test.js promise is error'
  206. })
  207. })
  208. }
  209. document.querySelector('.btn3').onclick = () => {
  210. throw ('这是一个手动扔出的错误')
  211. }
  212. </script>
  213. </body>
  214. </html>

扩展

  1. mounted() {
  2. this.$nextTick(() => {
  3. this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)
  4. })
  5. }

除了性能和错误监控,其实我们还可以收集更多的信息。

用户信息收集

navigator

使用 window.navigator 可以收集到用户的设备信息,操作系统,浏览器信息…

07. 什么时候需要监控 - 图2

UV(Unique visitor)

是指通过互联网浏览这个网页的访客,00:00-24:00 内相同的设备访问只被计算一次。一天内同个访客多次访问仅计算一个 UV。

在用户访问网站时,可以生成一个随机字符串+时间日期,保存在本地。在网页发生请求时(如果超过当天24小时,则重新生成),把这些参数传到后端,后端利用这些信息生成 UV 统计报告。

PV(Page View)

即页面浏览量或点击量,用户每 1 次对网站中的每个网页访问均被记录 1 个PV。用户对同一页面的多次访问,访问量累计,用以衡量网站用户访问的网页数量。

页面停留时间

传统网站

用户在进入 A 页面时,通过后台请求把用户进入页面的时间捎上。过了 10 分钟,用户进入 B 页面,这时后台可以通过接口捎带的参数可以判断出用户在 A 页面停留了 10 分钟。

SPA

可以利用 router 来获取用户停留时间,拿 Vue 举例,通过 router.beforeEachdestroyed 这两个钩子函数来获取用户停留该路由组件的时间。

浏览深度

通过 document.documentElement.scrollTop 属性以及屏幕高度,可以判断用户是否浏览完网站内容。

页面跳转来源

通过 document.referrer 属性,可以知道用户是从哪个网站跳转而来。

小结

通过分析用户数据,我们可以了解到用户的浏览习惯、爱好等等信息,想想真是恐怖,毫无隐私可言。

前面说的都是监控原理,但要实现还是得自己动手写代码。为了避免麻烦,我们可以用现有的工具 去做这件事。

sentry 是一个用 python 写的性能和错误监控工具,你可以使用 sentry 提供的服务(免费功能少),也可以自己部署服务。现在来看一下如何使用 sentry 提供的服务实现监控。

注册账号

打开 https://sentry.io/signup/ 网站,进行注册。

07. 什么时候需要监控 - 图3

选择项目,这里用 Vue 做示例。

安装 sentry 依赖

选完项目,下面会有具体的 sentry 依赖安装指南。

07. 什么时候需要监控 - 图4

根据提示,在你的 Vue 项目执行这段代码 npm install --save @sentry/browser @sentry/integrations @sentry/tracing,安装 sentry 所需的依赖。

再将下面的代码拷到你的 main.js,放在 new Vue() 之前。

  1. import * as Sentry from "@sentry/browser";
  2. import { Vue as VueIntegration } from "@sentry/integrations";
  3. import { Integrations } from "@sentry/tracing";
  4. Sentry.init({
  5. dsn: "xxxxx", // 这里是你的 dsn 地址,注册完就有
  6. integrations: [
  7. new VueIntegration({
  8. Vue,
  9. tracing: true,
  10. }),
  11. new Integrations.BrowserTracing(),
  12. ],
  13. // We recommend adjusting this value in production, or using tracesSampler
  14. // for finer control
  15. tracesSampleRate: 1.0,
  16. });

然后点击第一步中的 skip this onboarding,进入控制台页面。

如果忘了自己的 DSN,请点击左边的菜单栏选择 Settings -> Projects -> 点击自己的项目 -> Client Keys(DSN)

在你的 Vue 项目执行一个打印语句 console.log(b)

这时点开 sentry 主页的 issues 一项,可以发现有一个报错信息 b is not defined

这个报错信息包含了错误的具体信息,还有你的 IP、浏览器信息等等。

但奇怪的是,我们的浏览器控制台并没有输出报错信息。

这是因为被 sentry 屏蔽了,所以我们需要加上一个选项 logErrors: true

07. 什么时候需要监控 - 图5

然后再查看页面,发现控制台也有报错信息了:

上传 sourcemap

下面来看一下如何上传 sourcemap。

首先创建 auth token。

07. 什么时候需要监控 - 图6

07. 什么时候需要监控 - 图7

这个生成的 token 一会要用到。

安装 sentry-cli@sentry/webpack-plugin

  1. npm install sentry-cli-binary -g
  2. npm install --save-dev @sentry/webpack-plugin

安装完上面两个插件后,在项目根目录创建一个 .sentryclirc 文件(不要忘了在 .gitignore 把这个文件添加上,以免暴露 token),内容如下:

  1. [auth]
  2. token=xxx
  3. [defaults]
  4. url=https://sentry.io/
  5. org=woai3c
  6. project=woai3c

把 xxx 替换成刚才生成的 token。

org 是你的组织名称。

07. 什么时候需要监控 - 图8

project 是你的项目名称,根据下面的提示可以找到。

07. 什么时候需要监控 - 图9

在项目下新建 vue.config.js 文件,把下面的内容填进去:

填完以后,执行 npm run build,就可以看到 sourcemap 的上传结果了。

我们再来看一下没上传 sourcemap 和上传之后的报错信息对比。

未上传 sourcemap

07. 什么时候需要监控 - 图10

已上传 sourcemap

07. 什么时候需要监控 - 图11

可以看到,上传 sourcemap 后的报错信息更加准确。

切换中文环境和时区

07. 什么时候需要监控 - 图12

选完刷新即可。

性能监控

07. 什么时候需要监控 - 图13

打开 performance 选项,就能看到你每个项目的运行情况。具体的参数解释请看文档 Performance Monitoring

小结

随着 web 技术的发展,现在前端项目的规模也越来越大。在监控系统的帮助下,我们可以更加清楚的了解项目的运行情况,根据采集到的错误数据和性能数据对项目做针对性的优化。

下一章我们将讲解如何做性能优化。

参考资料