错误监控&性能上报

一、window.onerror

indow.onerror = function(message, source, lineno, colno, error) { ... }

onerror有五个入参:

参数 解释
msg 错误信息
url 错误所在文件
line 错误所在代码行,整型
colno 错误所在代码列,整型
erro 错误Error对象

只需要把这些信息回传到server端即可,再配合sourcemap的话我们就可以知道是源码中的哪一行出错了,从而实现完美的错误实时监控系统了。然而要完美还是需要做很多工作的。

1. 基本特性

以下三种方式可以引发onerror

  1. 运行时错误,例如无效的对象引用或安全限制
  2. 下载错误,如图片
  3. 在IE9中,获取多媒体数据失败也会引发

可以通过设置returnValue=true,或直接return true来阻止浏览器显示错误信息。但不会阻止script debuggers弹出的调试框。

只有运行错误才会触发onerror,语法错误不会触发。

<script> 标签不支持onerror

定义在 <body> 标签上的onerror属性相当于window.onerror (经测试,Firefox、Opera支持,IE9、chrome无反应)。

2. 浏览器兼容性

QuirksM3ode列出的各浏览器对onError的支持情况
•Chrome 13+
•Firefox 6.1+
•Internet Explorer 5.5+
•Safari 5.1+
•Opera 11.61+ (QuirksMode 测试到11.51尚不支持,我手头上的11.61已支持)
window对象外,支持onerror 的元素:

元素 支持情况
<img> 全支持
<script> IE9/IE10/safari 5.1+/chrome 13+ 支持
<css> 和 <iframe> 不支持onerror

3. 跨域

我们的js文件一般都是和网站不同域的,这是为了提高页面的渲染速度以及架构的可维护性(单独CDN域名,充分利用浏览器http并发数)。这样的js文件中发生错误我们直接监控你会发现你啥信息都收集不到。

实验一:

我们的站点是a.com,页面中引用了两个js文件,一个是a.com域名下的a.js,一个是b.com域名下的b.js,我们在a.js文件中添加window.onerror监控,在b.js文件中主动抛出错误

<!-- index.html  -->
<script type="text/javascript" src="http://a.com/a.js" ></script>
<script type="text/javascript" src="http://b.com/b.js" ></script>
// a.js
window.onerror = function (message, url, line, column, error) {
  console.log('log---onerror::::',message, url, line, column, error);
}
// b.js
throw new Error('this is the error happened in b.js');

我们可以看到下图的结果,onerror函数拿到的信息是Script error, a 0 null,啥卵用都没有,你完全不知道发生了什么错误,哪个文件发生的错误。

这是浏览器所做的安全限制措施,当加载自不同域(协议、域名、端口三者任一不同)的脚本中发生语法(?)错误时,为避免信息泄露,语法错误的细节将不会报告,而代之简单的"Script error."。

实验一的结果.png

但是我们确实是需要知道发生错误的具体信息啊,不然监控就没有意义了。既然又是类同源限制的问题,那肯定是可以通过CORS来解决了。

实验二

我们给b.js加上Access-Control-Allow-Origin:*response header,后面我们会发现还是没啥变化。

实验二的结果.png

实验三

我们继续给b.js加上crossorigin属性,发现可以了,想要的信息都收集到了

<!-- index.html  -->
<script type="text/javascript" src="http://a.com/a.js" ></script>
<script type="text/javascript" src="http://b.com/b.js"  crossorigin></script>

实验三的结果.png

结论:如果想通过onerror函数收集不同域的js错误,我们需要做两件事:

  1. 相关的js文件上加上Access-Control-Allow-Origin:*response header
  2. 引用相关的js文件时加上crossorigin属性

注意: 以上两步缺一不可。实验二告诉我们,如果只是加上Access-Control-Allow-Origin:*的话,错误还是无法捕获。如果只加上crossorigin属性,浏览器会报无法加载的错误,如下图:

仅仅加上crossorigin属性的script加载结果.png

可是。。。
如果你使用sentryraven.js的话,你会发现你什么都不用做,他依然可以帮你捕获到一些错误的非常具体信息,确实是有点神奇啊,具体怎么做的?关键就是raven源码中的install方法中调用的_instrumentTryCatch函数起了作用,他通过tryCatch的方式wrap了一些关键函数,使得这些函数里的报错能够捕获,_instrumentTryCatch的具体实现原理我们后面再说

install: function() {
        var self = this;

        if (self.isSetup() && !self._isRavenInstalled) {
            TraceKit.report.subscribe(function () {
                self._handleOnErrorStackInfo.apply(self, arguments);
            });
            if (self._globalOptions.instrument && self._globalOptions.instrument.tryCatch) {
              self._instrumentTryCatch();// 通过tryCatch来wrap关键函数,从而获得error的具体信息
            }

            if (self._globalOptions.autoBreadcrumbs)
                self._instrumentBreadcrumbs();

            // Install all of the plugins
            self._drainPlugins();

            self._isRavenInstalled = true;
        }

        Error.stackTraceLimit = self._globalOptions.stackTraceLimit;
        return this;
    },

其实如果你真的什么都不做,raven也只是能捕获一些异步错误,同步错误还是无法捕获,所以你即使使用了sentry等第三方的错误收集库,你还是需要加上Access-Control-Allow-Origin:*crossorigin属性

二、Performance

The Performance interface provides access to performance-related information for the current page. It's part of the High Resolution Time API, but is enhanced by the Performance Timeline API, the Navigation Timing API, the User Timing API, and the Resource Timing API.

Performace接口允许访问当前页面性能相关的信息。它是High Resolution Time API的一部分。但是它被Performance Timeline API, the Navigation Timing API,the User Timing API, 和the Resource Timing API扩展增强了。实际上Performance的主要功能都是由这几个API提供的。

单单看上面的内容,大家一定还是会感到疑惑,这performace究竟是个什么东西?ok,我们直接打开百度的网页,然后在控制台里输出Window.performance(window.performace返回的就是performance对象)


Performance对象里出现了4个属性。

timing

timing对象提供了各种与浏览器处理相关的时间数据。具体如下表

名称 作用(这里所有时间戳都代表UNIX毫秒时间戳)
connectEnd 浏览器与服务器之间的连接建立时的时间戳,连接建立指的是所有握手和认证过程全部结束
connectStart HTTP请求开始向服务器发送时的时间戳,如果是持久连接,则等同于fetchStart
domComplete 当前网页DOM结构生成时,也就是Document.readyState属性变为“complete”,并且相应的readystatechange事件触发时的时间戳。
domContentLoadedEventEnd 当前网页DOMContentLoaded事件发生时,也就是DOM结构解析完毕、所有脚本运行完成时的时间戳。
domContentLoadedEventStart 当前网页DOMContentLoaded事件发生时,也就是DOM结构解析完毕、所有脚本开始运行时的时间戳。
domInteractive 当前网页DOM结构结束解析、开始加载内嵌资源时,也就是Document.readyState属性变为“interactive”、并且相应的readystatechange事件触发时的时间戳。
domLoading 当前网页DOM结构开始解析时,也就是Document.readyState属性变为“loading”、并且相应的readystatechange事件触发时的时间戳。
domainLookupEnd 域名查询结束时的时间戳。如果使用持久连接,或者从本地缓存获取信息的,等同于fetchStart
domainLookupStart 域名查询开始时的时间戳。如果使用持久连接,或者从本地缓存获取信息的,等同于fetchStart
fetchStart 浏览器准备通过HTTP请求去获取页面的时间戳。在检查应用缓存之前发生。
loadEventEnd 当前网页load事件的回调函数结束时的时间戳。如果该事件还没有发生,返回0。
loadEventStart 当前网页load事件的回调函数开始时的时间戳。如果该事件还没有发生,返回0。
navigationStart 当前浏览器窗口的前一个网页关闭,发生unload事件时的时间戳。如果没有前一个网页,就等于fetchStart
redirectEnd 最后一次重定向完成,也就是Http响应的最后一个字节返回时的时间戳。如果没有重定向,或者上次重定向不是同源的。则为0
redirectStart 第一次重定向开始时的时间戳,如果没有重定向,或者上次重定向不是同源的。则为0
requestStart 浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳。
responseEnd 浏览器从服务器收到(或从本地缓存读取)最后一个字节时(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳
responseStart 浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。
secureConnectionStart 浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,则返回0。
unloadEventEnd 如果前一个网页与当前网页属于同一个域下,则表示前一个网页的unload回调结束时的时间戳。如果没有前一个网页,或者之前的网页跳转不是属于同一个域内,则返回值为0。
unloadEventStart 如果前一个网页与当前网页属于同一个域下,则表示前一个网页的unload事件发生时的时间戳。如果没有前一个网页,或者之前的网页跳转不是属于同一个域内,则返回值为0。

了解上面timing提供的各种属性之后,我们可以计算出网页在加载时候某一部分消耗的具体时间,可以精确到千分之一毫秒。例如要计算出发送请求到接受完数据所消耗的时间。

const timing = window.performance.timing
const contactDuration = timing.responseEnd - timing.requestStart
navagation

PerformanceNavigation接口呈现了如何导航到当前文档的信息。PerformanceNavigation有两个属性,一个是type,表示如何导航到当前页面的,主要有4个值。

  • type=0:表示当前页面是通过点击链接,书签和表单提交,或者脚本操作,或者在url中直接输入地址访问的。
  • type=1: 表示当前页面是点击刷新或者调用Location.reload()方法访问的。
  • type=2: 表示当前页面是通过历史记录或者前进后退按钮访问的。
  • type=255: 其他方式访问的
    另外一个属性是redirectCount,表示到达当前页面之前经过几次重定向。
其他属性

performance.timeOrigin
表示performance性能测试开始的时间。是一个高精度时间戳(千分之一毫秒)

performance.onresourcetimingbufferfull
表示当浏览器资源时间性能缓冲区已满时会触发的回调函数。下面是mdn上关于这个属性的一个demo。这个demo的主要内容是当缓冲区内容满时,调用buffer_full函数。

function buffer_full(event) {
  console.log("WARNING: Resource Timing Buffer is FULL!");
  performance.setResourceTimingBufferSize(200);
}
function init() {
  // Set a callback if the resource buffer becomes filled
  performance.onresourcetimingbufferfull = buffer_full;
}
<body onload="init()">

performance.memory
一个非标准属性,由chrome浏览器提供。这个属性提供了一个可以获取到基本内存使用情况的对象。

Performance.mark

The mark() method creates a timestamp in the browser's performance entry buffer with the given name.

这段话可以分解出三个关键词。首先timestamp,这里的timestamp指的是高精度时间戳(千分之一毫秒),其次是performance entry bufferperformance entry buffer指的是存储performance实例对象的区域,初始值为空。最后就是given name,表示生成的每一个timestamp都有相应的名称。
所以这句话就可以理解成,在浏览器的performance entry buffer中,根据名称生成高精度时间戳。也就是很多人说过的“打点”。

Performance.measure

The measure() method creates a named timestamp in the browser's performance entry buffer between two specified marks (known as the start mark and end mark, respectively). The named timestamp is referred to as a measure.

这段定义和上面mark的定义有些类似,其最核心的不同点在于这句话。between two specified marks。所以measur是指定两个mark点之间的时间戳。如果说mark可以理解为"打点"的话,measure就可以理解为"连线"。

一个小例子
我们来看一个使用markmeasure的小demo,这个例子也是引用MDN,这里做一下简单讲解。

// 标记一个开始点
performance.mark("mySetTimeout-start");

// 等待1000ms
setTimeout(function() {
  // 标记一个结束点
  performance.mark("mySetTimeout-end");

  // 标记开始点和结束点之间的时间戳
  performance.measure(
    "mySetTimeout",
    "mySetTimeout-start",
    "mySetTimeout-end"
  );

  // 获取所有名称为mySetTimeout的measures
  var measures = performance.getEntriesByName("mySetTimeout");
  var measure = measures[0];
  console.log("setTimeout milliseconds:", measure.duration)

  // 清除标记
  performance.clearMarks();
  performance.clearMeasures();
}, 1000);

结果:


可以看到,高精度的时间戳是非常精准的。(我们知道由于执行队列的原因,setTimeout不会在给定的1000ms之后就立即执行)
Performance API提供了很多方便测试我们程序性能的接口。比如markmeasure。很多优秀的框架也用到了这个API进行测试,比如我最近在看的Vue框架。它里面就频繁用到了markmeasure来测试程序性能。所以想要开发高性能的web程序,了解Performace API还是非常重要的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  •   随着 HTML5 的出现,面向未来 Web 应用的 JavaScript API 也得到了极大的发展。这些 A...
    霜天晓阅读 1,021评论 0 0
  • pyspark.sql模块 模块上下文 Spark SQL和DataFrames的重要类: pyspark.sql...
    mpro阅读 9,448评论 0 13
  • “你问我出生前在做什么我答我在天上挑妈妈看见你了觉得你特别好想做你的孩子又觉得自己可能没那个运气没想到第二天一早我...
    飞飞的碎碎念阅读 1,126评论 2 9
  • 我是从去年五月份开始在简书上写字,记得当初处于人生最灰暗时期,什么狗屁理想什么世相安稳岁月静好,只记得当时我的世界...
    南方姑娘NF阅读 197评论 2 4
  • 为什么要模块化?模块化使代码结构更清晰维护起来更方便。 为什么要模块化规范?有了模块,我们可以方便地使用别人的代码...
    LoveBugs_King阅读 275评论 0 0