UNPKG

qoe-pps

Version:

this is a qoe project of pps

292 lines (255 loc) 8.94 kB
import usertiming from 'usertiming' import {send, extend} from './utils' import {byName, byFilter, byRegExp, byType} from './entries' import {getUniversal, getJS, getAjax} from './get' import {global} from './global' const SESSION_TEST = '_Q_test_' const SESSION_QOE_HIT = '_QOE_HIT_' /** * 提供的方法: * config(): 设置QOE向用户漏出的属性 * checkHit(): 判断此访问是否可以发送统计数据 * mark(): 记录时间点,内部调用performance.mark() api * measure(): 计算两个时间点的差值,统计性能数据,内部调用performance.measure() api * sendGroup(): 发送页面访问过程中的基础数据,不需要使用方打点,并且可以配置的ajax(异步请求地址)属性的值,计算某个请求用的时间。 * sendSync(): 同步资源的性能统计,比如防盗链和广告sdk下载和执行。和业务逻辑关联。 * sendAjax(): 异步请求的性能统计,比如设置src的时间,需要用户打点统计数据。 * sendInterface(): 发送用户自定义的性能指标,用户可以自己定义key,支持多组数据一次性发送。 * sendAction(): 统计某个请求的性能指标。比如一个接口的调用时间。需要统计参数在consuming定义 * addError(): 添加错误信息对象到errorList中。 * sendError(): 发送错误信息。可以统计多个错误一次发送。 */ export default class QOE { /** * sampleRate: number{0-100}发送比例 默认%1比例发送 * p1: string 业务线名称 * bc: string 同一个p1下区分字业务线 */ constructor (options = {}) { this.hit = options.sampleRate this.p1 = options.p1 || '2_20_201' this.bc = options.bc || '1' // 默认是h5 this.config(options) } /** * pageName: string 页面名称, 用于统计页面信息 * extendParams: object 页面统计信息扩展参数 * consuming: 分别监控用户操作触发的、或者有可能多次被访问的异步请求 * ajax: 监控页面一组 ajax 请求(组件初始化、影响页面展示的请求等)的总耗时 */ config (options = {}) { if (!options.pageName) { console.warn('[QOE] error: there seems to be no pagename') return false } this._QOE = {} this.errorList = [] // 保存错误日志 this.actionIndex = 0 this.pageName = options.pageName const hit = this.checkHit(this.hit) if (hit) { this.ajax = options.ajax || [] this.consuming = options.consuming || [] this.extendParams = options.extendParams || {} } return hit } checkHit (sampleRate) { try { global.sessionStorage.removeItem(SESSION_TEST) global.sessionStorage.setItem(SESSION_TEST, 1) global.sessionStorage.removeItem(SESSION_TEST) } catch (e) { console.error('sessionStorage error: ' + JSON.stringify(e)) return false } let isHit = sessionStorage.getItem(SESSION_QOE_HIT) if (!isHit) { isHit = Math.round(Math.random() * 100) sessionStorage.setItem(SESSION_QOE_HIT, isHit) } return isHit >> 0 < (sampleRate || 1) } mark (name, isAuto) { usertiming.mark(name) const now = usertiming.now() if (isAuto) this._QOE[name] = now return now } measure (name, mark1, mark2, isAuto) { usertiming.measure(name, mark1, mark2) const duration = byName(name)[0].duration if (isAuto) this._QOE[name] = duration usertiming.clearMeasures(name) return duration } syncResource () { this.syncJsAndCssOn1stPage = byFilter(item => item.initiatorType !== 'css' && item.initiatorType !== 'img') } sendGroup () { if (!this.pageName) return this.mark('QOEPrepare') extend(this._QOE, getUniversal(this.syncJsAndCssOn1stPage, global.__first_paint_count || 0)) extend(this._QOE, getJS()) extend(this._QOE, getAjax(this.ajax)) let params = { type: 'qoe', groupname: 'm_page_' + this.pageName, _: +new Date() } extend(params, this.extendParams) this.mark('QOESend') this.measure('QOERun', 'QOEPrepare', 'QOESend', true) const baseData = { res_start: this._QOE.responseStart, ana_start: this._QOE.analysisStart, doc_load: this._QOE.docLoad, fst_paint: this._QOE.firstPaint, fst_page: this._QOE.firstPage, ana_end: this._QOE.analysisEnd, dom_comp: this._QOE.domComplete, ajax_ready: this._QOE.ajaxReady, adjs_start: this._QOE.ad_script_start, adjs_end: this._QOE.ad_script_end, fdl: this._QOE.fangdaolian, src_auth: this._QOE.sourceAuth, ad_req: this._QOE.ad_request, pbtn_show: this._QOE.playBtnShow, qoe_run: this._QOE.QOERun, net_info: this._QOE.networkInfomation, bro_ver: this._QOE.browserVersion, lookup_dns: this._QOE.lookupDomain, appcache: this._QOE.appcache, connect: this._QOE.connect, acc_type: this._QOE.accessType, doc_ana: Math.ceil(this._QOE.analysisEnd - this._QOE.firstPaint), trans: Math.ceil(this._QOE.firstPaint) } this.getApi(baseData, params) } sendSync () { if (!this.pageName) return this.mark('QOEPrepare') const params = { type: 'qoe', groupname: 'm_page_' + this.pageName, _: +new Date() } this.mark('QOESend') this.measure('QOERun', 'QOEPrepare', 'QOESend', true) const syncParam = { fdl_dl: Math.ceil(this._QOE.fangdaolianDownload), fdl_exc: Math.ceil(this._QOE.fangdaolian), ad_dl: Math.ceil(this._QOE.adDownload), ad_exc: Math.ceil(this._QOE.ad_request) } this.getApi(syncParam, params) } sendAjax () { if (!this.pageName) return this.mark('QOEPrepare') const params = { type: 'qoe', groupname: 'm_page_' + this.pageName, _: +new Date() } this.mark('QOESend') this.measure('QOERun', 'QOEPrepare', 'QOESend', true) const ajaxParam = { cooksdk_dl: Math.ceil(this._QOE.cooksdkDownload), cooksdk_exc: Math.ceil(this._QOE.cooksdkExecute), tmts_ajax: Math.ceil(this._QOE.sourceAuth), dfp_to: Math.ceil(this._QOE.dfpTimeout), set_src: Math.ceil(this._QOE.playBtnShow), can_play: Math.ceil(this._QOE.canPlayPreloaded), fst_play: Math.ceil(this._QOE.videoTimeupdate) } this.getApi(ajaxParam, params) } getApi (arr, params) { let index = 0 for (let o in arr) { if (arr[o] && arr[o] > 0 && isFinite(arr[o])) { index += 1 params[o] = (typeof arr[o] === 'number') ? parseInt(arr[o].toFixed(2)) : 0 } else { console.warn('[QOE] missing: ' + o) } } if (index !== 0) { params.p1 = this.p1 params.bc = this.bc send(params) } } /** * 发送用户自定义的指标,data为要发送的键值对 * @param {pageName} name * @param {指标键值对} data */ sendInterface (name, data) { data = typeof data !== 'object' ? { [name]: data } : data this._sendInterface(name, data) } sendIntreface (key, value) { // 1、兼容老的逻辑,为已经调用该方法的对接方,提供服务 // 2、数据容错处理 const parse = JSON.parse(value) const data = typeof parse === 'object' ? value : { [key]: value } this._sendInterface(key, data) } _sendInterface (name, data = {}) { const keys = Object.keys(data) || [] const params = { groupname: 'm_if_' + name, p1: this.p1, bc: this.bc } keys.forEach((key) => { let value = data[key] if (!value) { const startMark = key + 'Start' const endMark = key + 'End' if (byName(startMark).length && byName(endMark).length) { value = this.measure(key, startMark, endMark) } } params[key] = typeof value === 'number' ? parseInt(value.toFixed(2)) : value }) send(params) } sendAction () { const consuming = this.consuming const actionIndex = this.actionIndex const entries = byFilter(function (item, index) { return index > actionIndex && (item.initiatorType === 'script' || item.initiatorType === 'xmlhttprequest') }) consuming.forEach((item) => { byRegExp(item.pattern, entries).forEach((_item) => { this.sendIntreface(item.name, _item.duration) }) }) this.actionIndex = byType().length } addError (data) { this.errorList.push(data) } sendError () { if (this.errorList.length) { const concat = this.errorList.reduce((pre, current) => { return Object.assign(pre, current) }, {}) this._sendInterface('error', concat) } } get (property) { if (property && this._QOE[property]) { return this._QOE[property] } else if (property && !this._QOE[property]) { console.warn('there is no property called \'' + property + '\'') return null } else { return this._QOE } } }