UNPKG

spy-client

Version:

spy client

1,408 lines (1,389 loc) 54.7 kB
/*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ /* global Reflect, Promise */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; /** * @file utils * @author kaivean */ function assign() { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } var __assign = Object.assign || function __assign(t) { for (var s = void 0, i = 1, n = arguments.length; i < n; i++) { // eslint-disable-next-line prefer-rest-params s = arguments[i]; for (var p in s) { if (Object.prototype.hasOwnProperty.call(s, p)) { t[p] = s[p]; } } } return t; }; return __assign.apply(this, args); } function getUrlInfoFromURL(url) { if (URL && url) { try { var obj = new URL(url); if (obj.host !== undefined) { return { protocol: obj.protocol, host: obj.host, pathname: obj.pathname, ext: '', }; } } catch (e) { console.error(e); } } return; } function getUrlInfo(url) { var info = getUrlInfoFromURL(url); if (!info) { var parser = document.createElement('a'); parser.href = url; info = { protocol: parser.protocol, host: parser.host || location.host, pathname: parser.pathname, ext: '', }; } var split = info.pathname.split('.'); info.ext = split[split.length - 1]; return info; } function f$1(n) { return +n.toFixed(1); } function getResTiming(t) { return { wait: f$1(t.domainLookupStart - (t.navigationStart || t.fetchStart || t.startTime)), dns: f$1(t.domainLookupEnd - t.domainLookupStart), connect: f$1(t.connectEnd - t.connectStart), req: f$1(t.responseStart - t.requestStart), res: f$1(t.responseEnd - t.responseStart), }; } function getxpath(el) { if (!el) { return { xpath: '' }; } var xpath = []; while (el && el.nodeType === 1 && el !== el.parentNode) { var t = el.tagName.toLowerCase(); if (el.getAttribute('id')) { t += '[#' + el.getAttribute('id') + ']'; } else if (el.classList && el.classList.length) { t += '[.' + el.classList[el.classList.length - 1] + ']'; } xpath.push(t); if (el === document.body) { break; } el = el.parentNode; // 修复缺陷检查 } return { xpath: xpath.join('<'), }; } /** * @file SpyClient * @author kaivean */ var defaultLogServer = 'https://sp1.baidu.com/5b1ZeDe5KgQFm2e88IuM_a/mwb2.gif?'; // 基础版本兼容非浏览器环境 var ver = navigator && navigator.userAgent ? navigator.userAgent.toLowerCase().match(/cpu iphone os (.*?)_/) : ''; var isLtIos14 = ver && ver[2] && (+ver[2] < 14); function err(msg) { console.error("[SpyClient_log]" + msg); // throw new Error(msg); } function stringify(obj) { return Object.keys(obj).map(function (key) { var value = obj[key]; if (typeof value === 'undefined') { value = ''; } else if (typeof value !== 'string') { value = JSON.stringify(value); } return encodeURIComponent(key) + '=' + encodeURIComponent(value); }).join('&'); } function isArray(arr) { return Object.prototype.toString.call(arr) === '[object Array]'; } var SpyClient$1 = /** @class */ (function () { function SpyClient(option) { this.sample = {}; this.markCache = {}; if (!option.pid) { throw new Error('pid is required'); } this.option = { pid: option.pid, lid: option.lid, check: option.check !== false, sample: option.sample, localCache: option.localCache, logServer: option.logServer || defaultLogServer, }; } SpyClient.prototype.handle = function (logItem) { if (!this.check(logItem)) { return; } logItem = assign({ pid: this.option.pid, lid: this.option.lid, ts: Date.now(), group: 'common', }, logItem); if (this.option.localCache) { this.option.localCache.addLog(logItem); } // 当前api设置了抽样, if (typeof logItem.sample === 'number') { if (Math.random() > logItem.sample) { return; } } else if (typeof this.option.sample === 'number' && Math.random() > this.option.sample) { // 否则,用全局抽样 return; } delete logItem.sample; return logItem; }; SpyClient.prototype.send = function (data, post) { if (post === void 0) { post = false; } var logItems = isArray(data) ? data : [data]; var postData = []; for (var _i = 0, logItems_1 = logItems; _i < logItems_1.length; _i++) { var logItem = logItems_1[_i]; logItem = this.handle(logItem); if (!logItem) { continue; } postData.push(logItem); // 期望通过post方式上传日志 if (post) { continue; } var url = this.option.logServer + stringify(logItem); this.request(url); } if (post) { this.sendPost(postData); } }; SpyClient.prototype.check = function (query) { if (!this.option.check) { return true; } var types = ['perf', 'except', 'dist', 'count']; if (types.indexOf(query.type) === -1) { err('type only is one of ' + types.join(', ')); return false; } if (query.group && query.group.length > 30) { err('group length execeeds 30'); return false; } var simpleReg = /^[a-zA-Z0-9-_]{0,30}$/; if (query.type === 'except') { if (!(typeof query.info.msg === 'string' && query.info.msg.length)) { err('info.msg field must be not empty and is String'); return false; } } else { for (var _i = 0, _a = Object.keys(query.info); _i < _a.length; _i++) { var infoKey = _a[_i]; if (!simpleReg.test(infoKey)) { err("info." + infoKey + " is unexpected. " + 'Length must be not more than 30. ' + 'Supported chars: a-zA-Z0-9-_'); return false; } var infoVal = query.info[infoKey]; if (query.type === 'dist') { if (infoVal.length > 30) { err("info." + infoKey + " value length execeeds 30 when type == 'dist'"); return false; } } else if (typeof infoVal !== 'number') { err("info." + infoKey + " value must be number"); return false; } } } if (query.dim) { for (var _b = 0, _c = Object.keys(query.dim); _b < _c.length; _b++) { var dimKey = _c[_b]; if (!simpleReg.test(dimKey)) { err("dim key [" + dimKey + "] is unexpected. " + 'Length must be not more than 30. ' + 'Supported chars: a-zA-Z0-9-_'); return false; } var dimVal = query.dim[dimKey]; if (!/^[a-zA-Z0-9\-_\*\.\s\/#\+@\&\u4e00-\u9fa5]{0,30}$/.test(dimVal)) { err("dim." + dimKey + " value [" + dimVal + "] is unexpected. " + 'Length must be not more than 30. ' + 'Supported chars: a-zA-Z0-9-_*. /#+@& and Chinese'); return false; } } } return true; }; /** * * @param option 配置 */ SpyClient.prototype.sendPerf = function (option) { this.send(assign({ type: 'perf', }, option)); }; /** * * @param option 错误配置项 */ SpyClient.prototype.sendExcept = function (option) { this.send(assign({ type: 'except', }, option)); }; /** * * @param option 配置 */ SpyClient.prototype.sendDist = function (option) { this.send(assign({ type: 'dist', }, option)); }; /** * * @param option 配置 */ SpyClient.prototype.sendCount = function (option) { this.send(assign({ type: 'count', }, option)); }; /** * * @param e 错误实例 * @param option 错误配置项 */ SpyClient.prototype.sendExceptForError = function (e, option) { var newOpt = assign({}, option); newOpt.info = assign({}, option.info || {}, { msg: e.message, stack: e.stack, }); this.sendExcept(newOpt); }; SpyClient.prototype.startMark = function (sign) { this.markCache[sign] = { start: Date.now(), }; }; SpyClient.prototype.endMark = function (sign) { if (this.markCache[sign]) { this.markCache[sign].total = Date.now() - this.markCache[sign].start; return this.markCache[sign].total; } return 0; }; SpyClient.prototype.clearMark = function (sign) { if (this.markCache[sign]) { delete this.markCache[sign]; } }; SpyClient.prototype.getAllMark = function () { var ret = {}; for (var _i = 0, _a = Object.keys(this.markCache); _i < _a.length; _i++) { var sign = _a[_i]; ret[sign] = this.markCache[sign].total; } return ret; }; SpyClient.prototype.clearAllMark = function () { this.markCache = {}; }; // send(data, true) 也能以post发送,但是会有严格校验 // sendPost能以post发送,但没有校验 SpyClient.prototype.sendPost = function (data) { var logItems = isArray(data) ? data : [data]; var first = logItems[0]; // 需要在页面暴漏出来pid等基本信息,方式调试查看基本信息与兼容目前的监控 var query = { pid: first.pid, type: first.type, group: first.group, }; var url = this.option.logServer + stringify(query); this.request(url, data); }; // 有data时,意味着要用post发送请求 SpyClient.prototype.request = function (url, data) { if (!(!isLtIos14 && navigator && navigator.sendBeacon && navigator.sendBeacon(url, data ? JSON.stringify(data) : undefined))) { if (data) { this.fetch(url, data); } else { (new Image()).src = url; } } }; SpyClient.prototype.fetch = function (url, data) { if (!fetch) { err('Global fetch method doesn\'t exist'); return; } fetch(url, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); }; return SpyClient; }()); /** * @file FID * @author kaivean */ var spyclient$3 = window.__spyHead; var entryType$3 = 'first-input'; var FID = /** @class */ (function () { function FID() { var _this = this; this.observer = null; if (!window.PerformanceObserver) { return; } if (spyclient$3 && spyclient$3.entryMap && spyclient$3.entryMap[entryType$3]) { this.handle(spyclient$3.entryMap[entryType$3]); } try { this.observer = new PerformanceObserver(function (list) { _this.handle(list.getEntries()); }); this.observer.observe({ entryTypes: [entryType$3] }); } catch (e) { } } FID.prototype.listenFID = function (cb) { this.cb = cb; this.callCB(); }; FID.prototype.callCB = function () { if (this.finalValue !== undefined && this.cb) { this.cb({ fid: this.finalValue }); } }; FID.prototype.destroy = function () { this.observer && this.observer.disconnect(); this.observer = null; }; FID.prototype.handle = function (entries) { var lastEntry = entries.pop(); if (lastEntry) { this.finalValue = lastEntry.duration; this.callCB(); this.destroy(); } }; return FID; }()); /** * @file LayoutShift * @author kaivean */ var spyclient$2 = window.__spyHead; var entryType$2 = 'layout-shift'; var LayoutShift = /** @class */ (function () { function LayoutShift() { var _this = this; this.observer = null; this.onceLeave = false; if (!window.PerformanceObserver) { return; } // 先消费之前的 if (spyclient$2 && spyclient$2.entryMap && spyclient$2.entryMap[entryType$2]) { this.handle(spyclient$2.entryMap[entryType$2]); } try { this.observer = new PerformanceObserver(function (list) { _this.handle(list.getEntries()); }); this.observer.observe({ entryTypes: [entryType$2] }); } catch (e) { } } LayoutShift.prototype.listenLayoutShift = function (cb) { this.cb = cb; }; LayoutShift.prototype.leave = function () { if (!this.onceLeave) { this.onceLeave = true; this.observer && this.observer.takeRecords && this.observer.takeRecords(); this.finalValue = this.value; this.callCB(); this.destroy(); } }; LayoutShift.prototype.callCB = function () { if (this.finalValue !== undefined && this.cb) { this.cb({ layoutShift: this.finalValue }); } }; LayoutShift.prototype.destroy = function () { this.observer && this.observer.disconnect(); this.observer = null; }; LayoutShift.prototype.handle = function (entries) { var _this = this; entries.map(function (entry) { // hadRecentInput 如果过去500毫秒内有用户输入,则返回 true, 刚进入页面内500ms,不会认为是layoutShift if (!entry.hadRecentInput) { _this.value = (_this.value || 0) + (entry.value || 0); } }); }; return LayoutShift; }()); /** * @file 简单的公共数据模块,内部模块之间数据交换 * @author kaivean */ var data = {}; function setData(key, value) { data[key] = value; } function getData(key) { return data[key]; } /** * @file LCP * @author kaivean */ var spyclient$1 = window.__spyHead; var entryType$1 = 'largest-contentful-paint'; var LCP = /** @class */ (function () { function LCP() { var _this = this; this.observer = null; if (!window.PerformanceObserver) { return; } if (spyclient$1 && spyclient$1.entryMap && spyclient$1.entryMap[entryType$1]) { this.handle(spyclient$1.entryMap[entryType$1]); } try { // 仅在 img,image,svg,video,css url, block element with text nodes this.observer = new PerformanceObserver(function (list) { _this.handle(list.getEntries()); }); this.observer.observe({ entryTypes: [entryType$1] }); } catch (e) { } } LCP.prototype.listenLCP = function (cb) { this.cb = cb; this.callCB(); }; LCP.prototype.callCB = function () { if (this.finalValue && this.cb) { this.cb({ lcp: this.finalValue }); } }; LCP.prototype.load = function () { this.observer && this.observer.takeRecords && this.observer.takeRecords(); this.finalValue = this.value; setData('lcp', this.value); this.callCB(); this.destroy(); }; LCP.prototype.destroy = function () { this.observer && this.observer.disconnect(); this.observer = null; }; LCP.prototype.handle = function (entries) { var _this = this; entries.map(function (entry) { _this.value = entry.renderTime || entry.loadTime; }); }; return LCP; }()); function filterExcludeLogGifs(entry) { return entry.name.indexOf('.gif?') < 0; } var TTI = /** @class */ (function () { function TTI() { this.lastLongTask = 0; this.observer = null; this.interval = 5000; this.filterRequest = filterExcludeLogGifs; this.observerCallback = this.observerCallback.bind(this); this.ttiCheck = this.ttiCheck.bind(this); } TTI.prototype.check = function () { return window.PerformanceObserver && performance && performance.timing && performance.timing.navigationStart; }; TTI.prototype.listenTTI = function (cb, option) { if (!this.check()) { return; } // 监听获取longtask,持续20s this.observeLongtask(20000); this.cb = cb; option = option || {}; if (option.interval) { this.interval = option.interval; } if (option.filterRequest) { this.filterRequest = option.filterRequest; } }; TTI.prototype.load = function () { if (!this.check()) { return; } // 每 5 秒检查一次是否符合TTI要求 this.ttiTimer = setInterval(this.ttiCheck, this.interval); }; TTI.prototype.ttiCheck = function () { if (!this.cb) { return; } var now = performance.now(); if (now - this.lastLongTask < this.interval) { // debug('tti 没有达成条件,上次 longTask 离现在差' + (now - this.lastLongTask) + 'ms'); return; } var networkSilence = this.getNetworkSilenceAt(); if (networkSilence === false) { // debug('tti 没有达成条件,网络没有静默'); return; } if (now - networkSilence < this.interval) { // debug('tti 没有达成条件,上次网络请求离现在差' + (now - networkSilence) + 'ms'); return; } // debug('tti 达成条件'); clearTimeout(this.ttiTimer); var tti = this.lastLongTask; // 没有longtask,就用dom ready时间作为tti,默认是认为dom ready已经绑定好时间,可以交换了 if (!tti) { tti = performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart; } // 停止 long task 获取 this.stopObserveLongTask(); this.cb({ tti: tti, }); }; TTI.prototype.observerCallback = function (list) { var entries = list.getEntries(); var lastEntry = entries[entries.length - 1]; // debug('long task (no more than ' + lastEntry.duration + 'ms) detected'); // 最新的longTask完成时间 this.lastLongTask = lastEntry.startTime + lastEntry.duration; }; TTI.prototype.stopObserveLongTask = function () { if (this.observer) { this.observer.disconnect(); this.observer = null; } if (this.stopLongTaskTimeoutId) { clearTimeout(this.stopLongTaskTimeoutId); } }; TTI.prototype.getNetworkSilenceAt = function () { // const now = performance.now(); var resources = performance.getEntriesByType('resource') .filter(this.filterRequest); var lastResourceEnd = 0; for (var _i = 0, resources_1 = resources; _i < resources_1.length; _i++) { var item = resources_1[_i]; // 还没有responseEnd字段,说明网络请求还未结束 if (!item.responseEnd) { return false; } // 取responseEnd最大值 if (item.responseEnd > lastResourceEnd) { lastResourceEnd = item.responseEnd; } } return lastResourceEnd; }; TTI.prototype.observeLongtask = function (timeout) { this.stopObserveLongTask(); try { this.observer = new PerformanceObserver(this.observerCallback); this.observer.observe({ entryTypes: ['longtask'], }); this.stopLongTaskTimeoutId = setTimeout(this.stopObserveLongTask, timeout); } catch (e) { } }; return TTI; }()); /** * @file LCP * @author kaivean */ var Timing = /** @class */ (function () { function Timing() { } // constructor() {} Timing.prototype.listenTiming = function (cb) { if (!window.performance || !window.performance.timing) { // debug('no performance.timing api'); return; } this.cb = cb; }; Timing.prototype.load = function () { var _this = this; setTimeout(function () { _this.cb && _this.cb(_this.getMetric()); }, 500); }; Timing.prototype.getMetric = function () { var metric = {}; var timing = window.performance.timing; var startTime = timing.navigationStart; // 基础信息 metric.dns = timing.domainLookupEnd - timing.domainLookupStart; metric.tcp = timing.connectEnd - timing.connectStart; metric.request = timing.responseStart - timing.requestStart; metric.response = timing.responseEnd - timing.responseStart; // 这是从页面部分数据返回,浏览器开始解析doc元素到最底部的script脚本解析执行完成 // 脚本里触发的异步或绑定了更靠后的事件,不再纳入范围内 metric.parseHtml = timing.domInteractive - timing.domLoading; // 很多事件绑定是在domContentLoaded事件里的,所以等其结束,一般页面元素的事件绑定好了,用户可以正确交互 // 当然存在在该加载事件之后绑定元素事件情况,但不再此考虑范围内 metric.domReady = timing.domContentLoadedEventEnd - startTime; metric.loadEventHandle = timing.loadEventEnd - timing.loadEventStart; // 基本该做的都做完,资源也都加载完成了 // 当然在onload事件处理函数里启动了异步方法,不再纳入范围内 metric.load = timing.loadEventEnd - startTime; // 浏览器首次绘制与首次内容绘制, ios低版本无getEntriesByType api if (performance.getEntriesByType) { var paintEntries = performance.getEntriesByType('paint'); if (paintEntries && paintEntries.length) { paintEntries.forEach(function (_a) { var name = _a.name, duration = _a.duration, startTime = _a.startTime; var time = Math.ceil(duration + startTime); if (name === 'first-paint') { metric.fp = time; } if (name === 'first-contentful-paint') { metric.fcp = time; } }); } } // 端首次绘制与首屏 if (timing.domFirstPaint) { metric.t7FirstPaint = timing.domFirstPaint - startTime; } if (timing.domFirstScreenPaint) { metric.t7FirstScreen = timing.domFirstScreenPaint - startTime; } return metric; }; return Timing; }()); var ResType; (function (ResType) { ResType["JS"] = "js"; ResType["CSS"] = "css"; ResType["IMG"] = "img"; ResType["FONT"] = "font"; })(ResType || (ResType = {})); // 字体 // svg使用场景比较灵活,可能作为一种字体格式,也可能当做图片使用 // 但因woff在所有现代浏览器中都支持,在css中使用iconfont时 // 往往优先加载woff格式字体文件,所以这里将svg划分为图片一类 var FontsTypes; (function (FontsTypes) { FontsTypes[FontsTypes["ttf"] = 0] = "ttf"; FontsTypes[FontsTypes["eot"] = 1] = "eot"; FontsTypes[FontsTypes["woff"] = 2] = "woff"; FontsTypes[FontsTypes["woff2"] = 3] = "woff2"; })(FontsTypes || (FontsTypes = {})); // 忽略图片 // 下述图片路径往往是用来进行日志统计,不是正常图片 var ignoreDefaultPaths = [ '/mwb2.gif', ]; function ignorePath(url, paths) { for (var _i = 0, paths_1 = paths; _i < paths_1.length; _i++) { var path = paths_1[_i]; if (url.indexOf(path) > -1) { return true; } } return false; } function f(n) { return +n.toFixed(1); } var Resource = /** @class */ (function () { function Resource() { this.trigger = 'load'; this.jsList = []; this.cssList = []; this.imgList = []; this.fontList = []; this.hostList = {}; this.bigImgList = {}; this.httpResList = {}; this.slowList = {}; this.resOption = { ignorePaths: [], trigger: 'load' }; this.bigImgOption = { ignorePaths: [], maxSize: 150 * 1024, trigger: 'load' }; this.httpResOption = { ignorePaths: [], trigger: 'load' }; this.slowOption = { ignorePaths: [], trigger: 'load', threshold: 1000 }; } Resource.prototype.check = function () { return performance && performance.getEntriesByType; }; Resource.prototype.listenResource = function (cb, option) { if (option === void 0) { option = {}; } if (!this.check()) { return; } this.resOption = assign(this.resOption, option); this.trigger = this.resOption.trigger; this.cb = cb; }; Resource.prototype.listenBigImg = function (cb, option) { if (option === void 0) { option = {}; } if (!this.check()) { return; } this.bigImgOption = assign(this.bigImgOption, option); this.trigger = this.bigImgOption.trigger; this.bigImgCB = cb; }; Resource.prototype.listenHttpResource = function (cb, option) { if (option === void 0) { option = {}; } if (!this.check()) { return; } this.httpResOption = assign(this.httpResOption, option); this.trigger = this.httpResOption.trigger; this.httpResCB = cb; }; Resource.prototype.listenSlowResource = function (cb, option) { if (option === void 0) { option = {}; } if (!this.check()) { return; } this.slowOption = assign(this.slowOption, option); this.trigger = this.slowOption.trigger; this.slowResCB = cb; }; Resource.prototype.report = function () { var _this = this; var metricRes = this.getMetric(); if (metricRes) { var metric = metricRes.metric, hostMetric = metricRes.hostMetric; this.cb && this.cb(metric, hostMetric); } if (this.bigImgCB) { // 发送大图监控数据 if (typeof window.requestIdleCallback === 'function' && Object.keys(this.bigImgList).length) { window.requestIdleCallback(function () { for (var _i = 0, _a = Object.keys(_this.bigImgList); _i < _a.length; _i++) { var host = _a[_i]; for (var _b = 0, _c = _this.bigImgList[host]; _b < _c.length; _b++) { var entry = _c[_b]; var timing = entry.timing; var type = entry.type; try { var img = document.body.querySelector('img[src="' + timing.name + '"]'); _this.bigImgCB({ msg: timing.name, dur: timing.duration, xpath: getxpath(img).xpath, host: host, type: type, }); } catch (e) { console.error(e); } } } }); } } if (this.httpResCB) { // 发送http资源监控数据 if (typeof window.requestIdleCallback === 'function' && Object.keys(this.httpResList).length) { window.requestIdleCallback(function () { for (var _i = 0, _a = Object.keys(_this.httpResList); _i < _a.length; _i++) { var host = _a[_i]; for (var _b = 0, _c = _this.httpResList[host]; _b < _c.length; _b++) { var entry = _c[_b]; var timing = entry.timing; var type = entry.type; try { var img = document.body.querySelector('[src="' + timing.name + '"]'); _this.httpResCB({ msg: timing.name, dur: timing.duration, xpath: getxpath(img).xpath, host: host, type: type, }); } catch (e) { console.error(e); } } } }); } } if (this.slowResCB) { // 发送慢资源监控数据 if (typeof window.requestIdleCallback === 'function' && Object.keys(this.slowList).length) { window.requestIdleCallback(function () { for (var _i = 0, _a = Object.keys(_this.slowList); _i < _a.length; _i++) { var host = _a[_i]; for (var _b = 0, _c = _this.slowList[host]; _b < _c.length; _b++) { var entry = _c[_b]; var timing = entry.timing; var type = entry.type; try { var img = document.body.querySelector('[src="' + timing.name + '"]'); var info = getResTiming(timing); _this.slowResCB(__assign(__assign({}, info), { dur: timing.duration, msg: timing.name, xpath: getxpath(img).xpath, host: host, type: type })); } catch (e) { console.error(e); } } } }); } } }; Resource.prototype.load = function () { var _this = this; if (this.trigger !== 'load') { return; } if (!this.check()) { return; } setTimeout(function () { _this.report(); }, 500); }; Resource.prototype.leave = function () { if (this.trigger !== 'leave') { return; } this.report(); }; Resource.prototype.push = function (type, list, timing, urlInfo) { if (!ignorePath(urlInfo.pathname, this.resOption.ignorePaths || [])) { list.push(timing); this.pushWithHost(type, this.hostList, timing, urlInfo); } if (!ignorePath(urlInfo.pathname, this.slowOption.ignorePaths || [])) { if (timing.duration > this.slowOption.threshold) { this.pushWithHost(type, this.slowList, timing, urlInfo); } } }; Resource.prototype.pushWithHost = function (type, list, timing, urlInfo) { var host = urlInfo.host; if (!list[host]) { list[host] = []; } list[host].push({ timing: timing, type: type, }); }; Resource.prototype.collectHttpResInHttps = function (type, timing, urlInfo) { if (location.protocol === 'https:' && timing.name.indexOf('http://') === 0 && !ignorePath(urlInfo.pathname, this.httpResOption.ignorePaths || [])) { this.pushWithHost(type, this.httpResList, timing, urlInfo); } }; // 有些jsonp也属于script,这里只统计js后缀的文件 Resource.prototype.addScript = function (timing, urlInfo) { if (urlInfo.ext === 'js') { if (timing.decodedBodySize !== 0) { this.push('js', this.jsList, timing, urlInfo); } } }; // 暂时将css文件或代码块发起的请求归位三类(主要为这两类) // 1、加载字体 // 2、加载背景图(图片不容易区分,有的没有明确后缀名) // 3、光标文件(后缀为.cur,这里也划分为图片) // (svg当做图片,前述已说明) Resource.prototype.addResFromCss = function (timing, urlInfo) { if (urlInfo.ext && FontsTypes.hasOwnProperty(urlInfo.ext)) { this.push('font', this.fontList, timing, urlInfo); } else { this.addImg(timing, urlInfo); } }; // link一般加载css资源 // 也可以通过preload可以预下载一些资源 // 这里只统计js类型的preload Resource.prototype.addLink = function (timing, urlInfo) { if (urlInfo.ext === 'css') { this.push('css', this.cssList, timing, urlInfo); } // preload as script else if (urlInfo.ext === 'js') { this.push('js', this.jsList, timing, urlInfo); } }; Resource.prototype.addImg = function (timing, urlInfo) { this.push('img', this.imgList, timing, urlInfo); // 大于指定size的图片采集 if (timing.decodedBodySize > (this.bigImgOption.maxSize || 0) && !ignorePath(urlInfo.pathname, this.bigImgOption.ignorePaths || [])) { this.pushWithHost('img', this.bigImgList, timing, urlInfo); } }; Resource.prototype.handleTimings = function (list) { var len = list.length; for (var i = 0; i < len; i++) { var timing = list[i]; var urlInfo = getUrlInfo(timing.name); if (ignorePath(urlInfo.pathname, ignoreDefaultPaths)) { continue; } switch (list[i].initiatorType) { case 'script': this.addScript(timing, urlInfo); break; case 'css': this.addResFromCss(timing, urlInfo); break; case 'img': this.addImg(timing, urlInfo); break; case 'link': this.addLink(timing, urlInfo); break; case 'audio': this.collectHttpResInHttps('audio', timing, urlInfo); break; case 'video': this.collectHttpResInHttps('video', timing, urlInfo); break; } } }; Resource.prototype.getNumAndSize = function (type, list) { var obj = {}; var num = type + 'Num'; var size = type + 'Size'; var transferSize = type + 'TransferSize'; var cacheRate = type + 'CacheRate'; var duration = type + 'Duration'; obj[num] = 0; obj[size] = 0; obj[transferSize] = 0; var totalDurationTime = 0; list.forEach(function (timing) { obj[num]++; obj[size] += (timing.decodedBodySize / 1024); obj[transferSize] += (timing.transferSize / 1024); totalDurationTime += timing.duration; }); obj[duration] = f(obj[num] > 0 ? totalDurationTime / obj[num] : 0); if (obj[size]) { var diff = obj[size] - obj[transferSize]; obj[cacheRate] = f(diff >= 0 ? 100 * diff / obj[size] : 0); } obj[size] = f(obj[size]); obj[transferSize] = f(obj[transferSize]); return obj; }; Resource.prototype.getMetric = function () { // 原来代码 var mainPageTiming = performance.getEntriesByType('navigation')[0]; var resourceTimings = performance.getEntriesByType('resource'); if (mainPageTiming && resourceTimings && resourceTimings.length) { this.handleTimings(resourceTimings); var metric = __assign(__assign(__assign(__assign({}, this.getNumAndSize(ResType.JS, this.jsList)), this.getNumAndSize(ResType.CSS, this.cssList)), this.getNumAndSize(ResType.IMG, this.imgList)), this.getNumAndSize(ResType.FONT, this.fontList)); // 主文档大小 var pageTiming = mainPageTiming; metric.docSize = f(pageTiming.decodedBodySize / 1024); metric.docTransferSize = f(pageTiming.transferSize / 1024); metric.headerSize = f((pageTiming.transferSize - pageTiming.encodedBodySize || 0) / 1024); metric.allSize = f(metric.docSize + metric.jsSize + metric.cssSize + metric.imgSize + metric.fontSize); metric.allTransferSize = f(metric.docTransferSize + metric.jsTransferSize + metric.cssTransferSize + metric.imgTransferSize + metric.fontTransferSize); var hostMetric = {}; for (var _i = 0, _a = Object.keys(this.hostList); _i < _a.length; _i++) { var host = _a[_i]; var timings = this.hostList[host].map(function (item) { return item.timing; }); hostMetric[host] = this.getNumAndSize('host', timings); } return { metric: metric, hostMetric: hostMetric }; } return; }; return Resource; }()); /** * @file Memory * @author kaivean */ var initMemory = null; if (window.performance && window.performance.memory) { initMemory = window.performance.memory; } var Memory = /** @class */ (function () { function Memory() { this.onceLeave = false; } // constructor() {} Memory.prototype.listenMemory = function (cb) { this.cb = cb; }; Memory.prototype.leave = function () { if (!this.onceLeave && initMemory) { this.onceLeave = true; var curMemory = window.performance.memory; // fix 早期浏览器的memroy api 值不更新的问题,将此情况排除 if (curMemory.usedJSHeapSize === initMemory.usedJSHeapSize && curMemory.totalJSHeapSize === initMemory.totalJSHeapSize) { return; } var memory = window.performance.memory; this.cb && this.cb({ usedJSHeapSize: memory.usedJSHeapSize / 1024, totalJSHeapSize: memory.totalJSHeapSize / 1024, jsHeapSizeLimit: memory.jsHeapSizeLimit / 1024, usedJSHeapRate: 100 * memory.usedJSHeapSize / memory.totalJSHeapSize, }); } }; return Memory; }()); /** * @file NavigatorInfo * @author kaivean */ var NavigatorInfo = /** @class */ (function () { function NavigatorInfo() { } // private cb: NavigatorInfoCB; NavigatorInfo.prototype.getNavigatorInfo = function () { // this.cb = cb; var ret = {}; var dataConnection = navigator.connection; if (typeof dataConnection === 'object') { ret = { downlink: dataConnection.downlink, effectiveType: dataConnection.effectiveType, rtt: dataConnection.rtt, saveData: !!dataConnection.saveData, // 数据节约模式 }; } // 内存 G if (navigator.deviceMemory) { ret.deviceMemory = navigator.deviceMemory; } // 核数 if (navigator.hardwareConcurrency) { ret.hardwareConcurrency = navigator.deviceMemory; } return ret; }; NavigatorInfo.prototype.load = function () { }; return NavigatorInfo; }()); /** * @file Longtask * @author kaivean */ var spyclient = window.__spyHead; var entryType = 'longtask'; var Longtask = /** @class */ (function () { function Longtask() { var _this = this; this.lts = []; this.observer = null; this.onceLeave = false; if (spyclient && spyclient.entryMap && spyclient.entryMap[entryType]) { this.lts = this.lts.concat(spyclient.entryMap[entryType]); } try { this.observer = new PerformanceObserver(function (list) { _this.lts = _this.lts.concat(list.getEntries()); }); // buffered 兼容性太差 this.observer.observe({ entryTypes: [entryType] }); } catch (e) { } } Longtask.prototype.check = function () { return window.PerformanceObserver && performance && performance.timing && performance.timing.navigationStart; }; Longtask.prototype.listenFSPLongTask = function (cb) { if (!this.check()) { return; } this.fspCB = cb; }; Longtask.prototype.listenLCPLongTask = function (cb) { if (!this.check()) { return; } this.lcpCB = cb; }; Longtask.prototype.listenLoadLongTask = function (cb) { if (!this.check()) { return; } this.loadCB = cb; }; Longtask.prototype.listenPageLongTask = function (cb) { if (!this.check()) { return; } this.pageCB = cb; }; Longtask.prototype.load = function () { var _this = this; if (!this.check()) { return; } var data = this.getStatData(Date.now()); // lcp的值存入data模块也是在load期间,这里延迟一会再获取 setTimeout(function () { _this.loadCB && _this.loadCB({ loadLongtaskTime: data.time, loadTBT: data.tbt, loadTotalTime: data.totalTime, loadLongtaskRate: data.rate, loadLongtaskNum: data.num, }); if (performance.timing && performance.timing.domFirstScreenPaint) { var data_1 = _this.getStatData(performance.timing.domFirstScreenPaint); _this.fspCB && _this.fspCB({ fspLongtaskTime: data_1.time, fspTBT: data_1.tbt, fspTotalTime: data_1.totalTime, fspLongtaskRate: data_1.rate, fspLongtaskNum: data_1.num, }); } if (getData('lcp')) { var data_2 = _this.getStatData(performance.timing.navigationStart + getData('lcp')); _this.lcpCB && _this.lcpCB({ lcpLongtaskTime: data_2.time, lcpTBT: data_2.tbt, lcpTotalTime: data_2.totalTime, lcpLongtaskRate: data_2.rate, lcpLongtaskNum: data_2.num, }); } }, 200); }; Longtask.prototype.leave = function () { if (!this.onceLeave) { this.onceLeave = true; var data = this.getStatData(Date.now()); this.pageCB && this.pageCB({ pageLongtaskTime: data.time, pageTBT: data.tbt, pageTotalTime: data.totalTime, pageLongtaskRate: data.rate, pageLongtaskNum: data.num, pageIframeLongtaskTime: data.iframeTime, pageIframeLongtaskRate: data.iframeRate, pageIframeLongtaskNum: data.iframeNum, }); } }; Longtask.prototype.destroy = function () { this.lts = []; this.observer && this.observer.disconnect(); this.observer = null; }; Longtask.prototype.getStatData = function (finalTime) { var navigationStart = performance.timing.navigationStart; var time = 0; // Total Blocking Time var tbt = 0; var num = 0; var iframeTime = 0; var iframeNum = 0; var iframeLongtasks = {}; for (var index = 0; index < this.lts.length; index++) { var item = this.lts[index]; var duration = item.duration; var end = navigationStart + item.startTime + duration; if (end < finalTime) { time += duration; // 多出来的时间 tbt += (duration - 50 > 0 ? duration - 50 : 0); num++; if (item.attribution && item.attribution[0]) { var containerSrc = item.attribution[0].containerSrc; if (containerSrc && containerSrc !== location.href) { if (!iframeLongtasks[containerSrc]) { iframeLongtasks[containerSrc] = []; } iframeLongtasks[containerSrc].push(duration); iframeTime += duration; iframeNum++; } } } } var totalTime = finalTime - navigationStart; return { num: num, time: time, tbt: tbt, totalTime: totalTime, rate: 100 * time / totalTime, iframeTime: iframeTime, iframeNum: iframeNum, iframeRate: 100 * iframeTime / totalTime, iframeLongtasks: iframeLongtasks, }; }; return Longtask; }()); /** * @file SpyClient * @author kaivean */ var SpyClient = /** @class */ (function (_super) { __extends(SpyClient, _super); function SpyClient(option) { var _this = _super.call(this, option) || this; _this.modules = []; _this.register(new FID()); _this.register(new LCP()); _this.register(new LayoutShift()); _this.register(new TTI()); _this.register(new Timing()); _this.register(new Resource()); _this.register(new Memory()); _this.register(new NavigatorInfo()); _this.register(new Longtask()); _this.visibilitychangeCB = _this.visibilitychangeCB.bind(_this); _this.load = _this.load.bind(_this); _this.leave = _this.leave.bind(_this); if (document.readyState === 'complete') { _this.load(); } else { window.addEventListener('load', _this.load); } document.addEventListener('visibilitychange', _this.visibilitychangeCB); window.addEventListener('beforeunload', _this.leave, false); window.addEventListener('unload', _this.leave, false); _this.handleHead(); return _this; } SpyClient.prototype.handleHead = function () { var _this = this; if (this.option.localCache) { var spyHead = window.__spyHead; if (spyHead && spyHead.winerrors) { for (var index = 0; index < spyHead.winerrors.length; index++) { var obj = spyHead.winerrors[index]; this.option.localCache.addLog(obj); } // Head发送的异常,也保存一份到本地,主要是JS错误和资源加载异常 spyHead.interceptor = function (obj) { _this.option.localCache.addLog(obj); }; } } }; SpyClient.prototype.listenFID = function (cb) { this.invoke('listenFID', cb); }; SpyClient.prototype.listenLayoutShift = function (cb) { this.invoke('listenLayoutShift', cb); }; SpyClient.prototype.listenLCP = function (cb) { this.invoke('listenLCP', cb); }; SpyClient.prototype.listenFSPLongTask = function (cb) { this.invoke('listenFSPLo