前端埋点(打点)方案
前端开源埋点工具
- Google Analytics: 免费、易于使用,但功能有限。
- Amplitude: 付费,功能强大,提供详细的分析报告。
- Mixpanel: 付费,功能与 Amplitude 类似,专攻用户行为分析。
- Heap: 付费,无需代码,基于事件的数据收集。
- Pendo: 付费,提供深入的用户行为洞察和产品采用分析。
前端埋点 sdk 相关设计
1. 架构设计
// 基础架构
window.MyTracker = {init: function (config) {/* 初始化配置 */},track: function (eventName, params) {/* 发送埋点数据 */},setUser: function (userId) {/* 设置用户信息 */},pageView: function (pageInfo) {/* 页面访问埋点 */},
};
2. 核心功能实现
数据收集模块
// 数据收集模块
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,// ...其他设备信息};}// 生成会话IDgenerateSessionId() {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;
数据发送模块
// 数据发送模块
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;
主模块
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;// 自动记录UVif (this.config.autoUV !== false) {this.uv();}// 自动记录PVif (this.config.autoPV !== false) {this.pageView();}this.log("Tracker initialized");}// 用户浏览埋点(UV)uv(uvInfo = {}) {// 只在本会话内上报一次if (this.uvMarked) return;this.uvMarked = true;this.track("uv", uvInfo);}// 设置用户IDsetUser(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. 自动埋点实现
用户行为自动捕获
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 集成
import React, { useEffect, useContext, createContext } from "react";
import tracker from "./index";// 创建上下文
const TrackerContext = createContext();// 提供器组件
export function TrackerProvider({ children, config }) {useEffect(() => {// 初始化埋点SDKtracker.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. 示例
import React from "react";
import { TrackerProvider } from "./sdk/tracker/react-integration";
import Router from "@/router/index.jsx";function App() {return (<TrackerProviderconfig={{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;
组件中使用:
// 在组件中使用
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>{/* 组件式埋点 */}<TrackClickeventName="button_click"params={{ buttonName: "组件式埋点按钮" }}><button>点击我(组件式埋点)</button></TrackClick>{/* 代码式埋点 */}<button onClick={handleButtonClick}>点击我(代码式埋点)</button>{/* 曝光埋点 */}<divdata-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
- 压缩数据
- 本地存储失败重试队列