Initial TrueGrowth source import

This commit is contained in:
2026-07-07 09:36:36 +08:00
commit 3b6781d695
2283 changed files with 691996 additions and 0 deletions

View 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)