@ked3/ktrace
Version:
跨平台埋点分析SDK
253 lines (218 loc) • 7.17 kB
text/typescript
import { TrackerConfig, TrackEvent, EventType, EventKey, validateEventKeyType, getDefaultEventType } from "./types";
import { getSessionId, getDeviceId, formatDateTime } from "../utils";
/**
* 核心追踪器类
*/
class Tracker {
private config: TrackerConfig;
private sessionId: string;
public version: string = "1.0.0";
private eventQueue: TrackEvent[] = [];
private readonly BATCH_SIZE = 5;
/**
* 构造函数
*/
constructor(config?: Partial<TrackerConfig>) {
// 默认配置
const defaultConfig: TrackerConfig = {
project_id: "",
server_url: "https://api.zdcloud.click",
event_path: "/api/sdk/collect/track",
batch_event_path: "/api/sdk/collect/batch",
enable_auto_track: true,
debug: true,
};
this.config = { ...defaultConfig, ...config };
this.sessionId = getSessionId();
// 监听页面卸载事件,发送剩余事件
this.setupUnloadHandler();
}
/**
* 设置页面卸载处理器
*/
private setupUnloadHandler(): void {
if (typeof window !== "undefined") {
const handleUnload = () => {
if (this.eventQueue.length > 0) {
this.batchSend(true);
}
};
// 监听页面卸载事件
window.addEventListener('beforeunload', handleUnload);
window.addEventListener('pagehide', handleUnload);
// 对于现代浏览器,使用visibilitychange事件
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden' && this.eventQueue.length > 0) {
this.batchSend(true);
}
});
}
}
/**
* 验证事件数据
* @param event 事件数据
*/
private validateEvent(event: TrackEvent): void {
// 验证必填字段
if (!event.project_id?.trim()) {
throw new Error('项目ID不能为空');
}
if (!event.session_id?.trim()) {
throw new Error('会话ID不能为空');
}
if (!event.event_time) {
throw new Error('事件时间不能为空');
}
// 验证事件键
if (!Object.values(EventKey).includes(event.event_key)) {
throw new Error(`无效的事件键: ${event.event_key} \r\n支持的键: ${Object.values(EventKey).join(', ')}`);
}
// 验证事件类型
if (!Object.values(EventType).includes(event.event_type)) {
throw new Error(`无效的事件类型: ${event.event_type} \r\n支持的事件类型: ${Object.values(EventType).join(', ')}`);
}
// 验证事件键和事件类型的组合
if (!validateEventKeyType(event.event_key, event.event_type)) {
const expectedType = getDefaultEventType(event.event_key);
throw new Error(`事件键 "${event.event_key}" 与事件类型 "${event.event_type}" 的组合无效。该事件键应使用事件类型: ${expectedType}`);
}
}
/**
* 添加事件到队列
* @param event 事件数据
*/
private addToQueue(event: TrackEvent): void {
try {
// 客户端验证
this.validateEvent(event);
if (this.config.debug) {
console.log(`[KTrace] 添加事件到队列:`, event);
}
this.eventQueue.push(event);
// 检查是否需要发送
if (this.eventQueue.length >= this.BATCH_SIZE) {
this.batchSend();
}
} catch (error) {
console.error(`[KTrace] 事件验证失败:`, (error as Error).message);
}
}
/**
* 批量发送事件
* @param isUnload 是否是页面卸载时的发送
*/
private batchSend(isUnload: boolean = false): void {
if (this.eventQueue.length === 0) {
return;
}
// sendBeacon检查
if (typeof navigator === 'undefined' || !navigator.sendBeacon) {
console.error('[KTrace] sendBeacon不支持,无法发送事件');
return;
}
const server_url = this.config.server_url!;
const batch_event_path = this.config.batch_event_path!;
const url = server_url + batch_event_path;
// 构建批量数据
const batchData = {
events: [...this.eventQueue],
batch_info: {
batch_size: this.eventQueue.length,
timestamp: formatDateTime(new Date()),
is_unload: isUnload
}
};
try {
// 使用sendBeacon发送数据
const blob = new Blob([JSON.stringify(batchData)], {
type: 'application/json'
});
const success = navigator.sendBeacon(url, blob);
if (success) {
if (this.config.debug) {
console.log(`[KTrace] 批量发送成功,事件数量: ${this.eventQueue.length}`, batchData);
}
// 清空队列
this.eventQueue = [];
} else {
console.error('[KTrace] sendBeacon发送失败');
}
} catch (error) {
console.error('[KTrace] 批量发送事件时发生错误:', error);
}
}
/**
* 手动触发批量发送
* 用于在需要时立即发送当前队列中的所有事件
*/
public flushEvents(): void {
if (this.eventQueue.length > 0) {
this.batchSend();
}
}
/**
* 创建基础事件对象
*/
public createBaseEvent(
eventKey: EventKey,
eventType?: EventType,
error?: Error,
properties?: Record<string, any>
): TrackEvent {
// 如果没有指定事件类型,使用默认类型
const finalEventType = eventType || getDefaultEventType(eventKey);
const eventInfo = properties || {};
const page_url = typeof window !== "undefined" && window.location ? window.location.href : ""
const referrer = typeof document !== "undefined" ? document.referrer : ""
const metadata: any = {
...eventInfo,
page_url,
referrer,
}
if (finalEventType === EventType.ERROR) {
metadata.error_message = error?.message;
metadata.error_stack = error?.stack;
metadata.error_name = error?.name;
}
return {
project_id: this.config.project_id,
event_key: eventKey,
event_type: finalEventType,
event_time: new Date(),
session_id: this.sessionId,
device_id: getDeviceId(),
device_ua: typeof navigator !== "undefined" ? navigator.userAgent : "",
ip_address: '',
metadata: metadata,
};
}
/**
* 追踪自定义事件
* @param eventName 事件名称(必须是EventKey枚举值)
* @param properties 事件属性
*/
public trackEvent(eventName: EventKey, properties?: Record<string, any>): void {
const event = this.createBaseEvent(eventName, undefined, undefined, properties);
this.addToQueue(event);
}
/**
* 追踪错误事件
* @param error 错误对象
* @param eventKey 错误事件键(可选,默认使用JAVASCRIPT_ERROR)
* @param properties 附加属性
*/
public trackError(error: Error, eventKey: EventKey = EventKey.JAVASCRIPT_ERROR, properties?: Record<string, any>): void {
const event = this.createBaseEvent(eventKey, EventType.ERROR, error, properties);
this.addToQueue(event);
}
/**
* 获取当前队列长度
*/
public getQueueLength(): number {
return this.eventQueue.length;
}
public getConfig(): TrackerConfig {
return this.config;
}
}
export default Tracker;