Appearance
前端埋点库
这是一个轻量级、可扩展的前端埋点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