埋点项目实录之一:数据监控

469 阅读7分钟

埋点的目的

电商业务中前端埋点主要是为了运营人员以及开发人员集采用户行为数据,以及页面性能等进行后续的数据分析,例如,分析某件商品的转化率,从商品曝光到下单支付整个流程的转化情况,用户在某个页面的停留情况等等

数据监控

数据监控就是采集用户所有的行为数据,主要包括以下几点:

  1. PV(Page View)与UV(Unique Visitor)
  2. 商品或者非商品的曝光与点击
  3. 用户在页面的停留时间

详细数据信息

  • window.navigator.userAgent: 分析得出用户的系统以及浏览器信息(platform,os,browser)
  • window.location: 分析当前页面url并解析相关参数
  • document.referrer: 上一个页面的URL
  • timestamp: 上报的时间戳
  • distinct_id: 登录用户的唯一ID
  • temp_user_id: 前端生成的唯一ID,用户串联登录与未登录,以及UA数据。(目前规则:时间戳加上三组随机数)
  • version: 埋点代码的版本控制

详细API

IntersectionObserver

提供了一种异步观察目标元素与其祖先元素或顶级文档视窗交叉状态的方法。祖先元素与视窗被称为根(root)。

当一个IntersectionObserver对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦IntersectionObserver被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。

// 用法示例
var observe = new IntersectionObserver(callback, option);

// 开始监听一个目标元素
observe.observe(document.getElementById('example'));

// 停止监听特定目标元素
observe.unobserve(element);

// 停止监听工作
observe.disconnect();

// 返回所有观察目标的IntersectionObserverEntry对象数组。
observe.takeRecords()

var observe = new IntersectionObserver(
  entries => {
    console.log(entries)
  }, 
  option);

callback

目标元素的可见性变化时,就会调用观察器的回调函数callbackcallback一般会触发两次,一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。接收一个参数entries,即IntersectionObserverEntry实例。描述了目标元素与root的交叉状态。具体如下:

属性说明
boundingClientRect返回包含目标元素的边界信息,返回结果与element.getBoundingClientRect() 相同
intersectionRatio返回目标元素出现在可视区的比例
intersectionRect用来描述root和目标元素的相交区域
isIntersecting回一个布尔值,下列两种操作均会触发callback:1. 如果目标元素出现在root可视区,返回true。2. 如果从root可视区消失,返回false
rootBounds用来描述交叉区域观察者(intersection observer)中的根
target目标元素:与根出现相交区域改变的元素 (Element)
time返回一个记录从IntersectionObserver的时间原点到交叉被触发的时间的时间戳

一般常用的判断条件:isIntersectingintersectionRatio

options options是一个对象,用来配置参数,也可以不填。

属性说明
root所监听对象的具体祖先元素。如果未传入值或值为null,则默认使用顶级文档的视窗(一般为html)。
rootMargin计算交叉时添加到根(root)边界盒bounding box的矩形偏移量, 可以有效的缩小或扩大根的判定范围从而满足计算需要。所有的偏移量均可用像素(px)或百分比(%)来表达, 默认值为"0px 0px 0px 0px"。
threshold一个包含阈值的列表, 按升序排列, 列表中的每个阈值都是监听对象的交叉区域与边界区域的比率。当监听对象的任何阈值被越过时,都会触发callback。默认值为0。

以下是具体使用示例:

class Exposure{
  constructor(config, option){
    this.config = config;
    this.option = option || {
      root: null,
      margin: DataMonitor['margin'],
      threshold: DataMonitor['threshold']     
    };
    this.observer = IntersectionObserver || null;
    this.productMap = new WeakMap(); // 记录上报数据
    this.entryMap = new WeakMap(); // 记录窗口与元素的交互数据
    this.timerMap = new WeakMap(); // 记录定时器
    this.time = 3s; // 设定3s,当元素在窗口停留超过3s时,才埋点
    this.init();
  }
  
  init(){
    const self = this;
    this.observer = new IntersectionObserver((entries, observer) => {
        entries.forEach(item => {
            if (item.isIntersecting) {
              self.entryMap.set(item.target, item)
              self.upload(self.productMap.get(item.target));
              observer.unobserve(item.target);
              let timer = setTimeout(function(){
                 self.upload(self.productMap.get(item.target));
               	 observer.unobserve(item.target);
              }, self.time)
              self.timerMap.set(item.target, timer)
            }else{
              let _item = self.entryMap.get(item.target)
              if(_item){
                if(_item.time - item.time < self.time){                    
                  clearTimeout(self.timerMap.get(item.target));
                }
              }
            }
        })
    }, this.option);
  }
  
  // 监听元素
  add(el, data){
    this.observer && this.observer.observe(el);
    this.productMap.set(el, data);
  }

  // 发送数据
  upload(data){}
} 

MutationObserver 创建并返回一个新的观察器,它会在触发指定 DOM 事件时,调用指定的回调函数。MutationObserver 对 DOM 的观察不会立即启动;而必须先调用observe()方法来确定,要监听哪一部分的 DOM 以及要响应哪些更改。 主要用处:监听的元素发生刷新,或者延迟出现时

// 用法示例
let observer = new MutationObserver(callback);

// 开始观察
observer.observe(node, config)

// 停止观察
observer.disconnect()

// 返回已检测到但尚未由观察者的回调函数处理的所有匹配DOM更改的列表,使变更队列保持为空。此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。
observer.takeRecords()

config

属性说明
childListnode 的直接子节点的添加或删除,默认false
subtreenode 的所有后代的添加或删除,默认false
attributes观察受监视元素的属性值变更,默认值为 false
attributeFilter监视的特定属性名称的数组。如果未包含此属性,则对所有属性的更改都会触发变动通知。无默认值
characterData设为 true 监视指定目标节点或子节点树中节点所包含的字符数据的变化,无默认值
attributeOldValue为 true 将记录任何有改动的属性的上一个值
characterDataOldValue设为 true 以在文本在受监视节点上发生更改时记录节点文本的先前值

callback

一个 MutationRecord 对象列表传入第一个参数,而观察器自身作为第二个参数MutationRecord 对象具有以下属性:

  • type : 变动类型,以下类型之一:

    • "attributes":特性被修改了,
    • "characterData":数据被修改了,用于文本节点,
    • "childList":添加/删除了子元素。
  • target : 更改发生在何处:"attributes" 所在的元素,或 "characterData" 所在的文本节点,或 "childList" 变动所在的元素,

  • addedNodes/removedNodes : 添加/删除的节点,

  • previousSibling/nextSibling : 添加/删除的节点的上一个/下一个兄弟节点,

  • attributeName/attributeNamespace : 被更改的特性的名称/命名空间(用于 XML),

  • oldValue : 之前的值,仅适用于特性或文本更改,如果设置了相应选项 attributeOldValue/characterDataOldValue

附上部分代码

// 当前系统信息
function GetOS() {
  const OS = {
    WINDOWS: "Windows",
    MACINTOSH: "Mac",
    LINUX: "Linux",
    IOS: "iOS",
    ANDROID: "Android",
    BLACKBERRY: "BlackBerry",
    WINDOWS_PHONE: "WindowsPhone"
  }
  let userAgent = navigator.userAgent
  let platform = /Windows Phone (?:OS )?([d.]*)/,
    result = userAgent.match(platform)
  if (userAgent.match(platform)) {
    return { name: OS.WINDOWS_PHONE, version: result[1] }
  }
  // BlackBerry 10
  if (userAgent.indexOf("(BB10;") > 0) {
    platform = /\sVersion\/([\d.]+)\s/ // BlackBerry的regular expression
    result = userAgent.match(platform)
    if (result) {
      return { name: OS.BLACKBERRY, version: result[1] }
    } else {
      return { name: OS.BLACKBERRY, version: "10" }
    }
  }
  // iOS, Android, BlackBerry 6.0+:
  platform =
    /\(([a-zA-Z ]+);\s(?:[U]?[;]?)([\D]+)((?:[\d._]*))(?:.*[\)][^\d]*)([\d.]*)\s/
  result = userAgent.match(platform)
  if (result) {
    var appleDevices = /iPhone|iPad|iPod/
    var bbDevices = /PlayBook|BlackBerry/
    if (result[0].match(appleDevices)) {
      result[3] = result[3].replace(/_/g, ".")
      return { name: OS.IOS, version: result[3] } // iOS操作系统
    } else if (result[2].match(/Android/)) {
      result[2] = result[2].replace(/\s/g, "")
      return { name: OS.ANDROID, version: result[3] } // Android操作系统
    } else if (result[0].match(bbDevices)) {
      return { name: OS.BLACKBERRY, version: result[4] } // Blackberry
    }
  }
  //Android平台上的Firefox浏览器
  platform = /\((Android)[\s]?([\d][.\d]*)?;.*Firefox\/[\d][.\d]*/
  result = userAgent.match(platform)
  if (result) {
    return { name: OS.ANDROID, version: result.length == 3 ? result[2] : "" }
  }
  // desktop
  platform = navigator.platform
  if (platform.indexOf("Win") !== -1) {
    var rVersion = /Windows NT (\d+).(\d)/i
    var uaResult = userAgent.match(rVersion)
    var version = ""
    if (uaResult[1] == "6") {
      if (uaResult[2] == 1) {
        version = "7" // 说明当前运行在Windows 7 中
      } else if (uaResult[2] > 1) {
        version = "8" // 说明当前运行在Windows 8 中
      }
    } else {
      version = uaResult[1]
    }
    return { name: OS.WINDOWS, version: version }
  } else if (platform.indexOf("Mac") != -1) {
    result = userAgent.match(
      /\(([a-zA-Z ]+);\s(?:[U]?[;]?)([\D]+)((?:[\d._]*))(?:.*[\)][^\d]*)([\d.]*)\s/
    )
    return { name: OS.MACINTOSH, version: result[3].replace(/_/g, ".") } // Macintosh操作系统
  } else if (platform.indexOf("Linux") != -1) {
    return { name: OS.LINUX, version: "" } // 说明当前运行在Linux操作系统
  }
  return null
}

// 当前浏览器信息
function GetBrowser(){
  function getVersion(userAgent, reg) {
    var reBrowser = new RegExp(reg);
    reBrowser.test(userAgent);
    return parseFloat(RegExp['$1']);
  }
  var userAgent = navigator.userAgent;
  var version;
  if(/Instagram/i.test(userAgent)){
      version = getVersion(userAgent, 'Instagram\\s(\\d+\\.+\\d+)');
      return 'Instagram ' + version;
  }else if(/FBAN\/FBIOS/i.test(userAgent)){
      return 'Facebook App'
  }else if(/opera/i.test(userAgent) || /OPR/i.test(userAgent)){
      version = getVersion(userAgent, "OPR/(\\d+\\.+\\d+)");
      return 'Opera ' + version;
  }else if(/compatible/i.test(userAgent) && /MSIE/i.test(userAgent)){
      version = getVersion(userAgent, "MSIE (\\d+\\.+\\d+)");
      return 'IE ' + version;
  }else if(/Edge/i.test(userAgent)){
      version = getVersion(userAgent, "Edge/(\\d+\\.+\\d+)");
      return 'Edge ' + version;
  }else if(/Edg/i.test(userAgent)){
    version = getVersion(userAgent, "Edg/(\\d+\\.+\\d+)");
    return 'Edg ' + version;
  }else if(/Firefox/i.test(userAgent)){
      version = getVersion(userAgent, "Firefox/(\\d+\\.+\\d+)");
      return 'Firefox ' + version;
  }else if(/Safari/i.test(userAgent) && !/Chrome/i.test(userAgent)&& !/CriOS/i.test(userAgent)){
      version = getVersion(userAgent, "Safari/(\\d+\\.+\\d+)");
      return 'Safari ' + version;
  }else if(/Chrome/i.test(userAgent) && /Safari/i.test(userAgent)){
      version = getVersion(userAgent, "Chrome/(\\d+\\.+\\d+)");
      return 'Chrome ' + version;
  }else if(/CriOS/i.test(userAgent) && /Safari/i.test(userAgent)){
      version = getVersion(userAgent, "CriOS/(\\d+\\.+\\d+)");
      return 'Chrome ' + version;    
  }else if(!!window.ActiveXObject || "ActiveXObject" in window){
      version = 11;
      return 'IE ' + version;
  }
}

以上全部内容,如有疑问,欢迎指正。

6.webp