前端埋点-PC、H5

135 阅读3分钟

前言

前段时间公司有要做埋点的需求,于是想封装一个插件化的埋点工具,先总结一下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 rejection
  • errorHandler 函数是用于处理 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()
    }
}

事件埋点

其实就是监听常用的一些事件

  • click
  • mousedown
  • input
  • touchstart
// 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();
    }
}