dsh-heatmap 0.1.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 +184 -0
- package/cordis.patch.yml +10 -0
- package/examples/push-to-platform.mjs +94 -0
- package/lib/client.js +897 -0
- package/lib/client.js.map +1 -0
- package/package.json +74 -0
- package/src/client/ConsentModal.tsx +77 -0
- package/src/client/HeatmapOverlay.tsx +353 -0
- package/src/client/index.ts +28 -0
- package/src/client/storage.ts +144 -0
- package/src/client/tracker.ts +219 -0
- package/src/http.ts +172 -0
- package/src/index.ts +135 -0
- package/src/shared/types.ts +103 -0
- package/src/store.ts +146 -0
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# dsh-heatmap
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness 页面埋点与热力图分析插件。为后续产品设计优化提供量化数据:科学采集页面交互行为,本地展示热力图与统计,预留 CLI 与外部上传接口,上传前强制用户授权。
|
|
4
|
+
|
|
5
|
+
## 功能一览(在原始需求上补充后的完整设计)
|
|
6
|
+
|
|
7
|
+
| # | 功能 | 说明 |
|
|
8
|
+
|---|------|------|
|
|
9
|
+
| 1 | **科学埋点设计** | 四类事件(生命周期 / 交互 / 滚动曝光 / 热力图坐标)+ 稳定元素身份 + schema 版本化,参考主流产品分析(Amplitude / Mixpanel / Heap / Clarity)的埋点原则 |
|
|
10
|
+
| 2 | **热力图模式** | 点击 + 鼠标悬停密度热力图,色带「蓝→青→绿→黄→红」;面板标注「本地 / 本人数据」并展示聚合统计 |
|
|
11
|
+
| 3 | **CLI / 外部访问接口** | host 侧 `analytics_export` 工具(Agent 可调用)+ `webServer` 上的 `/dsh-heatmap/*` HTTP 路由,预留统一分析平台对接 |
|
|
12
|
+
| 4 | **上传授权弹窗** | 数据上传到外部/其他软件前弹出授权弹窗,用户「同意并上传」后才发送 |
|
|
13
|
+
| 5 | **隐私最小化**(补充) | 只采集控件身份、坐标、时间、视口;**绝不采集对话正文与输入内容**(输入仅记录长度) |
|
|
14
|
+
| 6 | **数据本地化**(补充) | 采集数据默认只存本地 localStorage;host 收集器用 NDJSON 落盘,可随时导出/清空 |
|
|
15
|
+
| 7 | **可配置**(补充) | 采样率、事件开关、热力图开关、鼠标热力图、上传地址、授权要求、存储上限均可配置 |
|
|
16
|
+
|
|
17
|
+
## 埋点设计(事件分类学)
|
|
18
|
+
|
|
19
|
+
### 事件类型
|
|
20
|
+
|
|
21
|
+
| 类型 | 触发时机 | 采集字段 | 产品用途 |
|
|
22
|
+
|------|---------|---------|---------|
|
|
23
|
+
| `session_start` / `session_end` | 页面加载 / 卸载或隐藏 | 会话 id、时间 | 会话数、会话时长 |
|
|
24
|
+
| `page_view` | 初始加载 + hash 路由变化 | path、hash | 页面/视图访问量 |
|
|
25
|
+
| `click` | 任何可点击元素被点击 | 元素身份 + 视口坐标 | 按钮点击率、**点击热力图** |
|
|
26
|
+
| `focus` / `blur` | 元素获得/失去焦点 | 元素身份 | 是否 focus、交互路径 |
|
|
27
|
+
| `hover` | 鼠标悬停(250ms 节流,可选) | 视口坐标 + 元素身份 | **悬停/注意力热力图** |
|
|
28
|
+
| `scroll_depth` | 滚动停止后(500ms 防抖) | 滚动深度 0..1 | 内容消费深度 |
|
|
29
|
+
| `visibility` | 页签可见性变化 | visible | **停留时长**(活跃时段累计) |
|
|
30
|
+
| `input` | 输入框输入(1s 节流) | 元素身份 + 输入**长度** | 输入活跃度(不含内容) |
|
|
31
|
+
| `impression` | 元素首次进入视口 ≥50%(IntersectionObserver) | 元素身份 | **曝光分析**(哪些面板/控件被看到) |
|
|
32
|
+
|
|
33
|
+
### 元素身份(稳定指纹)
|
|
34
|
+
|
|
35
|
+
采集端按优先级生成稳定的 `elementId`,保证跨会话可聚合到同一控件:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
data-testid > id > aria-label > role#tag > tag:文本摘要(≤40字)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
同时记录 `tag / role / ariaLabel / title / text` 供分析侧做控件维度下钻。
|
|
42
|
+
|
|
43
|
+
### 数据模型(schema v1)
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
interface AnalyticsEvent {
|
|
47
|
+
v: number // schema 版本
|
|
48
|
+
id: string // 事件 uuid
|
|
49
|
+
ts: number // epoch ms
|
|
50
|
+
sessionId: string // 会话 uuid
|
|
51
|
+
type: EventType
|
|
52
|
+
page: { path: string; hash: string }
|
|
53
|
+
target?: ElementRef // 元素身份
|
|
54
|
+
position?: { x: number; y: number } // 热力图坐标
|
|
55
|
+
depth?: number // 滚动深度
|
|
56
|
+
visible?: boolean // 可见性
|
|
57
|
+
inputLength?: number // 输入长度(无内容)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 架构
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
┌────────────────────────── browser 半部 ──────────────────────────┐
|
|
65
|
+
│ tracker.ts 全局埋点采集(document 级监听) │
|
|
66
|
+
│ storage.ts 本地环形缓冲 + 设置 + 授权(localStorage) │
|
|
67
|
+
│ HeatmapOverlay 热力图 + 统计面板 + 设置 + 上传入口(shell.overlay)│
|
|
68
|
+
│ ConsentModal 上传授权弹窗 │
|
|
69
|
+
└──────────────────────────────┬───────────────────────────────────┘
|
|
70
|
+
│ 授权后 fetch(POST)
|
|
71
|
+
┌──────────────────────────────▼────── host 半部 ──────────────────┐
|
|
72
|
+
│ /dsh-heatmap/ingest 批量上报(本地收集器) │
|
|
73
|
+
│ /dsh-heatmap/export 导出(JSON/NDJSON) │
|
|
74
|
+
│ /dsh-heatmap/stats 聚合统计 │
|
|
75
|
+
│ /dsh-heatmap/sessions 会话时间线(回放/复现) │
|
|
76
|
+
│ /dsh-heatmap/funnel 漏斗分析(有序步骤转化率) │
|
|
77
|
+
│ /dsh-heatmap/clear 清空 │
|
|
78
|
+
│ analytics_export 工具 stats/export/sessions/funnel/clear/upload │
|
|
79
|
+
│ AnalyticsStore NDJSON 落盘 + 内存环形缓冲 + 漏斗/会话聚合 │
|
|
80
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
数据默认只在本机流动:采集→本地;上传→(授权后)host 收集器或配置的外部地址。
|
|
84
|
+
|
|
85
|
+
## 安装
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
dsh plugin --profile web add ./dsh-heatmap
|
|
89
|
+
# 或使用 dsh-master:dsh_master_install 工具
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
然后重启 dsh(`dsh web` 或 `dsh --profile web`)。重启后页面右下角会出现 🔥 按钮,打开面板即可查看统计、开启热力图。
|
|
93
|
+
|
|
94
|
+
## 开发
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
# 在插件目录内
|
|
98
|
+
pnpm install
|
|
99
|
+
pnpm run build # 构建 lib/client.js(browser 半部)
|
|
100
|
+
pnpm run typecheck
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 配置
|
|
104
|
+
|
|
105
|
+
### host 配置(cordis.yml,部署级默认值)
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
- id: dsh-heatmap
|
|
109
|
+
config:
|
|
110
|
+
enabled: true # host 收集器与工具开关
|
|
111
|
+
dataDir: '' # 落盘目录;空则 $DSH_HOME/storages/dsh-heatmap
|
|
112
|
+
maxEvents: 20000 # 环形缓冲上限
|
|
113
|
+
uploadEndpoint: '' # 统一分析平台上传地址(analytics_export upload 目标)
|
|
114
|
+
consentRequired: true # 上传是否要求授权(文档化;实际弹窗在 browser 半部)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 客户端设置(localStorage,运行时在面板中调整)
|
|
118
|
+
|
|
119
|
+
| 设置 | 默认 | 说明 |
|
|
120
|
+
|------|------|------|
|
|
121
|
+
| 热力图模式 | 关 | 开启全屏热力图覆盖层 |
|
|
122
|
+
| 采集埋点 | 开 | 总开关 |
|
|
123
|
+
| 鼠标热力图 | 关 | 是否采集悬停坐标(开启会增大数据量) |
|
|
124
|
+
| 上传需授权 | 开 | 上传前是否弹授权窗 |
|
|
125
|
+
| 上传地址 | 空 | 空 = 同源 host 收集器 `/dsh-heatmap/ingest` |
|
|
126
|
+
|
|
127
|
+
## CLI / 外部访问接口
|
|
128
|
+
|
|
129
|
+
### 1. Agent 工具(`analytics_export`)
|
|
130
|
+
|
|
131
|
+
在会话中让 Agent 调用:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
动作:stats 聚合统计(事件总数/点击/聚焦/曝光/会话数/按类型分布)
|
|
135
|
+
动作:export 导出最近 5000 条事件
|
|
136
|
+
动作:sessions 会话时间线(最近 20 个会话的有序事件序列,用于回放/复现)
|
|
137
|
+
动作:funnel 漏斗分析(需 steps:有序事件类型数组,如 [session_start, click, input])
|
|
138
|
+
动作:clear 清空收集器数据
|
|
139
|
+
动作:upload 上传到 config.uploadEndpoint(统一分析平台)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 2. HTTP 路由
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
curl http://127.0.0.1:PORT/dsh-heatmap/health
|
|
146
|
+
curl http://127.0.0.1:PORT/dsh-heatmap/stats
|
|
147
|
+
curl http://127.0.0.1:PORT/dsh-heatmap/export?format=json&limit=100
|
|
148
|
+
curl http://127.0.0.1:PORT/dsh-heatmap/sessions?limit=20
|
|
149
|
+
curl -X POST http://127.0.0.1:PORT/dsh-heatmap/funnel -H 'content-type: application/json' -d '{"steps":["session_start","click","input"]}'
|
|
150
|
+
curl -X POST http://127.0.0.1:PORT/dsh-heatmap/ingest -H 'content-type: application/json' -d '{"v":1,"sessionId":"s","sentAt":0,"events":[...]}'
|
|
151
|
+
curl -X DELETE http://127.0.0.1:PORT/dsh-heatmap/clear
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
> 端口即 DSH 网页服务的端口(客户端与 host 同源)。
|
|
155
|
+
|
|
156
|
+
## 统一分析平台对接
|
|
157
|
+
|
|
158
|
+
预留了三条对接路径,按需选择:
|
|
159
|
+
|
|
160
|
+
1. **Agent 工具 `upload`**:配置 `uploadEndpoint` 后,让 Agent 调用 `analytics_export`(action=upload)把收集器数据批量 POST 到平台。
|
|
161
|
+
2. **HTTP 路由**:`GET /dsh-heatmap/export` 拉取原始事件,由你的平台 SDK/脚本转发。
|
|
162
|
+
3. **示例适配脚本**:[`examples/push-to-platform.mjs`](examples/push-to-platform.mjs) —— 从 `/export` 拉取、按 `mapEvent` 适配成平台形状、POST 到 `PLATFORM_ENDPOINT`(仅需改这一个适配函数)。
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
DSH_BASE=http://127.0.0.1:3080 \
|
|
166
|
+
PLATFORM_ENDPOINT=https://analytics.example.com/ingest \
|
|
167
|
+
PLATFORM_API_KEY=sk-xxx \
|
|
168
|
+
node examples/push-to-platform.mjs
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 隐私与合规
|
|
172
|
+
|
|
173
|
+
- 采集默认**最小化**:不采集对话正文、不采集输入内容(仅长度)、不采集 PII。
|
|
174
|
+
- 数据默认只在用户本机(localStorage / 本地 NDJSON)。
|
|
175
|
+
- 上传动作前弹出授权弹窗,明示「上传什么、上传到哪里、不含对话内容」,用户确认后才发送。
|
|
176
|
+
- 可随时「清除本地」或在 host 侧 `clear` 删除收集器数据。
|
|
177
|
+
|
|
178
|
+
## 后续演进(预留)
|
|
179
|
+
|
|
180
|
+
- 可视化会话回放:在面板内把 `sessions` 时间线渲染成逐步重放。
|
|
181
|
+
- 曝光时长:为 `impression` 记录进入/离开视口时长(当前只记首次曝光)。
|
|
182
|
+
- 性能埋点:`navigation`/`resource` 时序。
|
|
183
|
+
|
|
184
|
+
> 生成自 DSH-Master(dsh-master)脚手架并扩展。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# dsh-heatmap bundle layer.
|
|
2
|
+
#
|
|
3
|
+
# Inserted by the dsh loader when this bundle is part of a profile's
|
|
4
|
+
# dsh.profile.bundles list (see `dsh plugin --profile <name> add <this-dir>`).
|
|
5
|
+
# This row activates the host plugin (src/index.ts) and, through the package's
|
|
6
|
+
# `dsh.client` manifest, registers the client-side heatmap bundle (lib/client.js)
|
|
7
|
+
# into the client boot graph.
|
|
8
|
+
- insert:
|
|
9
|
+
- id: dsh-heatmap
|
|
10
|
+
name: dsh-heatmap
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 统一分析平台对接示例(dsh-heatmap)。
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* DSH_BASE=http://127.0.0.1:3080 \
|
|
7
|
+
* PLATFORM_ENDPOINT=https://analytics.example.com/ingest \
|
|
8
|
+
* PLATFORM_API_KEY=sk-xxx \
|
|
9
|
+
* node examples/push-to-platform.mjs
|
|
10
|
+
*
|
|
11
|
+
* 作用:从 DSH host 收集器(/dsh-heatmap/export)拉取埋点事件,映射成目标
|
|
12
|
+
* 平台的通用事件形状,再 POST 到平台。`mapEvent` 是唯一的「适配点」——对接
|
|
13
|
+
* 不同平台时只需改它,其余逻辑不变。
|
|
14
|
+
*
|
|
15
|
+
* 说明:这只是示例脚本。生产环境可复用 host 侧 `analytics_export upload`
|
|
16
|
+
* 动作(走 config.uploadEndpoint),或把本脚本改造成定时任务 / SDK。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const DSH_BASE = (process.env.DSH_BASE ?? 'http://127.0.0.1:3080').replace(/\/$/, '')
|
|
20
|
+
const PLATFORM_ENDPOINT = process.env.PLATFORM_ENDPOINT ?? ''
|
|
21
|
+
const PLATFORM_API_KEY = process.env.PLATFORM_API_KEY ?? ''
|
|
22
|
+
|
|
23
|
+
if (PLATFORM_ENDPOINT === '') {
|
|
24
|
+
console.error('缺少 PLATFORM_ENDPOINT(目标平台接收地址)')
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 适配点:把 dsh-heatmap 事件映射成目标平台的事件形状。
|
|
30
|
+
* 这里给的是通用形状(type / distinctId / properties / timestamp),
|
|
31
|
+
* 对接真实平台时按平台文档改这里的字段即可。
|
|
32
|
+
*/
|
|
33
|
+
function mapEvent(e) {
|
|
34
|
+
return {
|
|
35
|
+
event: e.type,
|
|
36
|
+
distinctId: e.sessionId,
|
|
37
|
+
timestamp: e.ts,
|
|
38
|
+
properties: {
|
|
39
|
+
page: e.page?.path ?? '',
|
|
40
|
+
hash: e.page?.hash ?? '',
|
|
41
|
+
elementId: e.target?.elementId,
|
|
42
|
+
tag: e.target?.tag,
|
|
43
|
+
role: e.target?.role,
|
|
44
|
+
text: e.target?.text,
|
|
45
|
+
x: e.position?.x,
|
|
46
|
+
y: e.position?.y,
|
|
47
|
+
depth: e.depth,
|
|
48
|
+
visible: e.visible,
|
|
49
|
+
inputLength: e.inputLength,
|
|
50
|
+
source: 'dsh-heatmap',
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
const exportUrl = `${DSH_BASE}/dsh-heatmap/export?format=json`
|
|
57
|
+
console.log(`拉取事件:${exportUrl}`)
|
|
58
|
+
const res = await fetch(exportUrl)
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
console.error(`导出失败:HTTP ${res.status}`)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
const { events } = await res.json()
|
|
64
|
+
if (!events?.length) {
|
|
65
|
+
console.log('没有待上传的事件。')
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const payload = {
|
|
70
|
+
source: 'dsh-heatmap',
|
|
71
|
+
sentAt: Date.now(),
|
|
72
|
+
events: events.map(mapEvent),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`上传 ${payload.events.length} 条 → ${PLATFORM_ENDPOINT}`)
|
|
76
|
+
const up = await fetch(PLATFORM_ENDPOINT, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
'content-type': 'application/json',
|
|
80
|
+
...(PLATFORM_API_KEY !== '' ? { authorization: `Bearer ${PLATFORM_API_KEY}` } : {}),
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(payload),
|
|
83
|
+
})
|
|
84
|
+
if (!up.ok) {
|
|
85
|
+
console.error(`平台返回 HTTP ${up.status}:${await up.text()}`)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
console.log('上传完成。')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main().catch((err) => {
|
|
92
|
+
console.error(err)
|
|
93
|
+
process.exit(1)
|
|
94
|
+
})
|