前端埋点(打点)方案

前端开源埋点工具

  • Google Analytics: 免费、易于使用,但功能有限。
  • Amplitude: 付费,功能强大,提供详细的分析报告。
  • Mixpanel: 付费,功能与 Amplitude 类似,专攻用户行为分析。
  • Heap: 付费,无需代码,基于事件的数据收集。
  • Pendo: 付费,提供深入的用户行为洞察和产品采用分析。

前端埋点 sdk 相关设计

1. 架构设计

js 复制代码
// 基础架构
window.MyTracker = {
  init: function (config) {
    /* 初始化配置 */
  },
  track: function (eventName, params) {
    /* 发送埋点数据 */
  },
  setUser: function (userId) {
    /* 设置用户信息 */
  },
  pageView: function (pageInfo) {
    /* 页面访问埋点 */
  },
};

2. 核心功能实现

数据收集模块

js 复制代码
// 数据收集模块
class Collector {
  constructor(config) {
    this.config = config;
    this.deviceInfo = this.getDeviceInfo();
    this.sessionId = this.generateSessionId();
  }

  // 获取设备信息
  getDeviceInfo() {
    return {
      userAgent: navigator.userAgent,
      screenWidth: window.screen.width,
      screenHeight: window.screen.height,
      language: navigator.language,
      platform: navigator.platform,
      // ...其他设备信息
    };
  }

  // 生成会话ID
  generateSessionId() {
    return (
      "session_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9)
    );
  }

  // 收集基础数据
  collectBaseInfo() {
    return {
      timestamp: Date.now(),
      url: location.href,
      referrer: document.referrer,
      sessionId: this.sessionId,
      deviceInfo: this.deviceInfo,
      // ...其他基础信息
    };
  }
}

export default Collector;

数据发送模块

js 复制代码
// 数据发送模块
class Sender {
  constructor(config) {
    this.config = config;
    this.queue = [];
    this.sending = false;

    // 如果配置了自动发送,则启动定时器
    if (config.autoSend !== false) {
      this.timer = setInterval(() => {
        this.flush();
      }, config.sendInterval || 5000);
    }

    // 页面关闭前尝试发送剩余数据
    window.addEventListener("beforeunload", () => {
      this.flush(true);
    });
  }

  // 添加数据到队列
  add(data) {
    this.queue.push(data);

    // 如果队列达到阈值,则立即发送
    if (this.queue.length >= (this.config.batchSize || 10)) {
      this.flush();
    }
  }

  // 发送数据
  flush(isSync = false) {
    if (this.sending || this.queue.length === 0) return;

    this.sending = true;
    const dataToSend = [...this.queue];
    this.queue = [];

    const sendUrl = this.config.apiUrl || "/api/tracker";

    if (isSync && navigator.sendBeacon) {
      // 使用 sendBeacon 在页面关闭时发送数据
      navigator.sendBeacon(sendUrl, JSON.stringify(dataToSend));
      this.sending = false;
    } else {
      // 使用 fetch 发送数据
      fetch(sendUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(dataToSend),
        keepalive: true,
      })
        .then(() => {
          this.sending = false;
        })
        .catch((err) => {
          console.error("埋点数据发送失败:", err);
          // 发送失败时,将数据放回队列
          this.queue = [...dataToSend, ...this.queue];
          this.sending = false;
        });
    }
  }
}

export default Sender;

主模块

js 复制代码
import Collector from "./collector";
import Sender from "./sender";

class Tracker {
  constructor() {
    this.initialized = false;
    this.userId = null;
    this.uvMarked = false;
  }

  init(config) {
    if (this.initialized) return;

    this.config = {
      apiUrl: "/api/tracker",
      appId: "default",
      debug: false,
      autoSend: true,
      sendInterval: 5000,
      batchSize: 10,
      autoUV: true, // 新增配置,是否自动上报uv
      ...config,
    };

    this.collector = new Collector(this.config);
    this.sender = new Sender(this.config);

    this.initialized = true;

    // 自动记录UV
    if (this.config.autoUV !== false) {
      this.uv();
    }

    // 自动记录PV
    if (this.config.autoPV !== false) {
      this.pageView();
    }

    this.log("Tracker initialized");
  }
  // 用户浏览埋点(UV)
  uv(uvInfo = {}) {
    // 只在本会话内上报一次
    if (this.uvMarked) return;
    this.uvMarked = true;
    this.track("uv", uvInfo);
  }

  // 设置用户ID
  setUser(userId) {
    this.userId = userId;
    this.log(`User set: ${userId}`);
  }

  // 页面访问埋点
  pageView(pageInfo = {}) {
    this.track("page_view", pageInfo);
  }

  // ...existing code...

  // 记录事件
  track(eventName, params = {}) {
    if (!this.initialized) {
      console.error("Tracker not initialized");
      return;
    }

    const data = {
      ...this.collector.collectBaseInfo(),
      eventName,
      params,
      userId: this.userId,
      appId: this.config.appId,
    };

    this.sender.add(data);
    this.log(`Event tracked: ${eventName}`, params);
  }

  // 调试日志
  log(...args) {
    if (this.config.debug) {
      console.log("[Tracker]", ...args);
    }
  }
}

// 创建单例
const tracker = new Tracker();

// 暴露全局变量
window.MyTracker = tracker;

export default tracker;

3. 自动埋点实现

用户行为自动捕获

js 复制代码
import tracker from "./index";

// 自动埋点模块
class AutoTracker {
  constructor(config = {}) {
    this.config = {
      clickTrack: true,
      clickAttributeName: "data-track-click",
      exposureTrack: true,
      exposureAttributeName: "data-track-exposure",
      ...config,
    };

    this.init();
  }

  init() {
    // 点击埋点
    if (this.config.clickTrack) {
      document.addEventListener("click", this.handleClick.bind(this), true);
    }

    // 曝光埋点
    if (this.config.exposureTrack) {
      // 创建交叉观察器
      this.initIntersectionObserver();

      // 初始化时扫描页面元素
      this.scanForExposureElements();

      // 监听DOM变化,扫描新增元素
      this.observeDomChanges();
    }
  }

  // 处理点击事件
  handleClick(event) {
    const target = event.target;

    // 向上查找带有埋点属性的元素
    let currentElement = target;
    while (currentElement && currentElement !== document.body) {
      const trackData = currentElement.getAttribute(
        this.config.clickAttributeName
      );
      if (trackData) {
        try {
          const eventData = JSON.parse(trackData);
          tracker.track("element_click", {
            ...eventData,
            elementPath: this.getElementPath(currentElement),
            elementContent: currentElement.textContent?.trim(),
          });
          break;
        } catch (err) {
          console.error("解析埋点数据失败:", err);
        }
      }
      currentElement = currentElement.parentElement;
    }
  }

  // 初始化交叉观察器
  initIntersectionObserver() {
    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            const element = entry.target;
            const trackData = element.getAttribute(
              this.config.exposureAttributeName
            );

            if (trackData) {
              try {
                const eventData = JSON.parse(trackData);
                tracker.track("element_exposure", {
                  ...eventData,
                  elementPath: this.getElementPath(element),
                });

                // 曝光后移除观察,防止重复上报
                this.observer.unobserve(element);
              } catch (err) {
                console.error("解析埋点数据失败:", err);
              }
            }
          }
        });
      },
      {
        threshold: [0.5], // 元素50%可见时触发
      }
    );
  }

  // 扫描页面中需要曝光埋点的元素
  scanForExposureElements() {
    const elements = document.querySelectorAll(
      `[${this.config.exposureAttributeName}]`
    );
    elements.forEach((element) => {
      this.observer.observe(element);
    });
  }

  // 监听DOM变化
  observeDomChanges() {
    const mutationObserver = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === "childList") {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === 1) {
              // 元素节点
              // 检查节点本身
              if (node.hasAttribute(this.config.exposureAttributeName)) {
                this.observer.observe(node);
              }

              // 检查子节点
              const childElements = node.querySelectorAll(
                `[${this.config.exposureAttributeName}]`
              );
              childElements.forEach((element) => {
                this.observer.observe(element);
              });
            }
          });
        }
      });
    });

    mutationObserver.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }

  // 获取元素路径
  getElementPath(element) {
    const path = [];
    let currentElement = element;

    while (currentElement && currentElement !== document.body) {
      let selector = currentElement.tagName.toLowerCase();

      if (currentElement.id) {
        selector += `#${currentElement.id}`;
      } else if (currentElement.className) {
        selector += `.${Array.from(currentElement.classList).join(".")}`;
      }

      path.unshift(selector);
      currentElement = currentElement.parentElement;
    }

    return path.join(" > ");
  }
}

export default AutoTracker;

4. React 集成

js 复制代码
import React, { useEffect, useContext, createContext } from "react";
import tracker from "./index";

// 创建上下文
const TrackerContext = createContext();

// 提供器组件
export function TrackerProvider({ children, config }) {
  useEffect(() => {
    // 初始化埋点SDK
    tracker.init(config);

    return () => {
      // 在组件卸载时发送剩余数据
      tracker.sender.flush(true);
    };
  }, [config]);

  return (
    <TrackerContext.Provider value={tracker}>
      {children}
    </TrackerContext.Provider>
  );
}

// 埋点Hook
export function useTracker() {
  const tracker = useContext(TrackerContext);

  if (!tracker) {
    throw new Error("useTracker must be used within a TrackerProvider");
  }

  return tracker;
}

// 页面埋点高阶组件
export function withPageTracking(WrappedComponent, pageInfo = {}) {
  return function WithPageTracking(props) {
    useEffect(() => {
      // 页面访问埋点
      tracker.pageView({
        pageName: WrappedComponent.displayName || WrappedComponent.name,
        ...pageInfo,
        ...props.pageTrackingParams,
      });
    }, [props.pageTrackingParams]);

    return <WrappedComponent {...props} />;
  };
}

// 点击埋点组件
export function TrackClick({ eventName, params, children, ...rest }) {
  const handleClick = () => {
    tracker.track(eventName, params);
  };

  return (
    <div onClick={handleClick} {...rest}>
      {children}
    </div>
  );
}

5. 示例

js 复制代码
import React from "react";
import { TrackerProvider } from "./sdk/tracker/react-integration";
import Router from "@/router/index.jsx";

function App() {
  return (
    <TrackerProvider
      config={{
        apiUrl: "https://analytics-api.example.com/collect",
        appId: "my-react-app",
        debug: process.env.NODE_ENV !== "production",
      }}
    >
      <div className="App">
        <Router />
      </div>
    </TrackerProvider>
  );
}

export default App;

组件中使用:

js 复制代码
// 在组件中使用
import React from "react";
import { useTracker, TrackClick } from "@/sdk/tracker/react-integration";

function MyComponent() {
  const tracker = useTracker();

  const handleButtonClick = () => {
    // 手动埋点
    tracker.track("custom_button_click", {
      buttonName: "自定义按钮",
      timestamp: Date.now(),
    });
  };

  return (
    <div>
      <h1>埋点演示</h1>

      {/* 自动埋点 */}
      <button data-track-click='{"event_type":"button_click","button_name":"普通按钮"}'>
        点击我(声明式埋点)
      </button>

      {/* 组件式埋点 */}
      <TrackClick
        eventName="button_click"
        params={{ buttonName: "组件式埋点按钮" }}
      >
        <button>点击我(组件式埋点)</button>
      </TrackClick>

      {/* 代码式埋点 */}
      <button onClick={handleButtonClick}>点击我(代码式埋点)</button>

      {/* 曝光埋点 */}
      <div
        data-track-exposure='{"event_type":"banner_exposure","banner_id":"homepage-top"}'
        style={{ height: "200px", background: "#f0f0f0", margin: "20px 0" }}
      >
        这是一个会触发曝光埋点的Banner
      </div>
    </div>
  );
}

export default MyComponent;

6. 性能优化

  • 使用批量发送
  • 降低埋点频率(节流/防抖)
  • 使用 sendBeacon 或 keepalive
  • 压缩数据
  • 本地存储失败重试队列
相关推荐
CC码码5 分钟前
解决前端多标签页通信:BroadcastChannel
前端·javascript·web
墨鸦_Cormorant9 分钟前
Vue 概述以及基本使用
前端·javascript·vue.js
JarvanMo22 分钟前
10 个能帮你节省大量开发时间的低估 Flutter 组件
前端
去伪存真25 分钟前
公司前端项目ESLint规则集统一化
前端
鹏多多29 分钟前
使用imaskjs实现js表单输入卡号/日期/货币等掩码的教程
前端·javascript·vue.js
w2vmany30 分钟前
postmessage xss初步学习
前端·学习·xss
小张成长计划..1 小时前
前端6:CSS3 2D转换,CSS3动画,CSS3 3D转换
前端·3d·css3
IT_陈寒1 小时前
Vue3性能优化实战:这7个技巧让我的应用加载速度提升50%!
前端·人工智能·后端
西西学代码2 小时前
Flutter---音效模式选择器
前端·html
TLucas2 小时前
Layui连线题编辑器组件(ConnectQuestion)
前端·编辑器·layui