前端数据采集的原理及实现
#前端埋点
目录
1. 总结
- 数据采点的方式
- ① 代码埋点
- 适用场景:核心业务数据采集,重要转化节点跟踪
- ② 可视化埋点
- 通过==可视化工具==配置采集点
- ③ 无埋点采集
- 自动采集页面所有事件
- 全量数据收集
- ① 代码埋点
- 数据采集的基本原理
- 核心采集类型
- 基础信息
- 页面URL
- 页面标题
- 时间戳
- 浏览器信息
- 用户行为
- 行为类型
- 操作目标
- 相关数据
- 性能数据
- 加载时间
- 首次绘制时间
- 资源加载信息
- 错误信息
- 错误类型
- 错误信息
- 错误堆栈
- 基础信息
- 基本实现原理
- Collector 数据采集器基==类==
- 数据上报方法
- Beacon API
- navigator.sendBeacon
- 图片上报
- Beacon API
- PV/UV 采集
- 路由变化时触发(SPA应用)
- History 模式
- Hash 模式
- 路由变化时触发(SPA应用)
- 用户行为采集
- 获取元素路径
- 性能数据采集
- 首次绘制时间:first-paint
- 首次内容绘制时间:first-contentful-pain
- LCP采集:largest-contentful-paint
- 资源加载情况:performance.getEntriesByType(‘resource’)
- 错误监控采集
- window.error
- window.addEventListener(‘error’
- unhandledrejection
- 核心采集类型
- 数据上报策略
- 根据错误级别
- 有些需要立即上报
- 或者队列,延迟上报
- 根据错误级别
- 数据处理与分析
- 数据清洗
- 移除敏感信息
- 格式化数据
- 注意脱敏处理,不采集==敏感信息==
- 采集代码不应影响主业务性能
- 设置合理的==采样率==
- 数据清洗
- 采集方案选择
- 核心业务采用代码埋点
- 非核心行为使用==无埋点==
- 灵活运用==可视化埋点==
- 性能优化
- 采用批量上报减少请求
- 控制采集频率和数据量
- 使用
requestIdleCallback
在空闲时处理
- 监控告警
- 设置告警阈值
- 是否立即拉群等等
- 安全合规
- 数据脱敏
- 权限控制
2. 数据采集的基本方式
2.1. 代码埋点
- 定义:在代码中特定位置插入采集代码
- 特点:
- 高度定制化
- 精准控制
- 需要开发人员手动添加
- 适用场景:
- 核心业务数据采集,重要转化节点跟踪
2.2. 可视化埋点
- 原理:
- 通过可视化工具配置采集点
- 优势:
- 降低技术门槛
- 运营人员可自主操作
- 灵活配置
2.3. 无埋点采集
- 原理:
- 自动采集页面所有事件
- 特点:
- 全量数据收集
- 实现成本低
- 数据量大
3. 数据采集的基本原理
3.1. 核心采集类型
interface CollectData {
// 基础信息
baseInfo: {
url: string; // 页面URL
title: string; // 页面标题
timestamp: number; // 时间戳
userAgent: string; // 浏览器信息
};
// 用户行为
behavior: {
type: string; // 行为类型
target: string; // 操作目标
data: any; // 相关数据
};
// 性能数据
performance: {
loadTime: number; // 加载时间
fpTime: number; // 首次绘制时间
resources: any[]; // 资源加载信息
};
// 错误信息
error: {
type: string; // 错误类型
message: string; // 错误信息
stack?: string; // 错误堆栈
};
}
3.2. 基本实现原理
// 数据采集器基类
class Collector {
private static instance: Collector;
private config: CollectorConfig;
constructor(config: CollectorConfig) {
this.config = config;
this.init();
}
// 单例模式
public static getInstance(config: CollectorConfig): Collector {
if (!Collector.instance) {
Collector.instance = new Collector(config);
}
return Collector.instance;
}
private init(): void {
this.initPV(); // 页面访问
this.initClick(); // 点击事件
this.initError(); // 错误监控
this.initPerformance(); // 性能监控
}
// 数据上报方法
private report(data: any): void {
// 1. Beacon API
if (navigator.sendBeacon) {
navigator.sendBeacon(this.config.reportUrl, JSON.stringify(data));
return;
}
// 2. 图片上报
new Image().src = `${this.config.reportUrl}?data=${encodeURIComponent(JSON.stringify(data))}`;
}
}
4. 具体实现方案
4.1. PV/UV 采集
class PVCollector {
private initPV(): void {
// 页面加载完成时触发
window.addEventListener('load', () => {
const pvData = {
url: location.href,
title: document.title,
timestamp: Date.now(),
referrer: document.referrer
};
this.report({
type: 'PV',
data: pvData
});
});
// 路由变化时触发(SPA应用)
this.listenRouteChange();
}
private listenRouteChange(): void {
// History 模式
const originalPushState = history.pushState;
history.pushState = function() {
originalPushState.apply(this, arguments);
// 触发 PV 采集
this.collectPV();
};
// Hash 模式
window.addEventListener('hashchange', () => {
this.collectPV();
});
}
}
4.2. 用户行为采集
class BehaviorCollector {
private initClick(): void {
// 点击事件代理
document.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
const behaviorData = {
type: 'click',
target: this.getElementPath(target),
timestamp: Date.now(),
data: {
innerText: target.innerText,
className: target.className,
id: target.id
}
};
this.report({
type: 'BEHAVIOR',
data: behaviorData
});
}, true);
}
// 获取元素路径
private getElementPath(element: HTMLElement): string {
const path: string[] = [];
let current = element;
while (current && current !== document.body) {
const tag = current.tagName.toLowerCase();
const id = current.id ? `#${current.id}` : '';
const className = current.className ?
`.${current.className.split(' ').join('.')}` : '';
path.unshift(`${tag}${id}${className}`);
current = current.parentElement as HTMLElement;
}
return path.join(' > ');
}
}
4.3. 性能数据采集
class PerformanceCollector {
private initPerformance(): void {
window.addEventListener('load', () => {
// 等待所有资源加载完成
setTimeout(() => {
const perfData = this.getPerformanceMetrics();
this.report({
type: 'PERFORMANCE',
data: perfData
});
}, 0);
});
}
private getPerformanceMetrics() {
const timing = performance.timing;
const paintMetrics = performance.getEntriesByType('paint');
return {
// 页面加载时间
loadTime: timing.loadEventEnd - timing.navigationStart,
// DOM 解析时间
domParseTime: timing.domComplete - timing.domLoading,
// 首次绘制时间
fpTime: paintMetrics.find(entry => entry.name === 'first-paint')?.startTime,
// 首次内容绘制时间
fcpTime: paintMetrics.find(entry => entry.name === 'first-contentful-paint')?.startTime,
// 资源加载情况
resources: performance.getEntriesByType('resource').map(entry => ({
name: entry.name,
duration: entry.duration,
size: entry.transferSize,
type: entry.initiatorType
}))
};
}
}
// 核心性能指标采集
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
if (entry.entryType === 'largest-contentful-paint') {
// LCP采集
collectPerformance('LCP', entry.startTime);
}
if (entry.entryType === 'layout-shift') {
// CLS采集
collectPerformance('CLS', entry.value);
}
});
});
observer.observe({ entryTypes: ['largest-contentful-paint', 'layout-shift'] });
4.4. 错误监控采集
class ErrorCollector {
private initError(): void {
// JS 运行时错误
window.addEventListener('error', (event) => {
const errorData = {
type: 'ERROR',
subType: 'js',
message: event.message,
filename: event.filename,
position: `${event.lineno}:${event.colno}`,
stack: event.error?.stack,
timestamp: Date.now()
};
this.report({
type: 'ERROR',
data: errorData
});
}, true);
// Promise 未捕获错误
window.addEventListener('unhandledrejection', (event) => {
const errorData = {
type: 'ERROR',
subType: 'promise',
message: event.reason?.message || event.reason,
stack: event.reason?.stack,
timestamp: Date.now()
};
this.report({
type: 'ERROR',
data: errorData
});
});
// 资源加载错误
window.addEventListener('error', (event) => {
if (event.target && (event.target as HTMLElement).nodeName) {
const target = event.target as HTMLElement;
const errorData = {
type: 'ERROR',
subType: 'resource',
url: (target as any).src || (target as any).href,
tagName: target.nodeName,
timestamp: Date.now()
};
this.report({
type: 'ERROR',
data: errorData
});
}
}, true);
}
}
5. 数据上报策略
5.1. 上报方法实现
- 根据错误级别,有些需要立即上报
- 或者队列,延迟上报
class Reporter {
private queue: any[] = [];
private timer: number | null = null;
constructor(private config: ReporterConfig) {
this.init();
}
private init(): void {
// 页面卸载前发送
window.addEventListener('beforeunload', () => {
this.flush();
});
}
// 添加到队列
public add(data: any): void {
this.queue.push({
...data,
timestamp: Date.now()
});
// 达到阈值立即发送
if (this.queue.length >= this.config.maxCache) {
this.flush();
return;
}
// 定时发送
if (!this.timer) {
this.timer = window.setTimeout(() => {
this.flush();
}, this.config.delay);
}
}
// 立即发送
private flush(): void {
if (this.queue.length === 0) return;
// 优先使用 Beacon API
if (navigator.sendBeacon) {
navigator.sendBeacon(
this.config.reportUrl,
JSON.stringify(this.queue)
);
} else {
// 降级方案:图片上报
const data = encodeURIComponent(JSON.stringify(this.queue));
new Image().src = `${this.config.reportUrl}?data=${data}`;
}
// 清空队列
this.queue = [];
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
}
5.2. 使用示例
// 配置初始化
const collector = Collector.getInstance({
reportUrl: 'https://analytics-api.example.com/collect',
delay: 1000, // 延迟发送
maxCache: 10, // 最大缓存数
sampling: 100, // 采样率
ignoreUrls: [ // 忽略的URL
/localhost/,
/127.0.0.1/
]
});
// 手动上报
collector.report({
type: 'CUSTOM',
data: {
event: 'button_click',
buttonId: 'submit-btn'
}
});
6. 数据处理与分析
6.1. 数据清洗
- 移除敏感信息
- 格式化数据
- 补充必要信息
interface CleanedData {
timestamp: number;
sessionId: string;
userId: string;
eventType: string;
eventData: any;
}
class DataProcessor {
static clean(rawData: any[]): CleanedData[] {
return rawData.map(item => ({
timestamp: item.timestamp,
sessionId: this.getSessionId(),
userId: this.getUserId(),
eventType: item.type,
eventData: this.normalizeData(item.data)
}));
}
private static normalizeData(data: any): any {
// 数据标准化处理
return {
...data,
// 移除敏感信息
// 格式化数据
// 补充必要信息
};
}
}
通过以上实现,我们可以构建一个完整的前端数据采集系统。需要注意以下几点:
- 性能影响:采集代码不应影响主业务性能
- 数据安全:注意脱敏处理,不采集==敏感信息==
- 采样控制:根据业务需求设置合理的采样率
- 容错处理:保证采集代码的健壮性
- 合规性:遵守数据保护相关法规
7. 最佳实践建议
7.1. 采集策略
- 核心业务采用代码埋点
- 非核心行为使用==无埋点==
- 灵活运用可视化埋点
7.2. 性能优化
- 采用批量上报减少请求
- 使用
requestIdleCallback
在空闲时处理 - 控制采集频率和数据量
7.3. 数据处理
- 建立数据清洗机制
- 实现实时分析能力
- 设置告警阈值
7.4. 监控维度
- 页面性能指标
- 用户行为数据
- 错误日志
- 业务指标
- 资源加载情况
8. 数据安全考虑
8.1. 数据脱敏
function maskSensitiveData(data) {
// 手机号脱敏
if (data.phone) {
data.phone = data.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
// 邮箱脱敏
if (data.email) {
data.email = data.email.replace(/(.{3}).+(@.+)/, '$1***$2');
}
return data;
}
8.2. 权限控制
class DataCollector {
constructor() {
this.allowedDomains = ['example.com', 'sub.example.com'];
}
checkPermission() {
const currentDomain = window.location.hostname;
return this.allowedDomains.some(domain =>
currentDomain === domain || currentDomain.endsWith(`.${domain}`)
);
}
collect(data) {
if (!this.checkPermission()) {
console.warn('Unauthorized data collection attempt');
return;
}
// 继续数据采集
}
}