@clipto/reporter 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  通用数据上报 SDK:攒批、重试、有界队列、崩溃恢复,浏览器与 Node 双端通用。
4
4
 
5
+ SDK 只负责**协议信封、身份字段、队列与重试、页面访问跟踪**等自身行为;`app` / `device` / `page` / `user` / `properties` 各业务节点的字段口径由接入方埋点方案约束,SDK 原样透传、不做业务硬编码。
6
+
5
7
  - **浏览器端**:`fetch(keepalive)` 发送 + `sendBeacon` 退出兜底,队列持久化到 IndexedDB(Dexie,不支持时降级 localStorage)
6
8
  - **Node 端**:axios 发送,队列持久化到 JSON 文件(conf,内部原子写,`persistDir` 配置后启用)
7
- - **内置组件**:不手写底层 IO,双端差异仅发送与持久化,核心(入队 / 攒批 / 重试 / 溢出策略)平台无关
8
- - 依赖按需动态加载:浏览器不会加载 axios / conf,Node 不会加载 Dexie
9
+ - **核心平台无关**:发送与持久化通过 `transport` / `storage` 接口注入,不注入时按平台内置;依赖按需动态加载(浏览器不加载 axios / conf,Node 不加载 Dexie)
10
+ - **身份字段由 SDK 管理**:`eventId` / `userId` / `anonymousId` / `visitId` / `clientId`,只读 getter 供转化跟踪读取
11
+ - **可选签名防污染**:`signer` 返回键值对作为请求头随包发送(如 `X-Clipto-Timestamp` / `X-Clipto-Signature`),服务端校验防伪造
9
12
 
10
13
  ## 安装
11
14
 
@@ -22,11 +25,13 @@ import { Reporter } from '@clipto/reporter';
22
25
 
23
26
  const reporter = new Reporter({
24
27
  endpoint: 'https://reporter.example.com/api/events',
25
- baseProperties: { productId: 'my.app' },
28
+ context: {
29
+ app: { productId: 'com.my.app', platform: 'web' },
30
+ },
26
31
  });
27
32
 
28
- reporter.track('user_action', { action: 'click', eid: 'btn' });
29
- reporter.flush(); // 立即冲刷(跳过退避),页面卸载时 SDK 自动走 sendBeacon 兜底
33
+ reporter.track('user_action', { action: 'click', target: 'btn-pay' });
34
+ reporter.flush(); // 立即冲刷(跳过退避);页面卸载时 SDK 自动 sendBeacon 兜底
30
35
  ```
31
36
 
32
37
  ### Node
@@ -36,95 +41,104 @@ import { Reporter } from '@clipto/reporter';
36
41
 
37
42
  const reporter = new Reporter({
38
43
  endpoint: 'https://reporter.example.com/api/events',
39
- platform: 'node', // 不传则按 typeof window 自动探测
40
- persistDir: './data', // 配置后待发队列落盘,进程重启后恢复重发;不传仅内存
44
+ platform: 'node', // 不传则按 typeof window 自动探测
45
+ persistDir: './data', // 待发队列落盘,进程重启恢复重发;不传仅内存
46
+ context: {
47
+ app: { productId: 'com.my.app', platform: 'desktop' },
48
+ },
41
49
  });
42
50
 
43
- reporter.track('conversion', { conversionType: 'purchase', value: 99 });
44
- await reporter.dispose(); // 停止并兜底 flush(进程退出信号自动触发)
51
+ reporter.track('user_action', { action: 'export', result: 'ok' });
52
+ await reporter.dispose(); // 进程退出信号自动触发,主动退出时显式调用
45
53
  ```
46
54
 
47
- ### 双端注入自定义实现
55
+ ### React / SPA
56
+
57
+ URL 无需特殊处理;路由 pattern、标题等业务口径通过**函数形式 context** 实时提供:
48
58
 
49
59
  ```ts
50
60
  new Reporter({
51
61
  endpoint,
52
- transport: { send: (events) => http.post(endpoint, events) }, // 自定义发送
53
- storage: { save, load, clear }, // 自定义持久化
62
+ autoPageView: true, // 页面访问生命周期(进入建立上下文,离开上报 pageView 汇总)
63
+ context: () => ({
64
+ app: { productId: 'com.my.app', platform: 'web' },
65
+ page: {
66
+ route: matchRoute(location.pathname)?.pattern, // /user/123 → /user/:id
67
+ title: document.title, // 或读自有标题状态
68
+ },
69
+ }),
70
+ userId: () => account.currentUserId() ?? null,
54
71
  });
55
72
  ```
56
73
 
57
- ## 配置项
74
+ ## 身份字段与转化跟踪
75
+
76
+ ```ts
77
+ reporter.userId; // 登录账号 id(实时读取 userId provider);未提供恒为 null
78
+ reporter.anonymousId; // 首次生成后持久化不变
79
+ reporter.clientId; // 与 google client id(_ga)一致
80
+ reporter.visitId; // 当前会话 id,30 分钟无活动轮换后为新值(同源多 tab 共享)
81
+ ```
82
+
83
+ ## 签名防污染
84
+
85
+ ```ts
86
+ new Reporter({
87
+ endpoint,
88
+ signer: async (payload) => {
89
+ const timestamp = Date.now().toString();
90
+ const sig = await hmacSign(secret, `${timestamp}\n${payload}`); // 口径由后端定义
91
+ return { 'X-Clipto-Timestamp': timestamp, 'X-Clipto-Signature': sig };
92
+ },
93
+ });
94
+ ```
95
+
96
+ ## 配置项(简表)
58
97
 
59
98
  | 配置 | 默认 | 说明 |
60
99
  | --- | --- | --- |
61
100
  | `endpoint` | 必填 | 上报端点 |
62
- | `platform` | 自动探测 | `'browser'` / `'node'`,驱动内置发送与持久化实现 |
63
- | `flushInterval` | 2000 | 攒批窗口(ms),0 表示关闭批量即时发送 |
64
- | `maxBatchSize` | 10 | 单批最大条数,达到即刷 |
65
- | `maxBatchBytes` | 64KB | 单批最大字节数,达到即刷 |
66
- | `maxQueueSize` | 1000 | 队列最大条数 |
67
- | `overflowPolicy` | `'drop-oldest'` | 队列溢出:`'drop-oldest'` / `'drop-newest'` / `'flush-now'` |
68
- | `maxAttempts` | 3 | 单事件最大发送尝试次数,超限丢弃 |
69
- | `backoffMs` | 1000 | 失败重发退避基数(ms),按 2^n 指数增长;`flush()` 跳过退避 |
70
- | `baseProperties` | `{}` | 合并进每个事件 properties 的公共业务字段 |
71
- | `context` | 无 | 上下文采集函数,每次入队实时调用并合并进 properties |
72
- | `enrich` | 无 | 逐事件顶层字段补充(`sessionId` / `userId` / `deviceId` / `context`),每次入队实时调用 |
101
+ | `context` | | 统一注入 `app` / `device` / `page` / `user` / `properties`;静态对象或实时函数 |
102
+ | `platform` | 自动探测 | `'browser'` / `'node'` |
103
+ | `userId` | | 登录账号 id 提供函数,每次入队实时读取 |
104
+ | `flushInterval` | 2000 | 攒批窗口(ms),0 关闭批量即时发送 |
105
+ | `maxBatchSize` / `maxBatchBytes` | 10 / 64KB | 单批最大条数 / 字节数,达到即刷 |
106
+ | `maxQueueSize` / `overflowPolicy` | 1000 / `drop-oldest` | 队列上限与溢出处理(`drop-newest` / `flush-now`) |
107
+ | `maxAttempts` / `backoffMs` | 3 / 1000 | 重试次数上限与退避基数(2^n 指数);`flush()` 跳过退避 |
73
108
  | `flushOnExit` | true | 退出兜底 flush(浏览器 sendBeacon / Node 进程信号) |
74
- | `persist` | true | 浏览器端:队列持久化到 IndexedDB |
75
- | `persistDir` | | Node 端:持久化目录,设置后落盘 |
76
- | `autoPageView` | false | 浏览器端:自动上报 page_view(初始化 + 页面重新可见) |
77
- | `headers` | 无 | 自定义请求头(内置传输实现使用) |
109
+ | `persist` / `persistDir` | true / 无 | 浏览器 IndexedDB 持久化 / Node 持久化目录 |
110
+ | `autoPageView` | false | 浏览器端自动上报 `pageView` 汇总 |
111
+ | `headers` | | 自定义请求头(内置传输使用) |
112
+ | `signer` | 无 | 可选签名,返回键值对作为请求头随包发送 |
78
113
  | `debug` | false | 调试日志 |
79
- | `transport` / `storage` | | 注入自定义发送 / 持久化实现 |
80
- | `onFlushed` / `onFailed` / `onDropped` | 无 | 批次成功 / 失败 / 事件丢弃(重试耗尽 / 溢出 / 非法)钩子 |
114
+ | `transport` / `storage` | 内置 | 注入自定义发送 / 持久化实现 |
115
+ | `onFlushed` / `onFailed` / `onDropped` | 无 | 批次成功 / 失败 / 事件丢弃钩子 |
81
116
 
82
- ## API
117
+ 完整配置与类型见 [docs/api.md](docs/api.md)。
83
118
 
84
- ```ts
85
- interface IReporter {
86
- /** 上报一条事件,返回事件 id(dispose 后或非法事件返回空串);immediate 跳过攒批立即发送 */
87
- track(name: string, properties?: Record<string, unknown>, options?: {
88
- immediate?: boolean;
89
- timestamp?: number;
90
- category?: string; // 事件类别顶层字段,默认 'custom'
91
- }): string;
92
- /** 立即冲刷队列(跳过退避等待),并等待在途批次完成 */
93
- flush(): Promise<void>;
94
- /** 队列中待发条数 */
95
- readonly size: number;
96
- /** 停止并兜底 flush(flushOnExit),之后拒绝新事件 */
97
- dispose(): Promise<void>;
98
- }
99
- ```
119
+ ## 文档
100
120
 
101
- ## 事件数据结构
102
-
103
- 事件顶层字段与历史 SDK(renderer `event-tracker-sdk`)保持一致,上报信封:
104
-
105
- ```jsonc
106
- {
107
- "events": [
108
- {
109
- "id": "evt_...",
110
- "eventName": "user_action", // SDK 内部字段名 name,序列化输出为 eventName
111
- "timestamp": 1756200000000,
112
- "sessionId": "session_...", // enrich 提供,缺省核心按实例生成
113
- "userId": null, // enrich 提供,缺省 null
114
- "deviceId": "device_...", // enrich 提供,缺省核心按实例生成
115
- "properties": { /* baseProperties + context + 业务属性 */ },
116
- "category": "custom", // track options 提供,默认 'custom'
117
- "context": {} // enrich 提供(页面 / 用户 / 浏览器 / 屏幕 / 视口)
118
- }
119
- ],
120
- "metadata": { "sdkVersion": "1.0.0", "timestamp": 1756200000000, "batchId": "evt_..." }
121
- }
121
+ - [接入指南](docs/guide.md) —— 最小接入、context 注入、React/SPA、转化跟踪、签名
122
+ - [API 参考](docs/api.md) —— 全部配置、核心类型、内置适配器、上报信封
123
+ - [架构与数据模型](docs/architecture.md) —— 分层、身份字段生命周期、会话与多 tab、队列与重试、签名
124
+ - [页面访问跟踪](docs/page-view.md) —— autoPageView 生命周期、endReason、SPA 路由与标题
125
+ - [CHANGELOG](CHANGELOG.md)
126
+
127
+ ## 测试
128
+
129
+ ```bash
130
+ npm test # 构建 + node:test 冒烟测试(8 例,覆盖攒批 / 重试 / 丢弃 / 溢出 / 恢复 / 签名)
122
131
  ```
123
132
 
124
- 事件 id 与 batchId 均为 `evt_${时间戳}_${随机串}` 格式;`attempts` 为 SDK 内部字段,不上报。
133
+ ## Roadmap
134
+
135
+ - `elementExposure` 功能曝光自动采集(可见面积 ≥50% 且连续可见 ≥1s,单元素去重;`eventName` 已预留)
136
+ - Node 端 `clientId` 跨重启持久化
137
+ - 浏览器专属行为(pageView 生命周期 / 多 tab 会话 / sendBeacon)的浏览器环境测试
125
138
 
126
139
  ## 注意事项
127
140
 
128
- - **conf 与打包器**:Node 持久化依赖 `conf`(含 Node 内置模块),源码中 `import('conf')` 带 `webpackIgnore` 注释,消费方 webpack 等打包器会跳过它,由 Node 运行时直接解析;浏览器端永不执行该分支。
129
- - **依赖按需加载**:`axios` / `conf` 仅在 Node 分支动态加载,`dexie` 仅在浏览器分支动态加载。
130
- - **dispose 后不可再用**:`dispose()` 执行退出兜底 flush 并拒绝新事件(返回空串)
141
+ - **conf 与打包器**:Node 持久化依赖 `conf`(含 Node 内置模块),源码中 `import('conf')` 带 `webpackIgnore` 注释,消费方打包器会跳过它、由 Node 运行时解析;浏览器端永不执行该分支
142
+ - **依赖按需加载**:`axios` / `conf` Node 分支动态加载,`dexie` 仅浏览器分支动态加载
143
+ - **dispose 后不可再用**:`dispose()` 执行退出兜底 flush 并拒绝新事件(`track` 返回空串);`signer` 仅内置 transport 生效,自定义 transport 自行处理签名
144
+ - **Node ≥22**:SDK 在 Node 平台不访问任何浏览器存储(实验性 `localStorage` 全局按平台短路),身份 / 会话为进程内生成