Quellcode durchsuchen

埋点系统的信息更新

liuchengsen vor 4 Wochen
Ursprung
Commit
884a49c9fc
2 geänderte Dateien mit 232 neuen und 1 gelöschten Zeilen
  1. 195 0
      docs/ANALYTICS_API.md
  2. 37 1
      static/index.html

+ 195 - 0
docs/ANALYTICS_API.md

@@ -0,0 +1,195 @@
+# 用户行为埋点 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` |
+| event_data | object | 否 | 事件附加数据,JSON 对象 |
+| page_url | string | 否 | 触发事件的页面 URL(建议传完整地址) |
+| referrer | string | 否 | 来源页面 URL |
+
+### 服务端自动记录
+
+以下字段由服务端从请求中自动提取,**无需前端传入**:
+
+| 字段 | 来源 |
+|------|------|
+| user_key | JWT Token 中的用户标识,未登录为 `anonymous` |
+| ip | 请求来源 IP |
+| user_agent | `User-Agent` 请求头 |
+
+### 响应
+
+```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",
+    "event_data": {},
+    "page_url": "https://pharmacopoeia.kailin.com.cn/?token=xxx"
+  }'
+```
+
+---
+
+## 二、标准事件类型定义
+
+### 页面行为
+
+| 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 |
+|-----------|---------|------------|
+| `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"}` |
+
+---
+
+## 三、前端接入示例
+
+### 最小接入(推荐封装 track 函数)
+
+```javascript
+function track(eventType, eventData) {
+  var payload = {
+    event_type: eventType,
+    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,方便分析不同页面的行为分布

+ 37 - 1
static/index.html

@@ -200,6 +200,40 @@
     } catch(e) {}
   }
 
+  // 自动埋点所有 fetch 调用
+  (function(){
+    var _fetch = window.fetch;
+    window.fetch = function(url, opts){
+      var t0 = Date.now();
+      var isTracked = (typeof url === 'string') && url.indexOf(API_BASE + '/api/v1/') === 0
+                   && url.indexOf('/analytics/events') === -1;
+      var trackUrl = typeof url === 'string' ? url.replace(API_BASE, '') : '';
+      var method = (opts && opts.method) || 'GET';
+      return _fetch.apply(this, arguments).then(function(r){
+        if (isTracked) {
+          track('api_call', {
+            url: trackUrl,
+            method: method,
+            status: r.status,
+            elapsed_ms: Date.now() - t0
+          });
+        }
+        return r;
+      }).catch(function(e){
+        if (isTracked) {
+          track('api_call', {
+            url: trackUrl,
+            method: method,
+            status: 0,
+            elapsed_ms: Date.now() - t0,
+            error: e.message || 'network_error'
+          });
+        }
+        throw e;
+      });
+    };
+  })();
+
   function authHeaders(){
     var h = {'Content-Type':'application/json'};
     if (TOKEN) h['Authorization'] = 'Bearer ' + TOKEN;
@@ -516,7 +550,7 @@
     var input=document.getElementById('userInput'),text=input.value.trim();
     var hasMedia=!!pendingMedia;
     if(!text&&!hasMedia)return;input.value='';if(STREAMING)return;
-    track('chat', {query: text});
+    track('chat', {query: text, has_media: hasMedia});
     doSend(text,hasMedia?pendingMedia:null);
   };
   function doSend(text,media){
@@ -604,6 +638,7 @@
   }
   window.searchDrugs=function(){
     drugKeyword=(document.getElementById('drugSearchInput').value||'').trim();
+    if (drugKeyword) track('search', {keyword: drugKeyword});
     loadDrugList(1);
     return false;
   };
@@ -639,6 +674,7 @@
       var r=await fetch(API_BASE+'/api/v1/drug/'+encodeURIComponent(drugId),{headers:authHeaders()});
       if(r.status===401){console.warn('需要登录');return}
       var d=await r.json();var drug=d.data||d;
+      track('drug_detail', {drug_id: drugId, drug_name: drug.name || ''});
       var dd=document.getElementById('tabDrugs');
       var topBar='<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #e0e0e0">';
       topBar+='<button onclick="showDrugList()" class="btn btn-secondary" style="padding:4px 12px;min-height:auto;font-size:12px">← 返回列表</button>';