# 用户行为埋点 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 (可选) ``` ### 请求体 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | 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` 维度统计)