Skip to content

前端埋点库

这是一个轻量级、可扩展的前端埋点SDK,用于收集用户行为数据、性能数据和设备信息。该SDK使用原生JavaScript编写,可以在任何前端框架中使用。

功能特点

  • 📱 设备信息收集:自动获取用户设备信息,包括屏幕尺寸、浏览器类型、系统版本等
  • 🖱️ 用户行为追踪:捕获用户点击事件,详细记录点击元素、路径及坐标位置
  • 🧭 路由变化监听:同时支持History API和Hash路由模式,自动记录页面跳转
  • ⏱️ 性能数据收集:监控页面加载时间、路由跳转耗时等关键性能指标
  • 🔌 插件机制:提供灵活的插件扩展机制,方便功能定制
  • 📊 数据批量上报:支持数据批量处理和上报,减少网络请求
  • 🎯 采样控制:提供采样率控制,在高流量场景下优化数据采集
  • 🔒 安全传输:支持数据加密和安全传输
  • 🧩 框架无关:可以集成到任何前端框架(Vue、React、Angular等)

安装

bash
# 直接复制 tracker.ts 文件到你的项目中

# 或者通过包管理器安装(如果已发布为npm包)
npm install @xiaolin/tracker
# 或
yarn add @xiaolin/tracker

快速开始

基础用法

javascript
import Tracker from './utils/tracker';

// 初始化埋点SDK
const tracker = Tracker.getInstance({
  appId: 'your-app-id',  // 应用标识
  userId: 'user-id',     // 可选的用户ID 
  uploadUrl: 'https://your-api.com/track', // 数据上报地址
});

// 记录自定义事件
tracker.trackEvent('button_click', {
  buttonId: 'submit-button',
  page: 'homepage'
});

// 页面关闭前确保数据发送
window.addEventListener('beforeunload', () => {
  tracker.flush();
});

配置选项

javascript
Tracker.getInstance({
  appId: 'your-app-id',                 // [必填] 应用ID
  userId: 'user123',                     // [可选] 用户ID
  uploadUrl: 'https://api.example.com/tracker', // [可选] 数据上报地址,不设置则只在控制台打印
  autoTrack: true,                       // [可选] 是否自动采集 (默认: true)
  sampleRate: 0.5,                       // [可选] 采样率 0-1 (默认: 1)
  maxBatchSize: 10,                      // [可选] 批量上报阈值 (默认: 10)
  maxCacheTime: 5000,                    // [可选] 自动上报间隔(ms) (默认: 5000)
  historyTracker: true,                  // [可选] 监听history路由变化 (默认: true)
  hashTracker: true,                     // [可选] 监听hash路由变化 (默认: true)
  plugins: [myPlugin1, myPlugin2],       // [可选] 插件列表
});

集成到各种框架

Vue 3

javascript
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import Tracker from './utils/tracker';

const app = createApp(App);

// 初始化埋点SDK
const tracker = Tracker.getInstance({
  appId: 'your-app-id',
});

// 可选:将埋点实例挂载到全局属性
app.config.globalProperties.$tracker = tracker;

// 使用provide/inject模式在组件中使用
app.provide('tracker', tracker);

app.mount('#app');

// 在组件中使用:
// <script setup>
// import { inject } from 'vue';
// const tracker = inject('tracker');
// 
// function handleClick() {
//   tracker.trackEvent('feature_used', { feature: 'search' });
// }
// </script>

React

javascript
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import Tracker from './utils/tracker';

// 初始化埋点SDK
const tracker = Tracker.getInstance({
  appId: 'your-app-id',
});

// 创建埋点上下文
const TrackerContext = React.createContext(tracker);

// 创建Provider组件
export const TrackerProvider = ({ children }) => {
  return (
    <TrackerContext.Provider value={tracker}>
      {children}
    </TrackerContext.Provider>
  );
};

// 创建Hook
export const useTracker = () => React.useContext(TrackerContext);

// 在应用入口处提供埋点服务
ReactDOM.render(
  <React.StrictMode>
    <TrackerProvider>
      <App />
    </TrackerProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

// 在组件中使用:
// import { useTracker } from './trackerContext';
// 
// function MyComponent() {
//   const tracker = useTracker();
//   
//   const handleButtonClick = () => {
//     tracker.trackEvent('button_click', { componentName: 'MyComponent' });
//   };
//   
//   return <button onClick={handleButtonClick}>Click me</button>;
// }

Angular

typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import Tracker from './utils/tracker';

// 创建追踪服务
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class TrackerService {
  private tracker: any;

  constructor() {
    this.tracker = Tracker.getInstance({
      appId: 'your-app-id',
    });
  }

  trackEvent(eventName: string, data: any) {
    this.tracker.trackEvent(eventName, data);
  }
}

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [TrackerService],
  bootstrap: [AppComponent]
})
export class AppModule { }

// 在组件中使用:
// import { Component } from '@angular/core';
// import { TrackerService } from './tracker.service';
// 
// @Component({
//   selector: 'app-example',
//   template: '<button (click)="onButtonClick()">Click me</button>'
// })
// export class ExampleComponent {
//   constructor(private trackerService: TrackerService) {}
// 
//   onButtonClick() {
//     this.trackerService.trackEvent('button_click', { component: 'ExampleComponent' });
//   }
// }

原生JS/jQuery

html
<!-- 直接在HTML中引入 -->
<script src="path/to/tracker.js"></script>
<script>
  // 初始化埋点SDK
  const tracker = Tracker.getInstance({
    appId: 'your-app-id',
  });
  
  // jQuery事件绑定
  $(document).ready(function() {
    $('#my-button').on('click', function() {
      tracker.trackEvent('button_click', { buttonId: 'my-button' });
    });
  });
</script>

自定义插件

可以通过创建符合插件接口的对象来扩展SDK功能:

javascript
// 创建一个自定义插件
const myPlugin = {
  name: 'myCustomPlugin',
  install: (tracker) => {
    // 扩展或修改tracker实例的功能
    // 例如,修改track方法
    const originalTrack = tracker.track;
    tracker.track = function(eventType, data) {
      // 添加自定义逻辑
      console.log(`[Custom Plugin] Event tracked: ${eventType}`);
      // 调用原始方法
      return originalTrack.call(tracker, eventType, data);
    };
    
    // 或者添加新方法
    tracker.newMethod = function() {
      // 实现新功能
    };
  }
};

// 使用插件
const tracker = Tracker.getInstance({
  appId: 'your-app-id',
  plugins: [myPlugin]
});

埋点数据格式

SDK收集的数据的基本格式如下:

javascript
{
  "type": "click", // 事件类型
  "timestamp": 1615432700000, // 时间戳
  "data": {
    // 设备信息
    "device": {
      "userAgent": "Mozilla/5.0...",
      "screenWidth": 1920,
      "screenHeight": 1080,
      "viewportWidth": 1200,
      "viewportHeight": 800,
      "devicePixelRatio": 2,
      "platform": "Win32",
      "language": "zh-CN",
      "connection": {
        "effectiveType": "4g",
        "downlink": 10,
        "rtt": 50,
        "saveData": false
      }
    },
    
    // 点击事件特有数据
    "click": {
      "x": 100, // 视口X坐标
      "y": 200, // 视口Y坐标
      "pageX": 100, // 页面X坐标
      "pageY": 500, // 页面Y坐标
      "target": {
        "tagName": "button",
        "id": "submit-button",
        "className": "btn btn-primary",
        "text": "提交",
        "attributes": {
          "data-track": "submit",
          "href": "#"
        }
      },
      "path": [
        // 事件冒泡路径上的元素
        { "tagName": "button", "id": "submit-button", "className": "btn btn-primary" },
        { "tagName": "div", "id": "form-actions", "className": "actions" },
        { "tagName": "form", "id": "my-form", "className": "form" },
        // ... 更多上层元素
      ]
    },
    
    // 路由变化数据示例
    "route": {
      "path": "/products",
      "fullPath": "/products?id=123",
      "query": { "id": "123" },
      "name": "ProductDetail"
    },
    
    // 性能数据示例
    "performance": {
      "type": "routeChange",
      "duration": 145.2, // 毫秒
      "from": "/home",
      "to": "/products"
    },
    
    // 通用元数据
    "url": "https://example.com/products?id=123",
    "appId": "your-app-id",
    "userId": "user123"
  }
}

高级用法

手动刷新队列

javascript
// 强制立即上报当前队列中的所有数据
tracker.flush();

错误处理和容错

SDK内部会自动处理上报失败的情况,并将失败的事件放回队列中以便下次重试。

自定义事件

javascript
// 自定义事件名称和数据
tracker.trackEvent('cart_add_item', {
  productId: '12345',
  productName: 'iPhone 13',
  price: 999,
  currency: 'USD',
  quantity: 1
});

性能考虑

  • SDK使用批量上报机制减少网络请求
  • 通过采样率控制可以在高流量场景下减少数据量
  • 事件处理使用防抖和节流技术避免频繁触发
  • 使用队列机制确保数据不丢失

隐私和安全

  • 默认不会收集表单输入内容和敏感数据
  • 可以通过配置控制收集数据的范围
  • 建议在生产环境中使用HTTPS上报数据
ts
// 埋点数据类型定义
export interface TrackerEvent {
    type: string             // 事件类型
    timestamp: number        // 事件时间戳
    data: Record<string, any> // 事件数据
  }
  
  // 设备信息
  export interface DeviceInfo {
    userAgent: string
    screenWidth: number
    screenHeight: number
    viewportWidth: number
    viewportHeight: number
    devicePixelRatio: number
    platform: string
    language: string
    connection?: {
      effectiveType?: string
      downlink?: number
      rtt?: number
      saveData?: boolean
    }
  }
  
  // 点击信息
  export interface ClickInfo {
    x: number           // 视口X坐标
    y: number           // 视口Y坐标
    pageX: number       // 页面X坐标
    pageY: number       // 页面Y坐标
    target: ElementInfo // 目标元素信息
    path: ElementInfo[] // 事件冒泡路径
  }
  
  // 元素信息
  export interface ElementInfo {
    tagName: string
    id: string
    className: string
    text?: string
    attributes?: Record<string, string>
  }
  
  // 路由信息
  export interface RouteInfo {
    path: string
    fullPath: string
    query: Record<string, string>
    name?: string | null
  }
  
  // 性能信息
  export interface PerformanceInfo {
    type: 'pageLoad' | 'routeChange'
    duration: number
    from?: string
    to: string
  }
  
  // 埋点选项
  export interface TrackerOptions {
    appId: string                              // 应用ID
    userId?: string                            // 用户ID
    uploadUrl?: string                         // 数据上报地址
    autoTrack?: boolean                        // 是否自动采集
    sampleRate?: number                        // 采样率 0-1
    maxBatchSize?: number                      // 最大批量上报大小
    maxCacheTime?: number                      // 最大缓存时间(ms)
    plugins?: TrackerPlugin[]                  // 插件列表
    historyTracker?: boolean                   // 是否跟踪 history 路由变化
    hashTracker?: boolean                      // 是否跟踪 hash 路由变化
  }
  
  // 插件接口
  export interface TrackerPlugin {
    name: string
    install: (tracker: Tracker) => void
  }
  
  // 网络连接信息接口(拓展Navigator类型)
  interface NavigatorWithConnection extends Navigator {
    connection?: {
      effectiveType?: string
      downlink?: number
      rtt?: number
      saveData?: boolean
    }
  }
  
  class Tracker {
    private readonly options: TrackerOptions
    private deviceInfo: DeviceInfo | null = null
    private queue: TrackerEvent[] = []
    private timer: number | null = null
    private startTime: number = 0
    private routeChangeStartTime: number = 0
    private isInstalled: boolean = false
    private originalPushState: History['pushState'] | null = null
    private originalReplaceState: History['replaceState'] | null = null
  
    constructor(options: TrackerOptions) {
      this.options = {
        sampleRate: 1,
        maxBatchSize: 10,
        maxCacheTime: 5000,
        autoTrack: true,
        historyTracker: true,
        hashTracker: true,
        ...options
      }
      
      // 立即收集设备信息
      this.collectDeviceInfo()
    }
  
    // 初始化埋点系统
    init(): void {
      if (this.isInstalled) return
      this.isInstalled = true
  
      if (this.options.autoTrack) {
        // 记录页面加载开始时间
        this.startTime = performance.now()
        
        // 监听DOM准备完成事件
        window.addEventListener('DOMContentLoaded', () => {
          this.trackPageLoadTime()
        })
  
        // 监听点击事件
        document.addEventListener('click', this.handleClick.bind(this), true)
        
        // 监听路由变化
        this.setupRouteTracking()
      }
  
      // 安装插件
      if (this.options.plugins) {
        for (const plugin of this.options.plugins) {
          plugin.install(this)
        }
      }
    }
  
    // 收集设备信息
    private collectDeviceInfo(): DeviceInfo {
      const deviceInfo: DeviceInfo = {
        userAgent: navigator.userAgent,
        screenWidth: screen.width,
        screenHeight: screen.height,
        viewportWidth: window.innerWidth,
        viewportHeight: window.innerHeight,
        devicePixelRatio: window.devicePixelRatio,
        platform: navigator.platform,
        language: navigator.language,
      }
  
      // 收集网络连接信息(如果可用)
      const nav = navigator as NavigatorWithConnection
      if (nav.connection) {
        deviceInfo.connection = {
          effectiveType: nav.connection.effectiveType,
          downlink: nav.connection.downlink,
          rtt: nav.connection.rtt,
          saveData: nav.connection.saveData
        }
      }
  
      this.deviceInfo = deviceInfo
      return deviceInfo
    }
  
    // 处理点击事件
    private handleClick(event: MouseEvent) {
      const target = event.target as HTMLElement
      if (!target) return
  
      // 构建元素路径
      const path: ElementInfo[] = []
      let element: HTMLElement | null = target
      
      while (element) {
        path.push(this.getElementInfo(element))
        element = element.parentElement
      }
  
      const clickInfo: ClickInfo = {
        x: event.clientX,
        y: event.clientY,
        pageX: event.pageX,
        pageY: event.pageY,
        target: this.getElementInfo(target),
        path
      }
  
      this.track('click', { click: clickInfo })
    }
  
    // 获取元素信息
    private getElementInfo(element: HTMLElement): ElementInfo {
      const info: ElementInfo = {
        tagName: element.tagName.toLowerCase(),
        id: element.id,
        className: element.className,
      }
  
      // 尝试获取元素文本内容
      if (element.textContent) {
        info.text = element.textContent.trim().substring(0, 50)
      }
  
      // 收集关键属性
      const attrs: Record<string, string> = {}
      for (const attr of ['data-track', 'href', 'src', 'alt', 'title']) {
        if (element.hasAttribute(attr)) {
          attrs[attr] = element.getAttribute(attr) || ''
        }
      }
  
      if (Object.keys(attrs).length > 0) {
        info.attributes = attrs
      }
  
      return info
    }
  
    // 设置路由跟踪 - 同时支持 history 和 hash 模式
    private setupRouteTracking() {
      const oldURL = window.location.href;
      
      // 记录路由变化开始时间
      this.routeChangeStartTime = performance.now();
      
      // 监听 history 模式的路由变化
      if (this.options.historyTracker && window.history) {
        // 保存原始方法
        this.originalPushState = window.history.pushState;
        this.originalReplaceState = window.history.replaceState;
        
        // 重写 pushState
        window.history.pushState = (...args) => {
          if (this.originalPushState) {
            this.originalPushState.apply(window.history, args);
          }
          this.historyRouteChange(oldURL);
        };
        
        // 重写 replaceState
        window.history.replaceState = (...args) => {
          if (this.originalReplaceState) {
            this.originalReplaceState.apply(window.history, args);
          }
          this.historyRouteChange(oldURL);
        };
        
        // 监听 popstate 事件
        window.addEventListener('popstate', () => {
          this.historyRouteChange(oldURL);
        });
      }
      
      // 监听 hash 模式的路由变化
      if (this.options.hashTracker) {
        window.addEventListener('hashchange', () => {
          this.hashRouteChange(oldURL);
        });
      }
    }
    
    // History 路由模式变化处理
    private historyRouteChange(oldURL: string) {
      const newURL = window.location.href;
      if (oldURL !== newURL) {
        this.trackRouteChange(oldURL, newURL);
      }
    }
    
    // Hash 路由模式变化处理
    private hashRouteChange(oldURL: string) {
      const newURL = window.location.href;
      if (oldURL !== newURL) {
        this.trackRouteChange(oldURL, newURL);
      }
    }
    
    // 跟踪路由变化
    private trackRouteChange(from: string, to: string) {
      // 计算路由变化耗时
      const duration = performance.now() - this.routeChangeStartTime;
      
      // 重置计时
      this.routeChangeStartTime = performance.now();
      
      // 获取路由信息
      const routeInfo: RouteInfo = {
        path: window.location.pathname,
        fullPath: window.location.pathname + window.location.search,
        query: this.getQueryParams(),
        name: null
      };
      
      // 跟踪路由变化
      this.track('routeChange', {
        route: routeInfo,
        performance: {
          type: 'routeChange',
          duration,
          from: new URL(from).pathname,
          to: new URL(to).pathname
        }
      });
    }
    
    // 获取URL查询参数
    private getQueryParams(): Record<string, string> {
      const params: Record<string, string> = {};
      const searchParams = new URLSearchParams(window.location.search);
      
      searchParams.forEach((value, key) => {
        params[key] = value;
      });
      
      return params;
    }
  
    // 跟踪页面加载时间
    private trackPageLoadTime() {
      const duration = performance.now() - this.startTime
      
      this.track('pageLoad', {
        performance: {
          type: 'pageLoad',
          duration,
          to: window.location.pathname
        }
      })
    }
  
    // 跟踪事件(公共方法)
    track(eventType: string, data: Record<string, any> = {}) {
      // 采样处理
      if (Math.random() > (this.options.sampleRate || 1)) {
        return
      }
  
      // 准备事件数据
      const event: TrackerEvent = {
        type: eventType,
        timestamp: Date.now(),
        data: {
          ...data,
          // 附加通用信息
          device: this.deviceInfo,
          url: window.location.href,
          appId: this.options.appId,
          userId: this.options.userId
        }
      }
  
      // 加入队列
      this.queue.push(event)
      
      // 触发上报逻辑
      this.scheduleUpload()
    }
  
    // 安排上传
    private scheduleUpload() {
      // 如果队列超过最大批量,立即上传
      if (this.queue.length >= (this.options.maxBatchSize || 10)) {
        this.upload()
        return
      }
      
      // 否则设置定时器延迟上传
      if (!this.timer && this.options.maxCacheTime) {
        this.timer = window.setTimeout(() => {
          this.upload()
          this.timer = null
        }, this.options.maxCacheTime)
      }
    }
  
    // 上传数据
    private upload() {
      if (this.queue.length === 0) return
  
      const events = [...this.queue]
      this.queue = []
  
      // 如果没有设置上报URL,则只打印到控制台
      if (!this.options.uploadUrl) {
        console.log('埋点数据:', events)
        return
      }
  
      // 发送数据到服务器
      fetch(this.options.uploadUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ events }),
        // 使用 keepalive 保证页面关闭时数据仍能发送
        keepalive: true
      }).catch(err => {
        console.error('埋点数据上报失败:', err)
        // 上报失败,将数据放回队列
        this.queue = [...events, ...this.queue]
      })
    }
  
    // 手动上报队列中的所有数据
    flush() {
      this.upload()
    }
  
    // 自定义事件上报方法
    trackEvent(eventName: string, eventData: Record<string, any> = {}) {
      this.track(`custom_${eventName}`, eventData)
    }
    
    // 获取SDK实例(单例模式)
    static instance: Tracker | null = null
    
    static getInstance(options: TrackerOptions): Tracker {
      if (!this.instance) {
        this.instance = new Tracker(options)
        this.instance.init()
      }
      return this.instance
    }
  }
  
  export default Tracker