前端埋点监控sdk怎么做?

前端埋点监控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. 安全与隐私考虑

  1. 数据脱敏:自动过滤敏感字段(密码、信用卡号等)
  2. GDPR合规:提供opt-out机制
  3. 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实现了:

  1. 自动收集用户行为、性能指标和错误信息
  2. 高效的批量上报机制
  3. 完善的会话和用户追踪
  4. 性能优化和隐私保护
  5. 可以根据实际需求进一步扩展功能,如:
  6. 自定义事件追踪
  7. A/B测试数据收集
  8. 用户行为路径分析
  9. 与后端错误监控系统集成
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 什么是前端监控?它指的是通过一定的手段来获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,为产品优化...
    涅槃快乐是金阅读 498评论 0 9
  • 前端监控和前端埋点,一个是目的,一个是实现方式,做前端埋点,是为了做前端监控,那为什么要做前端监控呢? 前端监控的...
    半掩时光阅读 8,905评论 0 15
  • 1.百度统计 1.1百度网站统计 使用百度统计跟踪网站的流量,需要在网站的每一个网页中加入百度统计的JavaScr...
    这货不是程序员阅读 5,581评论 0 3
  • 什么是前端监控和前端埋点 前端监控和前端埋点,一个是目的,一个是实现方式,做前端埋点,是为了做前端监控。前端监控的...
    前端辉羽阅读 502评论 0 5
  • 前端埋点:SDK开发实践 什么是前端埋点? 前端埋点是指在前端页面中通过代码手段,记录用户行为和页面性能数据的技术...
    嗨鲁哩岛_阅读 181评论 0 1