Files
doc_ai_backed/frontend-sdk/analytics.ts
2026-01-27 22:20:07 +08:00

309 lines
6.8 KiB
TypeScript

// Analytics SDK for Frontend
// 前端数据埋点 SDK
interface EventProperties {
[key: string]: any;
}
interface DeviceInfo {
user_agent?: string;
screen_width?: number;
screen_height?: number;
language?: string;
timezone?: string;
platform?: string;
}
interface MetaData {
task_id?: string | number;
[key: string]: any;
}
interface TrackEventParams {
event_name: string;
properties?: EventProperties;
device_info?: DeviceInfo;
meta_data?: MetaData;
}
interface AnalyticsConfig {
apiUrl: string;
token?: string;
userId?: number | string;
enableAutoTrack?: boolean;
debug?: boolean;
}
class Analytics {
private config: AnalyticsConfig;
private userId: number | string | null = null;
private eventQueue: TrackEventParams[] = [];
private isSending: boolean = false;
constructor(config: AnalyticsConfig) {
this.config = {
enableAutoTrack: true,
debug: false,
...config,
};
if (this.config.userId) {
this.userId = this.config.userId;
}
// 自动收集设备信息
if (this.config.enableAutoTrack) {
this.initAutoTrack();
}
}
/**
* 设置用户ID
*/
setUserId(userId: number | string) {
this.userId = userId;
}
/**
* 获取设备信息
*/
private getDeviceInfo(): DeviceInfo {
return {
user_agent: navigator.userAgent,
screen_width: window.screen.width,
screen_height: window.screen.height,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
platform: navigator.platform,
};
}
/**
* 记录单个事件
*/
async track(params: TrackEventParams): Promise<void> {
if (!this.userId) {
console.warn('Analytics: userId not set, event will not be tracked');
return;
}
const eventData = {
user_id: this.userId,
event_name: params.event_name,
properties: params.properties || {},
device_info: {
...this.getDeviceInfo(),
...params.device_info,
},
meta_data: {
timestamp: Date.now(),
...params.meta_data,
},
};
if (this.config.debug) {
console.log('Analytics Track:', eventData);
}
try {
const response = await fetch(`${this.config.apiUrl}/track`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.config.token && { Authorization: `Bearer ${this.config.token}` }),
},
body: JSON.stringify(eventData),
});
if (!response.ok) {
throw new Error(`Failed to track event: ${response.statusText}`);
}
if (this.config.debug) {
console.log('Analytics: Event tracked successfully');
}
} catch (error) {
console.error('Analytics: Failed to track event', error);
// 失败时加入队列,稍后重试
this.eventQueue.push(params);
}
}
/**
* 批量记录事件
*/
async trackBatch(events: TrackEventParams[]): Promise<void> {
if (!this.userId) {
console.warn('Analytics: userId not set, events will not be tracked');
return;
}
const batchData = {
events: events.map((params) => ({
user_id: this.userId,
event_name: params.event_name,
properties: params.properties || {},
device_info: {
...this.getDeviceInfo(),
...params.device_info,
},
meta_data: {
timestamp: Date.now(),
...params.meta_data,
},
})),
};
if (this.config.debug) {
console.log('Analytics Track Batch:', batchData);
}
try {
const response = await fetch(`${this.config.apiUrl}/track/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.config.token && { Authorization: `Bearer ${this.config.token}` }),
},
body: JSON.stringify(batchData),
});
if (!response.ok) {
throw new Error(`Failed to track batch events: ${response.statusText}`);
}
if (this.config.debug) {
console.log('Analytics: Batch events tracked successfully');
}
} catch (error) {
console.error('Analytics: Failed to track batch events', error);
}
}
/**
* 页面浏览事件
*/
trackPageView(pageName?: string) {
this.track({
event_name: 'page_view',
properties: {
page_url: window.location.href,
page_title: document.title,
page_name: pageName || document.title,
referrer: document.referrer,
},
});
}
/**
* 点击事件
*/
trackClick(elementName: string, properties?: EventProperties) {
this.track({
event_name: 'click',
properties: {
element_name: elementName,
page_url: window.location.href,
...properties,
},
});
}
/**
* 表单提交事件
*/
trackFormSubmit(formName: string, properties?: EventProperties) {
this.track({
event_name: 'form_submit',
properties: {
form_name: formName,
page_url: window.location.href,
...properties,
},
});
}
/**
* 任务相关事件
*/
trackTask(taskId: string | number, action: string, properties?: EventProperties) {
this.track({
event_name: `task_${action}`,
properties: {
action,
...properties,
},
meta_data: {
task_id: taskId,
},
});
}
/**
* 初始化自动埋点
*/
private initAutoTrack() {
// 页面加载完成时记录
if (document.readyState === 'complete') {
this.trackPageView();
} else {
window.addEventListener('load', () => this.trackPageView());
}
// 页面离开前发送队列中的事件
window.addEventListener('beforeunload', () => {
if (this.eventQueue.length > 0) {
this.flushQueue();
}
});
// 页面可见性变化
document.addEventListener('visibilitychange', () => {
if (document.hidden && this.eventQueue.length > 0) {
this.flushQueue();
}
});
}
/**
* 刷新队列中的事件
*/
private flushQueue() {
if (this.isSending || this.eventQueue.length === 0) {
return;
}
this.isSending = true;
const eventsToSend = [...this.eventQueue];
this.eventQueue = [];
this.trackBatch(eventsToSend).finally(() => {
this.isSending = false;
});
}
/**
* 手动刷新队列
*/
flush() {
this.flushQueue();
}
}
// 导出单例实例
let analyticsInstance: Analytics | null = null;
export function initAnalytics(config: AnalyticsConfig): Analytics {
analyticsInstance = new Analytics(config);
return analyticsInstance;
}
export function getAnalytics(): Analytics {
if (!analyticsInstance) {
throw new Error('Analytics not initialized. Call initAnalytics first.');
}
return analyticsInstance;
}
export default Analytics;