qoe-pps
Version:
this is a qoe project of pps
292 lines (255 loc) • 8.94 kB
JavaScript
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
}
}
}