rea 这名字乍一听像个随口拍出来的缩写,其实它全称是 React Event Analytics —— 一个我用业余时间写的轻量前端事件分析采集库。它的定位很朴素:把页面上的用户行为(点击、曝光、自定义事件)打包成结构化数据,安全送到自己的后端,再用 SQL 做统计。没有控制台、没有花哨仪表盘、也没有巨型 SDK,就专注解决一件事:让一个小团队也能拥有"知道用户做了什么"的能力。
这事的起因很现实。当时我们团队做一款移动端 H5 产品,产品经理三天两头来问:"首页这个按钮到底点了多少人?""从列表页到详情页的转化率是多少?"按理说这种问题应该交给数据分析平台回答,但公司预算买不起商业版,靠开源自建采集加数仓加报表又明显杀鸡用牛刀——我们连专职后端都只有一个人。于是我想,干脆自己封装一个采集工具,重点解决三个问题:埋点成本足够低、数据尽量不丢、后端查询足够简单。
如果你也面临类似的处境——团队小、需求急、不想被重型数据系统绑住手脚,那这篇文章应该对你有参考价值。我会把 rea 从设计到落地的完整过程拆开讲,包括事件模型怎么定、上报链路为什么选 Beacon、以及我在真实使用中反复踩过的几个坑。
1. 先想明白:小团队做数据上报为什么这么难
1.1 三座大山:埋点、可靠传输、查询门槛
第一座大山是埋点成本。传统做法是把采集代码写死在业务逻辑里,到处this.$track('click'),改一个按钮就得发一版。业务代码被埋点逻辑打乱,维护成本直线上升。时间一长,开发宁可新接一个需求也不愿意去动一段夹着十几个埋点调用的老代码。
第二座大山是数据可靠传输。用户网络不稳定,页面又随时可能被关闭,如果用普通XMLHttpRequest上报,页面一关请求就断了,事件静默丢失。这是很多自建上报系统最初没预料到的问题——等真上线了才发现,数据凭空少了百分之十几。
第三座大山是查询门槛。数据收回来了,但存成什么格式、怎么建表、要不要压缩,直接决定产品经理能不能自己查出结果。很多人做完采集才发现,数据在消息队列里转了一圈,最后还是得靠研发写脚本才能看到统计数字,那这套系统还不如不做。
1.2 为什么现成方案用起来别扭
当时市面上的开源埋点采集方案我其实试过几个,基本印象是:大而全的框架适合有专职数据工程师的团队,能扛得住它的复杂度和维护成本;小而美的方案又往往只解决传输层,业务集成还是要写大量胶水代码。还有个绕不开的现实问题——数据要经过第三方服务器,涉及用户账号、设备信息时,合规上很难交代清楚。
所以 rea 的定位从第一天起就很明确:不替你做数据分析,只负责把事件数据可靠、低成本地送到你自己控制的存储里。分析部分留给 SQL 和现成的开源可视化工具去解决。这个取舍让代码量控制得很小,对业务代码的侵入性也降到了最低。
2. 核心架构:事件模型、采集器与传输链路
2.1 事件模型:宁可多存字段,不要事后补数据
事件模型是上报系统的地基。我见过很多方案把事件定义成{eventId, timestamp, data}这种松散结构,随手塞 JSON,真到查询的时候就抓瞎了——JSON 字段没法直接建索引,想统计某个字段还得先写自定义函数,性能也扛不住。
rea 用的是扁平化统一事件模型,长这样:
{ "event": "element_click", "ts": 1647680000000, "page": "/home/index", "module": "home_banner", "target": "btn_enter_detail", "user_id": "u_123456", "session_id": "s_abc", "device": { "ua": "Mozilla/5.0 ...", "screen": "390x844" }, "extra": { "from": "share_link" } }固定字段全部抽到顶层,业务自定义字段统一塞进extra。好处是后端建表时只需要给固定字段建索引,extra整体存成 JSON 列,既保住核心查询速度,又保留业务扩展的灵活性。我的原则是顶层字段宁可多存,也不要漏存——数据一旦丢了,事后是补不回来的。
2.2 采集器设计:把埋点做成声明式配置
为了实现"写一行配置就能埋点",rea 的采集器分成两个部分:一个全局的 EventBus 负责接收和分发事件,一组声明式采集规则负责告诉 EventBus 该监听什么。
import { rea } from 'rea-tracker'; rea.init({ endpoint: '/api/event', appId: 'h5-main', autoTrack: ['click', 'pageview'], rules: [ { selector: '.banner-btn', event: 'banner_click', fields: ['position', 'campaign_id'] } ] });看到那个selector没有?这是整个设计里我最满意的一部分。它利用事件委托,在 document 上统一监听点击,再判断event.target是否匹配选择器。业务代码只需要给按钮加一个稳定的>function send(payload) { const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' }); if (navigator.sendBeacon && blob.size <= 64 * 1024) { navigator.sendBeacon(endpoint, blob); } else { fetch(endpoint, { method: 'POST', body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, keepalive: true }).catch(() => {}); } }
这套组合拳我在各种网络环境(Wifi、4G、弱网模拟)下反复实测过,数据丢失率从初版的百分之十几降到了千分之一以下。剩下那千分之一,大概率是系统层面的极端场景,我选择接受它。
3. 从零跑通 rea 最小可用系统
3.1 前端接入:三步之内完成
安装和初始化不复杂,按顺序走三步就够。第一步,安装依赖,可以走私有 npm 仓库,也可以直接引用构建产物:
npm install rea-tracker第二步,在应用入口初始化:
import { rea } from 'rea-tracker'; rea.init({ appId: 'h5-main', endpoint: '/api/event', autoTrack: ['click', 'pageview'], reportInterval: 5000 });第三步,在需要追踪的组件里加数据属性:
<button>app.post('/api/event', (req, res) => { const events = Array.isArray(req.body) ? req.body : [req.body]; for (const ev of events) { const row = normalize(ev); // 补默认值、过滤脏数据 enqueue(row); // 写入缓冲队列 } res.json({ code: 0 }); });为什么做成批量处理,而不是一条条同步写库?因为上报事件是高频小数据,每条都同步写库会把后端 IO 打成瓶颈。rea 前端默认每 5 秒批量上报一次,后端收到批量数据后再攒起来批量插入,实测一个单机数据库就能扛住日均几十万级别的数据量。
3.3 落库与查询:一张表搞定 80% 的需求
落库我用一张宽表,字段和事件模型一一对应。建表时最关键的是按时间范围做分区:
CREATE TABLE event_log ( event VARCHAR(64), ts BIGINT, page VARCHAR(255), module VARCHAR(64), target VARCHAR(64), user_id VARCHAR(64), session_id VARCHAR(64), extra JSON ) PARTITION BY RANGE (FROM_UNIXTIME(ts / 1000)) ( PARTITION p_2025_01 VALUES LESS THAN ('2025-01-01'), PARTITION p_2025_02 VALUES LESS THAN ('2025-02-01'), PARTITION p_2025_03 VALUES LESS THAN ('2025-03-01') );按时间分区之后,日常查询基本都落在单个分区里,速度非常理想。最常用的三个场景一下就能覆盖:
-- 按钮点击量 TOP10 SELECT target, COUNT(*) AS cnt FROM event_log WHERE event = 'element_click' AND ts >= 1735689600000 GROUP BY target ORDER BY cnt DESC LIMIT 10; -- 从首页到详情页的会话转化 SELECT COUNT(DISTINCT IF(page = '/home', session_id, NULL)) AS home_sessions, COUNT(DISTINCT IF(page = '/detail', session_id, NULL)) AS detail_sessions FROM event_log WHERE ts >= 1735689600000;产品经理要看数据时,我给他一个只读账号,他自己打开 SQL 编辑器查,再也不用蹲在我工位旁边排队了。
4. 真实使用中踩过的坑
4.1 页面关闭瞬间丢数据:Beacon 也不是万能药
前面说 Beacon 能解决关闭页面丢数据的问题,但那是单条报文的情况。事件量一大,用户又在弱网环境里关页面,Beacon 底层同样可能失败。我实测发现一个典型场景:用户在长列表页快速下滑,产生了三十多个曝光事件,然后立刻切到后台——这时还没到 5 秒的批量上报间隔,事件全堆在内存里。
解决方法是两个方向同时做。一是同时监听pagehide和visibilitychange,只要页面进入隐藏状态,就把缓冲区数据立即发送,不等那个 5 秒定时器。二是用 LocalStorage 做一层兜底缓存,发送失败的事件先落盘,下次页面打开再补发。这两层叠加之后,可观察到的丢失率才真正降到可以忽略的量级。
4.2 重复上报:200 不等于入库成功
HTTP 返回 200,不代表数据一定进了库。常见坑有两个:前端发送成功,后端也返回成功,但后端批量写入线程挂了,事件丢了;更隐蔽的是前端重试机制导致的数据重复。
举个例子,某次请求超时,前端判断失败并触发重试,实际上这个请求在后端已经处理完,只是响应没回来——同一条事件被插了两遍。我发现统计数据里"按钮点击量"比真实值高了约 3%,排查了很久才定位到这个原因。
解决方案是在后端基于session_id + ts + event + target建去重逻辑。关键是这个去重必须在批量插入的同一批数据里做内存级去重,才能避免两条相同事件在高并发下同时插入。前端可以随便搞重试策略,后端一定要有幂等兜底,这点一定要写进需求文档里。
4.3 跨域上报:接口地址不能随便写
如果把 endpoint 配成https://api.example.com/event,页面跑在https://www.example.com,那就是标准的跨域请求。配完 CORS 后 POST 能过,但注意sendBeacon对跨域模式限制比普通 fetch 更严格,部分老浏览器实现里跨域 Beacon 会直接被拦。
我的做法是:同域优先。真实业务里跨域需求不少,我会在后端网关层把事件接口做成同域代理,前端仍然请求相对路径/api/event,由网关转发到内网数据服务。这样既绕开跨域限制,又避免了把数据服务直接暴露到公网,安全上更稳。
4.4 隐私合规:别只盯着技术,忘了红线
做埋点系统,最麻烦的往往不是技术,而是你到底该采集什么。个人信息保护方面现在要求越来越严格,凡涉及用户账号、设备标识的信息,必须有明确的授权说明。我的建议很直接:能不上报的字段坚决不上报,尤其不要把手机号、身份证号、详细地址塞进extra。rea 在初始化时提供字段过滤规则,但这只是技术工具,流程上必须让产品或法务确认采集范围。
踩过一次坑之后,我养成一个习惯:每月出一份采集字段清单,发给相关负责人确认签字,没问题再继续跑。数据基建做得再好,合规出了事,那就不只是技术账了。
5. 再往前一步:从采集走向分析闭环
5.1 把报表接进现成工具
数据落库后,展示环节没必要自己做重活。我没给 rea 配控制台,而是直接做了个视图,把 PV/UV、事件点击量、转化率这些常用指标预计算成汇总表,再让开源可视化工具直连这个视图。产品经理每天打开面板就能看核心数字,上周上线的按钮点击率是涨是跌,扫一眼就有数。这一环省掉了我大量重复写 SQL 的时间。
5.2 从"采数据"往"做归因"扩展
如果精力允许,rea 下一步最值得加的能力是归因分析。现在记录的还只是单条事件,但用户行为是一条链:先看到首页 banner,点击进详情,分享给好友,好友通过分享链接下单——这里面每一步的来源怎么算,单靠事件表回答不了。我计划在会话级别维护一条refer_chain栈,保存最近 N 个事件的关联关系,随事件一起上报。这样将来查"分享带来多少转化"就不用拿 session_id 去手动 join 了。
用了这几个月,我越来越觉得这类小工具的关键在于克制。功能别贪多,边界要画清楚:采集可靠、查询简单。这两点做到位,就已经能解决小团队八成的数据问题了。