首页 > 技术文章 > 前端质量分析

bfmq 2018-08-13 14:19 原文

先上大王博客https://www.cnblogs.com/alex3714/articles/5714238.html,说的很有道理,我都快信了,所以一直想把这个项目写出来,怎奈大王一直不讲,唉,实在没办法只好自己写了。理念就是模仿大王的,实际操作流程有些不一样:

  1. 还是从前端js获取一些相关的前端性能指标
  2. 讲这些性能指标统一发送给我提供的接口
  3. 这个接口会对发送过来的数据进行一些处理
  4. 将处理的数据扔进kafka队列
  5. 从kafka队列里取出数据存进influxdb
  6. 从influxdb取出数据进行展示

我的流程就是这样,其实都是很简单易懂的,但是这里面有一些小处理,大概有:

  1. js我是百度了大佬的,他的js可以取出前端性能相关的指标,可是取出来的加载时间他喵的都是负值…..实在是心情复杂,还好最后改好了,这是最坑我的地方,因为我前端不好
  2. 本来是想直接把数据存进influxdb,但是仔细想想还是应当先放入kafka,然后由需要的地方自己去取就是了,当然我现在只有向influxdb存,之后可以继续加,这样比较好拓展
  3. 基于第二点,我需要一个接收数据并扔进kafka的api(这个很随意)以及一个(或多个)从kafka里取数据并存储到对应后端的进程,这个进程是一直监听kafka队列运行的

 

因为是放在业务前端里获取的数据,那么数据量肯定随着业务峰谷变化。再一个我原本想集成在我的django里,但是查了半天资料也没找到如何让django运行期间一直保持另外几个进程一直运行(本来是想用threading)。最后想想算了,直接用go写不就行了,性能好,开几个goroutine问题全都解决了,我只需要把前端展示集成下不就好了嘛。语言选好了,逻辑流程清晰了,那就开搞

 

从前端开始,我直接把改好的js贴进来,大家复制就是了,唯一需要更改的是第179行改成你的api地址(也就是go提供的接口)

  1 (function(window) {
  2     'use strict';
  3     
  4     /**
  5      * https://developer.mozilla.org/zh-CN/docs/Web/API/Window/performance
  6      */
  7     var performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance || {};
  8     performance.now = (function() {
  9         return performance.now    ||
 10         performance.webkitNow     ||
 11         performance.msNow         ||
 12         performance.oNow          ||
 13         performance.mozNow        ||
 14         function() { return new Date().getTime(); };
 15     })();
 16     
 17     /**
 18      * 默认属性
 19      */
 20     var defaults = {
 21         performance: performance, // performance对象
 22         ajaxs: [], //ajax监控
 23         //可自定义的参数
 24         param: {
 25             // rate: 0.5, //随机采样率
 26             // src: 'http://127.0.0.1:8000/thief/a', //请求发送数据
 27             // download: {img:'http://h5dev.eclicks.cn/libs/common/img/bandwidth-5.png', size:4511798}//网速设置
 28         }
 29     };
 30     
 31     if(window.primus.param) {
 32         for(var key in window.primus.param) {
 33             defaults.param[key] = window.primus.param[key];
 34         }
 35     }
 36     var primus = defaults;
 37     var firstScreenHeight = window.innerHeight;//第一屏高度
 38     var doc = window.document;
 39     
 40     /**
 41      * 异常监控
 42      * https://github.com/BetterJS/badjs-report
 43      * @param {String}  msg   错误信息
 44      * @param {String}  url      出错文件的URL
 45      * @param {Long}    line     出错代码的行号
 46      * @param {Long}    col   出错代码的列号
 47      * @param {Object}  error       错误信息Object https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error
 48      */
 49     window.onerror = function(msg, url, line, col, error) {
 50         var newMsg = msg;
 51         if (error && error.stack) {
 52             var stack = error.stack.replace(/\n/gi, "").split(/\bat\b/).slice(0, 9).join("@").replace(/\?[^:]+/gi, "");
 53             var msg = error.toString();
 54             if (stack.indexOf(msg) < 0) {
 55                 stack = msg + "@" + stack;
 56             }
 57             newMsg = stack;
 58         }
 59 //        if (Object.prototype.toString.call(newMsg) === "[object Event]") {
 60 //          newMsg += newMsg.type ? ("--" + newMsg.type + "--" + (newMsg.target ? (newMsg.target.tagName + "::" + newMsg.target.src) : "")) : "";
 61 //      }
 62         
 63         var obj = {msg:newMsg, target:url, rowNum:line, colNum:col};
 64         alert(obj.msg);
 65     };
 66     
 67     /**
 68      * ajax监控
 69      * https://github.com/HubSpot/pace
 70      */
 71     var _XMLHttpRequest = window.XMLHttpRequest;// 保存原生的XMLHttpRequest
 72     // 覆盖XMLHttpRequest
 73     window.XMLHttpRequest = function(flags) {
 74         var req;
 75         // 调用原生的XMLHttpRequest
 76         req = new _XMLHttpRequest(flags);
 77         // 埋入我们的“间谍”
 78         monitorXHR(req);
 79         return req;
 80     };
 81     var monitorXHR = function(req) {
 82         req.ajax = {};
 83         //var _change = req.onreadystatechange;
 84         req.addEventListener('readystatechange', function() {
 85             if(this.readyState == 4) {
 86                 req.ajax.end = primus.now();//埋点
 87 
 88                 if ((req.status >= 200 && req.status < 300) || req.status == 304 ) {//请求成功
 89                     req.ajax.endBytes = _kb(req.responseText.length * 2);//KB
 90                     //console.log('响应数据:'+ req.ajax.endBytes);//响应数据大小
 91                 }else {//请求失败
 92                     req.ajax.endBytes = 0;
 93                 }
 94                 req.ajax.interval = req.ajax.end - req.ajax.start;
 95                 primus.ajaxs.push(req.ajax);
 96                 //console.log('ajax响应时间:'+req.ajax.interval);
 97             }
 98         }, false);
 99         
100         // “间谍”又对open方法埋入了间谍
101         var _open = req.open;
102         req.open = function(type, url, async) {
103             req.ajax.type = type;//埋点
104             req.ajax.url = url;//埋点
105             return _open.apply(req, arguments);
106         };
107         
108         var _send = req.send;
109         req.send = function(data) {
110             req.ajax.start = primus.now();//埋点
111             var bytes = 0;//发送数据大小
112             if(data) {
113                 req.ajax.startBytes = _kb(JSON.stringify(data).length * 2 );
114             }
115             return _send.apply(req, arguments);
116         };
117     };
118     
119     /**
120      * 计算KB值
121      * http://stackoverflow.com/questions/1248302/javascript-object-size
122      */
123     function _kb(bytes) {
124         return (bytes / 1024).toFixed(2);//四舍五入2位小数
125     }
126     
127     /**
128      * 给所有在首屏的图片绑定load事件,计算载入时间
129      * TODO 忽略了异步加载
130      * CSS背景图 是显示的在param参数中设置backgroundImages图片路径数组加载
131      */
132     var imgLoadTime = 0;
133     function _setCurrent() {
134         var current = Date.now();
135         current > imgLoadTime && (imgLoadTime = current);
136     }
137     doc.addEventListener('DOMContentLoaded', function() {
138         var imgs = doc.querySelectorAll('img');
139         imgs = [].slice.call(doc.querySelectorAll('img'));
140         if(imgs) {
141             imgs.forEach(function(img) {
142                 if(img.getBoundingClientRect().top > firstScreenHeight) {
143                     return;
144                 }
145     //            var image = new Image();
146     //          image.src = img.getAttribute('src');
147                 if(img.complete) {
148                     _setCurrent();
149                 }
150                 //绑定载入时间
151                 img.addEventListener('load', function() {
152                     _setCurrent();
153                 }, false);
154             });
155         }
156         
157         //在CSS中设置了BackgroundImage背景
158         if(primus.param.backgroundImages) {
159             primus.param.backgroundImages.forEach(function(url) {
160                 var image = new Image();
161                 image.src = url;
162                 if(image.complete) {
163                     _setCurrent();
164                 }
165                 image.onload = function() {
166                     _setCurrent();
167                 };
168             });
169         }
170     }, false);
171 
172     window.addEventListener('load', function() {
173         //测试网速
174         //_measureConnectionSpeed();
175         setTimeout(function() {
176             var time = primus.getTimes();
177 
178             $.ajax({
179                 url: 'http://192.168.56.1:8080/',
180                 type: 'POST',
181                 dataType: 'json',
182                 data: time,
183                 success: function (data) {
184 
185                 }
186             });
187 
188             //通过网页大小测试网速
189 //            var duration = time.domReadyTime / 1000;
190 //            var pageSize = doc.documentElement.innerHTML.length * 2 * 8;
191 //            var speedBps = pageSize / duration;
192 //            console.log(speedBps/(1024*1024));
193 
194             var data = {ajaxs:primus.ajaxs, dpi:primus.dpi(), time:time};
195             primus.send(data);
196         }, 500);
197     });
198 
199     /**
200      * 打印特性 key:value格式
201      */
202     primus.print = function(obj, left, right, filter) {
203         var list = [], left = left || '', right = right || '';
204         for(var key in obj) {
205             if(filter) {
206                 if(filter(obj[key]))
207                     list.push(left + key + ':' + obj[key] + right);
208             }else {
209                 list.push(left + key + ':' + obj[key] + right);
210             }
211         }
212         return list;
213     };
214     
215     /**
216      * 请求时间统计
217      * 需在window.onload中调用
218      * https://github.com/addyosmani/timing.js
219      */
220     primus.getTimes = function() {
221         var timing = performance.timing;
222         if (timing === undefined) {
223             return false;
224         }
225         var api = {};
226         //存在timing对象
227         if (timing) {
228             // All times are relative times to the start time within the
229             // 白屏时间,也就是开始解析DOM耗时
230             var firstPaint = 0;
231 
232             // Chrome
233             if (window.chrome && window.chrome.loadTimes) {
234                 // Convert to ms
235                 firstPaint = window.chrome.loadTimes().firstPaintTime * 1000;
236                 api.firstPaintTime = firstPaint;
237             }
238             // IE
239             else if (typeof timing.msFirstPaint === 'number') {
240                 firstPaint = timing.msFirstPaint;
241                 api.firstPaintTime = firstPaint;
242             }
243             else {
244                 api.firstPaintTime = timing.navigationStart;
245             }
246             // Firefox
247             // This will use the first times after MozAfterPaint fires
248             //else if (window.performance.timing.navigationStart && typeof InstallTrigger !== 'undefined') {
249             //    api.firstPaint = window.performance.timing.navigationStart;
250             //    api.firstPaintTime = mozFirstPaintTime - window.performance.timing.navigationStart;
251             //}
252 
253             /**
254              * http://javascript.ruanyifeng.com/bom/performance.html
255              * 加载总时间
256              * 这几乎代表了用户等待页面可用的时间
257              * loadEventEnd(加载结束)-navigationStart(导航开始)
258              */
259             api.loadTime = timing.loadEventEnd - timing.navigationStart;
260 
261             /**
262              * Unload事件耗时
263              */
264             api.unloadEventTime = timing.unloadEventEnd - timing.unloadEventStart;
265 
266             /**
267              * 执行 onload 回调函数的时间
268              * 是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?
269              */
270             api.loadEventTime = timing.loadEventEnd - timing.loadEventStart;
271 
272             /**
273              * 用户可操作时间
274              */
275             api.domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart;
276 
277             /**
278              * 首屏时间
279              * 用户在没有滚动时候看到的内容渲染完成并且可以交互的时间
280              * 记录载入时间最长的图片
281              */
282             if(imgLoadTime == 0) {
283                 api.firstScreen = api.domReadyTime;
284             }else {
285                 api.firstScreen = imgLoadTime - timing.navigationStart;
286             }
287 
288             /**
289              * 解析 DOM 树结构的时间
290              * 期间要加载内嵌资源
291              * 反省下你的 DOM 树嵌套是不是太多了
292              */
293             api.parseDomTime = timing.domComplete - timing.domInteractive;
294 
295             /**
296              * 请求完毕至DOM加载耗时
297              */
298             api.initDomTreeTime = timing.domInteractive - timing.responseEnd;
299 
300             /**
301              * 准备新页面时间耗时
302              */
303             api.readyStart = timing.fetchStart - timing.navigationStart;
304 
305             /**
306              * 重定向的时间
307              * 拒绝重定向!比如,http://example.com/ 就不该写成 http://example.com
308              */
309             api.redirectTime = timing.redirectEnd - timing.redirectStart;
310 
311             /**
312              * DNS缓存耗时
313              */
314             api.appcacheTime = timing.domainLookupStart - timing.fetchStart;
315 
316             /**
317              * DNS查询耗时
318              * DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
319              * 可使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)
320              */
321             api.lookupDomainTime = timing.domainLookupEnd - timing.domainLookupStart;
322 
323             /**
324              * TCP连接耗时
325              */
326             api.connectTime = timing.connectEnd - timing.connectStart;
327 
328             /**
329              * 内容加载完成的时间
330              * 页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
331              */
332             api.requestTime = timing.responseEnd - timing.requestStart;
333 
334             /**
335              * 请求文档
336              * 开始请求文档到开始接收文档
337              */
338             api.requestDocumentTime = timing.responseStart - timing.requestStart;
339 
340             /**
341              * 接收文档
342              * 开始接收文档到文档接收完成
343              */
344             api.responseDocumentTime = timing.responseEnd - timing.responseStart;
345 
346             /**
347              * 读取页面第一个字节的时间
348              * 这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
349              * TTFB 即 Time To First Byte 的意思
350              * 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte
351              */
352             api.TTFB = timing.responseStart - timing.navigationStart;
353         }
354         return api;
355     };
356     
357     /**
358      * 与performance中的不同,仅仅是做时间间隔记录
359      * https://github.com/nicjansma/usertiming.js
360      */
361     var marks = {};
362     primus.mark = function(markName) {
363         var now = performance.now();
364         marks[markName] = {
365             startTime: Date.now(),
366             start: now,
367             duration: 0
368         };
369     };
370     
371     /**
372      * 计算两个时间段之间的时间间隔
373      */
374     primus.measure = function(startName, endName) {
375         var start = 0, end = 0;
376         if(startName in marks) {
377             start = marks[startName].start;
378         }
379         if(endName in marks) {
380             end = marks[endName].start;
381         }
382         return {
383             startTime: Date.now(),
384             start: start,
385             end: end,
386             duration: (end - start)
387         };
388     };
389     
390     /**
391      * 资源请求列表
392      * Safrai以及很多移动浏览器不支持
393      * https://github.com/nurun/performance-bookmarklet
394      * http://nicj.net/resourcetiming-in-practice/
395      */
396     primus.getEntries = function() {
397         if (performance.getEntries === undefined) {
398             return false;
399         }
400         
401         var entries = performance.getEntriesByType('resource');
402         var statis = [];
403         entries.forEach(function(t, index) {
404             var isRequest = t.name.indexOf("http") === 0;console.log(t.name)
405 //            if (isRequest) {
406 //                urlFragments = t.name.match(/:\/\/(.[^/]+)([^?]*)\??(.*)/);
407 //                
408 //                maybeFileName = t.name.split("/").pop();
409 //                fileExtension = maybeFileName.substr((Math.max(0, maybeFileName.lastIndexOf(".")) || Infinity) + 1);
410 //            } else {
411 //                urlFragments = ["", window.location.host];
412 //                fileExtension = t.name.split(":")[0];
413 //            }
414             var cur = {
415                 name: t.name,
416                 fileName: t.name.split("/").pop(),
417                 //initiatorType: t.initiatorType || fileExtension || "SourceMap or Not Defined",
418                 duration: t.duration
419                 //isRequestToHost: urlFragments[1] === location.host
420             };
421 
422             if (t.requestStart) {
423                 cur.requestStartDelay = t.requestStart - t.startTime;
424                 // DNS 查询时间
425                 cur.lookupDomainTime = t.domainLookupEnd - t.domainLookupStart;
426                 // TCP 建立连接完成握手的时间
427                 cur.connectTime = t.connectEnd - t.connectStart;
428                 // TTFB
429                 cur.TTFB = t.responseStart - t.startTime;
430                 // 内容加载完成的时间
431                 cur.requestTime = t.responseEnd - t.requestStart;
432                 // 请求区间
433                 cur.requestDuration = t.responseStart - t.requestStart;
434                 // 重定向的时间
435                 cur.redirectTime = t.redirectEnd - t.redirectStart;
436             }
437             
438             if (t.secureConnectionStart) {
439                 cur.ssl = t.connectEnd - t.secureConnectionStart;
440             }
441             
442             statis.push(cur);
443         });
444         return statis;
445     };
446     
447     /**
448      * 标记时间
449      * Date.now() 会受系统程序执行阻塞的影响不同
450      * performance.now() 的时间是以恒定速率递增的,不受系统时间的影响(系统时间可被人为或软件调整)
451      */
452     primus.now = function() {
453         return performance.now();
454     };
455     
456     /**
457      * 网络状态
458      * https://github.com/daniellmb/downlinkMax
459      * http://stackoverflow.com/questions/5529718/how-to-detect-internet-speed-in-javascript
460      */
461     primus.network = function() {
462         //2.2--4.3安卓机才可使用
463         var connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection;
464         var types = "Unknown Ethernet WIFI 2G 3G 4G".split(" ");
465         var network = {bandwidth:null, type:null}
466         if(connection && connection.type) {
467             network.type = types[connection.type];
468         }
469         
470         return network;
471     };
472     
473     /**
474      * 测试网速
475      */
476     function _measureConnectionSpeed() {
477         var startTime, endTime;
478         var download = new Image();
479         download.onload = function () {
480             endTime = primus.now();
481             var duration = (endTime - startTime) / 1000;
482             var bitsLoaded = downloadSize * 8;
483             var speedBps = (bitsLoaded / duration).toFixed(2);
484             var speedKbps = (speedBps / 1024).toFixed(2);
485             var speedMbps = (speedKbps / 1024).toFixed(2);
486             console.log(speedMbps);
487         }
488         startTime = primus.now();
489         var cacheBuster = "?rand=" + startTime;
490         download.src = imageAddr + cacheBuster;
491     }
492     
493     /**
494      * 代理信息
495      */
496     primus.ua = function() {
497         return USERAGENT.analyze(navigator.userAgent);
498 //        var parser = new UAParser();
499 //        return parser.getResult();
500     };
501     
502     /**
503      * 分辨率
504      */
505     primus.dpi = function() {
506         return {width:window.screen.width, height:window.screen.height};
507     };
508     
509     /**
510      * 组装变量
511      * https://github.com/appsignal/appsignal-frontend-monitoring
512      */
513     function _paramify(obj) {
514         return 'data=' + JSON.stringify(obj);
515     }
516     
517     /**
518      * 推送统计信息
519      */
520     primus.send = function(data) {
521         var ts = new Date().getTime().toString();
522         //采集率
523         if(primus.param.rate > Math.random(0, 1)) {
524             var img = new Image(0, 0);
525             img.src = primus.param.src +"?" + _paramify(data) + "&ts=" + ts;
526         }
527     };
528     
529     var currentTime = Date.now(); //这个脚本执行完后的时间 计算白屏时间
530     window.primus = primus;
531 })(this);
View Code

然后是你要检测的前端页面,直接把该js引用就ok了

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <script src="/statics/js/jquery-1.10.1.min.js" type="text/javascript"></script>

    <script type='text/javascript'>
        window.primus || (primus={});
    </script>
    <script src="/statics/js/primus.js"></script>
</head>
<body>

    <div>Index</div>

</body>
</html>

这样,每当有人访问该页面时,就会取出他此次访问页面相关质量然后post到我们的api上

然后是api进行处理,代码在git上:https://github.com/bfmq/Hermes

首先是前端质量的基本字段,然后再加上这个页面的url(因为你肯定不止检测一个页面嘛,之后前端展示的时候根据url进行不同的查询语句就可以了),最后根据访问ip调阿里的api获取了这个ip所属的地区,这样就可以知道地区进行一些判断了。在程序开始运行的时候就会打开一个一直从kafka队列取数据并录入influxdb的goroutine,你可以再加一些自己的插件进去,录入到大数据里一类的。当然conf下的配置你得改成你自己的服务器的。

 

后台内部处理完成了,就要在前端展示了,这个还是使用了python继承在django里了,前端出图用的还是echarts,数据就是从influxdb里取得(这里肯定又是python的api),数据还是那个数据,具体想用什么图展示就看你开心了,我就简单的直接全部展示了下,设置的是每一分钟会自动ajax再去取后刷新下xy轴(还是echarts里的功能)

当然了,现在展示的数据只是我测试页面的测试数据,所以都是几ms级别的,检测的页面几乎没写内容嘛毕竟,但是经过使用,还是ok的

最后再附上各名词对应关系跟我python获取数据的代码

"firstPaint""白屏时间"

"loadTime""加载总时间"

"unloadEventTime""Unload事件耗时"

"loadEventTime""onload"回调函数时间"

"domReadyTime""用户可操作时间"

"firstScreen""首屏时间"

"parseDomTime""DOM树结构解析时间"

"initDomTreeTime""请求完毕至DOM加载耗时"

"readyStart""准备新页面时间耗时"

"redirectTime""重定向的时间"

"appcacheTime""DNS缓存耗时"

"lookupDomainTime""DNS查询耗时"

"connectTime""TCP连接耗时"

"requestTime""内容加载完成的时间"

"requestDocumentTime""请求文档时间"

"responseDocumentTime""接收文档时间"

"TTFB""读取页面第一个字节的时间"
def get_influxdb_data(url, city):
    """
    从hermes库里获取数据
    :param url:  索引,你要查看的url
    :param city:  表名,你要查看的城市
    :return: 
    """
    data = {}
    query = """select  TTFB,appcacheTime,connectTime,domReadyTime,firstScreen,initDomTreeTime,loadEventTime,
    loadTime,lookupDomainTime,parseDomTime,readyStart,redirectTime,requestDocumentTime,requestTime,responseDocumentTime,
    unloadEventTime from "{0}" where url = '{1}' and time > now() - 1h;""".format(city, url)
    influxdb_obj = InfluxDBCFactory('hermes')
    query_ret = influxdb_obj.query(query)
    all_data = query_ret.raw['series'][0]
    all_data_columns = all_data['columns']
    all_data_values = all_data['values']

    for key in all_data_columns:
        key_index = all_data_columns.index(key)
        if key != 'time':
            key_list = [x[key_index] for x in all_data_values]
        else:
            key_list = [utc2local(x[key_index], local_format='%H:%M:%S') for x in all_data_values]
        data[FrontendData[key]] = key_list
    return data
#!/usr/bin/env python
# -*- coding:utf8 -*-
# __author__ = '北方姆Q'

from influxdb import InfluxDBClient
from plugins.duia.singleton import Singleton
from django.conf import settings


class InfluxDBCFactory(InfluxDBClient, Singleton):
    def __init__(self, database, host=settings.INFLUXDB_SERVER, port=settings.INFLUXDB_PORT):
        super().__init__(host=host, port=port, database=database)
#!/usr/bin/env python
import time
import datetime

# 格式自改
UTC_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
LOCAL_FORMAT = '%Y-%m-%d %H:%M:%S'


def utc2local(utc_str, utc_format=UTC_FORMAT, local_format=LOCAL_FORMAT):
    utc_st = datetime.datetime.strptime(utc_str, utc_format)
    local_time = datetime.datetime.fromtimestamp(time.time())
    utc_time = datetime.datetime.utcfromtimestamp(time.time())
    time_difference = local_time - utc_time
    local_st = utc_st + time_difference
    return local_st.strftime(local_format)

 

推荐阅读