export default function (options) {
var defaultOptions = {
responseValidate: function (response = {}, ctx) {
return response.code === 0
},
reportUrl: '/wofBuriedPoint/report',
expireTime: 2 * 60 * 60 * 1000, // 会话过期时间
types: {
url: 'url', // 地址上报
click: 'click', // click事件上报
request: 'request', // 请求后端接口上报
error: {
script: 'script-error', // 代码运行错误上报
request: 'request-error' // 请求错误上报
}
},
sessionKey: '_detect', // 会话存储键值
env: process.env.NODE_ENV
}
var { responseValidate, reportUrl, expireTime, types, sessionKey, env } = { ...defaultOptions, ...options }
if (env === 'production') {
XmlProxy()
ErrorProxy()
window.addEventListener('error', e => {
errTrigger(e.error)
})
history.pushState = _wr('pushState', history.pushState)
history.replaceState = _wr('replaceState', history.replaceState)
window.addEventListener('replaceState', urlListener)
window.addEventListener('pushState', urlListener)
window.addEventListener('popstate', urlListener)
window.addEventListener('hashchange', urlListener)
window.addEventListener('load', load)
window.addEventListener('unload', leave)
window.addEventListener('focus', function () {
visible()
})
window.addEventListener('blur', function () {
hide()
})
window.addEventListener('click', capturingClick, true)
}
function sessionGen () {
var location = parseLocation()
var performance = xhperf()
var agent = parseAgent()
let { host, hash, href, path } = location
let { domainLookupTime, connectTime, requestTime, responseTime, domParsingTime, domContentLoadedTime } = performance
return {
sid: getUUID(),
startTime: nowTime(), // 会话开始时间
step: 0,
during: 0,
visibleStartTime: nowTime(), // 页面显示开始时间
agent,
referrer: document.referrer,
domainLookupTime,
connectTime,
requestTime,
responseTime,
domParsingTime,
domContentLoadedTime,
locationHost: host,
locationHash: hash,
locationHref: href,
locationPath: path
}
}
// 内部跳转
function urlListener () {
// report from
leave()
// set to
enter()
}
// 页面加载
function load () {
var session = sessionGen()
setSession(session)
}
// 页面离开
function leave () {
var session = getSession()
if (session) {
session.during += nowTime() - session.visibleStartTime
var detect = _detect(session, types.url)
report(detect)
}
}
// 页面内部跳转进入
function enter () {
var session = getSession()
if (session) {
session.step += 1
session.during = 0
session.visibleStartTime = nowTime()
let { host, href, hash, path } = parseLocation()
session.locationHost = host
session.locationHref = href
session.locationPath = path
session.locationHash = hash
setSession(session)
}
}
// 页面显示
function visible () {
var session = getSession()
if (session) {
// 如果页面隐藏时间过长,视为从新建立会话
var leaveTime = nowTime() - session.visibleStartTime
if (leaveTime > expireTime) {
// 旧数据上报
var detect = _detect(session, types.url)
report(detect)
// 重置会话
session = sessionGen()
} else {
session.visibleStartTime = nowTime()
}
setSession(session)
}
}
// 页面隐藏
function hide () {
var session = getSession()
if (session) {
session.during += nowTime() - session.visibleStartTime
session.visibleStartTime = nowTime() // 重置显示开始时间以便再次显示时计算页面隐藏时间
setSession(session)
}
}
// 捕获点击事件
function capturingClick (e) {
var target = e.target
var btnName = ''
var result = isButton(target)
if (result) {
if (result.tagName === 'INPUT') {
btnName = result.value
} else {
btnName = result.outerText
}
var session = getSession()
if (session) {
const detect = _detect(session, types.click, btnName)
report(detect)
}
}
}
function report (detect) {
if (detect && detect.sid) {
window.requestIdleCallback
? window.requestIdleCallback(
function () {
request(detect)
},
{ timeout: 2000 }
)
: request(detect)
}
}
function _detect (session, type = types.url, content = '') {
let detect = {
...session,
content,
type,
time: nowTime()
}
// 格式化时间
detect.startTime = dateFormat('yyyy-MM-dd hh:mm:ss.S', new Date(detect.startTime))
detect.time = dateFormat('yyyy-MM-dd hh:mm:ss.S', new Date(detect.time))
return detect
}
function nowTime () {
return new Date().getTime()
}
function getSession () {
var session = sessionStorage.getItem(sessionKey)
return session ? JSON.parse(session) : session
}
function setSession (obj) {
var session = getSession()
session = { ...session, ...obj }
sessionStorage.setItem(sessionKey, JSON.stringify(session))
}
// 浏览器信息
function parseAgent () {
return window.navigator.userAgent
}
// 页面性能监控
function xhperf () {
if (window.performance) {
var timing = window.performance.timing
var domainLookupTime = timing.domainLookupEnd - timing.domainLookupStart // DNS 域名解析时长
var connectTime = timing.connectEnd - timing.connectStart // TCP 链接建立时长
var requestTime = timing.responseStart - (timing.requestStart || timing.responseStart + 1) // 页面请求时长
var responseTime = timing.responseEnd - timing.responseStart // 资源响应时长
timing.domContentLoadedEventStart ? responseTime < 0 && (responseTime = 0) : (responseTime = -1)
var domParsingTime = timing.domContentLoadedEventStart ? timing.domInteractive - timing.domLoading : -1 // DOM解析时长
var domContentLoadedTime = timing.domContentLoadedEventStart
? timing.domContentLoadedEventStart - timing.fetchStart
: -1 // 文档全解析时长
return {
domainLookupTime,
connectTime,
requestTime,
responseTime,
domParsingTime,
domContentLoadedTime
}
} else {
return ''
}
}
function parseLocation () {
var location = window.location
var host = location.hostname
var hash = location.hash
if (hash.includes('#')) {
hash = hash.toString().slice(1)
}
var search = location.search
if (search.includes('?')) {
var params = search
.toString()
.slice(1)
.split('&')
.reduce((pre, curr) => {
var arr = curr.split('=')
pre[arr[0]] = arr[1]
return pre
}, {})
}
var href = location.href
var path = location.pathname
return {
host,
hash,
params,
href,
path
}
}
// 判断是否是A和BUTTON或其子元素
function isButton (target) {
if (target === null) {
return false
} else {
if (
target.tagName === 'A' ||
target.tagName === 'BUTTON' ||
(target.tagName === 'INPUT' && target.type === 'button')
) {
return target
} else {
return isButton(target.parentElement)
}
}
}
function XmlProxy () {
var _open = XMLHttpRequest.prototype.open
if (_open) {
XMLHttpRequest.prototype.open = new Proxy(_open, {
apply: function (target, ctx, args) {
var _requestURL = args[1]
ctx._isReportUrl = _requestURL === reportUrl
// 上报接口不要拦截
if (!ctx._isReportUrl) {
ctx._session = getSession() // xhr打开时缓存session
ctx._method = args[0]
ctx._requestURL = args[1]
}
return Reflect.apply(...arguments)
}
})
}
var _send = XMLHttpRequest.prototype.send
if (_send) {
XMLHttpRequest.prototype.send = new Proxy(_send, {
apply: function (target, ctx, args) {
// 上报接口不要拦截
if (!ctx._isReportUrl) {
ctx._requestText = args[0]
ctx.onreadystatechange = onreadystatechangeProxy(ctx.onreadystatechange)
ctx.onerror = onerrorProxy(ctx.onerror)
}
return Reflect.apply(...arguments)
}
})
}
}
function onreadystatechangeProxy (_onreadystatechange) {
if (_onreadystatechange) {
return new Proxy(_onreadystatechange, {
apply: function (target, ctx, args) {
if (ctx.readyState === 4) {
var detect = null
var session = ctx._session
var content = null
if (ctx.status >= 200 && ctx.status < 300) {
if (responseValidate instanceof Function) {
var response = ctx.responseText ? JSON.parse(ctx.responseText) : {}
if (responseValidate(response, ctx)) {
content = requestFormat(ctx, true)
detect = _detect(session, types.request, content)
} else {
content = requestFormat(ctx)
detect = _detect(session, types.error.request, content)
}
}
} else if (ctx.status >= 400) {
content = requestFormat(ctx)
detect = _detect(session, types.error.request, content)
}
content && detect && report(detect)
}
return Reflect.apply(...arguments)
}
})
} else {
return _onreadystatechange
}
}
function onerrorProxy (_onerror) {
if (_onerror) {
return new Proxy(_onerror, {
apply: function (target, ctx, args) {
var session = ctx._session
var content = requestFormat(ctx)
var detect = _detect(session, types.error.request, content)
content && detect && report(detect)
return Reflect.apply(...arguments)
}
})
} else {
return _onerror
}
}
function ErrorProxy () {
console.error = new Proxy(console.error, {
apply: function (target, ctx, args) {
errTrigger(new Error(args))
Reflect.apply(...arguments)
}
})
}
function errTrigger (error = {}) {
if (error) {
var content = JSON.stringify({
message: error.message,
stack: error.stack
})
var session = getSession()
if (session) {
var detect = _detect(session, types.error.script, content)
report(detect)
}
}
}
function requestFormat (xhr, success = false) {
const result = {
status: xhr.status,
method: xhr._method,
path: xhr._requestURL,
requestText: (xhr._requestText || '').toString().slice(0, 500),
responseText: (success ? '' : xhr.responseText || '').toString().slice(0, 500) // 请求成功时,不必上报请求结果
}
return JSON.stringify(result)
}
// 添加监控事件
function _wr (type, orig) {
return new Proxy(orig, {
apply: function (target, ctx, args) {
var e = new Event(type)
e.arguments = arguments
window.dispatchEvent(e)
Reflect.apply(...arguments)
}
})
}
// 生成一个不重复的uuid
function getUUID () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (Math.random() * 16) | 0
let v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
function request (params) {
if (window.navigator.sendBeacon) {
window.navigator.sendBeacon(reportUrl, JSON.stringify(params))
} else {
let xhr = new XMLHttpRequest()
xhr.open('post', reportUrl)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(JSON.stringify(params))
}
}
function dateFormat (fmt, date) {
var o = {
'M+': date.getMonth() + 1, // 月份
'd+': date.getDate(), // 日
'h+': date.getHours(), // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
'S': date.getMilliseconds() // 毫秒
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
}
for (var k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
}
}
return fmt
}
}