# Data Model: 声明式埋点上报系统 **Feature**: 005-declarative-tracking **Date**: 2025-12-05 **Source**: Derived from spec.md Functional Requirements and research.md decisions ## Core Entities ### 1. TrackEvent (上报事件) **Description**: 代表一个待上报或已上报的埋点事件,包含事件名、参数、元数据 ```typescript interface TrackEvent { /** 事件名称(手动指定或自动生成) */ eventName: string; /** 事件参数(来自 track-params 属性,可选) */ params?: Record; /** 元数据(自动注入) */ metadata: TrackEventMetadata; /** 事件 ID(用于去重和缓存管理) */ id: string; /** 事件创建时间戳 */ createdAt: number; } interface TrackEventMetadata { /** 事件触发时间戳 */ timestamp: number; /** 当前页面完整 URL */ url: string; /** 项目版本号 */ version: string; /** 用户会话 ID */ sessionId: string; /** 浏览器 User-Agent(可选) */ userAgent?: string; /** 视口尺寸(可选) */ viewport?: { width: number; height: number; }; /** 事件类型(click, hover, focus 等) */ eventType: TrackEventType; } type TrackEventType = 'click' | 'hover' | 'focus' | 'blur' | 'input' | 'submit'; ``` **Validation Rules**: - `eventName`: 非空字符串,建议使用 snake_case 格式(如 `button_click_save`) - `params`: 如果存在,必须是可序列化的 JSON 对象(不能包含函数、循环引用) - `id`: UUID v4 格式,确保全局唯一 - `metadata.url`: 有效的 URL 字符串 - `metadata.version`: 符合 SemVer 格式(如 `1.0.0`) **Example**: ```json { "eventName": "button_click_save", "params": { "buttonId": "save-btn", "context": "editor" }, "metadata": { "timestamp": 1701849600000, "url": "https://opentu.ai/editor", "version": "1.2.3", "sessionId": "abc123-session", "eventType": "click" }, "id": "550e8400-e29b-41d4-a716-446655440000", "createdAt": 1701849600000 } ``` --- ### 2. TrackConfig (埋点配置) **Description**: 埋点系统的全局配置,控制行为和性能参数 ```typescript interface TrackConfig { /** 是否启用自动埋点 */ autoTrack: boolean; /** Umami API 上报端点 */ apiEndpoint: string; /** Umami website ID */ websiteId: string; /** 防抖时间(毫秒) */ debounceTime: number; /** 重试策略 */ retryPolicy: RetryPolicy; /** 缓存配置 */ cacheConfig: CacheConfig; /** 日志级别 */ logLevel: 'error' | 'debug' | 'silent'; /** 批量上报配置 */ batchConfig: BatchConfig; /** 自动埋点排除区域(CSS 选择器) */ excludedSelectors: string[]; /** 项目版本号(自动注入或手动配置) */ version?: string; /** 是否启用开发模式(输出详细日志) */ devMode: boolean; } interface RetryPolicy { /** 最大重试次数 */ maxRetries: number; /** 重试间隔(毫秒) */ retryInterval: number; /** 是否启用指数退避(retry interval * 2^attempt) */ exponentialBackoff: boolean; } interface CacheConfig { /** 缓存上限(事件数量) */ maxCacheSize: number; /** 缓存保留时间(毫秒) */ cacheTTL: number; /** 存储键名 */ storageKey: string; } interface BatchConfig { /** 批量大小(事件数量) */ batchSize: number; /** 批量超时时间(毫秒) */ batchTimeout: number; /** 是否启用批量上报 */ enabled: boolean; } ``` **Default Values**: ```typescript const DEFAULT_TRACK_CONFIG: TrackConfig = { autoTrack: false, apiEndpoint: '/api/send', // Umami 默认端点 websiteId: '', // 需要配置 debounceTime: 500, retryPolicy: { maxRetries: 3, retryInterval: 2000, exponentialBackoff: true }, cacheConfig: { maxCacheSize: 100, cacheTTL: 60 * 60 * 1000, // 1 小时 storageKey: 'tracking_cache' }, logLevel: 'error', batchConfig: { batchSize: 10, batchTimeout: 5000, // 5 秒 enabled: true }, excludedSelectors: [ 'nav', 'header', 'footer', '[data-track-ignore]' ], devMode: false }; ``` --- ### 3. TrackedElement (被监听元素) **Description**: 代表一个已被监听的 DOM 元素及其埋点配置 ```typescript interface TrackedElement { /** 元素引用(WeakRef 避免内存泄漏) */ elementRef: WeakRef; /** 事件名称 */ eventName: string; /** 事件参数(可选) */ params?: Record; /** 监听的事件类型 */ eventTypes: TrackEventType[]; /** 是否为自动埋点 */ isAutoTracked: boolean; /** 最后触发时间(用于防抖) */ lastTriggeredAt: number; /** 元素选择器(用于调试) */ selector: string; } ``` **Usage**: - `TrackedElement` 对象存储在 WeakMap 中,键为 Element 实例 - 当元素从 DOM 移除时,WeakMap 自动清理引用,避免内存泄漏 --- ### 4. CachedEvent (缓存事件) **Description**: 上报失败后缓存到 IndexedDB 的事件 ```typescript interface CachedEvent { /** 事件数据 */ event: TrackEvent; /** 缓存时间戳 */ cachedAt: number; /** 重试次数 */ retryCount: number; /** 最后重试时间 */ lastRetryAt?: number; /** 失败原因(用于调试) */ failureReason?: string; } ``` **Lifecycle**: 1. 上报失败 → 存入 IndexedDB 2. 定期重试(每 30 秒) 3. 重试成功 → 从缓存移除 4. 重试次数超过限制 OR TTL 过期 → 从缓存移除并丢弃 **Storage Structure**: ```typescript // IndexedDB (via localforage) { "tracking_cache": [ { "event": { /* TrackEvent */ }, "cachedAt": 1701849600000, "retryCount": 2, "lastRetryAt": 1701849602000, "failureReason": "Network error" }, // ... more cached events ] } ``` --- ## State Management ### 5. TrackingState (埋点系统状态) **Description**: 运行时状态,使用 RxJS Subject 管理 ```typescript interface TrackingState { /** 是否已初始化 */ initialized: boolean; /** 当前配置 */ config: TrackConfig; /** 待上报事件队列 */ pendingEvents: TrackEvent[]; /** 正在上报中(防止并发) */ uploading: boolean; /** 缓存的事件数量 */ cachedEventCount: number; /** 统计信息 */ stats: TrackingStats; } interface TrackingStats { /** 总事件数(自启动以来) */ totalEvents: number; /** 成功上报数 */ successfulUploads: number; /** 失败上报数 */ failedUploads: number; /** 批量上报次数 */ batchUploadCount: number; /** 防抖拦截次数 */ debouncedEvents: number; } ``` **State Updates** (via RxJS): ```typescript class TrackingService { private state$ = new BehaviorSubject(initialState); getState(): Observable { return this.state$.asObservable(); } updateState(partial: Partial): void { this.state$.next({ ...this.state$.value, ...partial }); } } ``` --- ## Data Flow ``` User Interaction (click, hover, etc.) ↓ Event Capture (event delegation / event listener) ↓ Debounce Check (shouldTrack?) ↓ YES Generate TrackEvent ↓ Enqueue to BatchService ↓ Batch Trigger (10 events OR 5 seconds) ↓ Send to Umami API ↓ Success? → Update Stats ↓ NO Cache to IndexedDB (CachedEvent) ↓ Retry Later (30s interval, max 3 retries) ``` --- ## Index Strategy ### IndexedDB Indexes ```typescript // localforage 自动处理,无需手动创建索引 // 但需要考虑查询性能: // - 按 cachedAt 排序查找过期事件 // - 按 retryCount 过滤可重试事件 ``` --- ## Migration Strategy **Version 1.0.0 → 1.1.0** (示例): - 添加新字段: `metadata.viewport` - 迁移逻辑: 读取旧数据,注入默认值 ```typescript async function migrateCache(): Promise { const cached = await localforage.getItem('tracking_cache'); if (!cached) return; const migrated = cached.map(c => ({ ...c, event: { ...c.event, metadata: { ...c.event.metadata, viewport: c.event.metadata.viewport || { width: 0, height: 0 } } } })); await localforage.setItem('tracking_cache', migrated); } ``` --- ## Summary | Entity | Purpose | Storage | Lifecycle | |--------|---------|---------|-----------| | TrackEvent | 上报事件 | 内存(队列) + IndexedDB(失败时) | 触发 → 上报 → 销毁 | | TrackConfig | 配置 | 内存(单例) | 应用启动 → 应用销毁 | | TrackedElement | 元素监听 | WeakMap | DOM 挂载 → DOM 卸载 | | CachedEvent | 失败缓存 | IndexedDB | 上报失败 → 重试成功/过期 | | TrackingState | 运行时状态 | RxJS Subject | 应用启动 → 应用销毁 | **Next Step**: 生成 API contracts (contracts/umami-api.md)