前端监控原理及兜底方案前端性能指标和sentry的使用

1,671 阅读14分钟

开篇:前端监控的意义,为什么需要做监控?

更快的发现问题和解决问题,没有监控系统,代码就等于在线上裸奔,只能靠测试前期找出问题,如果极小概率下没有被测试到,或者资源加载经常超时甚至出现崩溃等情况都很难被发现并及时得到修改。并且包括网页打开的速度等性能问题也需要得到重视,因为很有可能会导致用户的流失。

前端监控一般分为三类:数据监控、异常监控、性能监控。

数据监控,就是监听用户信息和行为,常见的监控项有:
  • PV(page view 页面访问量):即页面浏览量或点击量
  • UV(unique visitor 独立访客):指访问某个站点或点击某条新闻的不同 IP 地址的人数
  • 用户在每一个页面的停留时间
  • 用户通过什么入口来访问该网页
  • 用户在相应的页面中触发的行为
  • 统计这些数据是有意义的,比如我们知道了用户来源的渠道,可以促进产品的推广,知道用户在每一个页面停留的时间,可以针对停留较长的页面,增加广告推送等等。
性能监控

性能监控指的是监听前端的性能,常见的性能监控项包括:

  • 不同用户,不同机型和不同系统下的首屏加载时间
  • http 等请求的响应时间
  • 静态资源整体下载时间
  • 页面渲染时间
  • 页面交互动画完成时间
异常监控

由于产品的前端代码在执行过程中也会发生异常,因此需要引入异常监控。及时的上报异常情况,可以避免线上故障的发上。常见的需要监控的异常包括:

  • Javascript 的异常监控
  • 服务器请求的异常监控

前端监控流程

image.png

本次主要说一下异常监控及兜底策略和性能监控

js异常采集

1.常规运行时错误

 // 能捕获常规运行时错误,但是语法错误和异步错误捕获不了。处理很细致,缺点也比较明显
 try{
    console.log(this.a.b)
 }catch(){
     console.log(‘正常捕获’)
 }
try {
 var a = 10,
} catch (err) {
 console.log(err, "无法捕获");
}
try {
 setTimeout(()=> {
  console.log(this.a.b)
 })
} catch (err) {
 console.log(err, "无法捕获");
}
 

2.运行时错误+异步错误

// window.onerror,当 JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件。比try catch强一点,可以捕获异步错误但是也捕获不了语法错误和资源加载失败和promise错误
/**
* @param {String} message    错误信息
* @param {String} source    出错文件
* @param {Number} lineno    行号
* @param {Number} colno    列号
* @param {Object} error  Error对象
*/

window.onerror = function(message, source, lineno, colno, error) {
   console.log('捕获到异常:', {message, source, lineno, colno, error});
}

//当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,这些 error 事件不会向上冒泡到 window,但能被捕获。而window.onerror不能监测捕获。

3.资源加载错误

// 图片、script、css加载错误,都能被捕获

window.addEventListener('error'(error) => {
   console.log('捕获到异常:', error);
}, true);
<img src="https://static.taoche.com/images/ccc.png">
<script src="https://static.taoche.com/images/ccc.js"></script>
<link href="https://static.taoche.com/images/ccc.css" rel="stylesheet"/>
// 但是new Image().src = 'https://static.taoche.com/images/ccc.png' 不能被捕获
// featch请求不能被捕获

4.promise捕获

window.addEventListener("unhandledrejection"function(e){
  console.log('捕获到异常:', e);
});

5.vue+react错误

// 由于Vue会捕获所有Vue单文件组件或者Vue.extend继承的代码,所以在Vue里面出现的错误,并不会直接被window.onerror捕获,而是会抛给Vue.config.errorHandler。
/**
 * 全局捕获Vue错误,直接扔出给onerror处理
 */
Vue.config.errorHandler = function (err) {
  setTimeout(() => {
    throw err
  })
}

// react 通过componentDidCatch,声明一个错误边界的组件 但ErrorBoundary并不会捕捉以下错误:React事件处理,异步代码,ErrorBoundary自己抛出的错误。
componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToService(error, errorInfo);
}
render() {
    return (
    <ErrorBoundary>
      <Mycomponment />
    </ErrorBoundary>  
    )
 }
 

ErrorBoundary文档

前端报错兜底方案
1.资源加载失败如何优化

1.可以通过location.reload(true)强制浏览器刷新重新加载,同时通过sessionStorage记录刷新次数防止一直重复加载。 2.针对加载失败的资源可以通过替换域名进行重新加载。 3.如果始终加载失败,如临时断网、或浏览器突然异常,为了避免用户焦急等待可以跳转到一个错误页。 image.png

2.接口请求失败如何优化

1.利用localstorage每次请求接口后可以存一份数据在本地,如果请求失败避免白屏可以从上次的请求中拿到数据作为展示,同时可以多发送几次请求如果成功就替代缓存的数据资源。 2.也可以跳转到错误页,在错误页的url上拼接出错页面的url,点击重新加载时重新访问该页面。

性能监控

通过监控资源的加载情况来分析影响性能的原因,查看资源加载是否出现异常,耗时多少,缓存命中率多少等

window.addEventListener(
  'load',
  () => {
    // 罗列资源列表,PerformanceResourceTiming类型
    const resources = performance.getEntriesByType('resource');
    console.info(resources);
    // 映射initiatorType和错误类型
    const hashError = {
      script: ERROR_SCRIPT,
      link: ERROR_STYLE,
      img: ERROR_IMAGE,
      xmlhttprequest: ERROR_REQUEST,
    };
    // const hashErrorObj = {};
    resources.forEach((value) => {
      const type = hashError[value.initiatorType];
      // 非监控资源、响应时间在20秒内、监控资源是ma.gif或shin.js,则结束当前循环
      // type 非监控资源 如果是缓存的资源duration = 0,如果超过20s则认为超时 同时可以记录每一个资源的响应时间
      if (!type || value.duration < 20000) {
        return;
      }
      console.log(value, 'valiue');
    });
    return true;
  },
  false,
);
// 判断该资源是否命中缓存?
// 在这些资源对象中有一个  transferSize 字段,它表示获取资源的大小,包括响应头字段和响应数据的大小。  // 如果这个值为 0,说明是从缓存中直接读取的(强制缓存)。如果这个值不为 0,但是 encodedBodySize 字
// 段为 0,说明它走的是协商缓存(encodedBodySize 表示请求响应数据 body 的大小)。
function isCache(entry) {
  // 直接从缓存读取或 304
  return entry.transferSize === 0 || (entry.transferSize !== 0 &&       entry.encodedBodySize === 0)
}
// 上报资源加载信息
{
  name: entry.name, // 资源名称
  subType: entryType,
  type: 'performance',
  sourceType: entry.initiatorType, // 资源类型
  duration: entry.duration, // 资源加载耗时
  dns: entry.domainLookupEnd - entry.domainLookupStart, // DNS 耗时
  tcp: entry.connectEnd - entry.connectStart, // 建立 tcp 连接耗时
  redirect: entry.redirectEnd - entry.redirectStart, // 重定向耗时
  ttfb: entry.responseStart, // 首字节时间
  protocol: entry.nextHopProtocol, // 请求协议
  responseBodySize: entry.encodedBodySize, // 响应内容大小
  responseHeaderSize: entry.transferSize - entry.encodedBodySize, // 响应头部大小
  resourceSize: entry.decodedBodySize, // 资源解压后的大小
  isCache: isCache(entry), // 是否命中缓存
  startTime: performance.now(),
}

利用performance API来监听浏览器从请求html到渲染成功的时间来做具体的优化

image.png

setTimeout(() => {
  const {
    fetchStart,
    connectStart,
    connectEnd,
    requestStart,
    responseStart,
    responseEnd,
    domLoading,
    domInteractive,
    domContentLoadedEventStart,
    domContentLoadedEventEnd,
    loadEventStart,
  } = performance.timing;
  const obj = {
    type: 'timing',
    connectTime: connectEnd - connectStart, // TCP连接耗时
    ttfbTime: responseStart - requestStart, // ttfb
    responseTime: responseEnd - responseStart, // Response响应耗时
    parseDOMTime: loadEventStart - domLoading, // DOM解析渲染耗时
    domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart, // DOMContentLoaded事件回调耗时
    timeToInteractive: domInteractive - fetchStart, // 首次可交互时间
    loadTime: loadEventStart - fetchStart, // 完整的加载时间
  };
  console.log(obj);
}, 3000);
字段含义
navigationStart初始化页面,在同一个浏览器上下文中前一个页面unload的时间戳,如果没有前一个页面 的unload,则与fetchStart值相等
redirectStart第一个HTTP重定向发生的时间,有跳转且是同域的重定向,否则为0
redirectEnd最后一个重定向完成时的时间,否则为0
fetchStart浏览器准备好使用http请求获取文档的时间,这发生在检查缓存之前
domainLookupStartDNS域名开始查询的时间,如果有本地的缓存或keep-alive则时间为0
domainLookupEndDNS域名结束查询的时间
connectStartTCP开始建立连接的时间,如果是持久连接,则与fetchStart值相等
secureConnectionStarthttps 连接开始的时间,如果不是安全连接则为0
connectEndTCP完成握手的时间,如果是持久连接则与fetchStart值相等
requestStartHTTP请求读取真实文档开始的时间,包括从本地缓存读取
requestEndHTTP请求读取真实文档结束的时间,包括从本地缓存读取
responseStart返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳
responseEnd返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时的Unix毫秒时间戳
unloadEventStart前一个页面的unload的时间戳 如果没有则为0
unloadEventEndunloadEventStart相对应,返回的是unload函数执行完成的时间戳
domLoading返回当前网页DOM结构开始解析时的时间戳,此时document.readyState变成loading,并将抛出readyStateChange事件
domInteractive返回当前网页DOM结构结束解析、开始加载内嵌资源时时间戳,document.readyState 变成interactive,并将抛出readyStateChange事件(注意只是DOM树解析完成,这时候并没有开始加载网页内的资源)
domContentLoadedEventStart网页domContentLoaded事件发生的时间
domContentLoadedEventEnd网页domContentLoaded事件脚本执行完毕的时间,domReady的时间
domCompleteDOM树解析完成,且资源也准备就绪的时间,document.readyState变成complete.并将抛出readystatechange事件
loadEventStartload 事件发送给文档,也即load回调函数开始执行的时间
loadEventEndload回调函数执行完成的时间
计算方法
标题描述计算方式含义
redirect重定向耗时redirectEnd – redirectStart重定向的时间
appCache缓存时间domainLookupStart – fetchStart读取缓存耗时
dnsDNS 解析耗时domainLookupEnd – domainLookupStart可观察域名解析服务是否正常
tcpTCP 连接耗时connectEnd – connectStart建立连接的耗时
sslSSL 安全连接耗时connectEnd – secureConnectionStart反映数据安全连接建立耗时
ttfbTime to First Byte(TTFB)网络请求耗时responseStart – requestStartTTFB是发出页面请求到接收到应答数据第一个字节所花费的毫秒数
response响应数据传输耗时responseEnd – responseStart观察网络是否正常
DOMDOM解析耗时domInteractive – responseEnd观察DOM结构是否合理,是否有JS阻塞页面解析
DCLDOMContentLoaded 事件耗时domContentLoadedEventEnd – domContentLoadedEventStart当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载
resources资源加载耗时domComplete – domContentLoadedEventEnd可观察文档流是否过大
domReadyDOM阶段渲染耗时domContentLoadedEventEnd – fetchStartDOM树和页面资源加载完成时间,会触发domContentLoaded事件
首次渲染耗时首次渲染耗时responseEnd-fetchStart加载文档到看到第一帧非空图像的时间,也叫白屏时间
首次可交互时间首次可交互时间domInteractive-fetchStartDOM树解析完成时间,此时document.readyState为interactive
页面完全加载时间页面完全加载时间loadEventStart - fetchStart---
性能指标
字段描述备注
FPFirst Paint(首次绘制)表示浏览器从开始请求网站到屏幕渲染第一个像素点的时间
FCPFirst Content Paint(首次内容绘制)表示浏览器渲染出第一个内容的时间,这个内容可以是文本、图片或SVG元素等等,不包括iframe和白色背景的canvas元素
FMPFirst Meaningful Paint(首次有意义绘制)页面有意义的内容渲染的时间
LCP(Largest Contentful Paint)(最大内容渲染)代表在viewport中最大的页面元素加载的时间
DCL(DomContentLoaded)(DOM加载完成)当 HTML 文档被完全加载和解析完成之后, DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载
Lonload当依赖的资源全部加载完毕之后才会触发
TTI(Time to Interactive) 可交互时间用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点
FIDFirst Input Delay(首次输入延迟)用户首次和页面交互(单击链接,点击按钮等)到页面响应交互的时间

如何计算时间?

// FP 从页面加载开始到第一个像素绘制到屏幕上的时间。其实把 FP 理解成白屏时间也是没问题的。
const entryHandler = (list) => {        
    for (const entry of list.getEntries()) {
        if (entry.name === 'first-paint') {
            observer.disconnect()
        }

       console.log(entry)
    }
}

const observer = new PerformanceObserver(entryHandler)
// buffered 属性表示是否观察缓存数据,也就是说观察代码添加时机比事情触发时机晚也没关系。
observer.observe({ type'paint'bufferedtrue })

// FCP 从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间。对于该指标,"内容"指的是文本、图像(包括背景图像)、`<svg>`元素或非白色的`<canvas>`元素。
const entryHandler = (list) => {        
    for (const entry of list.getEntries()) {
        if (entry.name === 'first-contentful-paint') {
            observer.disconnect()
        }
        console.log(entry)
    }
}

const observer = new PerformanceObserver(entryHandler)
observer.observe({ type'paint'bufferedtrue })

LCP 从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间。LCP 指标会根据页面首次开始加载的时间点来报告可视区域内可见的最大图像或文本块完成渲染的相对时间。
const entryHandler = (list) => {
    if (observer) {
        observer.disconnect()
    }

    for (const entry of list.getEntries()) {
        console.log(entry)
    }
}

const observer = new PerformanceObserver(entryHandler)
observer.observe({ type'largest-contentful-paint'bufferedtrue })

如何上报数据

上报接口一般要选择独立得域名,如果选择项目相同得域名,会带上该项目得cookie,增加了传输得时间

  1. XMLHttpRequest

  2. navigator.sendBeacon 在不支持 sendBeacon 的浏览器下我们可以使用 XMLHttpRequest 来进行上报 3.image得方式,通过new Image().src = url得方式就可以上报,没有阻塞问题,没有跨域问题,相比PNG/JPG,GIF的体积最小通常使用1*1px。最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节。同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。

上报时机有三种:
  1. 采用 requestIdleCallback/setTimeout 延时上报。
  2. 在 beforeunload 回调函数里上报。
  3. 缓存上报数据,达到一定数量后再上报。

建议将三种方式结合一起上报:

  1. 先缓存上报数据,缓存到一定数量后,利用 requestIdleCallback/setTimeout 延时上报。
  2. 在页面离开时统一将未上报的数据进行上报。

sentry的使用

Sentry 是一个开源的实时错误监控的项目,它支持很多端的配置,包括 web 前端、服务器端、移动端及其游戏端。支持各种语言,例如 python、oc、java、node、javascript 等。也可以应用到各种不同的框架上面,如前端框架中的vue 、angular 、react 等最流行的前端框架。

主动上报信息

场景1:设置用户信息

Sentry.configureScope(scope => {
  scope.setTag('user_account', userAccount);
  scope.setTag('user_name', userName);
  scope.setTag('user_mobile', userMobile);
});

此时在 Tags 一栏就会有 user_accout 等等,(ps:上面tags里 _userAccount 也是自定义的,只是两次写的名字不一致

由于用户信息较为特殊,有一个 setUser 专门用来设置用户信息

sentry.configureScope(scope => {
    scope.setUser({
        user_account: userAccount,
        user_name: userName,
        user_mobile: userMobile
    })
})

场景2:使url 更醒目一些

sentry.setTag('url', location.href);

场景3:添加报错 block 信息 页面由一个个小组件组成,比如表格,题目,备注,导图等,想知道是哪个在编辑的时候报错了,可以在触发点击事件的时候添加组件信息

sentry.setExtra('model_data', {
   blockId: this.model.blockId,
   type: this.model.type
});

场景4:主动上报错误 有时候sentry认为这不是一个错误,但开发者认为是,此时可以主动上报一个错误

sentry.captureException(new Error('Good bye!!!'));

场景5:适当埋点 有时候需要监听一些重要的操作,无论是否报错,此时可以仅添加一个 message,类似于埋点的功能

sentry.captureMessage('创建一个message!');

场景6:为错误信息增加’面包屑 breadCrumbs里还原了错误发生时用户的关键操作路径,包括点击事件,xhr等,还有 Expection等,这里可以丰富breadCrumbs

sentry.addBreadcrumb({
  message: '创建面包屑!',
});

配置sourceMap 第一步:在您的项目中创建.sentryclirc文件 [defaults] url: 您的sentry后台页面地址 org: 组织名称 project:项目名称

[auth] token: sentry后台页面生成token

image.png

image.png

image.png

第二步: 安装依赖包 1.@sentry/webpack-plugin 2.@sentry/cli

  • 只需要安装1即可,但是项目打包过程中1会依赖2,如果没安装2会报错导致打包失败,所以我就安装了2. 第三步: 在vue.config.js中使用@sentry/webpack-plugin此插件

const SentryCliPlugin = require('@sentry/webpack-plugin');

module.exports = {
  productionSourceMap: true, // 开始sourceMap
  publicPath,
  chainWebpack: (webpackConfig) => {
    // 只上传sourceMap下的.map.js文件
    webpackConfig.output.sourceMapFilename('sourceMap/[name].[chunkhash].map.js'); 
    webpackConfig.plugin('sentry').use(SentryCliPlugin, [
      {
        include: `./dist/${path[3]}/sourceMap`, // 打包后的文件夹,path[3]是具体哪个页面,因为此项目是多页面应用,找到dist下某个页面内的js文件里的.map文件
        release: '1.0.0', // 引用配置的版本号,版本号需要一致,可以不写,sentry会默认生成
        configFile: 'sentry.properties', // 指定sentry上传配置,不需要修改
        ignoreFile: '.gitignore', // 指定忽略文件配置
        ignore: ['node_modules', 'tests'],
        urlPrefix: publicPath, // 保持与publicpath相符
      },
    ]);
  }
}

第四步:

在页面中引用,一般是main.js
if (process.env.NODE_ENV === 'production' && typeof Sentry !== 'undefined'){
  Sentry.init({
    Vue,
    dsn: 你的dsn,
    release: 需要和vue.config.js保持一致,可以不写,就是sentry默认,
    integrations: [
      new BrowserTracing({
        tracingOrigins: ["localhost", "你的页面的url", /^\//],
      }),
    ],
    logErrors: true, // 需要开启不然本地不会显示错误,不利于调试
    tracesSampleRate: 0.5, // 上报频率,是每个错误都上报还是上报的几率是多少,0-1
    beforeSend: (event, hint) => { // 可以屏蔽一些不需要上报的错误信息
      if (/getdetailsourceid|QK_middlewareReadModePageDetect|UCShellJava.sdkEventFire/.test(hint.originalException.message))
        return null;
      return event;
    },
  });
}

image.png

image.png 成功

Sentry一些指标的含义

image.png

如上图:50%的用户访问LCP值在5.49秒,p75:25%的访问是在7.29秒,p95:5%的用户LCP在8.73秒