前言
前段时间公司有要做埋点的需求,于是想封装一个插件化的埋点工具,先总结一下PC、H5端中的常用埋点吧!🐶
检测是否为白屏
其实简单说就是在屏幕的x和y轴上画多个点(一般各10个),判断点上是否存在元素,如果少于一定个数(一共少于16个)就可以认定为存在白屏的现象了。
class blankScreenLog {
constructor (option) {
// 需要画的点数
this.pointCount = option.pointCount || 10;
// 检查的点数
this.checkPointCount = option.checkPointCount || 16
// 空白点数
this.emptyPoint = 0;
// 屏蔽的元素
this.wrapperElements = option.wrapperElements || ['html','body','#app'];
}
// 检查是否是有效的
isWrapper (element) {
var selector = getSelector(element);
// 如果定位点元素是包裹元素,则默认为是无效的点
if (this.wrapperElements.indexOf(selector)!=-1) {
this.emptyPoint++
}
}
checkBlankScreen () {
this.emptyPoint = 0;
for( let i=0;i<this.pointCount;i++ ) {
let x = document.elementFromPoint(window.innerWidth*i/10,window.innerHeight/2);
let y = document.elementFromPoint(window.innerWidth/2 , window.innerHeight*i/10);
this.isWrapper(x);
this.isWrapper(y);
}
if (this.emptyPoint >= this.checkPointCount) {
let sendData = {
kind:'stability',
type:'blankScreen',
errorMessage:'出现白屏了',
time:new Date(),
screen:window.screen.width+'X'+window.screen.height,
viewport:window.innerWidth+'X'+window.innerHeight,
}
// 发送逻辑忽略
}
}
init () {
// complete表示文档已经完全加载并准备好供使用
if (document.readyState === 'complete') {
this.checkBlankScreen()
} else {
window.addEventListener('load', () => {
this.checkBlankScreen()
})
}
}
}
Js错误
这里分为三种(感觉vue的话直接errorHandler就够了)
error事件用于捕获和处理运行时错误,包括各种类型的错误unhandledrejection事件用于捕获和处理未处理的Promise rejectionerrorHandler函数是用于处理 Vue 语法错误的错误处理器。它被赋值给JsError类中的listenVueError方法中的this.vue.config.errorHandler属性
// lastEvent.js 用来获取最后一个事件
let lastEvent
['click', 'touchstart', 'mousedown', 'keydown', 'mouseover'].forEach(
(eventType) => {
document.addEventListener(
eventType,
(event) => {
lastEvent = event
},
{
capture: true, // 捕获阶段
passive: true, // 停止默认事件
}
)
}
)
export default function lastEvent () {
return lastEvent
}
// parseErrorStack.js 获取stack中的信息
export default function parseErrorStack (stackTrace) {
const stackArray = stackTrace.split('\n').slice(1).map((line) => {
const parts = line.match(/^(.*?)\s*\((.*?)\)$/);
if (parts) {
return {
functionName: parts[1],
fileName: parts[2],
};
} else {
return {
unknown: line,
};
}
});
return stackArray
}
class JsError {
constructor (vue) {
this.vue = vue;
}
listenJSError () {
window.addEventListener('error', (event) => {
let lastEvent = getLastEvent() // 最后一个交互事件
let sendData
// 资源加载错误
if (event.target && (event.target.src || event.target.href)) {
sendData = {
kind:'stability',
type: 'resourceError',
time:new Date(),
filename: event.target.src || event.target.href,
tagName: event.target.tagName
}
}else{
sendData = {
kind:'stability',
type: 'jsError',
time:new Date(),
message: event.message, // 错误信息
filename: event.filename, // 文件名
position: `${event.lineno}:${event.colno}`,
errorStack: parseErrorStack(event.error.stack),
}
}
// 发送逻辑忽略
})
}
listenPromiseError () {
window.addEventListener('unhandledrejection',(event) => {
let lastEvent = getLastEvent() // 最后一个交互事件
let message
let filename
let line = 0
let column = 0
let errorStack = ''
let reason = event.reason
if (typeof reason === 'string') {
message = reason
} else if (typeof reason === 'object') {
if (reason.stack) {
let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/)
filename = matchResult[1]
line = matchResult[2]
column = matchResult[3]
}
message = reason.message
errorStack = parseErrorStack(reason.stack)
}
let sendData = {
kind:'stability',
type:'promiseError',
errorMessage:message,
filename,
position: `${line}:${column}`,
time:new Date(),
errorStack
}
// 发送逻辑忽略
})
}
// 捕获 vue 语法错误
listenVueError () {
this.vue.config.errorHandler = (err,vm,info) => {
let sendData = {
kind:'stability',
type:err.name,
errorMessage:err.message,
time:new Date(),
errorStack:err.stack?parseErrorStack(err.stack):''
}
// 发送逻辑忽略
};
}
init () {
this.listenJSError();
this.listenPromiseError();
this.listenVueError()
}
}
事件埋点
其实就是监听常用的一些事件
clickmousedowninputtouchstart
// getUserAgentType.js 判断是pc还是移动端
export default function getUserAgentType () {
var userAgentInfo = navigator.userAgent;
var agents = ["Android", "iPhone","SymbianOS", "Windows Phone","iPad", "iPod"];
var flag = true;
for (var v = 0; v < agents.length; v++) {
if (userAgentInfo.indexOf(agents[v]) > 0) {
flag = false;
break;
}
}
if (flag) {
return 'pc'
} else {
return 'mobile'
}
}
// getSelector.js 获取选择器方法
export default function getSelector (element) {
if (!element) {
return ''
}
if (element.id) {
return '#'+element.id
} else
if (element.className) {
return '.'+element.className.split(' ').filter(item=>!!item).join('.')
} else {
return element.nodeName.toLowerCase();
}
}
class EventLog {
constructor (config) {
this.defaultEventList = {
pc:['click','mousedown','input'],
mobile:['touchstart']
}
this.eventTypeList = config.eventTypeList|| this.defaultEventList[getUserAgentType()]
}
listenEvent () {
this.eventTypeList.forEach(eventType => {
document.addEventListener(eventType,(event) => {
let selector = getSelector(event.target);
let sendData = {
kind:'event',
type:eventType,
dom:selector,
time:new Date(),
}
if (eventType === 'input' || eventType === 'focus' || eventType === 'blur') {
sendData.domValue = event.target.value
}
// 发送逻辑忽略
})
},{capture:true,passive:true})
}
init () {
this.listenEvent()
}
}
监控一些性能指标
没啥可说的百度一下有很多....
class PerformanceLog {
performanceTime () {
if ( performance.timing ) {
const {
connectEnd,
connectStart,
domContentLoadedEventEnd,
domContentLoadedEventStart,
domInteractive,
domLoading,
fetchStart,
loadEventStart,
requestStart,
responseEnd,
responseStart,
} = performance.timing
let sendData = {
kind:'experience',
type:'performanceTiming',
connectTime:connectEnd - connectStart, // 连接时间
ttfbTime:responseStart - requestStart, // 首字节到达时间
responseTime:responseEnd - responseStart, // 响应的读取时间
parseDOMTime:loadEventStart - domLoading, // DOM解析的时间
domContentLoadedTime:domContentLoadedEventEnd - domContentLoadedEventStart,
timeToInteractive:domInteractive-fetchStart, // 首次可交互时间
loadTime:loadEventStart - fetchStart, // 完整的加载时间
time:new Date(),
location:getPageObj(),
}
// 发送逻辑忽略
}
}
init() {
window.addEventListener('load', () => {
this.performanceTime()
})
}
}
请求监控
就是重写XMLHttpRequest
class RequestLog {
// 重写 XMLHttpRequest 方法
rewriteXMLHttpRequest () {
let _this = this;
let XMLHttpRequest = window.XMLHttpRequest;
// 存老的
let oldOpen = XMLHttpRequest.prototype.open;
let oldSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method,url,async) {
// 将logData挂载在XMLHttpRequest对象上,这样封装埋点的request上绑定了唯一key,如果存在唯一key则表示是埋点上发的信息,不做记录和上发
if (!this.key) {
this.logData = {method,url,async};
}
return oldOpen.apply(this,arguments)
}
XMLHttpRequest.prototype.send = function (body) {
if (this.logData) {
var startTime = new Date().getTime();
let handler = (type) => (event) => {
let sendData = {
kind:'httpRequest',
type:type,
duration:new Date().getTime()-startTime,
status:this.status,
requestUrl:this.logData.url,
method:this.logData.method,
body:body||'',
response:this.response?JSON.parse(this.response):'',
time:getTime(),
location:getPageObj(),
responseHeader:getResponseHeader(this.getAllResponseHeaders())
}
// 发送逻辑忽略
}
this.addEventListener('load',handler('load'));
this.addEventListener('abort',handler('abort'));
this.addEventListener('error',handler('error'))
}
return oldSend.apply(this,arguments)
}
}
init() {
this.rewriteXMLHttpRequest();
}
}
针对vue路由监控
这里只列举hash的监听,history大同小异
class pageListenLog {
listenHashChange () {
var preTime = new Date().getTime();
window.addEventListener('hashchange',(e) => {
if (e.type === 'hashchange') {
var oldPage = e.oldURL;
var newPage = e.newURL;
if (newPage && oldPage && newPage !== oldPage) {
var time = new Date().getTime();
var durationTime = time-preTime;
var fromPage = oldPage;
var toPage = newPage;
preTime = time;
let sendData = {
kind:'behavior',
type:'pageSwitch',
time:getTime(),
currentPage:fromPage.route,
formPage:fromPage,
toPage:toPage,
currentPageDuration:durationTime
}
// 发送逻辑忽略
}
}
})
}
init () {
this.listenHashChange();
}
}