【前端埋点】网站自动化埋点方案

2025/03/11 12:03:40

前端埋点的核心在于高效地收集、处理和上报用户行为数据。

本文中的埋点代码通过模块化设计和灵活的实现方式,提供了一个高效且易于扩展的前端埋点解决方案。

代码位置

https://gitee.com/zhangkb/example-code/tree/master/h5-trackopen in new window

整体架构设计

代码的整体架构基于一个核心类 Tracker,它负责初始化基础数据、监听用户行为事件、处理事件数据,并将数据推送到数据队列中。

Tracker 类通过与其他辅助模块(如 TrackerQueue、工具函数等)协作,实现了完整的埋点流程。

export class Tracker {
  basicParams = {
    user_info: {},
    device_info: {},
  };
  customParams = {};
  customFnMap = {};
  trackerQueue = null;
  areaVisitEntryMap = {};
  elementViewMap = {};
  counterMap = {};
  counterKey = "count";
  elementViewobserver = null;
  destroyFlag = false;

  constructor(pageType, { customParams = {}, customFnMap = {} } = {}) {
    this.basicParams.page_type = pageType;
    this.setCustomFnMap(customFnMap);
    this.setCustomParams(customParams);
    this.init();
  }

  async init() {
    this.initTrackerQueue();
    this.initBasicParams();
    await this.initBasicParamsAsync();
    this.initEvent();
  }
}

Tracker 类的作用

  • 作为埋点的核心类,Tracker 负责管理埋点的生命周期,从初始化基础数据到监听事件,再到数据上报。
  • 它通过 TrackerQueue 将数据批量发送到后端,避免频繁的网络请求,从而提高性能。
  • 提供了灵活的扩展机制,允许通过 customParamscustomFnMap 添加自定义参数。

这种模块化的设计使得代码结构清晰,易于维护和扩展。

基础信息初始化

在前端埋点中,基础信息的初始化是至关重要的一步,它为后续的数据上报提供了上下文信息。代码中通过 initBasicParamsinitBasicParamsAsync 方法完成了这一任务。

initBasicParams() {
  const deviceInfo = getDeviceInfo();

  // 页面信息
  this.basicParams["page_url"] = window.location.pathname;
  this.basicParams["page_params"] = getQuery();

  // 会话 ID,会话内唯一
  this.basicParams["session_id"] = getSessionId();

  // 设备信息
  this.basicParams["device_info"] = deviceInfo;
}

async initBasicParamsAsync() {
  const userInfo = await UserService.getUserInfo();

  // 用户信息
  if (userInfo) {
    this.basicParams["user_info"]["customer_id"] = userInfo.openId;
  }
}
  1. 同步初始化(initBasicParams

    • 页面信息:通过 window.location.pathnamegetQuery 获取当前页面的 URL 和查询参数,这些信息有助于区分不同的页面和用户行为。
    • 会话 ID:通过 getSessionId 获取会话 ID,用于标识用户在同一会话中的行为。
    • 设备信息:通过 getDeviceInfo 获取设备信息,如屏幕分辨率、浏览器类型等,这些信息有助于分析用户设备的多样性。
  2. 异步初始化(initBasicParamsAsync

    • 用户信息:通过 UserService.getUserInfo 异步获取用户信息,如用户 ID、登录状态等。这些信息对于分析用户行为和个性化推荐至关重要。

通过同步和异步结合的方式,代码确保了基础信息的完整性,为后续的数据分析提供了丰富的上下文。

事件监听与处理

前端埋点的核心在于监听和处理各种用户行为事件。代码中实现了多种事件的监听和处理逻辑,包括页面曝光、点击事件、滚动事件、元素曝光与隐藏以及区域访问等。

  1. 页面曝光(pageView 页面曝光事件(pageView)是用户进入页面时触发的第一个事件。代码通过 trackPageViewEvent 方法在页面加载时自动触发该事件,记录用户进入页面的时间和页面信息。

    trackPageViewEvent() {
      this.trackEvent(TrackEventBackend.pageView);
    }
    

    这种设计确保了页面曝光数据的完整性和实时性,为后续的页面流量分析提供了基础数据。

  2. 点击事件(click 点击事件是用户行为分析中最常见的事件之一。代码通过 handleClick 方法实现了对点击事件的监听和处理。

    async handleClick(event) {
      const target = event.target;
      const trackClickNode = findTrackNode(target, TrackEvent.click);
    
      if (trackClickNode) {
        const trackData = await this.getTrackData(trackClickNode, TrackEvent.click);
        if (trackData) {
          this.trackClickEvent(trackData.elementName, trackData.params);
        }
      }
    }
    

    当用户点击页面上的某个元素时,handleClick 方法会通过事件委托捕获点击事件,并通过 findTrackNode 方法查找最近的带有埋点属性的元素。如果找到目标元素,代码会调用 getTrackData 方法获取埋点数据,并通过 trackClickEvent 方法将点击事件数据推送到数据队列中。

    这种设计不仅提高了代码的复用性,还通过事件委托减少了事件监听器的数量,提高了性能。

  3. 滚动事件(scroll 滚动事件用于分析用户在页面上的滚动行为,例如滚动深度。代码通过 handleScroll 方法实现了滚动事件的监听。

    async handleScroll(event) {
      const totalHeight = document.documentElement.scrollHeight;
      const viewportHeight = window.innerHeight;
      const scrollY = window.scrollY;
      const scrollPercentage = (scrollY / (totalHeight - viewportHeight)) * 100;
    
      this.trackEvent(TrackEventBackend.pageScroll, null, {
        scroll_depth: scrollPercentage
      });
    }
    

    在滚动事件中,代码计算了当前滚动的深度百分比(scrollPercentage),并通过 trackEvent 方法将滚动深度数据上报到后端。为了防止滚动事件过于频繁触发,代码中可以结合 throttle 方法对滚动事件进行节流处理,从而提高性能。

  4. 元素曝光与隐藏(elementViewelementHide 元素曝光和隐藏事件用于分析页面上特定元素的可见性。代码通过 IntersectionObserver 实现了对元素曝光和隐藏的监听。

    initListenerElementView() {
      if (IntersectionObserver) {
        const nodes = document.querySelectorAll("[data-track-elementview]");
        const observer = new IntersectionObserver(this.handleElementView.bind(this));
    
        nodes.forEach((element) => {
          observer.observe(element);
        });
    
        this.elementViewobserver = observer;
      } else {
        console.error("elementView Error:不支持 IntersectionObserver");
      }
    }
    
    handleElementView(entries, observer) {
      entries.forEach(async (entry) => {
        const trackData = await this.getTrackData(entry.target, TrackEvent.elementView);
        const elementName = trackData.elementName;
    
        if (trackData) {
          if (entry.isIntersecting) {
            this.trackElementViewEvent(elementName, trackData.params);
          } else if (this.elementViewMap[elementName]) {
            this.trackElementHideEvent(elementName, trackData.params);
          }
        }
      });
    }
    

    initListenerElementView 方法中,代码创建了一个 IntersectionObserver 实例,并对带有 data-track-elementview 属性的元素进行监听。当元素进入视口时,触发 elementView 事件;当元素离开视口时,触发 elementHide 事件。通过这种方式,代码可以记录元素的曝光时长,为分析用户对页面元素的关注程度提供数据支持。

  5. 区域访问(areaVisit 区域访问事件用于分析用户在页面特定区域的行为。代码通过 handleClick 方法实现了对区域访问事件的监听。

    const trackAreavisitNode = findTrackNode(target, TrackEvent.areaVisit);
    
    if (trackAreavisitNode) {
      const trackData = await this.getTrackData(
        trackAreavisitNode,
        TrackEvent.areaVisit
      );
      if (trackData && !this.areaVisitEntryMap[trackData.elementName]) {
        trackData.timestamp = Date.now();
        this.areaVisitEntryMap[trackData.elementName] = trackData;
        this.trackEvent(
          TrackEventBackend.areaVisitEntry,
          trackData.elementName,
          trackData.params
        );
      }
    }
    

    当用户点击某个区域时,代码会检查是否存在 areaVisit 事件,并记录进入和离开该区域的时间。通过这种方式,代码可以计算用户在特定区域的停留时长,从而为分析用户行为模式提供数据支持。

自定义参数与动态参数

在实际的业务场景中,除了基础的埋点数据外,往往还需要添加一些自定义参数来丰富数据的维度。代码通过 customParamscustomFnMapgetDynamicParams 方法实现了自定义参数的灵活扩展。

setCustomParams(customParams) {
  this.customParams = customParams;
}

setCustomFnMap(customFnMap) {
  this.customFnMap = customFnMap;
}

async getTrackData(element, type) {
  try {
    const elementName = element.getAttribute(`data-track-${type}`);
    const dynamicParams = getDynamicParams(element);
    const customFnParams = await this.getCustomFnParams(element);

    return {
      elementName,
      params: {
        ...dynamicParams,
        ...customFnParams
      }
    };
  } catch (error) {
    console.error("获取埋点数据失败 getTrackData:", error);
    return null;
  }
}
  1. 自定义参数(customParamscustomFnMap

    • customParams 是一个对象,用于存储通用的自定义参数。开发者可以在初始化 Tracker 实例时传入这些参数,它们会被附加到每一条埋点数据中。
    • customFnMap 是一个函数映射表,允许开发者通过函数动态生成自定义参数。在 getCustomFnParams 方法中,代码会根据元素的 data-track-fn 属性调用对应的函数,并将返回的结果作为自定义参数。
    • 这种设计使得埋点数据能够根据具体业务需求灵活扩展,提高了代码的通用性和可扩展性。
  2. 动态参数(getDynamicParams

    • 动态参数是指根据当前页面环境动态生成的参数。代码通过 getDynamicParams 方法从 DOM 元素中提取这些参数。
    • 例如,开发者可以在 DOM 元素上添加 data-track-dynamic 属性,getDynamicParams 方法会解析这些属性并将其值作为动态参数附加到埋点数据中。
    • 这种设计使得埋点数据能够根据页面的具体情况动态调整,进一步丰富了数据的维度。

数据上报机制

数据上报是前端埋点的最后一步,也是至关重要的一步。代码通过 TrackerQueueHttp.post 方法实现了数据的上报。

async send(data) {
  console.log("埋点数据上报:", data);

  if (this.destroyFlag && typeof navigator.sendBeacon === "function") {
    const jsonData = JSON.stringify(data);
    const blob = new Blob([jsonData], { type: "application/json" });

    const result = navigator.sendBeacon("https://api.haoma.cn/event-tracking/hmzj/user-behavior-events/send", blob);
    console.log("navigator.sendBeacon 推送数据", result);
    return;
  }

  const response = await Http.post({
    url: "https://api.haoma.cn/event-tracking/hmzj/user-behavior-events/send",
    headers: {
      "content-type": "application/json;charset=utf-8"
    },
    data: JSON.stringify(data)
  });
}
  1. 队列管理(TrackerQueue

    • TrackerQueue 是一个数据队列,用于管理埋点数据的存储和上报。它将埋点数据批量发送到后端,减少了网络请求的次数,提高了性能。
    • trackEvent 方法中,代码将每一条埋点数据推送到 TrackerQueue 中。当队列中的数据达到一定数量时,TrackerQueue 会自动触发数据上报。
    • 这种设计不仅提高了数据上报的效率,还通过批量发送减少了网络请求的开销。
  2. 发送方式

    • 在数据上报时,代码根据当前的页面状态选择不同的发送方式。如果页面即将离开(beforeunload 事件触发),代码会使用 navigator.sendBeacon 方法发送数据。
    • navigator.sendBeacon 是一种高效的异步发送方式,它可以在页面卸载时确保数据被发送到后端,而不会阻塞页面的卸载过程。
    • 如果页面处于正常状态,代码会使用 Http.post 方法发送数据。通过这种方式,代码确保了数据上报的可靠性和效率。

参考

浅谈前端埋点 & 监控open in new window

前端埋点实现方案 ✔open in new window

浅析前端监控与埋点open in new window

DOMContentLoaded与load的区别open in new window