无侵入埋点
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 } }