前端用户行为追踪的那些坑我替你踩过了
项目初期的技术选型
上个月接了个数据可视化项目,客户需要实时监控用户的操作行为,包括页面停留时间、点击热力图、滚动轨迹这些。说实话刚开始接到需求的时候有点懵,之前做的项目都是简单埋点上报,这次要求的颗粒度细太多了。
一开始想着直接用现成的统计工具比如百度统计或者Google Analytics,但仔细一想不行,客户的隐私政策比较严格,所有数据都得自己存自己处理,而且还要支持实时分析。没办法,只能自己搞一套用户行为采集系统。
技术选型这块折腾了好几天,最后选择了基于MutationObserver监听DOM变化,配合事件委托捕获用户交互,再通过WebSocket实时推送到后端。说实话这套方案当时觉得挺复杂,但现在回过头看还行吧,至少能满足需求。
核心实现代码
整个系统的核心就是行为采集模块,这部分写了不少代码,主要分三个部分:DOM变动监听、用户交互捕获、数据格式化上传。
class UserBehaviorTracker {
constructor() {
this.events = [];
this.pageStartTime = Date.now();
this.init();
}
init() {
// 监听DOM变动
this.observeDOM();
// 捕获用户交互
this.captureEvents();
// 定时上报
setInterval(() => {
this.reportEvents();
}, 3000);
}
observeDOM() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList' || mutation.type === 'attributes') {
this.recordEvent({
type: 'dom_change',
element: mutation.target.tagName,
action: mutation.type,
timestamp: Date.now()
});
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
}
captureEvents() {
const events = ['click', 'scroll', 'input', 'focus', 'blur'];
events.forEach(eventType => {
document.addEventListener(eventType, (e) => {
this.handleUserEvent(e, eventType);
}, true);
});
}
handleUserEvent(event, eventType) {
let targetInfo = {};
if (event.target) {
targetInfo = {
tagName: event.target.tagName,
className: event.target.className,
id: event.target.id,
textContent: event.target.textContent?.substring(0, 50),
rect: event.target.getBoundingClientRect()
};
}
this.recordEvent({
type: eventType,
target: targetInfo,
x: event.clientX || 0,
y: event.clientY || 0,
timestamp: Date.now()
});
}
recordEvent(eventData) {
this.events.push({
...eventData,
pageUrl: window.location.href,
sessionId: this.getSessionId()
});
// 防止数组过大
if (this.events.length > 100) {
this.reportEvents();
}
}
getSessionId() {
if (!localStorage.getItem('session_id')) {
const sessionId = Date.now().toString(36) + Math.random().toString(36).substr(2);
localStorage.setItem('session_id', sessionId);
}
return localStorage.getItem('session_id');
}
async reportEvents() {
if (this.events.length === 0) return;
try {
await fetch('https://jztheme.com/api/user-behavior', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
events: this.events,
pageStayTime: Date.now() - this.pageStartTime,
userAgent: navigator.userAgent
})
});
this.events = []; // 清空已上报的事件
} catch (error) {
console.error('上报用户行为失败:', error);
// 失败时暂存本地,下次重试
localStorage.setItem('pending_events', JSON.stringify(this.events));
}
}
}
// 初始化
new UserBehaviorTracker();
最大的坑:性能问题
开始测试的时候发现页面卡得要命,特别是复杂的管理后台页面,每秒能产生几百个DOM变动事件,CPU直接干到100%。这个问题折腾了我好几天才找到原因,主要是MutationObserver的回调函数执行太频繁了。
解决方法是加了个防抖机制,在观察器回调里用setTimeout限制触发频率:
observeDOM() {
let debounceTimer = null;
const observer = new MutationObserver((mutations) => {
// 防抖处理
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList' || mutation.type === 'attributes') {
this.recordEvent({
type: 'dom_change',
element: mutation.target.tagName,
action: mutation.type,
timestamp: Date.now()
});
}
});
}, 100); // 100ms内最多执行一次
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
}
但这样又引入了新的问题:有些快速的DOM变动会被漏掉。后来改成记录变动次数而不是具体细节,减少了内存占用,页面终于能正常跑了。
数据存储和后端处理
前端数据采集完成后,后端需要用Node.js+MongoDB来接收和存储。考虑到数据量可能很大,设计了分表策略,按天切割数据表,避免单表过大影响查询效率。
// 后端接收接口示例
app.post('/api/user-behavior', async (req, res) => {
try {
const { events, pageStayTime, userAgent } = req.body;
const collectionName = behavior_${formatDate(new Date())};
const db = client.db('user_behavior');
await db.collection(collectionName).insertMany(events.map(event => ({
...event,
pageStayTime,
userAgent,
createdAt: new Date()
})));
res.json({ success: true });
} catch (error) {
console.error('保存用户行为数据失败:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
function formatDate(date) {
return date.toISOString().split('T')[0].replace(/-/g, '');
}
数据库索引也得合理设计,按session_id和timestamp建了复合索引,查询单次会话的行为轨迹会快不少。
隐私合规考虑
做这个功能的时候还得考虑GDPR这些隐私法规,敏感信息不能随便收集。所以对输入框内容只记录长度不记录具体内容,密码框直接过滤掉,IP地址也做了脱敏处理。
用户退出登录的时候会清除本地的session_id,重新登录生成新的会话标识。还有个用户授权开关,在用户同意后再启动行为追踪,这个在实际项目中很重要,不然容易踩法律红线。
最终效果评估
上线后跑了几周,整体还算稳定。平均每个页面会话能收集到几十到几百条行为记录,数据量控制在可接受范围内。客户反馈说数据准确度还不错,热力图和用户路径分析都能正常使用。
不过还是有些小问题没完全解决:比如移动端的touch事件处理不够完善,某些特殊页面的DOM变动检测会有延迟,但这些问题影响不大,暂时就没花精力去优化。性能方面通过各种优化手段,现在页面卡顿基本没有了,用户无感知。
回顾与反思
这套用户行为采集系统总的来说算是成功了,虽然过程中踩了不少坑,但学到了很多东西。特别是对浏览器API的深度使用有了更深理解,MutationObserver这种平时不太用的API,这次也算彻底摸透了。
如果再来一次的话,可能会考虑用现有的开源方案比如OpenReplay或者rrweb,自己从零搭建确实挺费劲的。不过自己写的好处是可控性强,可以根据具体需求定制,灵活性高一些。
以上是我踩坑后的总结,希望对你有帮助。

暂无评论