无侵入埋点

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
  }
}

 

posted @ 2020-03-31 15:07  zhoulixue  阅读(584)  评论(0编辑  收藏  举报