feat: add track point
This commit is contained in:
308
frontend-sdk/analytics.ts
Normal file
308
frontend-sdk/analytics.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user