Files
TrueGrowth/specs/005-declarative-tracking/data-model.md

8.6 KiB

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: 代表一个待上报或已上报的埋点事件,包含事件名、参数、元数据

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:

{
  "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: 埋点系统的全局配置,控制行为和性能参数

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:

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 元素及其埋点配置

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 的事件

interface CachedEvent {
  /** 事件数据 */
  event: TrackEvent;

  /** 缓存时间戳 */
  cachedAt: number;

  /** 重试次数 */
  retryCount: number;

  /** 最后重试时间 */
  lastRetryAt?: number;

  /** 失败原因(用于调试) */
  failureReason?: string;
}

Lifecycle:

  1. 上报失败 → 存入 IndexedDB
  2. 定期重试(每 30 秒)
  3. 重试成功 → 从缓存移除
  4. 重试次数超过限制 OR TTL 过期 → 从缓存移除并丢弃

Storage Structure:

// 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 管理

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

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

// localforage 自动处理,无需手动创建索引
// 但需要考虑查询性能:
// - 按 cachedAt 排序查找过期事件
// - 按 retryCount 过滤可重试事件

Migration Strategy

Version 1.0.0 → 1.1.0 (示例):

  • 添加新字段: metadata.viewport
  • 迁移逻辑: 读取旧数据,注入默认值
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)