前端埋点监控SDK开发指南
前端埋点监控SDK是用于收集用户行为数据、性能指标和错误信息的工具,帮助产品团队分析用户行为、优化产品体验。下面我将详细介绍如何从零开发一个完整的前端埋点监控SDK。
1. 核心功能设计
一个完整的前端埋点SDK应包含以下核心功能模块:
用户行为追踪:页面访问、点击事件、滚动行为等
性能监控:页面加载时间、资源加载时间等
错误收集:JS错误、资源加载错误、API请求错误等
会话追踪:用户访问路径分析
数据上报:将收集的数据发送到服务器
2. 基础架构
class TrackingSDK {
constructor(config) {
this.config = {
appId: '', // 应用ID
reportUrl: '', // 上报地址
autoTrack: true, // 是否自动追踪
...config
};
this.queue = []; // 数据队列
this.sessionId = this.generateSessionId(); // 会话ID
this.init();
}
init() {
// 初始化各种监听器
this.setupErrorTracking();
this.setupPerformanceTracking();
if (this.config.autoTrack) {
this.setupAutoTracking();
}
// 页面卸载前上报剩余数据
this.setupBeforeUnload();
}
// ...其他方法
}
3. 用户行为追踪实现
3.1 页面访问追踪
trackPageView(pageData = {}) {
const data = {
type: 'pageview',
url: window.location.href,
title: document.title,
referrer: document.referrer,
timestamp: Date.now(),
...pageData
};
this.send(data);
}
setupAutoTracking() {
// 监听history变化
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = (...args) => {
originalPushState.apply(history, args);
this.trackPageView();
};
history.replaceState = (...args) => {
originalReplaceState.apply(history, args);
this.trackPageView();
};
// 监听popstate事件
window.addEventListener('popstate', () => {
this.trackPageView();
});
// 初始页面记录
this.trackPageView();
}
3.2 点击事件追踪
setupClickTracking() {
document.addEventListener('click', (e) => {
const target = e.target;
const data = {
type: 'click',
element: target.tagName,
id: target.id || '',
class: target.className || '',
text: target.textContent.trim().slice(0, 50),
x: e.clientX,
y: e.clientY,
timestamp: Date.now()
};
this.send(data);
}, { capture: true });
}
4. 性能监控实现
setupPerformanceTracking() {
if (!window.performance) return;
// 等待所有性能条目可用
setTimeout(() => {
const timing = performance.timing;
const navigation = performance.getEntriesByType('navigation')[0];
const resources = performance.getEntriesByType('resource');
const perfData = {
type: 'performance',
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
ttfb: timing.responseStart - timing.requestStart,
download: timing.responseEnd - timing.responseStart,
domReady: timing.domComplete - timing.domLoading,
load: timing.loadEventEnd - timing.navigationStart,
fp: this.getFirstPaint(),
fcp: this.getFirstContentfulPaint(),
resources: resources.map(res => ({
name: res.name,
duration: res.duration,
type: res.initiatorType
}))
};
this.send(perfData);
}, 3000);
}
getFirstPaint() {
const entries = performance.getEntriesByType('paint');
const fpEntry = entries.find(entry => entry.name === 'first-paint');
return fpEntry ? fpEntry.startTime : null;
}
getFirstContentfulPaint() {
const entries = performance.getEntriesByType('paint');
const fcpEntry = entries.find(entry => entry.name === 'first-contentful-paint');
return fcpEntry ? fcpEntry.startTime : null;
}
5. 错误收集实现
setupErrorTracking() {
// JS运行时错误
window.addEventListener('error', (event) => {
const errorData = {
type: 'error',
category: 'js',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
timestamp: Date.now()
};
this.send(errorData);
}, true);
// 资源加载错误
window.addEventListener('error', (event) => {
const target = event.target;
if (target !== window && target.nodeName) {
const errorData = {
type: 'error',
category: 'resource',
tagName: target.nodeName,
src: target.src || target.href,
timestamp: Date.now()
};
this.send(errorData);
}
}, true);
// Promise未捕获错误
window.addEventListener('unhandledrejection', (event) => {
const errorData = {
type: 'error',
category: 'promise',
reason: event.reason?.toString(),
stack: event.reason?.stack,
timestamp: Date.now()
};
this.send(errorData);
});
// 跨域脚本错误捕获
window.addEventListener('error', (event) => {
if (event.message && !event.filename) {
const errorData = {
type: 'error',
category: 'cross-origin',
message: event.message,
timestamp: Date.now()
};
this.send(errorData);
}
}, true);
}
6. 数据上报机制
6.1 基础上报实现
send(data) {
// 添加公共字段
const finalData = {
...data,
appId: this.config.appId,
sessionId: this.sessionId,
userId: this.getUserId(),
ua: navigator.userAgent,
screen: `${window.screen.width}x${window.screen.height}`,
language: navigator.language,
url: window.location.href
};
// 加入队列
this.queue.push(finalData);
// 达到阈值或延迟上报
if (this.queue.length >= (this.config.batchSize || 5)) {
this.flush();
} else {
this.debouncedFlush();
}
}
flush() {
if (!this.queue.length) return;
const dataToSend = [...this.queue];
this.queue = [];
const reportUrl = this.config.reportUrl;
const blob = new Blob([JSON.stringify(dataToSend)], { type: 'application/json' });
// 使用navigator.sendBeacon优先,回退到fetch或img
if (navigator.sendBeacon) {
navigator.sendBeacon(reportUrl, blob);
} else {
fetch(reportUrl, {
method: 'POST',
body: blob,
keepalive: true
}).catch(e => {
console.error('Tracking SDK report error:', e);
// 上报失败重新加入队列
this.queue.unshift(...dataToSend);
});
}
}
// 防抖处理
debouncedFlush = debounce(() => this.flush(), 5000);
6.2 页面卸载前上报
setupBeforeUnload() {
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flush();
}
});
window.addEventListener('pagehide', () => {
this.flush();
});
window.addEventListener('beforeunload', () => {
this.flush();
});
}
7. 用户识别与会话管理
// 生成唯一用户ID
getUserId() {
let userId = localStorage.getItem('tracking_user_id');
if (!userId) {
userId = this.generateUUID();
localStorage.setItem('tracking_user_id', userId);
}
return userId;
}
// 生成会话ID
generateSessionId() {
const now = Date.now();
let sessionId = sessionStorage.getItem('tracking_session_id');
const lastActivity = sessionStorage.getItem('tracking_last_activity');
// 30分钟无活动视为新会话
if (!sessionId || !lastActivity || (now - lastActivity) > 30 * 60 * 1000) {
sessionId = this.generateUUID();
sessionStorage.setItem('tracking_session_id', sessionId);
}
sessionStorage.setItem('tracking_last_activity', now);
return sessionId;
}
// 生成UUID
generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
8. SDK优化与扩展
8.1 性能优化
// 使用requestIdleCallback延迟非关键任务
scheduleTask(task) {
if ('requestIdleCallback' in window) {
requestIdleCallback(task, { timeout: 2000 });
} else {
setTimeout(task, 0);
}
}
// 节流高频事件
setupScrollTracking() {
const scrollHandler = throttle(() => {
const data = {
type: 'scroll',
scrollY: window.scrollY,
scrollX: window.scrollX,
viewportHeight: window.innerHeight,
documentHeight: document.documentElement.scrollHeight,
timestamp: Date.now()
};
this.send(data);
}, 1000);
window.addEventListener('scroll', scrollHandler);
}
8.2 采样率控制
shouldSample(eventType) {
const sampleRates = this.config.sampleRates || {
pageview: 1,
click: 0.1,
error: 1,
performance: 0.5
};
const rate = sampleRates[eventType] || 1;
return Math.random() < rate;
}
send(data) {
if (!this.shouldSample(data.type)) return;
// ...原有逻辑
}
9. 安全与隐私考虑
- 数据脱敏:自动过滤敏感字段(密码、信用卡号等)
- GDPR合规:提供opt-out机制
- IP匿名化:上报前去除IP最后一段
sanitizeData(data) {
// 移除敏感表单字段
if (data.element === 'INPUT') {
const inputTypes = ['password', 'email', 'tel', 'creditcard'];
if (inputTypes.includes(data.inputType)) {
data.value = '[FILTERED]';
}
}
// 其他脱敏逻辑...
return data;
}
10. 部署与使用
10.1 打包发布
// rollup.config.js
export default {
input: 'src/index.js',
output: {
file: 'dist/tracking-sdk.js',
format: 'umd',
name: 'TrackingSDK'
}
};
10.2 使用示例
<script src="tracking-sdk.js"></script>
<script>
const tracker = new TrackingSDK({
appId: 'YOUR_APP_ID',
reportUrl: 'https://api.yourdomain.com/track'
});
// 手动追踪事件
tracker.send({
type: 'custom',
event: 'purchase',
amount: 100,
items: ['item1', 'item2']
});
</script>
总结
这个SDK实现了:
- 自动收集用户行为、性能指标和错误信息
- 高效的批量上报机制
- 完善的会话和用户追踪
- 性能优化和隐私保护
- 可以根据实际需求进一步扩展功能,如:
- 自定义事件追踪
- A/B测试数据收集
- 用户行为路径分析
- 与后端错误监控系统集成