Initial TrueGrowth source import
This commit is contained in:
419
specs/005-declarative-tracking/data-model.md
Normal file
419
specs/005-declarative-tracking/data-model.md
Normal file
@@ -0,0 +1,419 @@
|
||||
# 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<string, any>;
|
||||
|
||||
/** 元数据(自动注入) */
|
||||
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<Element>;
|
||||
|
||||
/** 事件名称 */
|
||||
eventName: string;
|
||||
|
||||
/** 事件参数(可选) */
|
||||
params?: Record<string, any>;
|
||||
|
||||
/** 监听的事件类型 */
|
||||
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<TrackingState>(initialState);
|
||||
|
||||
getState(): Observable<TrackingState> {
|
||||
return this.state$.asObservable();
|
||||
}
|
||||
|
||||
updateState(partial: Partial<TrackingState>): 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<void> {
|
||||
const cached = await localforage.getItem<CachedEvent[]>('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)
|
||||
Reference in New Issue
Block a user