0%

前端埋点 Hook 实践——数据追踪与性能监控方案

最近在公司内部开发了一套埋点系统,发现市面上的埋点方案要么太重,要么不够灵活。于是花了两周时间研究了一套基于 Hooks 的轻量级埋点方案,支持全局监听、自定义事件等特性。给有类似需求的同学参考。

前端埋点概述

  前端埋点是指在用户界面的关键节点植入数据收集代码,用于追踪用户行为、分析产品使用情况、优化用户体验的重要手段。通过埋点系统,我们可以了解用户的使用习惯、页面访问路径、功能使用频率等关键指标,为产品迭代和商业决策提供数据支撑。

  在现代 Web 应用中,埋点系统通常需要满足以下要求:

  1. 数据采集准确性:确保捕获到用户的真实行为
  2. 性能影响最小化:不影响用户体验和页面性能
  3. 灵活性:支持自定义事件和属性
  4. 扩展性:能够适应业务发展的需要
  5. 易用性:开发人员能够快速集成和使用

埋点类型分类

  1. 页面埋点: 记录页面访问、停留时长等
  2. 点击埋点: 追踪用户点击行为
  3. 曝光埋点: 检测元素是否在可视区域内
  4. 表单埋点: 监控表单填写和提交
  5. 错误埋点: 收集 Javascript 错误和性能问题

核心埋点系统设计

数据结构设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// types/tracking.JS
/**
* 埋点事件数据结构
* @typedef {Object} TrackingEvent
* @property {string} eventId - 事件唯一标识
* @property {string} eventType - 事件类型
* @property {string} timestamp - 时间戳
* @property {string} pageUrl - 页面 URL
* @property {string} pageTitle - 页面标题
* @property {Object} properties - 事件属性
* @property {Object} userAgent - 用户代理信息
* @property {string} userId - 用户 ID
* @property {string} sessionId - 会话 ID
*/

/**
* 埋点配置选项
* @typedef {Object} TrackingOptions
* @property {string} apiKey - API 密钥
* @property {string} endpoint - 上报端点
* @property {number} batchSize - 批量上报数量
* @property {number} timeout - 请求超时时间
* @property {boolean} enableAutoTrack - 是否启用自动追踪
* @property {boolean} enableSPA - 是否启用 SPA 模式
*/

事件收集器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// core/event-collector.JS
class EventCollector {
constructor() {
this.queue = [];
this.pendingFlush = null;
this.batchSize = 10;
this.flushInterval = 1000; // 1秒
}

/**
* 添加事件到队列
* @param {TrackingEvent} event - 事件数据
*/
addEvent(event) {
this.queue.push(event);

// 如果达到批次大小,立即上报 if (this.queue.length >= this.batchSize) {
this.flushEvents();
} else if (!this.pendingFlush) {
// 否则设置延时上报 this.pendingFlush = setTimeout(() => {
this.flushEvents();
this.pendingFlush = null;
}, this.flushInterval);
}
}

/**
* 立即上报事件队列
*/
async flushEvents() {
if (this.queue.length === 0) return;

const events = [...this.queue];
this.queue = [];

try {
await this.sendEvents(events);
} catch (error) {
console.error('事件上报失败:', error);
// 失败后重新加入队列(可根据策略决定是否重试)
this.queue.unshift(...events);
}
}

/**
* 发送事件到服务器
* @param {Array<TrackingEvent>} events - 事件数组
*/
async sendEvents(events) {
// 使用 fetch API 发送数据 const response = await fetch('/API/tracking/batch', {
method: 'POST',
headers: {
'Content-Type': 'application/Json',
'Authorization': `Bearer ${this.apiKey}`
},
body: Json.stringify({
events,
timestamp: Date.now()
})
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return response.Json();
}

/**
* 获取队列大小
*/
getQueueSize() {
return this.queue.length;
}

/**
* 清空队列
*/
clearQueue() {
this.queue = [];
if (this.pendingFlush) {
clearTimeout(this.pendingFlush);
this.pendingFlush = null;
}
}
}

事件处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// core/event-handler.JS
class EventHandler {
constructor(collector) {
this.collector = collector;
this.autoTrackEnabled = false;
this.spaTrackingEnabled = false;
}

/**
* 初始化事件处理器
*/
initialize(options = {}) {
this.options = options;
this.apiKey = options.apiKey;
this.endpoint = options.endpoint;

if (options.enableAutoTrack) {
this.enableAutoTracking();
}

if (options.enableSPA) {
this.enableSPATracking();
}
}

/**
* 追踪自定义事件
* @param {string} eventName - 事件名称
* @param {Object} properties - 事件属性
*/
track(eventName, properties = {}) {
const event = this.buildEvent(eventName, properties);
this.collector.addEvent(event);
}

/**
* 构建事件对象
* @param {string} eventName - 事件名称
* @param {Object} properties - 事件属性
* @returns {TrackingEvent}
*/
buildEvent(eventName, properties = {}) {
return {
eventId: this.generateEventId(),
eventType: eventName,
timestamp: Date.now(),
pageUrl: window.location.href,
pageTitle: document.title,
properties: {
...properties,
userAgent: navigator.userAgent,
screenResolution: `${screen.width}x${screen.height}`,
viewportSize: `${window.innerWidth}x${window.innerHeight}`,
referrer: document.referrer
},
userId: this.getUserId(),
sessionId: this.getSessionId(),
customProperties: properties
};
}

/**
* 生成事件 ID
* @returns {string}
*/
generateEventId() {
return `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}

/**
* 获取用户 ID
* @returns {string|null}
*/
getUserId() {
return localStorage.getItem('user_id') || null;
}

/**
* 获取会话 ID
* @returns {string}
*/
getSessionId() {
let sessionId = sessionStorage.getItem('session_id');

if (!sessionId) {
sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
sessionStorage.setItem('session_id', sessionId);

// 记录会话开始时间 sessionStorage.setItem('session_start', Date.now().toString());
}

return sessionId;
}

/**
* 启用自动追踪
*/
enableAutoTracking() {
this.autoTrackEnabled = true;

// 监听点击事件 document.addEventListener('click', this.handleClick.bind(this), true);

// 监听表单提交 document.addEventListener('submit', this.handleSubmit.bind(this), true);

// 监听页面可见性变化 document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));

// 监听页面卸载(上报剩余事件)
window.addEventListener('beforeunload', () => {
this.collector.flushEvents();
});
}

/**
* 处理点击事件
* @param {Event} event - 点击事件
*/
handleClick(event) {
const target = event.target;
const trackingInfo = this.getElementTrackingInfo(target);

if (trackingInfo) {
this.track('click', {
elementId: target.id,
elementClass: target.className,
elementTag: target.tagName,
elementText: this.getElementText(target),
x: event.clientX,
y: event.clientY,
...trackingInfo
});
}
}

/**
* 处理表单提交事件
* @param {Event} event - 表单提交事件
*/
handleSubmit(event) {
const form = event.target;
const formData = new FormData(form);

this.track('form_submit', {
formId: form.id,
formAction: form.action,
formData: Object.fromEntries(formData.entries())
});
}

/**
* 处理页面可见性变化
*/
handleVisibilityChange() {
if (document.visibilityState === 'hidden') {
this.track('page_hide');
} else if (document.visibilityState === 'visible') {
this.track('page_show');
}
}

/**
* 启用 SPA 追踪
*/
enableSPATracking() {
this.spaTrackingEnabled = true;

// 监听路由变化 const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;

history.pushState = (...args) => {
originalPushState.apply(history, args);
this.handleRouteChange();
};

history.replaceState = (...args) => {
originalReplaceState.apply(history, args);
this.handleRouteChange();
};

// 监听 hash 变化 window.addEventListener('hashchange', this.handleRouteChange.bind(this));
}

/**
* 处理路由变化
*/
handleRouteChange() {
// 上报页面离开事件 this.track('page_leave', {
previousUrl: document.referrer,
pageStayDuration: this.getPageStayDuration()
});

// 重新设置页面进入时间 sessionStorage.setItem('page_entry_time', Date.now().toString());

// 上报页面进入事件 setTimeout(() => {
this.track('page_enter', {
currentUrl: window.location.href,
pageDepth: window.location.pathname.split('/').length - 1
});
}, 0);
}

/**
* 获取页面停留时长
* @returns {number}
*/
getPageStayDuration() {
const entryTime = sessionStorage.getItem('page_entry_time');
return entryTime ? Date.now() - parseInt(entryTime) : 0;
}

/**
* 获取元素埋点信息
* @param {HtmlElement} element - DOM 元素
* @returns {Object|null}
*/
getElementTrackingInfo(element) {
// 检查是否有 data-track 属性 const trackAttr = element.getAttribute('data-track');
if (trackAttr) {
try {
return Json.parse(trackAttr);
} catch {
return { category: trackAttr };
}
}

// 检查父元素是否有跟踪属性 let parent = element.parentElement;
while (parent && parent !== document) {
const parentTrackAttr = parent.getAttribute('data-track');
if (parentTrackAttr) {
try {
return Json.parse(parentTrackAttr);
} catch {
return { category: parentTrackAttr };
}
}
parent = parent.parentElement;
}

return null;
}

/**
* 获取元素文本内容
* @param {HtmlElement} element - DOM 元素
* @returns {string}
*/
getElementText(element) {
return element.innerText || element.textContent || element.value || '';
}
}

React Hooks 实现

核心 Hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// hooks/useTracking.JS
import { useEffect, useRef, useCallback } from 'React';

// 全局追踪实例 let trackingInstance = null;

/**
* 创建追踪实例
* @param {TrackingOptions} options - 配置选项
* @returns {EventHandler}
*/
export function createTrackingInstance(options) {
if (!trackingInstance) {
const collector = new EventCollector();
const handler = new EventHandler(collector);
handler.initialize(options);
trackingInstance = handler;
}
return trackingInstance;
}

/**
* 使用追踪 Hook
* @param {TrackingOptions} options - 配置选项
* @returns {Object} 追踪 API
*/
export function useTracking(options = {}) {
const trackingRef = useRef(null);

useEffect(() => {
trackingRef.current = createTrackingInstance(options);
}, [options]);

const track = useCallback((eventName, properties = {}) => {
if (trackingRef.current) {
trackingRef.current.track(eventName, properties);
}
}, []);

const buildEvent = useCallback((eventName, properties = {}) => {
if (trackingRef.current) {
return trackingRef.current.buildEvent(eventName, properties);
}
return null;
}, []);

return {
track,
buildEvent,
instance: trackingRef.current
};
}

页面访问追踪 Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// hooks/usePageTracking.JS
import { useEffect } from 'React';
import { useTracking } from './useTracking';

/**
* 页面访问追踪 Hook
* @param {string} pageName - 页面名称
* @param {Object} pageProperties - 页面属性
*/
export function usePageTracking(pageName, pageProperties = {}) {
const { track } = useTracking();

useEffect(() => {
// 页面进入 track('page_view', {
pageName,
url: window.location.href,
...pageProperties
});

// 页面停留时间追踪 const startTime = Date.now();

return () => {
const duration = Date.now() - startTime;
track('page_exit', {
pageName,
duration,
...pageProperties
});
};
}, [pageName, track, pageProperties]);
}

点击事件追踪 Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// hooks/useClickTracking.JS
import { useCallback } from 'React';
import { useTracking } from './useTracking';

/**
* 点击事件追踪 Hook
* @param {string} eventName - 事件名称
* @param {Object} eventProperties - 事件属性
* @returns {Function} 追踪点击事件的回调函数
*/
export function useClickTracking(eventName, eventProperties = {}) {
const { track } = useTracking();

const handleClick = useCallback((event) => {
const element = event.target;

track(eventName, {
elementId: element.id,
elementClass: element.className,
elementTag: element.tagName,
elementText: element.innerText || element.textContent || element.value || '',
x: event.clientX,
y: event.clientY,
...eventProperties
});

// 如果提供了原始事件处理函数,继续执行 if (eventProperties.onEvent) {
eventProperties.onEvent(event);
}
}, [track, eventName, eventProperties]);

return handleClick;
}

/**
* 批量追踪 Hook
* @param {Array<Object>} trackingConfigs - 追踪配置数组
*/
export function useBatchTracking(trackingConfigs) {
const { track } = useTracking();

const trackMultiple = useCallback((baseEventName, baseProperties = {}) => {
trackingConfigs.forEach(config => {
const finalProperties = {
...baseProperties,
...config.properties
};
track(`${baseEventName}_${config.name}`, finalProperties);
});
}, [track, trackingConfigs]);

return { trackMultiple };
}

曝光追踪 Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// hooks/useExposureTracking.JS
import { useEffect, useRef } from 'React';
import { useTracking } from './useTracking';

/**
* 曝光追踪 Hook
* @param {string} eventName - 事件名称
* @param {Object} eventProperties - 事件属性
* @param {Object} options - 选项配置
* @returns {Object} ref 对象
*/
export function useExposureTracking(eventName, eventProperties = {}, options = {}) {
const { track } = useTracking();
const elementRef = useRef(null);
const hasTracked = useRef(false);
const observerRef = useRef(null);

const {
threshold = 0.5, // 50%可见时触发 rootMargin = '0px',
trackOnce = true // 是否只追踪一次
} = options;

useEffect(() => {
if (!elementRef.current) return;

// 创建 Intersection Observer
observerRef.current = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && (!trackOnce || !hasTracked.current)) {
track(eventName, {
elementId: elementRef.current?.id,
elementClass: elementRef.current?.className,
intersectionRatio: entry.intersectionRatio,
...eventProperties
});

if (trackOnce) {
hasTracked.current = true;
}
}
});
}, {
threshold,
rootMargin
});

observerRef.current.observe(elementRef.current);

// 清理函数 return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, [eventName, eventProperties, threshold, rootMargin, trackOnce, track]);

return elementRef;
}

组件化埋点实现

高阶组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// hoc/withTracking.JS
import React, { forwardRef } from 'React';
import { useTracking } from '../hooks/useTracking';

/**
* 追踪 HOC
* @param {React.Component} WrappedComponent - 包装的组件
* @param {Object} trackingConfig - 追踪配置
*/
export function withTracking(WrappedComponent, trackingConfig = {}) {
const TrackedComponent = forwardRef((props, ref) => {
const { track } = useTracking();

// 自动追踪组件挂载 React.useEffect(() => {
track('component_mount', {
componentName: WrappedComponent.displayName || WrappedComponent.name,
...trackingConfig
});

return () => {
track('component_unmount', {
componentName: WrappedComponent.displayName || WrappedComponent.name,
...trackingConfig
});
};
}, [track]);

return <WrappedComponent ref={ref} {...props} />;
});

TrackedComponent.displayName = `withTracking(${
WrappedComponent.displayName || WrappedComponent.name
})`;

return TrackedComponent;
}

自定义追踪组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// components/TrackedButton.JS
import React from 'React';
import { useClickTracking } from '../hooks/useClickTracking';

const TrackedButton = ({
children,
onClick,
trackEvent = 'button_click',
trackProperties = {},
...props
}) => {
const handleClick = useClickTracking(trackEvent, {
...trackProperties,
onEvent: onClick
});

return (
<button
onClick={handleClick}
data-track={Json.stringify(trackProperties)}
{...props}
>
{children}
</button>
);
};

export default TrackedButton;

// 组件用法示例
/*
<TrackedButton
trackEvent="login_button_click"
trackProperties={{
buttonType: 'primary',
page: 'login'
}}
>
登录
</TrackedButton>
*/

表单追踪组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// components/TrackedForm.JS
import React, { useCallback } from 'React';
import { useTracking } from '../hooks/useTracking';

const TrackedForm = ({
children,
onSubmit,
formName = 'unknown_form',
...props
}) => {
const { track } = useTracking();

const handleSubmit = useCallback((event) => {
event.preventDefault();

// 追踪表单提交 track('form_submit_start', {
formName,
timestamp: Date.now()
});

// 执行原始提交逻辑 if (onSubmit) {
Promise.resolve(onSubmit(event))
.then(() => {
track('form_submit_success', {
formName,
submitTime: Date.now()
});
})
.catch((error) => {
track('form_submit_error', {
formName,
error: error.message,
submitTime: Date.now()
});
});
}
}, [onSubmit, formName, track]);

return (
<form onSubmit={handleSubmit} {...props}>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
...child.props,
'data-track': Json.stringify({ formName })
});
}
return child;
})}
</form>
);
};

export default TrackedForm;

高级追踪功能

错误追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// hooks/useErrorTracking.JS
import { useEffect } from 'React';
import { useTracking } from './useTracking';

/**
* 错误追踪 Hook
*/
export function useErrorTracking() {
const { track } = useTracking();

useEffect(() => {
// 捕获 Javascript 错误 const handleError = (event) => {
track('javascript_error', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
url: window.location.href
});
};

// 捕获 Promise 拒绝 const handleUnhandledRejection = (event) => {
track('unhandled_rejection', {
reason: event.reason?.toString(),
stack: event.reason?.stack,
url: window.location.href
});
};

window.addEventListener('error', handleError);
window.addEventListener('unhandledrejection', handleUnhandledRejection);

return () => {
window.removeEventListener('error', handleError);
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
};
}, [track]);
}

性能追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// hooks/usePerformanceTracking.JS
import { useEffect } from 'React';
import { useTracking } from './useTracking';

/**
* 性能追踪 Hook
*/
export function usePerformanceTracking() {
const { track } = useTracking();

useEffect(() => {
// 页面加载完成时追踪性能数据 const trackPerformance = () => {
const perfData = performance.getEntriesByType('navigation')[0];
const paintMetrics = performance.getEntriesByType('paint');

track('performance_metrics', {
// 加载时间 loadTime: perfData?.loadEventEnd - perfData?.fetchStart,
domContentLoaded: perfData?.domContentLoadedEventEnd - perfData?.fetchStart,
firstPaint: paintMetrics.find(p => p.name === 'first-paint')?.startTime,
firstContentfulPaint: paintMetrics.find(p => p.name === 'first-contentful-paint')?.startTime,

// 网络信息 connection: navigator.connection?.effectiveType,
downlink: navigator.connection?.downlink,

// 设备信息 deviceMemory: navigator.deviceMemory,
hardwareConcurrency: navigator.hardwareConcurrency
});
};

// 页面加载完成后追踪 if (document.readyState === 'complete') {
trackPerformance();
} else {
window.addEventListener('load', trackPerformance);
}

return () => {
window.removeEventListener('load', trackPerformance);
};
}, [track]);
}

转化漏斗追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// hooks/useFunnelTracking.JS
import { useState, useCallback } from 'React';
import { useTracking } from './useTracking';

/**
* 漏斗追踪 Hook
*/
export function useFunnelTracking(funnelName, steps = []) {
const { track } = useTracking();
const [completedSteps, setCompletedSteps] = useState([]);

const completeStep = useCallback((stepName) => {
if (!completedSteps.includes(stepName)) {
setCompletedSteps(prev => [...prev, stepName]);

track('funnel_step_complete', {
funnelName,
stepName,
stepIndex: steps.indexOf(stepName),
totalSteps: steps.length,
completedSteps: prev.length + 1
});
}
}, [completedSteps, funnelName, steps, track]);

const resetFunnel = useCallback(() => {
setCompletedSteps([]);
track('funnel_reset', { funnelName });
}, [funnelName, track]);

const isFunnelComplete = steps.every(step => completedSteps.includes(step));

if (isFunnelComplete) {
track('funnel_complete', {
funnelName,
completionTime: Date.now(),
totalSteps: steps.length
});
}

return {
completeStep,
resetFunnel,
completedSteps,
isFunnelComplete,
progress: (completedSteps.length / steps.length) * 100
};
}

使用示例

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// App.JS
import React from 'React';
import { TrackingProvider } from './context/TrackingProvider';
import { usePageTracking } from './hooks/usePageTracking';
import { useErrorTracking } from './hooks/useErrorTracking';
import { usePerformanceTracking } from './hooks/usePerformanceTracking';

function App() {
return (
<TrackingProvider
options={{
apiKey: 'your-API-key',
endpoint: '/API/tracking',
enableAutoTrack: true,
enableSPA: true
}}
>
<MainApp />
</TrackingProvider>
);
}

function MainApp() {
// 页面追踪 usePageTracking('home_page', { category: 'landing' });

// 错误追踪 useErrorTracking();

// 性能追踪 usePerformanceTracking();

return (
<div>
<h1>主页</h1>
</div>
);
}

高级用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// components/ProductPage.JS
import React from 'React';
import { useTracking } from '../hooks/useTracking';
import { usePageTracking } from '../hooks/usePageTracking';
import { useExposureTracking } from '../hooks/useExposureTracking';
import { useFunnelTracking } from '../hooks/useFunnelTracking';
import TrackedButton from '../components/TrackedButton';

function ProductPage({ product }) {
// 页面追踪 usePageTracking('product_detail', {
productId: product.id,
productName: product.name,
categoryId: product.category
});

// 漏斗追踪 const {
completeStep,
progress
} = useFunnelTracking('purchase_funnel', [
'product_view',
'add_to_cart',
'checkout_start',
'payment_complete'
]);

// 暴露追踪 const productImageRef = useExposureTracking('product_image_exposed', {
productId: product.id
});

const { track } = useTracking();

const handleAddToCart = () => {
track('add_to_cart', {
productId: product.id,
productName: product.name,
price: product.price
});

completeStep('add_to_cart');
};

return (
<div>
<div ref={productImageRef}>
<img src={product.image} alt={product.name} />
</div>

<h1>{product.name}</h1>
<p>¥{product.price}</p>

<TrackedButton
onClick={handleAddToCart}
trackEvent="add_to_cart_click"
trackProperties={{ productId: product.id }}
>
加入购物车
</TrackedButton>

<div>购买进度: {progress}%</div>
</div>
);
}

性能优化

防抖和节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// utils/throttle.JS
export function throttle(func, delay) {
let timeoutId;
let lastExecTime = 0;

return function (...args) {
const currentTime = Date.now();

if (currentTime - lastExecTime > delay) {
func.apply(this, args);
lastExecTime = currentTime;
} else {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
lastExecTime = Date.now();
}, delay - (currentTime - lastExecTime));
}
};
}

// utils/debounce.JS
export function debounce(func, delay) {
let timeoutId;

return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}

批量处理优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// core/optimized-collector.JS
class OptimizedEventCollector extends EventCollector {
constructor() {
super();
this.compressionEnabled = true;
this.duplicateDetection = true;
this.eventCache = new Map();
}

addEvent(event) {
// 防止重复事件 if (this.duplicateDetection && this.isDuplicate(event)) {
return;
}

// 压缩事件数据 if (this.compressionEnabled) {
event = this.compressEvent(event);
}

super.addEvent(event);
}

isDuplicate(event) {
const key = `${event.eventType}_${event.timestamp}`;
const cached = this.eventCache.get(key);

if (cached && Date.now() - cached < 1000) { // 1秒内防重复 return true;
}

this.eventCache.set(key, Date.now());
// 清理过期缓存 setTimeout(() => {
this.eventCache.delete(key);
}, 10000); // 10秒后清理 return false;
}

compressEvent(event) {
// 移除冗余字段 const compressed = { ...event };

// 移除过长的字符串 if (compressed.properties.userAgent && compressed.properties.userAgent.length > 500) {
compressed.properties.userAgent = compressed.properties.userAgent.substring(0, 500) + '...';
}

return compressed;
}
}

最佳实践

1. 数据采样策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// utils/sampling.JS
export class SamplingStrategy {
constructor(rate = 1.0) { // 1.0 = 100%采样 this.rate = rate;
}

shouldSample() {
return Math.random() < this.rate;
}

getAdjustedValue(originalValue) {
if (this.rate === 0) return 0;
return originalValue / this.rate;
}
}

// 使用示例 const sampling = new SamplingStrategy(0.1); // 10%采样 if (sampling.shouldSample()) {
track('high_frequency_event', { value: sampling.getAdjustedValue(realValue) });
}

2. 数据脱敏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// utils/anonymizer.JS
export class DataAnonymizer {
static anonymize(data) {
if (typeof data === 'string') {
// 脱敏邮箱 data = data.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
(match) => match.replace(/(.*)@(.*)/, '***@***'));

// 脱敏手机号 data = data.replace(/\b\d{3}[-.]?\d{4}[-.]?\d{4}\b/g, '****-****-****');
} else if (typeof data === 'object' && data !== null) {
Object.keys(data).forEach(key => {
data[key] = this.anonymize(data[key]);
});
}

return data;
}
}

3. 事件验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// utils/validator.JS
export class EventValidator {
static validate(event) {
const errors = [];

if (!event.eventType) {
errors.push('eventType is required');
}

if (!event.timestamp) {
errors.push('timestamp is required');
}

if (typeof event.properties !== 'object') {
errors.push('properties must be an object');
}

// 限制属性数量 if (event.properties && Object.keys(event.properties).length > 50) {
errors.push('properties count exceeds limit of 50');
}

// 限制单个属性值长度 Object.values(event.properties || {}).forEach(value => {
if (typeof value === 'string' && value.length > 1000) {
errors.push('property value length exceeds limit of 1000');
}
});

return {
valid: errors.length === 0,
errors
};
}
}

总结

  • 前端埋点是数据分析的基础,需要精心设计
  • 使用 React Hooks 可以优雅地实现追踪功能
  • 自动追踪和手动追踪相结合提供完整覆盖
  • 性能优化是埋点系统的关键考量
  • 数据安全和用户隐私保护不可忽视
  • 灵活的配置和扩展性确保系统可持续发展

做埋点系统最有成就感的时刻就是看到数据图表上清晰地显示出用户行为模式。原来用户在某个页面停留这么久是因为加载问题,那个按钮点击率这么低是因为位置不合理…这些洞察为产品优化提供了明确的方向。

扩展阅读

  • Google Analytics SDK
  • Segment Analytics
  • Web Performance Best Practices
  • Privacy by Design
  • Data Collection Ethics

参考资料

  • Event Tracking Standards: https://www.w3.org/TR/beacon/
  • Privacy Regulations: https://gdpr-info.eu/
  • Performance Monitoring: https://www.w3.org/TR/navigation-timing/
  • Web Analytics: https://www.w3.org/TR/2013/NOTE-wd-perf-20130919/
bulb