# 用户行为埋点 API 文档

## 基本信息

| 项目 | 说明 |
|------|------|
| 接口路径 | `POST /api/v1/analytics/events` |
| 请求方式 | POST |
| Content-Type | application/json |
| 返回格式 | `{code, message, data}` |
| 认证 | 无需认证（有 Token 时自动记录用户标识，无 Token 记录为 anonymous） |

---

## 一、上报行为埋点

### 请求

```
POST /api/v1/analytics/events
Content-Type: application/json
Authorization: Bearer <token>   (可选)
```

### 请求体

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| event_type | string | 否 | 事件类型，不传则默认为 `unknown` |
| system | string | 否 | 来源系统，用于区分不同业务系统的埋点。不传默认 `AI药典`；接入方按自身系统传值即可，如 `中医` |
| event_data | object | 否 | 事件附加数据，JSON 对象 |
| page_url | string | 否 | 触发事件的页面 URL（建议传完整地址） |
| referrer | string | 否 | 来源页面 URL |

### 服务端自动记录

以下字段由服务端从请求中自动提取，**无需前端传入**：

| 字段 | 来源 |
|------|------|
| user_key | JWT Token 中的用户标识，未登录为 `anonymous` |
| ip | 请求来源 IP |
| user_agent | `User-Agent` 请求头 |
| system_source | 取自请求体 `system`，缺省记为 `AI药典`，超长自动截断至 64 字符 |

### 响应

```json
{
  "code": 202,
  "message": "accepted",
  "data": {
    "status": "accepted"
  }
}
```

> 注意：接口返回 202，写入是异步的（fire-and-forget），前端无需等待或处理结果。

### 示例

```bash
curl -X POST https://pharmacopoeia.kailin.com.cn/api/v1/analytics/events \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "page_view",
    "system": "AI药典",
    "event_data": {},
    "page_url": "https://pharmacopoeia.kailin.com.cn/?token=xxx"
  }'
```

---

## 二、标准事件类型定义

> 以下为推荐约定，**接入方也可使用任意自定义 `event_type`**（后端原样存储，超长截断至 64 字符）。自定义类型会出现在 `GET /event-types` 与事件列表中，可正常查询；但概览看板的 `page_views / searches / ai_qa` 等聚合指标按固定类型统计，自定义类型不纳入。

### 页面行为

| event_type | 触发时机 | event_data |
|-----------|---------|------------|
| `page_view` | 页面加载/进入 | `{}` 或 `{referrer: "来源URL"}` |

### 用户交互

| event_type | 触发时机 | event_data |
|-----------|---------|------------|
| `click` | 点击快捷提问标签 | `{label: "标签名称"}` |
| `switch_tab` | 切换导航标签页 | `{tab: "chat" \| "drugs"}` |

### 对话行为

| event_type | 触发时机 | event_data |
|-----------|---------|------------|
| `chat` | 用户发送提问 | `{query: "用户输入的问题", has_media: true \| false}` |

### 搜索行为

| event_type | 触发时机 | event_data |
|-----------|---------|------------|
| `search` | 搜索药品 | `{keyword: "搜索关键词"}` |

### 浏览行为

| event_type | 触发时机 | event_data |
|-----------|---------|------------|
| `drug_detail` | 查看药品详情 | `{drug_id: "H20000001", drug_name: "阿莫西林"}` |

### 品牌推荐

| event_type | 触发时机 | event_data |
|-----------|---------|------------|
| `brand_recommend` | 品牌推荐匹配命中 | `{recommendations: [{brand_name, matched_keyword, tier}]}` |

> 品牌推荐匹配由后端在 SSE 流中自动注入 `brand_recommend` 事件，前端收到后渲染推荐卡片。事件数据详见 HANDOVER.md 第十六章节。

### 接口调用（自动埋点）

| event_type | 触发时机 | event_data |
|-----------|---------|------------|
| `api_call` | 任意 API 调用完成 | `{url: "/api/v1/chat/stream", method: "POST", status: 200, elapsed_ms: 342}` |
| `api_call` | API 调用失败 | `{url: "/api/v1/drug/search", method: "GET", status: 0, elapsed_ms: 5000, error: "network_error"}` |

> **服务端已自动埋点，前端勿重复拦截**：本后端对所有 `/api/**` 入站请求（`/api/v1/analytics/events` 自身除外）已由服务端 `ApiTrackingFilter` 自动记录一条 `api_call`（`system` 固定为 `AI药典`，`url` 为相对路径）。因此：
> - 调用**本后端 API** 的 `api_call` 已被自动覆盖，前端**不要再拦截 fetch 重复上报**，否则同一请求会产生两份记录（client 绝对 URL + server 相对路径，难以去重）。
> - 下文「自动拦截 fetch」示例**仅适用于**追踪「调用接入方自家后端 / 外部资源」的请求；对本后端 API 不要启用。

---

## 三、来源系统（system）

埋点按 `system` 字段区分来源系统，便于多业务系统共用一套埋点时分别统计。

### 取值约定

| 传入值 | 含义 |
|--------|------|
| 不传 / 空 | 默认归入 `AI药典`（当前业务系统） |
| `AI药典` | 本系统 |
| `中医` | 中医系统（示例，按接入方实际系统名传值） |
| 其它字符串 | 接入方自定义系统名，后端原样存储，自动截断至 64 字符 |

> 服务端自动埋点（`api_call`）统一记为 `AI药典`，无需接入方处理。

---

## 四、前端接入示例

### 最小接入（推荐封装 track 函数）

```javascript
// SYSTEM 为当前接入方系统名，不传后端默认记为 AI药典
var SYSTEM = '中医';

function track(eventType, eventData) {
  var payload = {
    event_type: eventType,
    system: SYSTEM,
    event_data: eventData || {},
    page_url: window.location.href,
    referrer: document.referrer || ''
  };
  try {
    fetch('https://pharmacopoeia.kailin.com.cn/api/v1/analytics/events', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
      keepalive: true   // 页面关闭时也能发出
    }).catch(function(){});
  } catch(e) {}
}
```

### 使用示例

```javascript
// 页面浏览
track('page_view');

// 搜索
track('search', { keyword: '阿莫西林' });

// 点击
track('click', { label: '感冒药推荐', position: 1 });

// 提问
track('chat', { query: '布洛芬怎么吃', has_media: false });

// 自定义事件
track('custom_event', { key1: 'value1', key2: 'value2' });
```

### 自动拦截 fetch（可选，自动记录所有 API 调用）

```javascript
(function(){
  var _fetch = window.fetch;
  window.fetch = function(url, opts){
    var t0 = Date.now();
    var method = (opts && opts.method) || 'GET';
    return _fetch.apply(this, arguments).then(function(r){
      track('api_call', {
        url: typeof url === 'string' ? url : '',
        method: method,
        status: r.status,
        elapsed_ms: Date.now() - t0
      });
      return r;
    }).catch(function(e){
      track('api_call', {
        url: typeof url === 'string' ? url : '',
        method: method,
        status: 0,
        elapsed_ms: Date.now() - t0,
        error: e.message || 'network_error'
      });
      throw e;
    });
  };
})();
```

---

## 五、注意事项

1. **fire-and-forget** — 埋点接口返回 202，异步写入，前端不要 await，不要阻塞主流程
2. **keepalive** — fetch 带上 `keepalive: true`，确保页面关闭/跳转时也能发出
3. **容错** — `track()` 必须放在 try/catch 里，埋点失败不能影响正常功能
4. **user_key** — 建议前端传 JWT Token（`Authorization: Bearer xxx`），后端自动提取用户标识；不传则记为 `anonymous`
5. **event_data** — 建议始终传 `{}` 而非 `null`，方便后端统一处理
6. **page_url** — 建议传完整 URL，方便分析不同页面的行为分布

---

## 六、限流与错误响应

埋点写入接口按来源 IP 限流，默认 **600 次/分钟**（后端配置项 `rate-limit.analytics-per-minute`，可调整）。超出阈值返回 429，响应体**不是** `{code, message, data}` 形状：

```json
{
  "error": "请求过于频繁，请稍后再试",
  "detail": "请求过于频繁，请稍后再试",
  "code": 429
}
```

> 接入方需单独处理 429：埋点丢失不应影响业务，建议失败指数退避重试，并控制上报频率在阈值内；如需集中回补大量历史事件，请联系后端临时调参。

---

## 七、第三方接入须知

本接口定位为「多业务系统共用的行为埋点 sink」。第三方接入前请知悉以下限制，部分为**当前未实现、属待办**（见末尾清单）：

### 1. 用户标识（user_key）
`user_key` 由后端从**本系统 JWT** 提取；埋点接口走 `permitAll`，**不经过鉴权**，因此第三方传入的 Token 不会被校验或解析，第三方事件会记为 `anonymous`。如需按用户分群，请把用户标识放进 `event_data`（如 `{user_id: "xxx"}`）——但该标识**不会进入 `dimension=user` 维度统计**（该维度基于服务端提取的 `user_key`）。

### 2. 来源系统（system）无白名单
`system` 为自由文本，仅截断至 64 字符，**后端不做取值校验**。任意调用方都能以 `system:"中医"` 等名义写入事件，存在被冒名灌脏数据的风险。建议接入方约定稳定系统名并知悉该风险。

### 3. 接口无鉴权（安全风险）
埋点接口 `permitAll` + CORS 回射任意 Origin，**无 API Key / 共享密钥**，任何网络方都能写入任意事件。**请勿在 `event_data` 中放置敏感个人信息（PII）、密钥、token 等。**

### 4. event_data 体积
`event_data` 整块 JSON 原样存储（JSONB），**后端不做体积截断**（仅 `page_url / referrer / user_agent / system / event_type / user_key / ip` 等标量字段截断）。请接入方自行控制单条 `event_data` 大小，避免超大 payload。

### 待办（需后端改造，未含在本文档承诺内）
- 接入方密钥 / 签名鉴权
- `system` 取值白名单
- `event_data` 体积上限校验
- 第三方 `external_user_id` 字段（接入方自填，进入 `user` 维度统计）
