前端日志埋点SDK
2023/10/8大约 4 分钟
前端日志埋点SDK
什么是日志埋点
日志埋点是一种前端数据采集技术,通过在应用程序中植入代码来收集用户行为数据、系统错误信息和性能指标。这些数据可以帮助产品团队了解用户行为、优化产品体验、排查线上问题。
核心考虑点
1. 数据采集和捕获
提供统一的 API,通过埋点收集三类核心数据:
- 用户行为数据:点击、滚动、输入等交互行为
- 系统错误数据:JavaScript 错误、资源加载失败等
- 性能数据:页面加载时间、接口响应时间等
2. 数据处理和过滤
前端对收集到的数据做前期处理:
- 去重:避免重复上报相同数据
- 字段校验:确保数据格式正确
- 数据脱敏:去除敏感信息(如密码、Token)
3. 数据传输
通过 HTTPS 将数据可靠传输到服务器,支持:
- 异步传输,不阻塞主线程
- 批量传输,减少请求次数
- 离线缓存,网络恢复后自动补传
4. 性能优化
尽量减轻数据收集对页面性能的影响:
- 使用
requestIdleCallback延迟非紧急任务 - 批量合并数据后统一发送
- 采用
navigator.sendBeacon保证页面卸载时的可靠传输
常见采集点
1. 用户行为数据
| 采集点 | 描述 | 触发时机 |
|---|---|---|
| 页面浏览 | 用户访问页面 | 页面加载完成 |
| 点击事件 | 用户点击元素 | 元素被点击 |
| 页面停留 | 用户在页面停留时长 | 页面隐藏/关闭 |
| 表单提交 | 用户提交表单 | 表单提交时 |
| 滚动深度 | 用户滚动页面深度 | 页面滚动时 |
2. 系统错误数据
| 采集点 | 描述 | 触发时机 |
|---|---|---|
| JavaScript 错误 | 代码运行时错误 | 错误抛出时 |
| 资源加载失败 | 图片/脚本加载失败 | 资源加载error |
| Promise 拒绝 | 未捕获的 Promise 拒绝 | Promise reject 时 |
| 接口请求错误 | API 请求失败 | 请求响应error |
3. 性能数据
| 采集点 | 描述 | 触发时机 |
|---|---|---|
| 页面加载时间 | FP/FCP/LCP 等指标 | 页面加载时 |
| 接口响应时间 | API 请求耗时 | 接口返回时 |
| 资源加载时间 | 静态资源加载耗时 | 资源加载完成 |
| 首次交互时间 | TTI 指标 | 页面可交互时 |
SDK 核心实现
基础结构
interface LogConfig {
endpoint: string;
appId: string;
env?: "production" | "development";
enabled?: boolean;
batchSize?: number;
delay?: number;
}
interface LogData {
type: "behavior" | "error" | "performance";
event: string;
timestamp: number;
pageUrl: string;
userId?: string;
data: Record<string, any>;
}
class LogSDK {
private config: LogConfig;
private queue: LogData[] = [];
private timer: number | null = null;
constructor(config: LogConfig) {
this.config = {
enabled: true,
batchSize: 10,
delay: 1000,
env: "production",
...config,
};
if (this.config.enabled) {
this.init();
}
}
private init() {
this.setupErrorListener();
this.setupPerformanceObserver();
this.startBatchUpload();
}
}日志上报方法
log(event: string, data: Record<string, any> = {}, type: LogData['type'] = 'behavior') {
const logData: LogData = {
type,
event,
timestamp: Date.now(),
pageUrl: window.location.href,
data: this.sanitizeData(data)
};
this.queue.push(logData);
if (this.queue.length >= this.config.batchSize!) {
this.flush();
}
}
private sanitizeData(data: Record<string, any>): Record<string, any> {
const sensitiveKeys = ['password', 'token', 'secret', 'authorization'];
const sanitized = { ...data };
for (const key of Object.keys(sanitized)) {
if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) {
sanitized[key] = '***';
}
}
return sanitized;
}可靠的传输方式
private async upload(data: LogData[]) {
const payload = JSON.stringify(data);
if (navigator.sendBeacon) {
const blob = new Blob([payload], { type: 'application/json' });
const result = navigator.sendBeacon(this.config.endpoint, blob);
if (!result) {
await this.uploadWithFetch(payload);
}
} else {
await this.uploadWithFetch(payload);
}
}
private async uploadWithFetch(payload: string) {
try {
await fetch(this.config.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload,
keepalive: true
});
} catch (error) {
console.error('Log upload failed:', error);
}
}
flush() {
if (this.queue.length === 0) return;
const data = [...this.queue];
this.queue = [];
this.upload(data);
}
private startBatchUpload() {
if (this.timer) clearInterval(this.timer);
this.timer = window.setInterval(() => {
this.flush();
}, this.config.delay);
}错误监听
private setupErrorListener() {
window.addEventListener('error', (event) => {
this.log('js_error', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack
}, 'error');
});
window.addEventListener('unhandledrejection', (event) => {
this.log('promise_rejection', {
reason: event.reason?.message || String(event.reason),
stack: event.reason?.stack
}, 'error');
});
}性能监控
private setupPerformanceObserver() {
if (!window.PerformanceObserver) return;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'navigation') {
const navigation = entry as PerformanceNavigationTiming;
this.log('page_performance', {
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
tcp: navigation.connectEnd - navigation.connectStart,
ttfb: navigation.responseStart - navigation.requestStart,
download: navigation.responseEnd - navigation.responseStart,
load: navigation.loadEventEnd - navigation.fetchStart,
domReady: navigation.domContentLoadedEventEnd - navigation.fetchStart
}, 'performance');
}
if (entry.entryType === 'resource') {
const resource = entry as PerformanceResourceTiming;
this.log('resource_loading', {
name: resource.name,
duration: resource.duration,
size: resource.transferSize
}, 'performance');
}
}
});
observer.observe({ entryTypes: ['navigation', 'resource', 'paint'] });
}页面停留时长
private setupPageStay() {
const startTime = Date.now();
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
const duration = Date.now() - startTime;
this.log('page_stay', { duration }, 'behavior');
this.flush();
}
});
}使用示例
const logger = new LogSDK({
endpoint: "/api/log",
appId: "my-app",
env: "production",
enabled: true,
batchSize: 10,
delay: 2000,
});
logger.log("btn_click", { buttonId: "submit-form" });
logger.log("form_submit", {
formId: "login-form",
fields: { username: "user***" },
});
window.addEventListener("load", () => {
logger.log("page_view", { referrer: document.referrer });
});数据脱敏处理
在采集数据时,需要对敏感信息进行脱敏处理:
private sanitizeData(data: Record<string, any>): Record<string, any> {
const sensitiveKeys = [
'password', 'pwd', 'secret', 'token', 'access_token',
'refresh_token', 'authorization', 'credit_card', 'card_no',
'phone', 'id_card', 'mobile', 'email', 'address'
];
const result: Record<string, any> = {};
for (const [key, value] of Object.entries(data)) {
const isSensitive = sensitiveKeys.some(
sk => key.toLowerCase().includes(sk)
);
if (isSensitive) {
result[key] = '***';
} else if (typeof value === 'object' && value !== null) {
result[key] = this.sanitizeData(value);
} else {
result[key] = value;
}
}
return result;
}总结
前端日志埋点 SDK 的核心在于:
- 统一 API:提供简洁的调用接口
- 多维度采集:覆盖行为、错误、性能三大类数据
- 可靠传输:使用 sendBeacon + fetch keepalive 确保数据不丢失
- 性能友好:批量上传、异步处理,不影响用户体验
- 数据安全:敏感信息脱敏,保护用户隐私