fe-tracking
v1.0.6
Published
前端可观测 SDK:RUM 监控平面 + CLS 取证平面,跨 Web / 小程序双端
Downloads
1,001
Maintainers
Readme
fe-tracking — 前端可观测 SDK
双平面架构:RUM 监控平面(告警/大盘,走 aegis)+ CLS 取证平面(完整操作链路,走批量日志)。 一套 core 同时服务 Web 端与小程序端(Taro),平台能力靠 adapter 注入。
定位
- 核心零运行时依赖:
core/全部纯 TS;平台能力(请求 / 存储 / aegis 实例)通过 adapter 注入 - 平台无关:
source(日志来源=哪个应用在上报)由接入方自定义,SDK 只透传不做分支判断;运行平台由选用的 adapter / bundle 入口决定,不占配置字段 - 可插拔后端:取证平面的后端是
TrackingSink = (events) => Promise<void>一个契约,CLS / 自建 / 任意后端都能接
安装
npm i fe-trackingWeb 端不需要额外 peer 依赖。小程序端(Taro)需要 @tarojs/taro,已声明为 optional peer —— Web 端项目不装它不会报警。
消费方 TS 解析兼容(1.0.1 起)
本包同时支持两种 TypeScript 模块解析策略,不要求消费方改 tsconfig:
| 消费方 moduleResolution | 类型走哪条路 |
|---|---|
| bundler / node16 / nodenext | exports 字段 |
| node(旧 node10 策略,Taro 项目常见默认) | main / types + typesVersions 前缀映射 |
⚠️ 1.0.0 在旧
node策略下有 bug:typesVersions用了通配"*": ["dist/*"], 它把裸包名也套进映射、盖掉了types字段,导致import from 'fe-tracking'报TS2307 Cannot find module(而 subpath 反而正常)。1.0.1 改为按前缀显式展开修复。这个 bug 在 SDK 仓自测里看不到 —— 本仓 tsconfig 用的是新策略,走
exports不经typesVersions。只有装进moduleResolution: "node"的项目才会现形。新增 subpath 目录时 记得同步往typesVersions加一条前缀,否则旧策略消费方取不到该目录的类型。
镜像事件字段变更(1.0.2 起)
Web 端「镜像模式」(aegis beforeRequest → CLS)的两项修正,消费方无需改代码,
升级即生效;两项都只是补字段,不改既有字段语义:
| 事件 | 变更 | 原因 |
|---|---|---|
| userAction(点击) | 新增 payload.viewPath = 完整 DOM XPath | 1.0.1 及之前把 aegis 的 view_type(标签名)与 view_tag(XPath)当成一个字段,写成 viewTag: d.view_type ?? d.view_tag——view_type 恒有值使 ?? 兜底永不触发,XPath 从未进过 CLS |
| apiCall | action 由 200 <url> 变为 200 <METHOD> <url> | aegis 日志里本就有 req method:,此前被丢弃;补上后与小程序端自采格式 200 GET /api/... 同形,两端链路可放在一起看 |
XPath 丢失的实际代价(2026-08-12 cms 分类页实测):同一秒的两条表格点击在 CLS 里 都只剩
{viewText:"", viewTag:"INPUT"},完全同形,被误判成「点了没反应反复点」; 补上 XPath 才看出是TR[8]与TR[9]——勾选了两个不同的行。
viewPath是纯结构(标签名 + 序号),不含用户数据;截断至 300 字符。method正则限定[A-Z]+,非 HTTP 动词形状一律不透传。
⚠️ action 格式变化会影响已有的 CQL 精确匹配:若有按 action:"200 https://…"
写死的查询或告警,需改成前缀/通配匹配。按 traceId / category / session 的查询不受影响。
请求参数镜像(1.0.3 起,默认关)
「启用/禁用」类操作常常开与关共用同一端点同一 method(如 PUT /admin/:id/enable),
方向只在请求体里。不镜像请求参数时,CLS 里两个相反操作完全同形——链路只能还原
「点了某个开关」,还原不了「开还是关」。
const mirror = createBeforeRequestMirror(tracking, {
shouldIgnore: (log) => /* …防自反馈,见上 */,
includeReqParam: true, // ⚠️ 开之前先读下面这段
});🔴 默认关是安全红线,不是保守默认:这一段的内容是消费方
api.reqParamHandler的输出, 而 aegis 在该 handler 未配置时会原样上报请求体——登录接口的请求体含明文密码, 此时开启等于把密码写进 CLS。SDK 无从判断消费方配了什么,因此默认拒绝、由消费方 在确认自己的 handler 是字段白名单后显式承担这个判断。即使开启,
parseReqParam仍有三重限幅:必须能解析成对象(数组/裸串丢弃)、 值必须是 string/number/boolean 原始类型(嵌套对象往往是整包透传的信号)、 最多 8 个键且序列化后 ≤200 字符。
消费方侧的配套写法(cms 实例,api.reqParamHandler 按接口+字段白名单):
const REQ_PARAM_WHITELIST = [
{ test: /\/admin\/categories\/[^/]+\/enable$/, pick: 'enabled' },
{ test: /\/admin\/products\/[^/]+\/visibility$/, pick: 'isLiveVisible' },
];
reqParamHandler: (body, url) => {
const hit = REQ_PARAM_WHITELIST.find((w) => w.test.test(String(url ?? '')));
if (!hit) return ''; // 默认拒绝
const b = typeof body === 'string' ? JSON.parse(body) : body;
return JSON.stringify({ [hit.pick]: Boolean(b?.[hit.pick]) }); // 只取一个字段并重建
}Taro 版本支持范围
peer 范围声明为 >=4,但这是已验证范围,不是技术下限:
- 已验证:Taro
4.1.11(单测与三个接入端均在此版本) - 理论可用:Taro 3.x —— SDK 小程序侧只用到
Taro.request、Taro.getCurrentPages、useDidShow三个 API,均无 Taro 4 专属特性;官方 hooks 文档中useLaunch/useLoad/useUnload等标注了 "Taro v3.5.0+",而useDidShow未标版本,说明它更早就已提供 - 未验证:3.x 下的类型定义与
getCurrentPages()返回结构未实测,故不写进 peer 范围
Taro 3 项目装了会看到 peer 不匹配告警(npm 7+ 对 peer 只警告不拦截),功能预期可用。如需正式支持 3.x,请提 issue,我们补一轮 3.x 验证后放宽范围。
板块划分
| 板块 | 目录 | 职责 | 平台相关 |
|------|------|------|---------|
| 类型与配置 | src/types.ts | ITrackingConfig / TrackEvent / ITrackingAdapter 契约 | 否 |
| 事件模型 | src/core/schema.ts | 统一事件 builder(who/when/where/what/correlate)+ CLS contents 扁平化 | 否 |
| 会话 | src/core/session.ts | session_id 生成;storage 注入决定策略(内存单例 / sessionStorage 跨刷新) | 否 |
| 队列 | src/core/queue.ts | 队列 / 面包屑 / LRU / 批量 / 定时 flush / 失败重发 | 否 |
| CLS sink | src/sinks/cls.ts | 批量 POST /tracklog?topic_id=,事件 → logs[].contents 组包 | 否(request 注入) |
| RUM sink | src/sinks/rum.ts | 封装注入的 aegis 实例(error 主动补报走这里进 RUM) | 否(aegis 注入) |
| aegis 钩子工厂 | src/aegis/ | beforeReport(error 回流,防循环)/ beforeRequest(镜像 + level→category mapper) | 否(入参是 log 对象) |
| 小程序平台件 | src/platform-mp/ | Taro adapter(Taro.request / 内存 storage)+ usePageReport 工厂 | 仅小程序端 |
| Web 平台件 | src/platform-web/ | fetch adapter + sessionStorage | 仅 Web 端 |
| 门面 | src/index.ts | createTracking(config, adapter):组装 session + queue + sinks + 路由 | 否 |
事件模型不含
version字段——需要的话由接入方按需放进payload,属有意裁剪。
数据流(谁调谁)
业务代码 track()/trackError() aegis 自动采集
│ │ beforeReport / beforeRequest 钩子
▼ ▼
index.ts 门面 ──路由──► error 且非 aegis 来源 → sinks/rum.ts(进 RUM 告警)
│
▼
core/queue.ts(level 路由:all→队列+面包屑 / errorChain→仅面包屑;error 触发合并 flush)
│
▼
sinks/cls.ts ──批量 POST──► CLS /tracklog防循环关键:aegis 钩子回流的 error 走内部 ingestAegisError(),只进 CLS 不再回写 aegis。
接入形态
// 小程序端(Taro):app.tsx
import { createTracking } from 'fe-tracking';
import { createMpAdapter } from 'fe-tracking/platform-mp/adapter';
// 第三参传 getter 而非实例:createTracking() 常先于 aegis 实例就绪,
// 直接传实例会把此刻的 null 冻结进闭包,永久拿不到后续初始化好的 aegis。
export const tracking = createTracking(TRACKING_CONFIG, createMpAdapter(), () => getRum());
// aegis 侧必配(防 SDK 自身的 CLS 请求被镜像/回流,形成自反馈回路):
new Aegis({
// ...其余配置
beforeReport: createBeforeReportHook(tracking, {
shouldIgnore: (log) => [log.url, log.msg].some((v) => String(v ?? '').includes(tracking.getClsUrl())),
}),
});
// Web 端:在 aegis 的 beforeRequest 里加镜像分支(必配,同上)
const mirror = createBeforeRequestMirror(tracking, {
shouldIgnore: (log) => [log.url, log.msg].some((v) => String(v ?? '').includes(tracking.getClsUrl())),
});
beforeRequest: (data) => { mirror(data); return existingFilter(data); }小程序页面侧只有两个 API:usePageReport()(pageView 自动)和它返回的 trackUserAction(action)。
后端可插拔与多平台同时上报
// 场景 A:换成自建后端(config.cls 可省略;getClsUrl() 此时返回 '',shouldIgnore 需按自建 URL 排除)
const mySink: TrackingSink = (events) =>
fetch('https://log.example.com/batch', { method: 'POST', body: JSON.stringify(events) })
.then((r) => { if (!r.ok) throw new Error(String(r.status)); });
const t = createTracking({ env: 'prod', source: 'web' }, createWebAdapter(), null, { sinks: mySink });
// 场景 B:CLS + 自建双写(过渡期)——opts.sinks 是完全替换,保留 CLS 需显式组合
const t2 = createTracking(cfg, adapter, null, {
sinks: [createClsSink({ ...cfg.cls!, source: cfg.source }, adapter.request), mySink],
});多 sink 失败语义(有意设计):全部失败 → 整批保留、下次 flush 对所有 sink 重发(都没收到过,无重复风险);部分成功 → 不重发,失败侧丢该批——重发会让成功侧产生重复事件,对取证数据质量的伤害大于次要后端丢一批。
换监控平面(如用 Sentry 替代 aegis)需新写对应钩子工厂(照 src/aegis/ 模式),core 不动。
分发方式
npm 是唯一分发方式(npm i fe-tracking)。历史上曾有一套「编译产物直拷」的 sync 分发脚本(scripts/sync.mjs + sync.targets.json + tsup bundle 产物),服务于消费仓幽灵依赖治理完成前、不便动包管理器的过渡期;三个消费仓(lanbao mp / cms / kol-mp)自 1.0.4 起均已改走 npm 依赖,该链路已于 2026-08 整体移除。如需考古,见 git history 中 scripts/sync.mjs 及 docs/archive/2026-08-03-tracking-sdk-implementation-plan.md。
开发
npm test # jest,128 用例
npm run typecheck # tsc --noEmit
npm run build # tsc 输出 dist/(npm 包发布用)prepublishOnly 会串跑 typecheck + test + build,发布前自动拦一道。
