Files

8.8 KiB

Umami Analytics API Contract

Feature: 005-declarative-tracking Date: 2025-12-05 API Version: Umami v2.x Integration Type: Client SDK Wrapper

Overview

本文档定义声明式埋点系统与 Umami Analytics 的集成契约,包括事件上报接口、数据格式、错误处理。

Integration Strategy

Approach: 复用现有 UmamiAnalytics 工具 (utils/umami-analytics.ts)

  • 使用项目已有的 analytics 单例,避免代码重复
  • UmamiAdapter 作为适配器层,调用 analytics.track()
  • 自动注入声明式埋点专属元数据(version, url, sessionId, viewport, eventType)
  • 与现有 AI 生成事件追踪保持一致的日志格式
  • 保持与 Umami 更新的兼容性

代码位置:

  • 现有工具: packages/drawnix/src/utils/umami-analytics.ts
  • 适配器: packages/drawnix/src/services/tracking/umami-adapter.ts

API Endpoint

1. Track Event (上报事件)

Method: Umami SDK 封装 (umami.track())

Request (通过 SDK):

umami.track(eventName: string, eventData?: Record<string, any>): Promise<void>

Internal HTTP Request (SDK 内部实现):

POST https://{umami-domain}/api/send
Content-Type: application/json

{
  "type": "event",
  "payload": {
    "website": "{website-id}",
    "name": "event-name",
    "data": {
      // 自定义事件属性
      "version": "1.0.0",
      "url": "https://opentu.ai/editor",
      "param1": "value1"
    }
  }
}

Response (成功):

HTTP/1.1 200 OK
Content-Type: application/json

{
  "ok": true
}

Response (失败):

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": "Invalid request"
}

Data Contract

Event Data Structure

Outbound (发送给 Umami):

interface UmamiEventPayload {
  /** 事件名称 */
  name: string;

  /** 事件数据(自定义属性) */
  data: {
    /** 项目版本号(必需) */
    version: string;

    /** 当前页面 URL(必需) */
    url: string;

    /** 事件参数(来自 track-params,可选) */
    [key: string]: any;
  };
}

Example:

{
  "name": "button_click_save",
  "data": {
    "version": "1.2.3",
    "url": "https://opentu.ai/editor?id=abc123",
    "buttonId": "save-btn",
    "context": "editor"
  }
}

Validation Rules:

  • name: 必需,非空字符串,长度 1-100 字符
  • data: 可选,对象类型,总大小 <10KB (Umami 限制)
  • data.version: 必需,SemVer 格式
  • data.url: 必需,有效 URL
  • 其他 data.* 字段: 可选,支持字符串、数字、布尔值、对象、数组

Constraints:

  • 事件名不能包含特殊字符(仅允许 a-z, 0-9, _, -)
  • 自定义属性键名不能以 _ 开头(Umami 保留)
  • 单个事件数据大小 <10KB
  • 批量上报时,单次请求最多 50 个事件

Batch Upload Contract

Batch Event Upload

虽然 Umami SDK 不直接支持批量上报,但可以通过连续调用 umami.track() 实现。

Implementation:

async function sendBatch(events: TrackEvent[]): Promise<void> {
  for (const event of events) {
    try {
      await umami.track(event.eventName, {
        version: event.metadata.version,
        url: event.metadata.url,
        ...event.params
      });
    } catch (error) {
      // 单个事件失败不影响其他事件
      console.error(`Failed to track ${event.eventName}:`, error);
    }
  }
}

Alternative (如果 Umami 支持批量端点):

POST https://{umami-domain}/api/batch
Content-Type: application/json

{
  "events": [
    {
      "type": "event",
      "payload": { "website": "...", "name": "...", "data": {...} }
    },
    {
      "type": "event",
      "payload": { "website": "...", "name": "...", "data": {...} }
    }
  ]
}

Error Handling

Error Codes

HTTP Status Meaning Action
200 OK 成功 继续
400 Bad Request 请求格式错误(验证失败) 记录错误,不重试
401 Unauthorized Website ID 无效 记录错误,不重试
429 Too Many Requests 速率限制 等待后重试(指数退避)
500 Internal Server Error 服务器错误 重试(最多 3 次)
503 Service Unavailable 服务不可用 缓存并稍后重试

Retry Strategy

interface RetryConfig {
  maxRetries: 3;
  initialDelay: 2000;      // 2 秒
  exponentialBackoff: true;
  maxDelay: 30000;         // 最大 30 秒
}

async function uploadWithRetry(event: TrackEvent): Promise<void> {
  let attempt = 0;
  let delay = 2000;

  while (attempt < 3) {
    try {
      await umami.track(event.eventName, event.data);
      return; // 成功
    } catch (error) {
      attempt++;

      if (attempt >= 3 || !isRetryable(error)) {
        // 缓存到 IndexedDB
        await cacheEvent(event);
        throw error;
      }

      // 指数退避
      await sleep(delay);
      delay = Math.min(delay * 2, 30000);
    }
  }
}

function isRetryable(error: any): boolean {
  const status = error.response?.status;
  return status >= 500 || status === 429;
}

Security & Authentication

Website ID

Umami 使用 website ID 识别站点:

// 初始化 Umami (通常在 index.html 或应用入口)
<script defer src="https://umami-domain/script.js" data-website-id="{website-id}"></script>

// 或在代码中配置
window.umami = {
  websiteId: '{website-id}'
};

Security:

  • Website ID 公开可见(前端代码),不需要保密
  • Umami 通过 CORS 和域名限制保护数据

Rate Limiting

Umami 限制 (根据官方文档):

  • 每秒最多 10 个事件(同一用户)
  • 超过限制返回 429 Too Many Requests

应对策略:

  • 客户端防抖(500ms)
  • 批量上报减少请求频率
  • 429 错误时使用指数退避

Testing Contract

Mock Response (开发/测试环境)

// 模拟 Umami SDK
const mockUmami = {
  track: jest.fn(async (name, data) => {
    if (Math.random() < 0.1) {
      // 10% 概率失败
      throw new Error('Network error');
    }
    console.log('Mock track:', name, data);
  })
};

global.umami = mockUmami;

Contract Validation

Test Cases:

  1. 成功上报: umami.track() 返回成功
  2. 格式验证: 事件名、数据格式符合要求
  3. 自定义参数: version, url 正确注入
  4. 错误处理: 500 错误触发重试
  5. 速率限制: 429 错误触发退避
  6. 缓存: 上报失败后事件存入 IndexedDB

Integration Checklist

  • 确认 Umami SDK 已加载(window.umami 存在)
  • 配置正确的 website ID
  • 测试事件上报(开发环境)
  • 验证自定义参数(version, url)在 Umami 面板中显示
  • 测试失败场景(离线、500 错误)
  • 验证缓存和重试机制
  • 测试速率限制(快速点击)
  • 验证批量上报效果(网络请求数量减少 60%+)

Example Integration Code

// services/tracking/umami-adapter.ts
import { analytics } from '../../utils/umami-analytics';

export class UmamiTrackingAdapter {
  /**
   * Check if Umami SDK is available
   */
  isAvailable(): boolean {
    return analytics.isAnalyticsEnabled();
  }

  /**
   * Track event using existing analytics utility
   */
  async track(event: TrackEvent): Promise<void> {
    if (!this.isAvailable()) {
      throw new Error('Umami SDK not loaded');
    }

    // Enrich event data with declarative tracking metadata
    const enrichedData = {
      ...event.params,
      version: event.metadata.version,
      url: event.metadata.url,
      timestamp: event.metadata.timestamp,
      sessionId: event.metadata.sessionId,
      eventType: event.metadata.eventType,
    };

    // Optional: Add viewport if available
    if (event.metadata.viewport) {
      enrichedData.viewport = `${event.metadata.viewport.width}x${event.metadata.viewport.height}`;
    }

    try {
      // Use existing analytics.track() method (复用现有工具)
      analytics.track(event.eventName, enrichedData);
    } catch (error) {
      console.error('[Tracking] Umami track failed:', error);
      throw error; // 由上层处理重试和缓存
    }
  }
}

集成优势:

  • 复用现有 UmamiAnalytics 类,避免代码重复
  • 保持与 AI 生成事件追踪的日志格式一致
  • 统一的错误处理和调试输出 [Analytics] / [Tracking]
  • 自动受益于现有工具的任何改进

API Version Compatibility

Umami Version Supported Notes
v1.x ⚠️ Partial 需要手动实现 umami.track() 包装
v2.0+ Yes 原生支持 track() 方法
v3.0+ Yes 向后兼容

References


Next Step: 生成 quickstart.md