生产级前端埋点方案:技术选型与完整实现
结合 Vue + Spring Boot 技术栈,以及”后端自动从 Cookie 取当前登录用户”的核心诉求,本文档提供生产级的技术选型对比方案和完整实现样例。
核心区别一句话
- GIF 埋点:靠浏览器加载图片资源时自动带 Cookie、页面卸载也能发、天生不跨域,是前端监控的”隐形守护者”。
- RESTful 异步接口(fetch/axios):标准的 AJAX 请求,业务可控性强,但页面关闭时极易丢失数据,且跨域和拦截配置相对繁琐。
两种方案的优缺点对比
为了让你更直观地评估,以下是针对核心维度的深度对比:
| 对比维度 | GIF 1x1 图片埋点 (GET) | RESTful 异步接口 (POST/Fetch) |
|---|---|---|
| 用户身份获取 | 极优。同源下浏览器自动带上 Cookie,Spring Boot 后端直接解析 Session/Cookie 拿用户,前端无需传参,最安全省心。 | 中等。同源可带 Cookie;若跨域或第三方站点,需严格配置 withCredentials + CORS,否则后端取不到当前用户。 |
| 页面关闭/跳转上报 | 极稳。在 beforeunload / unload 阶段触发,浏览器会优先完成资源请求,数据几乎不丢。 | 易丢。页面一关,异步请求会被浏览器强制中断。即使加 keepalive,兼容性和稳定性也不如图片请求。 |
| 性能与阻塞 | 无感。图片加载属于浏览器底层异步行为,完全不阻塞主线程和页面渲染。 | 轻微。JS 异步执行虽不阻塞,但相比浏览器底层的图片加载,代码逻辑和资源开销略重。 |
| 数据安全与防刷 | 较弱。GET 请求参数暴露在 URL 中,容易被刷。需在 URL 中拼接签名和时间戳来防御。 | 较强。支持 POST,参数放 Body 不暴露;可灵活添加自定义请求头、Token 和复杂签名。 |
| 后端处理模式 | 一致。收到请求后立刻丢入 MQ/队列,并返回 1x1 像素的透明 GIF 响应。 | 一致。收到请求后立刻丢入 MQ/队列,并返回标准 HTTP 200 状态码。 |
使用场景与产品形态决策
在理解了两种方案的优缺点后,接下来需要根据你的使用场景和产品形态来做技术选型决策。
一、使用场景分类
1. 前端监控类(高频、轻量、实时性要求高)
- 性能监控:页面加载时间、首屏渲染、资源加载耗时、FCP/LCP/FID 等 Web Vitals 指标
- 错误监控:JS 错误、Promise 异常、资源加载失败、接口报错
- 用户行为:PV(页面浏览)、UV(独立访客)、页面停留时长、页面跳转路径
- 页面生命周期:页面进入、页面离开、页面可见性变化
特点:
- 数据量大、频率高(每个用户每次访问都会产生)
- 对实时性要求高(需要快速发现线上问题)
- 数据相对简单(主要是时间戳、URL、错误信息等)
- 页面关闭时也必须上报(如页面停留时长、离开事件)
2. 业务埋点类(中频、结构化、业务关联强)
- 转化漏斗:注册流程、购买流程、表单提交各步骤的转化率
- 功能使用:按钮点击、功能模块使用频率、特定功能的使用路径
- 用户画像:用户偏好、行为标签、兴趣分类
- A/B 测试:不同版本的功能效果对比、灰度发布数据收集
特点:
- 数据量中等(只在特定操作时触发)
- 数据结构复杂(包含业务上下文、用户状态、操作详情)
- 需要关联用户身份(用于用户画像和个性化推荐)
- 对安全性要求较高(可能包含敏感业务数据)
3. 敏感数据上报类(低频、高安全、强防刷)
- 支付行为:支付金额、支付方式、订单信息
- 账户操作:登录、注册、密码修改、实名认证
- 资金操作:充值、提现、转账
- 权限操作:角色变更、权限授予、敏感数据访问
特点:
- 数据量小但价值高(每条数据都很重要)
- 必须关联用户身份(用于审计和风控)
- 安全性要求极高(不能暴露在 URL 中)
- 需要强防刷机制(防止恶意刷量)
4. 数据分析类(大数据量、离线处理)
- 热力图:用户点击位置、滚动深度、鼠标轨迹
- 会话录制:用户操作回放、DOM 变化记录
- 完整表单数据:用户填写的完整表单内容(用于分析流失原因)
- 批量日志:前端日志批量上报、离线数据同步
特点:
- 单次数据量大(可能包含大量 DOM 信息或用户操作序列)
- 对实时性要求低(可以批量上报、离线处理)
- 需要压缩和优化(避免影响用户体验)
- 可能需要采样(不是所有用户都需要录制)
二、产品形态分类
1. Web 浏览器应用
- PC 网站:传统桌面浏览器访问的网站(Chrome、Firefox、Safari、Edge)
- H5 移动站:移动浏览器访问的响应式网站或移动版网站
- 单页应用(SPA):Vue/React/Angular 构建的前后端分离应用
- 多页应用(MPA):传统的服务端渲染应用(SSR)
技术特点:
- ✅ 完整的浏览器 Cookie 机制
- ✅ 支持
<img>标签自动携带同源 Cookie - ✅ 支持
beforeunload事件(页面关闭前上报) - ✅ 同源策略下无需额外配置
2. 原生 APP
- iOS APP:Swift/Objective-C 开发的原生应用
- Android APP:Kotlin/Java 开发的原生应用
技术特点:
- ❌ 无浏览器 Cookie 机制
- ❌ 使用 Token(存储在 Keychain/SharedPreferences)
- ❌ 图片加载无法自定义请求头
- ✅ 网络请求完全可控(可自定义所有请求头)
3. 混合 APP
- React Native:使用 JS 开发,部分原生能力
- Flutter:使用 Dart 开发,自绘 UI
- Cordova/Ionic:WebView + 原生插件
- uni-app:跨平台开发框架
技术特点:
- ⚠️ WebView 中有 Cookie,但与原生层隔离
- ⚠️ 需要 JSBridge 同步用户信息
- ⚠️ 跨域和 Cookie 策略更严格
- ✅ 可以通过原生能力发送网络请求
4. 小程序
- 微信小程序:微信生态内的轻应用
- 支付宝小程序:支付宝生态内的轻应用
- 抖音小程序:抖音生态内的轻应用
- 百度小程序:百度 APP 内的轻应用
技术特点:
- ❌ 无浏览器 Cookie 机制
- ❌ 使用独立的
storage存储 - ❌
<image>组件无法自定义请求头 - ✅
wx.request可以自定义请求头
5. 桌面应用
- Electron:基于 Chromium 的桌面应用(如 VS Code、Slack)
- Tauri:基于 WebView 的轻量级桌面应用
技术特点:
- ✅ 完整的浏览器环境(Electron)
- ✅ 支持 Cookie 和本地存储
- ✅ 可以使用 Node.js 能力发送请求
6. 后台管理系统
- 内部管理后台:企业内部使用的管理系统
- SaaS 平台:多租户的云端管理平台
技术特点:
- ✅ 通常是 Web 浏览器环境
- ✅ 用户身份明确(登录后才能访问)
- ⚠️ 可能需要更严格的权限控制和审计日志
三、技术决策矩阵
根据产品形态 × 使用场景,以下是推荐的技术方案:
| 产品形态 ↓ / 使用场景 → | 前端监控类 | 业务埋点类 | 敏感数据上报类 | 数据分析类 |
|---|---|---|---|---|
| Web 浏览器(PC/H5) | ✅ GIF 埋点 自动带 Cookie 页面关闭不丢数据 | ⚠️ GIF 或 POST 简单业务用 GIF 复杂业务用 POST | ✅ POST 接口 参数放 Body 支持复杂签名 | ✅ POST 接口 支持大数据量 可批量压缩 |
| 原生 APP(iOS/Android) | ✅ POST 接口 请求头传 Token 统一架构 | ✅ POST 接口 请求头传 Token 数据结构化 | ✅ POST 接口 请求头传 Token 高安全性 | ✅ POST 接口 支持大数据量 可离线缓存 |
| 混合 APP(RN/Flutter) | ✅ POST 接口 避免 Cookie 同步 统一架构 | ✅ POST 接口 请求头传 Token 跨平台一致 | ✅ POST 接口 请求头传 Token 高安全性 | ✅ POST 接口 支持大数据量 可离线缓存 |
| 小程序(微信/支付宝) | ✅ POST 接口 wx.request 传 Token 无 Cookie 机制 | ✅ POST 接口 请求头传 Token 数据结构化 | ✅ POST 接口 请求头传 Token 高安全性 | ✅ POST 接口 支持大数据量 可批量上报 |
| Electron 桌面应用 | ✅ GIF 埋点 完整浏览器环境 也可用 Node.js | ⚠️ GIF 或 POST 简单业务用 GIF 复杂业务用 POST | ✅ POST 接口 可用 Node.js 高安全性 | ✅ POST 接口 可用 Node.js 支持大数据量 |
| 后台管理系统 | ✅ GIF 埋点 自动带 Cookie 实现简单 | ✅ POST 接口 需要审计日志 权限控制 | ✅ POST 接口 必须审计 高安全性 | ✅ POST 接口 操作日志 可追溯 |
图例说明:
- ✅ 推荐方案:最适合该场景的技术选型
- ⚠️ 可选方案:根据具体需求灵活选择
四、典型场景推荐
场景 1:纯 Web 电商网站(PC + H5)
需求:监控页面性能、用户行为、商品点击、加购、下单流程
推荐方案:
- 前端监控:GIF 埋点(PV、UV、页面停留、错误日志)
- 商品曝光/点击:GIF 埋点(高频、轻量)
- 加购/下单:POST 接口(业务数据复杂、需要防刷)
理由:充分利用浏览器 Cookie 机制,高频监控用 GIF,关键业务用 POST。
场景 2:跨平台电商 APP(iOS + Android + 小程序)
需求:统一的埋点体系,支持用户行为分析、转化漏斗、A/B 测试
推荐方案:
- 所有埋点统一使用 POST 接口
- 请求头传递 JWT Token:
Authorization: Bearer <token> - 后端统一处理:一套接口服务所有平台
理由:APP 和小程序无 Cookie 机制,统一架构降低维护成本。
场景 3:SaaS 后台管理系统(纯 Web)
需求:用户操作审计、权限变更日志、敏感数据访问记录
推荐方案:
- 页面访问监控:GIF 埋点(自动带 Cookie)
- 操作审计日志:POST 接口(需要完整的操作上下文)
- 敏感操作:POST 接口 + 数字签名(防篡改)
理由:监控用 GIF 简化实现,审计日志必须用 POST 确保数据完整性。
场景 4:混合 APP(WebView + 原生)
需求:WebView 页面和原生页面的埋点统一管理
推荐方案:
- 统一使用 POST 接口
- WebView 通过 JSBridge 获取原生 Token
- 原生页面直接使用本地 Token
理由:避免 WebView Cookie 与原生 Token 的同步问题,统一架构。
场景 5:Electron 桌面应用
需求:用户行为分析、崩溃日志、性能监控
推荐方案:
- 渲染进程(Web):GIF 埋点(利用浏览器环境)
- 主进程(Node.js):直接使用 HTTP 库发送 POST 请求
- 崩溃日志:主进程捕获后通过 POST 上报
理由:充分利用 Electron 的双进程架构,渲染进程用 GIF,主进程用 Node.js。
五、快速决策流程图
开始
↓
是否为 Web 浏览器环境?
├─ 是 → 是否为高频监控类埋点?
│ ├─ 是 → 使用 GIF 埋点 ✅
│ └─ 否 → 是否包含敏感数据?
│ ├─ 是 → 使用 POST 接口 ✅
│ └─ 否 → 数据量是否很大?
│ ├─ 是 → 使用 POST 接口 ✅
│ └─ 否 → 使用 GIF 埋点 ✅
│
└─ 否 → 是否为 APP/小程序?
├─ 是 → 统一使用 POST 接口 + Authorization 头 ✅
└─ 否 → 是否为 Electron?
├─ 是 → 渲染进程用 GIF,主进程用 POST ✅
└─ 否 → 根据具体环境评估
⚠️ GIF 方案在 APP/小程序场景的重要局限
核心问题:无法自动获取用户身份
GIF 埋点方案的最大优势是"浏览器自动携带 Cookie",但这个优势仅限于 Web 浏览器环境。在以下场景中,GIF 方案会失效:
1. 原生 APP(iOS/Android)
问题:
- 原生 APP 中的 HTTP 请求(包括图片加载)不会自动携带浏览器 Cookie
- APP 通常使用 Token(存储在本地 SharedPreferences/Keychain)进行身份认证
<img>标签或原生 ImageView 加载图片时,无法自定义请求头(无法传递 Authorization Token)
结果:
// 后端代码
Long userId = (Long) session.getAttribute("userId"); // ❌ 永远为 null
String token = request.getHeader("Authorization"); // ❌ 拿不到 Token
2. 混合 APP(WebView)
问题:
- WebView 中的 Cookie 与原生 APP 的 Token 体系是隔离的
- 即使 WebView 设置了 Cookie,也需要手动同步原生层的用户信息
- 跨域场景下,WebView 的 Cookie 策略更加严格
结果:
- 如果用户在原生页面登录,WebView 中的 GIF 请求拿不到用户信息
- 需要额外的 JSBridge 通信来同步用户标识
3. 微信小程序 / 支付宝小程序
问题:
- 小程序没有浏览器 Cookie 机制
- 小程序使用
wx.request或my.request发起网络请求,使用独立的storage存储数据 <image>组件加载图片时,无法携带自定义请求头
结果:
// 小程序代码
<image src="https://api.example.com/track?event=page_view" />
// ❌ 后端无法获取用户信息,因为没有 Cookie,也无法传 Token
技术原因分析
| 环境 | Cookie 支持 | 自定义请求头 | 用户身份获取方式 |
|---|---|---|---|
| Web 浏览器 | ✅ 自动携带同源 Cookie | ❌ <img> 标签不支持 | 后端从 Cookie/Session 获取 |
| 原生 APP | ❌ 无 Cookie 机制 | ❌ ImageView 不支持 | 需在 URL 参数或请求体传递 Token |
| WebView | ⚠️ 有 Cookie 但与原生隔离 | ❌ <img> 标签不支持 | 需 JSBridge 同步或 URL 传参 |
| 小程序 | ❌ 无 Cookie 机制 | ❌ <image> 组件不支持 | 需在 URL 参数传递 Token |
关键限制:
<img>标签、原生 ImageView、小程序<image>组件在加载图片时,都无法自定义 HTTP 请求头- 这意味着无法通过
Authorization: Bearer <token>的标准方式传递用户身份
针对 APP/小程序的解决方案
方案 A:URL 参数传递用户标识(不推荐)
实现方式:
// 小程序示例
const userId = wx.getStorageSync('userId');
const token = wx.getStorageSync('token');
// 在 URL 中拼接用户信息
const trackUrl = `https://api.example.com/track?event=page_view&userId=${userId}&token=${token}`;
// 使用 image 组件加载
<image src="{{trackUrl}}" style="width:1px;height:1px;" />
后端处理:
@GetMapping("/track")
public ResponseEntity<byte[]> track(
@RequestParam String userId,
@RequestParam String token,
@RequestParam Map<String, String> params
) {
// 验证 Token 有效性
if (!tokenService.validate(userId, token)) {
return ResponseEntity.status(403).body(TRANSPARENT_GIF);
}
// 处理埋点...
}
缺点:
- ❌ 安全性极差:Token 暴露在 URL 中,会被记录到服务器日志、代理日志、浏览器历史
- ❌ 容易被劫持:URL 参数容易被中间人攻击截获
- ❌ 不符合安全规范:违反 OAuth 2.0 和 JWT 的最佳实践
方案 B:使用 POST 接口 + 自定义请求头(强烈推荐)
实现方式:
小程序示例:
// utils/tracker.js
class MiniProgramTracker {
track(event, data = {}) {
const token = wx.getStorageSync('token');
wx.request({
url: 'https://api.example.com/api/track/post',
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // ✅ Token 放在请求头
},
data: {
event,
timestamp: Date.now(),
...data
},
success: () => console.log('埋点成功'),
fail: (err) => console.error('埋点失败', err)
});
}
}
export default new MiniProgramTracker();
原生 APP 示例(Android):
// TrackUtil.java
public class TrackUtil {
public static void track(String event, JSONObject data) {
String token = SharedPreferencesUtil.getToken();
OkHttpClient client = new OkHttpClient();
JSONObject payload = new JSONObject();
payload.put("event", event);
payload.put("timestamp", System.currentTimeMillis());
payload.put("data", data);
RequestBody body = RequestBody.create(
payload.toString(),
MediaType.parse("application/json")
);
Request request = new Request.Builder()
.url("https://api.example.com/api/track/post")
.addHeader("Authorization", "Bearer " + token) // ✅ Token 放在请求头
.post(body)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
Log.d("Track", "埋点成功");
}
@Override
public void onFailure(Call call, IOException e) {
Log.e("Track", "埋点失败", e);
}
});
}
}
后端处理(Spring Boot):
@PostMapping("/api/track/post")
public Map<String, Object> trackPost(
@RequestBody Map<String, Object> payload,
@RequestHeader("Authorization") String authHeader
) {
try {
// 从 Authorization 头提取 Token
String token = authHeader.replace("Bearer ", "");
// 验证 Token 并获取用户信息
Long userId = jwtService.getUserIdFromToken(token);
String username = jwtService.getUsernameFromToken(token);
// 构建埋点数据
payload.put("userId", userId);
payload.put("username", username);
// 异步处理
trackService.asyncTrack(payload);
return Map.of("code", 200, "message", "success");
} catch (Exception e) {
log.error("埋点处理失败", e);
return Map.of("code", 500, "message", "error");
}
}
优势:
- ✅ 安全性高:Token 在请求头中传输,不会被记录到 URL 日志
- ✅ 符合标准:遵循 RESTful API 和 OAuth 2.0 最佳实践
- ✅ 灵活性强:可以传递复杂的 JSON 数据结构
- ✅ 统一架构:Web、APP、小程序使用同一套后端接口
最终建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| Web 浏览器(PC/H5) | GIF 埋点 | 自动携带 Cookie,实现简单,性能最优 |
| 原生 APP | POST 接口 + Authorization 头 | 无 Cookie 机制,必须通过请求头传递 Token |
| 混合 APP(WebView) | POST 接口 + Authorization 头 | 避免 Cookie 同步问题,统一使用 Token |
| 微信/支付宝小程序 | POST 接口 + Authorization 头 | 无 Cookie 机制,必须通过请求头传递 Token |
核心原则:
- Web 浏览器:优先使用 GIF 埋点,利用浏览器的 Cookie 机制
- 非浏览器环境:统一使用 POST 接口 + JWT Token,确保安全性和一致性
最终选型结论
1. Web 浏览器环境(PC/H5)
做前端监控、PV、UV、错误日志、页面离开事件
- ✅ 优先用 GIF 埋点
- 理由:同源自动带 Cookie,后端能无缝拿到当前登录用户;页面关闭也能保证上报成功;实现极简,兼容性无敌(从古董 IE 到现代浏览器通吃)。
做重要业务埋点、敏感数据上报、需要强防刷、大数据量传输
- ✅ 用 RESTful POST + fetch(keepalive)
- 理由:支持 POST,参数放在请求体(Body)中,不会暴露在 URL 里;可以加复杂的 Token 和签名机制,防刷能力更强;且没有 URL 长度限制,适合传输大量结构化数据。
2. APP/小程序环境
所有埋点场景(包括监控、业务埋点)
- ✅ 统一使用 POST 接口 + Authorization 请求头
- 理由:
- APP 和小程序没有 Cookie 机制,GIF 方案无法自动获取用户身份
<img>标签和原生图片组件无法自定义请求头,无法传递 Token- POST 接口可以在请求头中安全传递 JWT Token,符合安全规范
- 统一的接口设计,便于跨平台维护
针对不同场景的最佳组合(生产标准)
Web 浏览器项目(Vue + Spring Boot)
推荐采用 ”GIF 为主,Fetch 兜底” 的组合拳:
- 常规监控与身份关联:前端统一使用 GIF 1x1 埋点。利用浏览器特性自动携带 Cookie,让你的 Spring Boot 后端能够零成本、高安全地自动提取当前登录用户信息。
- 后端架构:无论前端发来的是图片请求还是普通接口,后端统一做成异步非阻塞。接收到请求后,迅速将数据抛入消息队列(MQ)进行削峰填谷,然后立刻返回(图片或 200 状态码),确保不影响主业务接口的性能。
跨平台项目(Web + APP + 小程序)
推荐采用 ”统一 POST 接口” 方案:
- 所有平台统一使用 POST 接口:Web 端虽然可以用 GIF,但为了保持架构一致性,建议也使用 POST 接口
- 统一的身份认证:所有平台都通过
Authorization: Bearer <token>请求头传递 JWT Token - 后端统一处理:一套接口服务所有平台,降低维护成本
这也是目前绝大多数大厂在做前端监控系统时的通用标准方案。
完整实现样例
方案一:GIF 1x1 图片埋点(推荐用于常规监控)
前端实现(Vue 3)
1. 创建埋点工具类 tracker.js
// src/utils/tracker.js
class GifTracker {
constructor(baseUrl = '/api/track') {
this.baseUrl = baseUrl;
this.queue = [];
this.isProcessing = false;
}
/**
* 发送埋点数据
* @param {string} eventType - 事件类型(如 'page_view', 'click', 'error')
* @param {object} data - 埋点数据
*/
track(eventType, data = {}) {
const params = {
event: eventType,
timestamp: Date.now(),
url: window.location.href,
referrer: document.referrer,
...data
};
this._sendGif(params);
}
/**
* 通过 GIF 图片发送数据
*/
_sendGif(params) {
const queryString = this._buildQueryString(params);
const img = new Image(1, 1);
// 图片加载成功或失败都不影响业务
img.onload = img.onerror = () => {
img = null;
};
// 发送请求(浏览器会自动带上 Cookie)
img.src = `${this.baseUrl}?${queryString}`;
}
/**
* 构建查询字符串
*/
_buildQueryString(params) {
return Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(params[key]))}`)
.join('&');
}
/**
* 页面浏览埋点
*/
trackPageView(pageName) {
this.track('page_view', {
page: pageName,
title: document.title
});
}
/**
* 点击事件埋点
*/
trackClick(elementId, elementName, extraData = {}) {
this.track('click', {
element_id: elementId,
element_name: elementName,
...extraData
});
}
/**
* 错误埋点
*/
trackError(error, errorInfo = {}) {
this.track('error', {
message: error.message,
stack: error.stack,
...errorInfo
});
}
/**
* 页面离开埋点(在 beforeunload 时调用)
*/
trackPageLeave(duration) {
this.track('page_leave', {
duration,
page: window.location.pathname
});
}
}
// 导出单例
export default new GifTracker();
2. 在 Vue 应用中集成
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import tracker from './utils/tracker';
const app = createApp(App);
// 全局注册埋点工具
app.config.globalProperties.$tracker = tracker;
// 路由切换时自动上报 PV
router.afterEach((to) => {
tracker.trackPageView(to.path);
});
// 页面加载时记录进入时间
let pageEnterTime = Date.now();
// 页面离开时上报停留时长
window.addEventListener('beforeunload', () => {
const duration = Date.now() - pageEnterTime;
tracker.trackPageLeave(duration);
});
// 全局错误捕获
app.config.errorHandler = (err, instance, info) => {
tracker.trackError(err, { info });
};
app.use(router).mount('#app');
3. 在组件中使用
<!-- src/views/ProductDetail.vue -->
<template>
<div class="product-detail">
<h1>{{ product.name }}</h1>
<button @click="handleAddToCart">加入购物车</button>
<button @click="handleBuyNow">立即购买</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const product = ref({ name: '商品A', id: 123 });
// 页面加载时上报商品曝光
onMounted(() => {
window.$tracker.track('product_view', {
product_id: product.value.id,
product_name: product.value.name,
source: route.query.from || 'direct'
});
});
// 加入购物车埋点
const handleAddToCart = () => {
window.$tracker.trackClick('btn-add-cart', '加入购物车', {
product_id: product.value.id,
action: 'add_to_cart'
});
// 业务逻辑...
};
// 立即购买埋点
const handleBuyNow = () => {
window.$tracker.trackClick('btn-buy-now', '立即购买', {
product_id: product.value.id,
action: 'buy_now'
});
// 业务逻辑...
};
</script>
后端实现(Spring Boot)
1. 创建埋点控制器
// TrackController.java
package com.example.tracker.controller;
import com.example.tracker.service.TrackService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/track")
public class TrackController {
@Autowired
private TrackService trackService;
/**
* GIF 埋点接口
* 浏览器会自动带上 Cookie,后端可直接从 Session 获取用户信息
*/
@GetMapping
public ResponseEntity<byte[]> track(
@RequestParam Map<String, String> params,
HttpServletRequest request,
HttpSession session
) {
try {
// 从 Session 中获取当前登录用户(Spring Security 或自定义登录都可以)
Long userId = (Long) session.getAttribute("userId");
String username = (String) session.getAttribute("username");
// 获取客户端信息
String ip = getClientIp(request);
String userAgent = request.getHeader("User-Agent");
// 构建埋点数据
Map<String, Object> trackData = new HashMap<>();
trackData.put("userId", userId);
trackData.put("username", username);
trackData.put("ip", ip);
trackData.put("userAgent", userAgent);
trackData.putAll(params);
// 异步处理埋点数据(投递到 MQ)
trackService.asyncTrack(trackData);
// 立即返回 1x1 透明 GIF
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_GIF_VALUE)
.header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate")
.header(HttpHeaders.PRAGMA, "no-cache")
.header(HttpHeaders.EXPIRES, "0")
.body(TRANSPARENT_GIF);
} catch (Exception e) {
log.error("埋点处理失败", e);
// 即使失败也返回 GIF,不影响前端
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_GIF_VALUE)
.body(TRANSPARENT_GIF);
}
}
/**
* 1x1 透明 GIF 的字节数组
*/
private static final byte[] TRANSPARENT_GIF = new byte[]{
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00,
0x01, 0x00, (byte) 0x80, 0x00, 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
0x00, 0x00, 0x00, 0x21, (byte) 0xF9, 0x04, 0x01, 0x00,
0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44,
0x01, 0x00, 0x3B
};
/**
* 获取客户端真实 IP
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
2. 创建异步处理服务
// TrackService.java
package com.example.tracker.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Map;
@Slf4j
@Service
public class TrackService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private ObjectMapper objectMapper;
private static final String TRACK_TOPIC = "user-track-events";
/**
* 异步处理埋点数据
* 使用 @Async 注解确保不阻塞主线程
*/
@Async("trackExecutor")
public void asyncTrack(Map<String, Object> trackData) {
try {
String jsonData = objectMapper.writeValueAsString(trackData);
// 发送到 RocketMQ
rocketMQTemplate.convertAndSend(TRACK_TOPIC, jsonData);
log.debug("埋点数据已投递到 RocketMQ: {}", jsonData);
} catch (Exception e) {
log.error("埋点数据投递失败", e);
// 可以考虑写入本地日志文件作为兜底
}
}
}
3. 配置异步线程池
// AsyncConfig.java
package com.example.tracker.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("trackExecutor")
public Executor trackExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("track-");
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
方案二:RESTful POST 接口(用于敏感数据上报)
前端实现(Vue 3)
1. 创建 Fetch 埋点工具类
// src/utils/fetchTracker.js
class FetchTracker {
constructor(baseUrl = '/api/track/post') {
this.baseUrl = baseUrl;
}
/**
* 发送埋点数据(POST 方式)
* @param {string} eventType - 事件类型
* @param {object} data - 埋点数据
*/
async track(eventType, data = {}) {
const payload = {
event: eventType,
timestamp: Date.now(),
url: window.location.href,
referrer: document.referrer,
...data
};
try {
await fetch(this.baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
credentials: 'include', // 重要:携带 Cookie
keepalive: true // 页面关闭时也尽量发送
});
} catch (error) {
console.error('埋点发送失败:', error);
}
}
/**
* 敏感操作埋点(如支付、提现)
*/
trackSensitiveAction(action, amount, extraData = {}) {
this.track('sensitive_action', {
action,
amount,
...extraData
});
}
/**
* 大数据量埋点(如完整表单数据)
*/
trackFormSubmit(formName, formData) {
this.track('form_submit', {
form_name: formName,
form_data: formData
});
}
}
export default new FetchTracker();
2. 在组件中使用
<!-- src/views/PaymentPage.vue -->
<template>
<div class="payment-page">
<h1>支付页面</h1>
<button @click="handlePay">确认支付</button>
</div>
</template>
<script setup>
import fetchTracker from '@/utils/fetchTracker';
const handlePay = async () => {
// 支付前上报敏感操作
await fetchTracker.trackSensitiveAction('payment', 99.99, {
order_id: 'ORDER123456',
payment_method: 'alipay'
});
// 执行支付逻辑...
};
</script>
后端实现(Spring Boot)
// TrackPostController.java
package com.example.tracker.controller;
import com.example.tracker.service.TrackService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/track/post")
public class TrackPostController {
@Autowired
private TrackService trackService;
/**
* POST 埋点接口
* 用于敏感数据上报
*/
@PostMapping
public Map<String, Object> trackPost(
@RequestBody Map<String, Object> payload,
HttpServletRequest request,
HttpSession session
) {
try {
// 从 Session 获取用户信息
Long userId = (Long) session.getAttribute("userId");
String username = (String) session.getAttribute("username");
// 获取客户端信息
String ip = getClientIp(request);
String userAgent = request.getHeader("User-Agent");
// 构建埋点数据
payload.put("userId", userId);
payload.put("username", username);
payload.put("ip", ip);
payload.put("userAgent", userAgent);
// 异步处理
trackService.asyncTrack(payload);
return Map.of("code", 200, "message", "success");
} catch (Exception e) {
log.error("POST 埋点处理失败", e);
return Map.of("code", 500, "message", "error");
}
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
使用场景对照表
| 场景 | 推荐方案 | 示例代码 |
|---|---|---|
| 页面浏览(PV) | GIF 埋点 | tracker.trackPageView('/home') |
| 页面离开 | GIF 埋点 | tracker.trackPageLeave(duration) |
| 按钮点击 | GIF 埋点 | tracker.trackClick('btn-id', '按钮名') |
| 错误日志 | GIF 埋点 | tracker.trackError(error) |
| 支付操作 | POST 接口 | fetchTracker.trackSensitiveAction('payment', 99.99) |
| 表单提交 | POST 接口 | fetchTracker.trackFormSubmit('register', formData) |
| 大数据上报 | POST 接口 | fetchTracker.track('bulk_data', largeObject) |
生产环境注意事项
1. RocketMQ 依赖与配置
Maven 依赖:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
application.yml 配置:
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: track-producer-group
send-message-timeout: 3000
retry-times-when-send-failed: 2
2. Cookie 配置
确保后端 Session Cookie 配置正确:
# application.yml
server:
servlet:
session:
cookie:
http-only: true
secure: false # 生产环境改为 true(需要 HTTPS)
same-site: lax # 防止 CSRF 攻击
path: /
3. CORS 配置(如果前后端分离)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://your-frontend-domain.com")
.allowedMethods("GET", "POST")
.allowCredentials(true); // 重要:允许携带 Cookie
}
}
4. 防刷机制
// 在 TrackController 中添加防刷逻辑
@Autowired
private RedisTemplate<String, String> redisTemplate;
private boolean isRateLimited(String userId, String ip) {
String key = "track:limit:" + userId + ":" + ip;
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, 1, TimeUnit.MINUTES);
}
// 每分钟最多 100 次请求
return count > 100;
}
5. 数据消费端(RocketMQ Consumer 示例)
@Component
@Slf4j
@RocketMQMessageListener(
topic = "user-track-events",
consumerGroup = "track-consumer-group"
)
public class TrackConsumer implements RocketMQListener<String> {
@Autowired
private TrackRepository trackRepository;
@Override
public void onMessage(String message) {
try {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> trackData = mapper.readValue(message, Map.class);
TrackEvent event = new TrackEvent();
event.setUserId((Long) trackData.get("userId"));
event.setEvent((String) trackData.get("event"));
event.setTimestamp((Long) trackData.get("timestamp"));
trackRepository.save(event);
log.info("埋点数据已入库: {}", message);
} catch (Exception e) {
log.error("埋点数据消费失败", e);
}
}
}
总结
- GIF 埋点:适合高频、轻量、需要自动携带用户身份的场景,是前端监控的首选方案
- POST 接口:适合敏感数据、大数据量、需要强防刷的业务埋点场景
- 组合使用:在实际生产中,两种方案配合使用,既保证了监控的完整性,又满足了业务的安全性需求