386 lines
8.8 KiB
Markdown
386 lines
8.8 KiB
Markdown
# 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):
|
|
```typescript
|
|
umami.track(eventName: string, eventData?: Record<string, any>): Promise<void>
|
|
```
|
|
|
|
**Internal HTTP Request** (SDK 内部实现):
|
|
```http
|
|
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
|
|
HTTP/1.1 200 OK
|
|
Content-Type: application/json
|
|
|
|
```
|
|
|
|
**Response** (失败):
|
|
```http
|
|
HTTP/1.1 400 Bad Request
|
|
Content-Type: application/json
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## Data Contract
|
|
|
|
### Event Data Structure
|
|
|
|
**Outbound** (发送给 Umami):
|
|
```typescript
|
|
interface UmamiEventPayload {
|
|
/** 事件名称 */
|
|
name: string;
|
|
|
|
/** 事件数据(自定义属性) */
|
|
data: {
|
|
/** 项目版本号(必需) */
|
|
version: string;
|
|
|
|
/** 当前页面 URL(必需) */
|
|
url: string;
|
|
|
|
/** 事件参数(来自 track-params,可选) */
|
|
[key: string]: any;
|
|
};
|
|
}
|
|
```
|
|
|
|
**Example**:
|
|
```json
|
|
{
|
|
"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**:
|
|
```typescript
|
|
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 支持批量端点):
|
|
```http
|
|
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
|
|
|
|
```typescript
|
|
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` 识别站点:
|
|
|
|
```typescript
|
|
// 初始化 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 (开发/测试环境)
|
|
|
|
```typescript
|
|
// 模拟 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
- [Umami Documentation](https://umami.is/docs)
|
|
- [Umami Tracker API](https://umami.is/docs/tracker-functions)
|
|
- [Event Data Limits](https://umami.is/docs/event-data)
|
|
|
|
---
|
|
|
|
**Next Step**: 生成 quickstart.md
|