前端质量分析
先上大王博客https://www.cnblogs.com/alex3714/articles/5714238.html,说的很有道理,我都快信了,所以一直想把这个项目写出来,怎奈大王一直不讲,唉,实在没办法只好自己写了。理念就是模仿大王的,实际操作流程有些不一样:
- 还是从前端js获取一些相关的前端性能指标
- 讲这些性能指标统一发送给我提供的接口
- 这个接口会对发送过来的数据进行一些处理
- 将处理的数据扔进kafka队列
- 从kafka队列里取出数据存进influxdb
- 从influxdb取出数据进行展示
我的流程就是这样,其实都是很简单易懂的,但是这里面有一些小处理,大概有:
- js我是百度了大佬的,他的js可以取出前端性能相关的指标,可是取出来的加载时间他喵的都是负值…..实在是心情复杂,还好最后改好了,这是最坑我的地方,因为我前端不好
- 本来是想直接把数据存进influxdb,但是仔细想想还是应当先放入kafka,然后由需要的地方自己去取就是了,当然我现在只有向influxdb存,之后可以继续加,这样比较好拓展
- 基于第二点,我需要一个接收数据并扔进kafka的api(这个很随意)以及一个(或多个)从kafka里取数据并存储到对应后端的进程,这个进程是一直监听kafka队列运行的
因为是放在业务前端里获取的数据,那么数据量肯定随着业务峰谷变化。再一个我原本想集成在我的django里,但是查了半天资料也没找到如何让django运行期间一直保持另外几个进程一直运行(本来是想用threading)。最后想想算了,直接用go写不就行了,性能好,开几个goroutine问题全都解决了,我只需要把前端展示集成下不就好了嘛。语言选好了,逻辑流程清晰了,那就开搞
从前端开始,我直接把改好的js贴进来,大家复制就是了,唯一需要更改的是第179行改成你的api地址(也就是go提供的接口)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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);
然后是你要检测的前端页面,直接把该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)