前端开源埋点工具
- 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
- 压缩数据
- 本地存储失败重试队列