UNPKG

@ked3/ktrace

Version:

跨平台埋点分析SDK

253 lines (218 loc) 7.17 kB
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;