我独到的技术见解--前端监控体系搭建

通常前端建立搭建监控体系,主要是为了解决两个问题:如何及时发现问题如何快速定位并解决问题

一般来说,结合开发和产品的角度来看,前端监控体系需要做的事情包括:

  1. 页面的整体访问情况,包括常见的 PV、UV、用户行为上报。
  2. 页面的性能情况,包括加载耗时、接口耗时统计。
  3. 灰度发布与有效的监控能力,方便及时发现问题。
  4. 用户反馈问题,需要足够的日志定位问题。

这些问题可以从两个角度来解决:数据收集数据上报

一、数据收集

要进行有效地监控,首先我们需要将监控数据进行上报。传统的页面开发过程中,系统的质量通常从三方面来评估,针对页面的监控和数据采集也分别从这些方面来进行:

  • 页面访问速度
  • 页面稳定性/异常
  • 外部服务调用情况

异常收集

首先,我们需要收集项目运行过程中的一些错误,因为一般来说脚本执行异常很可能会直接导致功能不可用。当 HTML 文档执行异常时,我们可以通过window.onerrordocument.addEventlistener(error)XMLHttpRequest status等方法拦截错误异常。例如,通过监听window.onerror事件,我们可以获取项目中的错误和分析堆栈,将错误信息自动上报到后台服务中。

常见的前端异常包括:

  • 逻辑错误:开发实现功能的时候,逻辑梳理不符合预期
    • 业务逻辑判断条件错误
    • 事件绑定顺序错误
    • 调用栈时序错误
    • 错误的操作 js 对象
  • 代码健壮性:代码边界情况考虑不周,异常逻辑执行出错
    • 将 null 视作对象读取 property
    • 将 undefined 视作数组进行遍历
    • 将字符串形式的数字直接用于加运算
    • 函数参数未传
  • 网络错误:用户网络情况异常、后台服务异常等错误
    • 服务端未返回数据但仍 200,前端按正常进行数据遍历
    • 提交数据时网络中断
    • 服务端 500 错误时前端未做任何错误处理
  • 系统错误:代码运行环境兼容性问题、内存不够用等问题导致出错
  • 页面内容异常:缺少内容、绑定事件异常、样式异常

生命周期数据

生命周期包括页面加载的关键时间点,常常包括页面打开、更新、关闭等耗时数据。

一般来说,我们可以通过 PerformanceTiming 属性获取到一些生命周期相关的数据,包括:

  • PerformanceTiming.navigationStart:当前浏览器窗口的前一个网页关闭,发生 unload 事件时的时间戳
  • PerformanceTiming.domLoading:返回当前网页 DOM 结构开始解析时(即Document.readyState属性变为“loading”、相应的readystatechange事件触发时)的时间戳
  • PerformanceTiming.domInteractive:返回当前网页 DOM 结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的时间戳
  • PerformanceTiming.domComplete:返回当前文档解析完成(即Document.readyState变为"complete"且相对应的readystatechange)被触发时的时间戳
  • PerformanceTiming.loadEventStart:返回该文档下,load 事件被发送时的时间戳
  • PerformanceTiming.loadEventEnd:返回当 load 事件结束,即加载事件完成时的时间戳

除此之外,当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded事件被触发,而无需等待样式表、图像和子框架的完全加载。由于前端框架的出现,很多时候页面的渲染交给框架来控制,因此DOMContentLoaded事件已经失去了原本的作用,很多时候我们会在框架本身提供的生命周期函数中进行数据的收集。

我们还可以使用MutationObserver接口,该提供了监听页面 DOM 树变化的能力,结合performance获取到具体的时间:

代码语言:js
复制
// 注册监听函数
const observer = new MutationObserver((mutations) => {
  console.log(`时间:${performance.now()},DOM树发生了变化!有以下变化类型:`);
  for (let i = 0; i < mutations.length; i++) {
    console.log(mutations[0].type);
  }
});
// 开始监听document的节点变化
observer.observe(document, {
  childList: true,
  subtree: true,
});

HTTP 测速数据

请求相关的数据,我们同样可以通过 PerformanceTiming 属性获取:

  • PerformanceTiming.redirectStart:返回第一个 HTTP 跳转开始时的时间戳
  • PerformanceTiming.redirectEnd:返回最后一个 HTTP 跳转结束时(即跳转回应的最后一个字节接受完成时)的时间戳
  • PerformanceTiming.fetchStart:返回浏览器准备使用 HTTP 请求读取文档时的时间戳,该事件在网页查询本地缓存之前发生
  • PerformanceTiming.domainLookupStart/PerformanceTiming.domainLookupEnd:返回域名查询开始/结束时的时间戳
  • PerformanceTiming.connectStart:返回 HTTP 请求开始向服务器发送时的时间戳
  • PerformanceTiming.connectEnd:返回浏览器与服务器之间的连接建立时的时间戳,连接建立指的是所有握手和认证过程全部结束
  • PerformanceTiming.secureConnectionStart:返回浏览器与服务器开始安全链接的握手时的时间戳
  • PerformanceTiming.requestStart:返回浏览器向服务器发出 HTTP 请求时(或开始读取本地缓存时)的时间戳
  • PerformanceTiming.responseStart:返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳
  • PerformanceTiming.responseEnd:返回浏览器从服务器收到(或从本地缓存读取)最后一个字节时(如果在此之前 HTTP 连接已经关闭,则返回关闭时)的时间戳

通过这些数据,我们可以观察后端服务是否稳定、是否还有优化空间。

用户行为数据

除了常见的前端页面加载、请求耗时数据,我们还可以关注用户的一些行为数据,包括页面浏览量或点击量、用户在每一个页面的停留时间、用户通过什么入口来访问该页面、用户在相应的页面中触发的行为。用户行为数据可以通过一些 DOM 元素的操作事件来获取。

这些数据通常用来统计分析用户行为,来针对性调整页面功能、更好地发挥页面的作用。同时,我们还可以通过一些用户交互数据,来观测系统功能是否正常。

用户日志

系统出现异常的时候,通常使用日志进行定位。日志的存储通常包括两种方案:

  1. 上报到服务器。由于日志内容很多,如果全量上报到服务器会导致存储成本过大,同时频繁的上报也会增加接口的维护成本。除此之外,由于网络原因等还可能导致部分或全部的日志丢失等问题。
  2. 本地存储。该方案需要引导用户手动操作提交本地日志,才可以定位到具体异常出现的位置。如果无法联系到用户,则可能由于异常无法重现而无法修复。

日志通常用户定位用户问题的时候使用,但我们常常需要提前在代码中打印日志。否则,当我们需要定位问题的时候,才发现自己并没有输出相关的日志,有些问题由于复现困难,再补上日志发布后也未必能复现,这样就会比较被动。

可以通过全局挟持关键模块和函数等方式来进行日志的自动打印,举个例子:

在每个功能模块运行时,通过使用约定的格式来打印输入参数、执行信息、输出参数,则可以通过解析日志的方式,梳理本次操作的完整调用关系、功能模块执行信息:

埋点方案

前端常见的埋点方案包括三种:

代码埋点

可视化埋点

无痕埋点

使用方式

手动编码

可视化圈选

嵌入 SDK

自定义数据

可自定义

较难自定义

难以自定义

业界成熟产品

友盟、百度统计等第三方数据统计服务商

Mixpanel

GrowingIO

更新代价

需要版本更新

需要下发配置

不需要

使用成本

无痕埋点一般是通过上述数据采集中使用的一些 API 来进行数据的采集,但由于无痕埋点的自定义能力很弱,通常我们可以配合代码埋点的方式进行。

标准化埋点数据

不管是哪种埋点方式,我们都需要对它们进行标准化处理。一般来说,通过和后台约定好具体的参数,然后前端在埋点采集的时候,自动转换成接口需要的一些数据格式进行本地存储。

通过这些行为信息,可以实时计算出每个用户在时间轴上的操作顺序,以及每个步骤的操作时间、操作内容等,通过可视化系统直观地展示用户的链路情况,包括系统的入口来源、打开或关闭的页面、每个功能点的点击和操作时间、功能异常的情况等。

使用标准化的方式获取用户点击流以及页面使用情况,将页面和每个功能的操作行为上报到服务器,实时对操作时间、操作名称等信息来分析得到用户的操作链路、每个页面和功能操作步骤间的耗时和转化率,并进行有效监控。通过该方式,可以高效直观地观察产品的使用情况、分析用户的行为习惯,然后确定产品方向、完善产品功能。

数据上报

数据采集完成后,我们需要将这些数据上报到后台服务:

如图,当页面打开、更新、关闭等生命周期、用户在页面中的操作行为、系统异常等触发时,系统底层通过埋点监听这些事件,获取相关数据数据并进行标准化处理后,进行本地收集然后上报到实时数据分析系统。

相关的数据信息包括时间、名称、会话标记、版本号等信息,通过这些数据,可以实时计算出每个埋点的使用数量、埋点间的执行时间、埋点间的转换率等,通过可视化系统直观地展示完整的页面使用情况,包括每个页面打开、更新、关闭情况、每个功能点的点击和加载情况、功能异常的情况等。

上报方式

一般来说,我们埋点的数据、运行的日志都需要通过上报发送到后台服务再进行转换、存储和监控。

批量上报

对于前端来说,过于频繁的请求可能会影响到用户其他正常请求的体验,因此通常我们需要将收集到的数据存储在本地。当收集到一定数量之后再打包一次性上报,或者按照一定的频率(时间间隔)打包上传,打包上传将多次数据合并为一次,可以减轻服务器的压力。

关键生命周期上报

由于用户可能在使用过程中遇到异常,或者在使用过程中退出,因此我们还需要在异常触发的时候、用户退出程序前进行上传,以避免问题没能及时发现和定位。

用户主动提交

一些异常和使用体验问题,我们会给用户提供主动上传的选项。当用户经过引导后进行上传的时候,我们则可以将本地的数据和日志一并进行提交。

二、数据监控

数据上报完成后,我们需要搭建管理端对这些数据进行有效的监控,主要包括三部分的数据:

  • 性能监控
    • 网页加载性能
    • 网络请求性能
  • 异常监控
    • JS Error
  • 数据监控
    • 页面 PV/UV
    • 页面来源

日常监控中,我们可以通过对这些监控数据配置告警阈值等方式,结合邮件、机器人等方式推送到相关的人员,来及时发现并解决问题。

发布过程监控

多人协作的项目,由于每次发版都会把好几个小伙伴开发的功能一起合并发布,人工保证功能的正确是很低效的,人工测试也不一定能覆盖到很完整的功能、自动化测试也常常因为性价比等问题无法做得很完善。所以除了自动化测试、改动相关的功能自测之外,我们上报过程会带上每次的版本号,同时可以根据版本来观察新版本的曲线情况,在灰度过程也需要小心注意观察:

  • 小程序错误告警是否有新增错误,可通过错误内容找到报错位置修复
  • 全版本监控观察:整体的功能点覆盖曲线是否正常,是否有异常涨跌
  • 分版本监控观察:功能是否覆盖完整、灰度占比是否正常、新旧版本的转化率是否一致

在灰度发布过程中,我们就能通过上报数据功能曲线是否正常、异常是否在预期范围、曲线突变跟灰度时间点是否吻合等,来确认是否有异常、哪里可能有异常。当出现数据异常的时候,可配合相应的告警渠道来及时通知相应的负责人,及时修复功能异常。

结束语

很多时候,前端项目中都会进行一些异常、耗时测速等监控,也会进行一些用户行为的数据上报。其实我们还可以思考将这些过程更加自动化地实现,同时数据在上报之后还可以进行筛选、统计、转换,计算出产品各种维度的使用情况,甚至还可以做全链路监控、或是给到一些实用的产品方向引导。

查看Github有更多内容噢: https://github.com/godbasin

我正在参与2024腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!