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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["btn"],"sources":["../src/client/storage.ts","../src/client/tracker.ts","../src/client/ConsentModal.tsx","../src/client/HeatmapOverlay.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * browser 半部:本地事件环形缓冲 + 运行时设置 + 授权状态(全部 localStorage)。\n *\n * 设计要点:\n * - 数据默认只在用户本地(localStorage),热力图读取本地数据,无需授权;\n * - 上传动作才可能触发授权弹窗,授权结果也持久化在本地;\n * - 运行时设置是「当前」可变对象,供埋点引擎与面板共享。\n */\nimport type { AnalyticsBatch, AnalyticsEvent } from '../shared/types.ts'\n\nconst EVENTS_KEY = 'dsh-heatmap.events.v1'\nconst SETTINGS_KEY = 'dsh-heatmap.settings.v1'\nconst CONSENT_KEY = 'dsh-heatmap.consent.v1'\n\nexport interface ClientSettings {\n /** 是否启用热力图覆盖层。 */\n heatmapMode: boolean\n /** 是否启用埋点采集。 */\n trackingEnabled: boolean\n /** 是否采集鼠标悬停坐标(供热力图,默认关闭以控制数据量)。 */\n trackMouse: boolean\n /** 采样率 0..1。 */\n sampleRate: number\n /** 上传目标;空则上传到同源 host 收集器 `/dsh-heatmap/ingest`。 */\n uploadEndpoint: string\n /** 上传前是否要求授权弹窗。 */\n consentRequired: boolean\n /** 本地最多保留事件条数。 */\n maxEvents: number\n}\n\nexport const DEFAULT_SETTINGS: ClientSettings = {\n heatmapMode: false,\n trackingEnabled: true,\n trackMouse: false,\n sampleRate: 1,\n uploadEndpoint: '',\n consentRequired: true,\n maxEvents: 5000,\n}\n\nfunction readSettings(): ClientSettings {\n try {\n const raw = localStorage.getItem(SETTINGS_KEY)\n if (!raw) return { ...DEFAULT_SETTINGS }\n return { ...DEFAULT_SETTINGS, ...JSON.parse(raw) as Partial<ClientSettings> }\n } catch {\n return { ...DEFAULT_SETTINGS }\n }\n}\n\n/** 当前生效设置(可变引用,供埋点引擎实时读取)。 */\nlet currentSettings: ClientSettings = readSettings()\n\nexport function getSettings(): ClientSettings {\n return currentSettings\n}\n\nexport function saveSettings(next: ClientSettings): void {\n currentSettings = { ...next }\n try {\n localStorage.setItem(SETTINGS_KEY, JSON.stringify(currentSettings))\n } catch {\n /* storage unavailable */\n }\n}\n\nlet currentSessionId = ''\n\nexport function getSessionId(): string {\n return currentSessionId\n}\n\nexport function setSessionId(id: string): void {\n currentSessionId = id\n}\n\n/** 本地事件环形缓冲(内存 + localStorage 持久化)。 */\nclass ClientEventStore {\n private events: AnalyticsEvent[] = []\n private loaded = false\n\n private ensureLoaded(): void {\n if (this.loaded) return\n this.loaded = true\n try {\n const raw = localStorage.getItem(EVENTS_KEY)\n this.events = raw ? JSON.parse(raw) as AnalyticsEvent[] : []\n } catch {\n this.events = []\n }\n }\n\n push(event: AnalyticsEvent, maxEvents: number): void {\n this.ensureLoaded()\n this.events.push(event)\n if (this.events.length > maxEvents) this.events = this.events.slice(-maxEvents)\n }\n\n flush(): void {\n try {\n localStorage.setItem(EVENTS_KEY, JSON.stringify(this.events))\n } catch {\n /* quota exceeded / storage unavailable — keep in-memory only */\n }\n }\n\n all(): AnalyticsEvent[] {\n this.ensureLoaded()\n return this.events\n }\n\n clear(): void {\n this.events = []\n try {\n localStorage.removeItem(EVENTS_KEY)\n } catch {\n /* ignore */\n }\n }\n\n buildBatch(sessionId: string): AnalyticsBatch {\n this.ensureLoaded()\n return { v: 1, sessionId, sentAt: Date.now(), events: this.events }\n }\n}\n\nexport const clientStore = new ClientEventStore()\n\nexport function hasConsented(): boolean {\n try {\n return localStorage.getItem(CONSENT_KEY) === '1'\n } catch {\n return false\n }\n}\n\nexport function setConsented(value: boolean): void {\n try {\n localStorage.setItem(CONSENT_KEY, value ? '1' : '0')\n } catch {\n /* ignore */\n }\n}\n","/**\n * browser 半部:全局埋点采集引擎。\n *\n * 通过 document 级(capture)监听,捕获页面生命周期、交互、滚动曝光与热力图\n * 坐标。核心原则:只记录「元素身份 + 位置 + 时间 + 视口」,绝不采集输入内容\n * 或对话正文(input 仅记录长度)。\n */\nimport type { AnalyticsEvent, ElementRef } from '../shared/types.ts'\nimport { clientStore, getSettings, setSessionId } from './storage.ts'\n\nexport interface Tracker {\n dispose(): void\n}\n\nfunction uuid(): string {\n try {\n return crypto.randomUUID()\n } catch {\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n }\n}\n\n/** 参与「元素身份」识别的语义标签;点击落在子节点时向上寻找最近的可交互元素。 */\nconst INTERACTIVE_TAGS = new Set([\n 'BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'OPTION', 'LABEL',\n 'SUMMARY', 'DETAILS', 'LI', 'DIV', 'SPAN', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',\n])\n\nfunction nearestInteractive(el: Element | null): Element | null {\n let cur: Element | null = el\n while (cur && cur !== document.body && cur !== document.documentElement) {\n if (INTERACTIVE_TAGS.has(cur.tagName)) return cur\n cur = cur.parentElement\n }\n return el\n}\n\nfunction textOf(el: Element): string | undefined {\n const t = (el.textContent ?? '').replace(/\\s+/g, ' ').trim()\n if (t === '') return undefined\n return t.length > 40 ? `${t.slice(0, 40)}…` : t\n}\n\n/**\n * 生成稳定的元素身份(优先 data-testid > id > aria-label > role+tag > placeholder/name > tag+text)。\n * 隐私保护:input/textarea/select 与 contenteditable 元素绝不采集其内容/文案。\n */\nfunction identify(el: Element | null): ElementRef | undefined {\n if (!el || el === document.body || el === document.documentElement) return undefined\n const tag = el.tagName.toLowerCase()\n const testId = el.getAttribute('data-testid') ?? el.getAttribute('data-test-id') ?? undefined\n const id = el.id || undefined\n const ariaLabel = el.getAttribute('aria-label') ?? undefined\n const role = el.getAttribute('role') ?? undefined\n const title = el.getAttribute('title') ?? undefined\n const isInputLike = tag === 'input' || tag === 'textarea' || tag === 'select'\n || (el as HTMLElement).isContentEditable === true\n const name = isInputLike ? (el.getAttribute('name') ?? undefined) : undefined\n const placeholder = isInputLike ? (el.getAttribute('placeholder') ?? undefined) : undefined\n const text = isInputLike ? undefined : textOf(el)\n const elementId = testId ?? id ?? ariaLabel\n ?? (role ? `${role}#${tag}` : undefined)\n ?? (placeholder ? `${tag}[${placeholder}]` : undefined)\n ?? (name ? `${tag}#${name}` : undefined)\n ?? `${tag}${text ? `:${text}` : ''}`\n return {\n tag,\n ...(id !== undefined ? { id } : {}),\n ...(role !== undefined ? { role } : {}),\n ...(ariaLabel !== undefined ? { ariaLabel } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(testId !== undefined ? { testId } : {}),\n ...(name !== undefined ? { name } : {}),\n ...(placeholder !== undefined ? { placeholder } : {}),\n ...(text !== undefined ? { text } : {}),\n elementId,\n }\n}\n\nexport function createTracker(): Tracker {\n const sessionId = uuid()\n setSessionId(sessionId)\n\n const page = () => ({ path: window.location.pathname, hash: window.location.hash })\n\n const emit = (type: AnalyticsEvent['type'], partial: Partial<AnalyticsEvent>): void => {\n const s = getSettings()\n if (!s.trackingEnabled) return\n if (s.sampleRate < 1 && Math.random() > s.sampleRate) return\n const event: AnalyticsEvent = {\n v: 1,\n id: uuid(),\n ts: Date.now(),\n sessionId,\n type,\n page: page(),\n ...partial,\n }\n clientStore.push(event, s.maxEvents)\n }\n\n emit('session_start', {})\n emit('page_view', {})\n\n const onClick = (e: MouseEvent): void => {\n emit('click', {\n target: identify(nearestInteractive(e.target as Element | null)),\n position: { x: e.clientX, y: e.clientY },\n })\n }\n\n const onFocusIn = (e: FocusEvent): void => {\n emit('focus', { target: identify(e.target as Element | null) })\n }\n const onFocusOut = (e: FocusEvent): void => {\n emit('blur', { target: identify(e.target as Element | null) })\n }\n\n let lastHover = 0\n const onMouseMove = (e: MouseEvent): void => {\n if (!getSettings().trackMouse) return\n const now = Date.now()\n if (now - lastHover < 250) return\n lastHover = now\n emit('hover', {\n position: { x: e.clientX, y: e.clientY },\n target: identify(nearestInteractive(e.target as Element | null)),\n })\n }\n\n let scrollTimer: number | undefined\n const onScroll = (): void => {\n if (scrollTimer !== undefined) return\n scrollTimer = window.setTimeout(() => {\n scrollTimer = undefined\n const doc = document.documentElement\n const max = doc.scrollHeight - doc.clientHeight\n const depth = max > 0 ? Math.min(1, Math.max(0, doc.scrollTop / max)) : 0\n emit('scroll_depth', { depth: Math.round(depth * 100) / 100 })\n }, 500)\n }\n\n const onVisibility = (): void => {\n const visible = document.visibilityState === 'visible'\n emit('visibility', { visible })\n if (!visible) emit('session_end', {})\n }\n\n let lastInput = 0\n const onInput = (e: Event): void => {\n const now = Date.now()\n if (now - lastInput < 1000) return\n const el = e.target as HTMLInputElement | HTMLTextAreaElement | null\n if (!el || !('value' in el)) return\n lastInput = now\n emit('input', { target: identify(el), inputLength: el.value.length })\n }\n\n const onHashChange = (): void => {\n emit('page_view', {})\n }\n\n const onPageHide = (): void => {\n emit('session_end', {})\n clientStore.flush()\n }\n\n // 元素曝光:IntersectionObserver 上报「哪些面板/控件被用户看到」。\n // 只观察有稳定身份的区域(testid、显式标记、ARIA landmark),控制开销。\n const impressed = new WeakSet<Element>()\n const IMPRESSION_SELECTOR = '[data-testid], [data-impression], [role=\"main\"], [role=\"region\"], [role=\"dialog\"], [role=\"navigation\"], [role=\"complementary\"]'\n const impressionObserver = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting && entry.intersectionRatio >= 0.5 && !impressed.has(entry.target)) {\n impressed.add(entry.target)\n impressionObserver.unobserve(entry.target)\n emit('impression', { target: identify(entry.target) })\n }\n }\n }, { threshold: [0.5] })\n const scanImpressions = (): void => {\n document.querySelectorAll(IMPRESSION_SELECTOR).forEach((el) => {\n if (!impressed.has(el)) impressionObserver.observe(el)\n })\n }\n scanImpressions()\n const impressionTimer = window.setInterval(scanImpressions, 2500)\n\n document.addEventListener('click', onClick, true)\n document.addEventListener('focusin', onFocusIn, true)\n document.addEventListener('focusout', onFocusOut, true)\n document.addEventListener('mousemove', onMouseMove, true)\n document.addEventListener('scroll', onScroll, true)\n document.addEventListener('input', onInput, true)\n document.addEventListener('visibilitychange', onVisibility)\n window.addEventListener('hashchange', onHashChange)\n window.addEventListener('pagehide', onPageHide)\n\n const flushTimer = window.setInterval(() => clientStore.flush(), 3000)\n\n return {\n dispose(): void {\n document.removeEventListener('click', onClick, true)\n document.removeEventListener('focusin', onFocusIn, true)\n document.removeEventListener('focusout', onFocusOut, true)\n document.removeEventListener('mousemove', onMouseMove, true)\n document.removeEventListener('scroll', onScroll, true)\n document.removeEventListener('input', onInput, true)\n document.removeEventListener('visibilitychange', onVisibility)\n window.removeEventListener('hashchange', onHashChange)\n window.removeEventListener('pagehide', onPageHide)\n window.clearInterval(flushTimer)\n window.clearInterval(impressionTimer)\n impressionObserver.disconnect()\n if (scrollTimer !== undefined) window.clearTimeout(scrollTimer)\n clientStore.flush()\n },\n }\n}\n","/**\n * 上传授权弹窗:数据上传到外部/其他软件前,用户确认后才允许访问。\n *\n * 明确告知:将上传什么数据、目的地、以及「不含对话正文与输入内容」的隐私承诺。\n */\nimport type { ReactNode } from 'react'\n\nexport interface ConsentModalProps {\n /** 数据将发送到的目的地(展示给用户)。 */\n destination: string\n /** 即将上传的事件条数。 */\n count: number\n /** 用户取消。 */\n onCancel: () => void\n /** 用户同意并上传。 */\n onAgree: () => void\n}\n\nconst backdrop: React.CSSProperties = {\n position: 'fixed',\n inset: 0,\n background: 'rgba(0, 0, 0, 0.55)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: 20000,\n pointerEvents: 'auto',\n}\n\nconst dialog: React.CSSProperties = {\n width: 420,\n maxWidth: 'calc(100vw - 32px)',\n background: '#1e1f24',\n border: '1px solid #3a3c44',\n borderRadius: 10,\n padding: 18,\n color: '#e6e6e6',\n boxShadow: '0 12px 40px rgba(0,0,0,0.5)',\n fontSize: 13,\n lineHeight: 1.6,\n}\n\nexport default function ConsentModal(props: ConsentModalProps): ReactNode {\n return (\n <div style={backdrop} onClick={props.onCancel}>\n <div style={dialog} onClick={e => e.stopPropagation()}>\n <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 10 }}>数据上传授权</div>\n <p style={{ margin: '0 0 10px' }}>\n 即将把 <b>{props.count}</b> 条使用埋点数据(点击、聚焦、停留、滚动、鼠标位置等)上传到:\n </p>\n <div style={{ background: '#141519', border: '1px solid #3a3c44', borderRadius: 6, padding: '8px 10px', marginBottom: 10, wordBreak: 'break-all', fontFamily: 'monospace', fontSize: 12 }}>\n {props.destination}\n </div>\n <p style={{ margin: '0 0 12px', color: '#aab' }}>\n 隐私承诺:数据仅包含页面控件身份、坐标、时间与视口信息,<b>不包含对话正文、输入内容或个人身份信息</b>。\n 只有在你点击「同意并上传」后,数据才会被发送。\n </p>\n <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>\n <button onClick={props.onCancel} style={btn('#2a2b30', '#ccc')}>取消</button>\n <button onClick={props.onAgree} style={btn('#3a82f7', '#fff')}>同意并上传</button>\n </div>\n </div>\n </div>\n )\n}\n\nfunction btn(bg: string, color: string): React.CSSProperties {\n return {\n background: bg,\n color,\n border: '1px solid #4a4c55',\n borderRadius: 6,\n padding: '6px 14px',\n fontSize: 13,\n cursor: 'pointer',\n }\n}\n","/**\n * 热力图覆盖层 + 统计面板 + 设置 + 上传(含授权弹窗)。\n *\n * 注册进 `shell.overlay`(additive list slot)。热力图数据只来自本地采集,\n * 面板明确标注「本地 / 本人」数据。\n */\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { AnalyticsEvent } from '../shared/types.ts'\nimport {\n clientStore,\n getSessionId,\n getSettings,\n hasConsented,\n saveSettings,\n setConsented,\n type ClientSettings,\n} from './storage.ts'\nimport ConsentModal from './ConsentModal.tsx'\n\ninterface Stats {\n total: number\n clicks: number\n focus: number\n impressions: number\n sessions: number\n dwellMs: number\n maxDepth: number\n top: { elementId: string; count: number }[]\n}\n\nfunction computeStats(events: AnalyticsEvent[]): Stats {\n let clicks = 0\n let focus = 0\n let impressions = 0\n const sessions = new Set<string>()\n const top = new Map<string, number>()\n let dwellMs = 0\n let visibleStart = 0\n let maxDepth = 0\n for (const e of events) {\n sessions.add(e.sessionId)\n if (e.type === 'click') {\n clicks += 1\n if (e.target?.elementId) top.set(e.target.elementId, (top.get(e.target.elementId) ?? 0) + 1)\n }\n if (e.type === 'focus') focus += 1\n if (e.type === 'impression') impressions += 1\n if (e.type === 'visibility') {\n if (e.visible) {\n if (visibleStart === 0) visibleStart = e.ts\n } else if (visibleStart > 0) {\n dwellMs += e.ts - visibleStart\n visibleStart = 0\n }\n }\n if (e.type === 'scroll_depth' && e.depth !== undefined && e.depth > maxDepth) maxDepth = e.depth\n }\n if (visibleStart > 0) dwellMs += Date.now() - visibleStart\n const topList = [...top.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([elementId, count]) => ({ elementId, count }))\n return { total: events.length, clicks, focus, impressions, sessions: sessions.size, dwellMs, maxDepth, top: topList }\n}\n\n/** 热度色带:透明 → 蓝 → 青 → 绿 → 黄 → 红。 */\nfunction heatColor(t: number): string {\n const stops: [number, [number, number, number]][] = [\n [0.0, [0, 60, 255]],\n [0.35, [0, 220, 255]],\n [0.6, [30, 230, 60]],\n [0.8, [255, 220, 0]],\n [1.0, [255, 30, 0]],\n ]\n let a = stops[0]\n let b = stops[stops.length - 1]\n for (let i = 0; i < stops.length - 1; i += 1) {\n if (t >= stops[i][0] && t <= stops[i + 1][0]) {\n a = stops[i]\n b = stops[i + 1]\n break\n }\n }\n const span = b[0] - a[0] || 1\n const k = Math.min(1, Math.max(0, (t - a[0]) / span))\n const r = Math.round(a[1][0] + (b[1][0] - a[1][0]) * k)\n const g = Math.round(a[1][1] + (b[1][1] - a[1][1]) * k)\n const bl = Math.round(a[1][2] + (b[1][2] - a[1][2]) * k)\n const alpha = 0.28 + 0.5 * t\n return `rgba(${r}, ${g}, ${bl}, ${alpha})`\n}\n\nfunction drawHeatmap(canvas: HTMLCanvasElement, events: AnalyticsEvent[]): void {\n const dpr = window.devicePixelRatio || 1\n const w = window.innerWidth\n const h = window.innerHeight\n canvas.width = Math.floor(w * dpr)\n canvas.height = Math.floor(h * dpr)\n canvas.style.width = `${w}px`\n canvas.style.height = `${h}px`\n const ctx = canvas.getContext('2d')\n if (!ctx) return\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n ctx.clearRect(0, 0, w, h)\n\n const CELL = 22\n const cols = Math.ceil(w / CELL)\n const rows = Math.ceil(h / CELL)\n const grid = new Int32Array(rows * cols)\n let max = 0\n for (const e of events) {\n if (!e.position) continue\n const gx = Math.floor(e.position.x / CELL)\n const gy = Math.floor(e.position.y / CELL)\n if (gx < 0 || gy < 0 || gx >= cols || gy >= rows) continue\n const idx = gy * cols + gx\n // 点击比悬停权重更高(点击热力图是核心价值)。\n grid[idx] += e.type === 'click' ? 3 : 1\n if (grid[idx] > max) max = grid[idx]\n }\n if (max === 0) return\n\n for (let gy = 0; gy < rows; gy += 1) {\n for (let gx = 0; gx < cols; gx += 1) {\n const v = grid[gy * cols + gx]\n if (v === 0) continue\n const t = v / max\n ctx.fillStyle = heatColor(t)\n ctx.beginPath()\n const radius = CELL * (0.35 + 0.55 * t)\n ctx.arc(gx * CELL + CELL / 2, gy * CELL + CELL / 2, radius, 0, Math.PI * 2)\n ctx.fill()\n }\n }\n}\n\nfunction fmtDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`\n const s = Math.floor(ms / 1000)\n if (s < 60) return `${s}s`\n const m = Math.floor(s / 60)\n return `${m}m ${s % 60}s`\n}\n\nconst panelStyle: React.CSSProperties = {\n position: 'fixed',\n right: 16,\n bottom: 16,\n width: 300,\n background: '#1e1f24',\n border: '1px solid #3a3c44',\n borderRadius: 10,\n padding: 14,\n color: '#e6e6e6',\n boxShadow: '0 8px 30px rgba(0,0,0,0.45)',\n fontSize: 12,\n zIndex: 10000,\n pointerEvents: 'auto',\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n}\n\nexport default function HeatmapOverlay(): ReactNode {\n const [settings, setSettings] = useState<ClientSettings>(() => getSettings())\n const [open, setOpen] = useState(false)\n const [consentOpen, setConsentOpen] = useState(false)\n const [uploadMsg, setUploadMsg] = useState<string | null>(null)\n const [tick, setTick] = useState(0)\n const canvasRef = useRef<HTMLCanvasElement | null>(null)\n\n // 定时刷新统计 + 热力图重绘。\n useEffect(() => {\n const timer = window.setInterval(() => setTick(v => v + 1), 1000)\n return () => window.clearInterval(timer)\n }, [])\n\n useEffect(() => {\n if (!settings.heatmapMode || !canvasRef.current) return\n const redraw = () => drawHeatmap(canvasRef.current!, clientStore.all())\n redraw()\n const resize = () => redraw()\n window.addEventListener('resize', resize)\n const timer = window.setInterval(redraw, 1000)\n return () => {\n window.removeEventListener('resize', resize)\n window.clearInterval(timer)\n }\n }, [settings.heatmapMode, tick])\n\n const s = computeStats(clientStore.all())\n const destination = settings.uploadEndpoint !== '' ? settings.uploadEndpoint : `${window.location.origin}/dsh-heatmap/ingest`\n\n const update = (patch: Partial<ClientSettings>): void => {\n const next = { ...settings, ...patch }\n setSettings(next)\n saveSettings(next)\n }\n\n const doUpload = useCallback(async () => {\n const batch = clientStore.buildBatch(getSessionId())\n setConsentOpen(false)\n setUploadMsg('上传中…')\n try {\n const res = await fetch(destination, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(batch),\n })\n if (res.ok) setUploadMsg(`已上传 ${batch.events.length} 条埋点数据`)\n else setUploadMsg(`上传失败(HTTP ${res.status})`)\n } catch (err) {\n setUploadMsg(`上传失败:${err instanceof Error ? err.message : String(err)}`)\n }\n }, [destination])\n\n const onUploadClick = (): void => {\n setUploadMsg(null)\n if (settings.consentRequired && !hasConsented()) {\n setConsentOpen(true)\n return\n }\n void doUpload()\n }\n\n const onAgree = (): void => {\n setConsented(true)\n void doUpload()\n }\n\n return (\n <>\n {/* 热力图画布(pointer-events: none,不遮挡交互) */}\n {settings.heatmapMode && (\n <canvas\n ref={canvasRef}\n style={{ position: 'fixed', inset: 0, zIndex: 9000, pointerEvents: 'none' }}\n />\n )}\n\n {/* 悬浮开关按钮 */}\n <button\n onClick={() => setOpen(v => !v)}\n style={{\n position: 'fixed', right: 16, bottom: 16, zIndex: 10001,\n width: 44, height: 44, borderRadius: 22, cursor: 'pointer',\n background: settings.heatmapMode ? '#e0483e' : '#2a2b30',\n color: '#fff', border: '1px solid #4a4c55', fontSize: 18,\n boxShadow: '0 4px 16px rgba(0,0,0,0.4)', pointerEvents: 'auto',\n }}\n title=\"dsh-heatmap 埋点与热力图\"\n >\n 🔥\n </button>\n\n {open && (\n <div style={panelStyle}>\n <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>\n <b style={{ fontSize: 13 }}>dsh-heatmap</b>\n <span style={{ fontSize: 10, background: '#123', color: '#9cf', padding: '1px 6px', borderRadius: 8 }}>\n 本地 / 本人数据\n </span>\n </div>\n\n <div style={{ background: '#141519', borderRadius: 6, padding: '8px 10px', display: 'flex', flexDirection: 'column', gap: 3 }}>\n <Row label=\"事件总数\" value={String(s.total)} />\n <Row label=\"点击\" value={String(s.clicks)} />\n <Row label=\"聚焦\" value={String(s.focus)} />\n <Row label=\"曝光\" value={String(s.impressions)} />\n <Row label=\"会话数\" value={String(s.sessions)} />\n <Row label=\"停留时长\" value={fmtDuration(s.dwellMs)} />\n <Row label=\"最大滚动\" value={`${Math.round(s.maxDepth * 100)}%`} />\n </div>\n\n {s.top.length > 0 && (\n <div style={{ background: '#141519', borderRadius: 6, padding: '8px 10px' }}>\n <div style={{ marginBottom: 4, fontWeight: 600 }}>点击最多的控件</div>\n {s.top.map(t => (\n <div key={t.elementId} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: '#bbb' }}>\n <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 200 }}>{t.elementId}</span>\n <span>{t.count}</span>\n </div>\n ))}\n </div>\n )}\n\n <Toggle label=\"热力图模式\" checked={settings.heatmapMode} onChange={v => update({ heatmapMode: v })} />\n <Toggle label=\"采集埋点\" checked={settings.trackingEnabled} onChange={v => update({ trackingEnabled: v })} />\n <Toggle label=\"鼠标热力图\" checked={settings.trackMouse} onChange={v => update({ trackMouse: v })} />\n <Toggle label=\"上传需授权\" checked={settings.consentRequired} onChange={v => update({ consentRequired: v })} />\n\n <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>\n <label style={{ fontSize: 11, color: '#aaa' }}>上传地址(留空 = 本地收集器)</label>\n <input\n value={settings.uploadEndpoint}\n onChange={e => update({ uploadEndpoint: e.target.value })}\n placeholder={destination}\n style={{ fontSize: 11, padding: '4px 6px', background: '#141519', color: '#ddd', border: '1px solid #3a3c44', borderRadius: 5 }}\n />\n </div>\n\n {uploadMsg && <div style={{ fontSize: 11, color: uploadMsg.startsWith('已上传') ? '#8e8' : '#e88' }}>{uploadMsg}</div>}\n\n <div style={{ display: 'flex', gap: 6 }}>\n <button onClick={onUploadClick} style={btn('#3a82f7', '#fff', true)}>上传数据</button>\n <button onClick={() => { clientStore.clear(); setTick(v => v + 1) }} style={btn('#2a2b30', '#ccc')}>清除本地</button>\n </div>\n </div>\n )}\n\n {consentOpen && (\n <ConsentModal\n destination={destination}\n count={clientStore.all().length}\n onCancel={() => setConsentOpen(false)}\n onAgree={onAgree}\n />\n )}\n </>\n )\n}\n\nfunction Row({ label, value }: { label: string; value: string }): ReactNode {\n return (\n <div style={{ display: 'flex', justifyContent: 'space-between' }}>\n <span style={{ color: '#999' }}>{label}</span>\n <span style={{ fontWeight: 600 }}>{value}</span>\n </div>\n )\n}\n\nfunction Toggle({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }): ReactNode {\n return (\n <label style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}>\n <span>{label}</span>\n <input type=\"checkbox\" checked={checked} onChange={e => onChange(e.target.checked)} />\n </label>\n )\n}\n\nfunction btn(bg: string, color: string, primary = false): React.CSSProperties {\n return {\n flex: primary ? 1.4 : 1,\n background: bg,\n color,\n border: '1px solid #4a4c55',\n borderRadius: 6,\n padding: '6px 10px',\n fontSize: 12,\n cursor: 'pointer',\n }\n}\n","/**\n * dsh-heatmap — browser 半部。\n *\n * 1) 启动全局埋点采集引擎(document 级监听,卸载时全部移除);\n * 2) 把热力图覆盖层(含统计面板 + 上传授权弹窗)注册进 `shell.overlay`。\n *\n * 数据只存在本地,热力图展示「本地 / 本人」数据,无需授权;上传动作才触发授权弹窗。\n */\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\n// 仅引入类型,把 `shell.overlay` 等 SlotMap 键合并进类型系统(type-only,构建时被擦除)。\nimport type {} from '@deepseek-ai/dsh-client-ui-layout/client'\n\nimport { createTracker } from './tracker.ts'\nimport HeatmapOverlay from './HeatmapOverlay.tsx'\n\nexport const inject = ['slots']\n\nexport function apply(ctx: ClientContext): void {\n // 埋点采集引擎:dispose 时移除所有 document/window 监听并 flush。\n const tracker = createTracker()\n ctx.effect(() => () => tracker.dispose(), 'dsh-heatmap: tracker')\n\n // 热力图覆盖层(list slot,additive,不遮挡其他 shell.overlay 条目)。\n ctx.slots.register(\n { name: 'shell.overlay', id: 'dsh-heatmap-overlay', order: 1000 },\n HeatmapOverlay,\n )\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-heatmap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DeepSeek Harness 页面埋点与热力图分析插件:科学埋点采集(点击/聚焦/停留/滚动/曝光)、本地热力图与统计面板、预留 CLI 与外部上传接口、上传前用户授权弹窗。",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"plugin",
|
|
10
|
+
"cordis",
|
|
11
|
+
"analytics",
|
|
12
|
+
"heatmap",
|
|
13
|
+
"telemetry"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "src/index.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./src/index.ts",
|
|
20
|
+
"./client": {
|
|
21
|
+
"default": "./lib/client.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"lib",
|
|
28
|
+
"examples",
|
|
29
|
+
"cordis.patch.yml",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"dsh": {
|
|
33
|
+
"bundle": {
|
|
34
|
+
"patch": "./cordis.patch.yml"
|
|
35
|
+
},
|
|
36
|
+
"client": {
|
|
37
|
+
"platform": "web",
|
|
38
|
+
"inject": [
|
|
39
|
+
"slots"
|
|
40
|
+
],
|
|
41
|
+
"external": []
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"bundle": "tsdown",
|
|
46
|
+
"build": "npm run bundle",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=22"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
54
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
55
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
59
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-layout": "^0.1.1-rc.2",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
65
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-layout": "^0.1.1-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
|
|
68
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
69
|
+
"@types/react": "~18.3.1",
|
|
70
|
+
"react": "^18.2.0",
|
|
71
|
+
"tsdown": "0.22.2",
|
|
72
|
+
"typescript": "^5.5.0"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上传授权弹窗:数据上传到外部/其他软件前,用户确认后才允许访问。
|
|
3
|
+
*
|
|
4
|
+
* 明确告知:将上传什么数据、目的地、以及「不含对话正文与输入内容」的隐私承诺。
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
|
|
8
|
+
export interface ConsentModalProps {
|
|
9
|
+
/** 数据将发送到的目的地(展示给用户)。 */
|
|
10
|
+
destination: string
|
|
11
|
+
/** 即将上传的事件条数。 */
|
|
12
|
+
count: number
|
|
13
|
+
/** 用户取消。 */
|
|
14
|
+
onCancel: () => void
|
|
15
|
+
/** 用户同意并上传。 */
|
|
16
|
+
onAgree: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const backdrop: React.CSSProperties = {
|
|
20
|
+
position: 'fixed',
|
|
21
|
+
inset: 0,
|
|
22
|
+
background: 'rgba(0, 0, 0, 0.55)',
|
|
23
|
+
display: 'flex',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
justifyContent: 'center',
|
|
26
|
+
zIndex: 20000,
|
|
27
|
+
pointerEvents: 'auto',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const dialog: React.CSSProperties = {
|
|
31
|
+
width: 420,
|
|
32
|
+
maxWidth: 'calc(100vw - 32px)',
|
|
33
|
+
background: '#1e1f24',
|
|
34
|
+
border: '1px solid #3a3c44',
|
|
35
|
+
borderRadius: 10,
|
|
36
|
+
padding: 18,
|
|
37
|
+
color: '#e6e6e6',
|
|
38
|
+
boxShadow: '0 12px 40px rgba(0,0,0,0.5)',
|
|
39
|
+
fontSize: 13,
|
|
40
|
+
lineHeight: 1.6,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function ConsentModal(props: ConsentModalProps): ReactNode {
|
|
44
|
+
return (
|
|
45
|
+
<div style={backdrop} onClick={props.onCancel}>
|
|
46
|
+
<div style={dialog} onClick={e => e.stopPropagation()}>
|
|
47
|
+
<div style={{ fontSize: 15, fontWeight: 700, marginBottom: 10 }}>数据上传授权</div>
|
|
48
|
+
<p style={{ margin: '0 0 10px' }}>
|
|
49
|
+
即将把 <b>{props.count}</b> 条使用埋点数据(点击、聚焦、停留、滚动、鼠标位置等)上传到:
|
|
50
|
+
</p>
|
|
51
|
+
<div style={{ background: '#141519', border: '1px solid #3a3c44', borderRadius: 6, padding: '8px 10px', marginBottom: 10, wordBreak: 'break-all', fontFamily: 'monospace', fontSize: 12 }}>
|
|
52
|
+
{props.destination}
|
|
53
|
+
</div>
|
|
54
|
+
<p style={{ margin: '0 0 12px', color: '#aab' }}>
|
|
55
|
+
隐私承诺:数据仅包含页面控件身份、坐标、时间与视口信息,<b>不包含对话正文、输入内容或个人身份信息</b>。
|
|
56
|
+
只有在你点击「同意并上传」后,数据才会被发送。
|
|
57
|
+
</p>
|
|
58
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
|
59
|
+
<button onClick={props.onCancel} style={btn('#2a2b30', '#ccc')}>取消</button>
|
|
60
|
+
<button onClick={props.onAgree} style={btn('#3a82f7', '#fff')}>同意并上传</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function btn(bg: string, color: string): React.CSSProperties {
|
|
68
|
+
return {
|
|
69
|
+
background: bg,
|
|
70
|
+
color,
|
|
71
|
+
border: '1px solid #4a4c55',
|
|
72
|
+
borderRadius: 6,
|
|
73
|
+
padding: '6px 14px',
|
|
74
|
+
fontSize: 13,
|
|
75
|
+
cursor: 'pointer',
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 热力图覆盖层 + 统计面板 + 设置 + 上传(含授权弹窗)。
|
|
3
|
+
*
|
|
4
|
+
* 注册进 `shell.overlay`(additive list slot)。热力图数据只来自本地采集,
|
|
5
|
+
* 面板明确标注「本地 / 本人」数据。
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
8
|
+
import type { ReactNode } from 'react'
|
|
9
|
+
import type { AnalyticsEvent } from '../shared/types.ts'
|
|
10
|
+
import {
|
|
11
|
+
clientStore,
|
|
12
|
+
getSessionId,
|
|
13
|
+
getSettings,
|
|
14
|
+
hasConsented,
|
|
15
|
+
saveSettings,
|
|
16
|
+
setConsented,
|
|
17
|
+
type ClientSettings,
|
|
18
|
+
} from './storage.ts'
|
|
19
|
+
import ConsentModal from './ConsentModal.tsx'
|
|
20
|
+
|
|
21
|
+
interface Stats {
|
|
22
|
+
total: number
|
|
23
|
+
clicks: number
|
|
24
|
+
focus: number
|
|
25
|
+
impressions: number
|
|
26
|
+
sessions: number
|
|
27
|
+
dwellMs: number
|
|
28
|
+
maxDepth: number
|
|
29
|
+
top: { elementId: string; count: number }[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function computeStats(events: AnalyticsEvent[]): Stats {
|
|
33
|
+
let clicks = 0
|
|
34
|
+
let focus = 0
|
|
35
|
+
let impressions = 0
|
|
36
|
+
const sessions = new Set<string>()
|
|
37
|
+
const top = new Map<string, number>()
|
|
38
|
+
let dwellMs = 0
|
|
39
|
+
let visibleStart = 0
|
|
40
|
+
let maxDepth = 0
|
|
41
|
+
for (const e of events) {
|
|
42
|
+
sessions.add(e.sessionId)
|
|
43
|
+
if (e.type === 'click') {
|
|
44
|
+
clicks += 1
|
|
45
|
+
if (e.target?.elementId) top.set(e.target.elementId, (top.get(e.target.elementId) ?? 0) + 1)
|
|
46
|
+
}
|
|
47
|
+
if (e.type === 'focus') focus += 1
|
|
48
|
+
if (e.type === 'impression') impressions += 1
|
|
49
|
+
if (e.type === 'visibility') {
|
|
50
|
+
if (e.visible) {
|
|
51
|
+
if (visibleStart === 0) visibleStart = e.ts
|
|
52
|
+
} else if (visibleStart > 0) {
|
|
53
|
+
dwellMs += e.ts - visibleStart
|
|
54
|
+
visibleStart = 0
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (e.type === 'scroll_depth' && e.depth !== undefined && e.depth > maxDepth) maxDepth = e.depth
|
|
58
|
+
}
|
|
59
|
+
if (visibleStart > 0) dwellMs += Date.now() - visibleStart
|
|
60
|
+
const topList = [...top.entries()]
|
|
61
|
+
.sort((a, b) => b[1] - a[1])
|
|
62
|
+
.slice(0, 5)
|
|
63
|
+
.map(([elementId, count]) => ({ elementId, count }))
|
|
64
|
+
return { total: events.length, clicks, focus, impressions, sessions: sessions.size, dwellMs, maxDepth, top: topList }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 热度色带:透明 → 蓝 → 青 → 绿 → 黄 → 红。 */
|
|
68
|
+
function heatColor(t: number): string {
|
|
69
|
+
const stops: [number, [number, number, number]][] = [
|
|
70
|
+
[0.0, [0, 60, 255]],
|
|
71
|
+
[0.35, [0, 220, 255]],
|
|
72
|
+
[0.6, [30, 230, 60]],
|
|
73
|
+
[0.8, [255, 220, 0]],
|
|
74
|
+
[1.0, [255, 30, 0]],
|
|
75
|
+
]
|
|
76
|
+
let a = stops[0]
|
|
77
|
+
let b = stops[stops.length - 1]
|
|
78
|
+
for (let i = 0; i < stops.length - 1; i += 1) {
|
|
79
|
+
if (t >= stops[i][0] && t <= stops[i + 1][0]) {
|
|
80
|
+
a = stops[i]
|
|
81
|
+
b = stops[i + 1]
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const span = b[0] - a[0] || 1
|
|
86
|
+
const k = Math.min(1, Math.max(0, (t - a[0]) / span))
|
|
87
|
+
const r = Math.round(a[1][0] + (b[1][0] - a[1][0]) * k)
|
|
88
|
+
const g = Math.round(a[1][1] + (b[1][1] - a[1][1]) * k)
|
|
89
|
+
const bl = Math.round(a[1][2] + (b[1][2] - a[1][2]) * k)
|
|
90
|
+
const alpha = 0.28 + 0.5 * t
|
|
91
|
+
return `rgba(${r}, ${g}, ${bl}, ${alpha})`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function drawHeatmap(canvas: HTMLCanvasElement, events: AnalyticsEvent[]): void {
|
|
95
|
+
const dpr = window.devicePixelRatio || 1
|
|
96
|
+
const w = window.innerWidth
|
|
97
|
+
const h = window.innerHeight
|
|
98
|
+
canvas.width = Math.floor(w * dpr)
|
|
99
|
+
canvas.height = Math.floor(h * dpr)
|
|
100
|
+
canvas.style.width = `${w}px`
|
|
101
|
+
canvas.style.height = `${h}px`
|
|
102
|
+
const ctx = canvas.getContext('2d')
|
|
103
|
+
if (!ctx) return
|
|
104
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
105
|
+
ctx.clearRect(0, 0, w, h)
|
|
106
|
+
|
|
107
|
+
const CELL = 22
|
|
108
|
+
const cols = Math.ceil(w / CELL)
|
|
109
|
+
const rows = Math.ceil(h / CELL)
|
|
110
|
+
const grid = new Int32Array(rows * cols)
|
|
111
|
+
let max = 0
|
|
112
|
+
for (const e of events) {
|
|
113
|
+
if (!e.position) continue
|
|
114
|
+
const gx = Math.floor(e.position.x / CELL)
|
|
115
|
+
const gy = Math.floor(e.position.y / CELL)
|
|
116
|
+
if (gx < 0 || gy < 0 || gx >= cols || gy >= rows) continue
|
|
117
|
+
const idx = gy * cols + gx
|
|
118
|
+
// 点击比悬停权重更高(点击热力图是核心价值)。
|
|
119
|
+
grid[idx] += e.type === 'click' ? 3 : 1
|
|
120
|
+
if (grid[idx] > max) max = grid[idx]
|
|
121
|
+
}
|
|
122
|
+
if (max === 0) return
|
|
123
|
+
|
|
124
|
+
for (let gy = 0; gy < rows; gy += 1) {
|
|
125
|
+
for (let gx = 0; gx < cols; gx += 1) {
|
|
126
|
+
const v = grid[gy * cols + gx]
|
|
127
|
+
if (v === 0) continue
|
|
128
|
+
const t = v / max
|
|
129
|
+
ctx.fillStyle = heatColor(t)
|
|
130
|
+
ctx.beginPath()
|
|
131
|
+
const radius = CELL * (0.35 + 0.55 * t)
|
|
132
|
+
ctx.arc(gx * CELL + CELL / 2, gy * CELL + CELL / 2, radius, 0, Math.PI * 2)
|
|
133
|
+
ctx.fill()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fmtDuration(ms: number): string {
|
|
139
|
+
if (ms < 1000) return `${ms}ms`
|
|
140
|
+
const s = Math.floor(ms / 1000)
|
|
141
|
+
if (s < 60) return `${s}s`
|
|
142
|
+
const m = Math.floor(s / 60)
|
|
143
|
+
return `${m}m ${s % 60}s`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const panelStyle: React.CSSProperties = {
|
|
147
|
+
position: 'fixed',
|
|
148
|
+
right: 16,
|
|
149
|
+
bottom: 16,
|
|
150
|
+
width: 300,
|
|
151
|
+
background: '#1e1f24',
|
|
152
|
+
border: '1px solid #3a3c44',
|
|
153
|
+
borderRadius: 10,
|
|
154
|
+
padding: 14,
|
|
155
|
+
color: '#e6e6e6',
|
|
156
|
+
boxShadow: '0 8px 30px rgba(0,0,0,0.45)',
|
|
157
|
+
fontSize: 12,
|
|
158
|
+
zIndex: 10000,
|
|
159
|
+
pointerEvents: 'auto',
|
|
160
|
+
display: 'flex',
|
|
161
|
+
flexDirection: 'column',
|
|
162
|
+
gap: 8,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export default function HeatmapOverlay(): ReactNode {
|
|
166
|
+
const [settings, setSettings] = useState<ClientSettings>(() => getSettings())
|
|
167
|
+
const [open, setOpen] = useState(false)
|
|
168
|
+
const [consentOpen, setConsentOpen] = useState(false)
|
|
169
|
+
const [uploadMsg, setUploadMsg] = useState<string | null>(null)
|
|
170
|
+
const [tick, setTick] = useState(0)
|
|
171
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
|
172
|
+
|
|
173
|
+
// 定时刷新统计 + 热力图重绘。
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const timer = window.setInterval(() => setTick(v => v + 1), 1000)
|
|
176
|
+
return () => window.clearInterval(timer)
|
|
177
|
+
}, [])
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!settings.heatmapMode || !canvasRef.current) return
|
|
181
|
+
const redraw = () => drawHeatmap(canvasRef.current!, clientStore.all())
|
|
182
|
+
redraw()
|
|
183
|
+
const resize = () => redraw()
|
|
184
|
+
window.addEventListener('resize', resize)
|
|
185
|
+
const timer = window.setInterval(redraw, 1000)
|
|
186
|
+
return () => {
|
|
187
|
+
window.removeEventListener('resize', resize)
|
|
188
|
+
window.clearInterval(timer)
|
|
189
|
+
}
|
|
190
|
+
}, [settings.heatmapMode, tick])
|
|
191
|
+
|
|
192
|
+
const s = computeStats(clientStore.all())
|
|
193
|
+
const destination = settings.uploadEndpoint !== '' ? settings.uploadEndpoint : `${window.location.origin}/dsh-heatmap/ingest`
|
|
194
|
+
|
|
195
|
+
const update = (patch: Partial<ClientSettings>): void => {
|
|
196
|
+
const next = { ...settings, ...patch }
|
|
197
|
+
setSettings(next)
|
|
198
|
+
saveSettings(next)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const doUpload = useCallback(async () => {
|
|
202
|
+
const batch = clientStore.buildBatch(getSessionId())
|
|
203
|
+
setConsentOpen(false)
|
|
204
|
+
setUploadMsg('上传中…')
|
|
205
|
+
try {
|
|
206
|
+
const res = await fetch(destination, {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
headers: { 'content-type': 'application/json' },
|
|
209
|
+
body: JSON.stringify(batch),
|
|
210
|
+
})
|
|
211
|
+
if (res.ok) setUploadMsg(`已上传 ${batch.events.length} 条埋点数据`)
|
|
212
|
+
else setUploadMsg(`上传失败(HTTP ${res.status})`)
|
|
213
|
+
} catch (err) {
|
|
214
|
+
setUploadMsg(`上传失败:${err instanceof Error ? err.message : String(err)}`)
|
|
215
|
+
}
|
|
216
|
+
}, [destination])
|
|
217
|
+
|
|
218
|
+
const onUploadClick = (): void => {
|
|
219
|
+
setUploadMsg(null)
|
|
220
|
+
if (settings.consentRequired && !hasConsented()) {
|
|
221
|
+
setConsentOpen(true)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
void doUpload()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const onAgree = (): void => {
|
|
228
|
+
setConsented(true)
|
|
229
|
+
void doUpload()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<>
|
|
234
|
+
{/* 热力图画布(pointer-events: none,不遮挡交互) */}
|
|
235
|
+
{settings.heatmapMode && (
|
|
236
|
+
<canvas
|
|
237
|
+
ref={canvasRef}
|
|
238
|
+
style={{ position: 'fixed', inset: 0, zIndex: 9000, pointerEvents: 'none' }}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{/* 悬浮开关按钮 */}
|
|
243
|
+
<button
|
|
244
|
+
onClick={() => setOpen(v => !v)}
|
|
245
|
+
style={{
|
|
246
|
+
position: 'fixed', right: 16, bottom: 16, zIndex: 10001,
|
|
247
|
+
width: 44, height: 44, borderRadius: 22, cursor: 'pointer',
|
|
248
|
+
background: settings.heatmapMode ? '#e0483e' : '#2a2b30',
|
|
249
|
+
color: '#fff', border: '1px solid #4a4c55', fontSize: 18,
|
|
250
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.4)', pointerEvents: 'auto',
|
|
251
|
+
}}
|
|
252
|
+
title="dsh-heatmap 埋点与热力图"
|
|
253
|
+
>
|
|
254
|
+
🔥
|
|
255
|
+
</button>
|
|
256
|
+
|
|
257
|
+
{open && (
|
|
258
|
+
<div style={panelStyle}>
|
|
259
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
260
|
+
<b style={{ fontSize: 13 }}>dsh-heatmap</b>
|
|
261
|
+
<span style={{ fontSize: 10, background: '#123', color: '#9cf', padding: '1px 6px', borderRadius: 8 }}>
|
|
262
|
+
本地 / 本人数据
|
|
263
|
+
</span>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div style={{ background: '#141519', borderRadius: 6, padding: '8px 10px', display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
267
|
+
<Row label="事件总数" value={String(s.total)} />
|
|
268
|
+
<Row label="点击" value={String(s.clicks)} />
|
|
269
|
+
<Row label="聚焦" value={String(s.focus)} />
|
|
270
|
+
<Row label="曝光" value={String(s.impressions)} />
|
|
271
|
+
<Row label="会话数" value={String(s.sessions)} />
|
|
272
|
+
<Row label="停留时长" value={fmtDuration(s.dwellMs)} />
|
|
273
|
+
<Row label="最大滚动" value={`${Math.round(s.maxDepth * 100)}%`} />
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{s.top.length > 0 && (
|
|
277
|
+
<div style={{ background: '#141519', borderRadius: 6, padding: '8px 10px' }}>
|
|
278
|
+
<div style={{ marginBottom: 4, fontWeight: 600 }}>点击最多的控件</div>
|
|
279
|
+
{s.top.map(t => (
|
|
280
|
+
<div key={t.elementId} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: '#bbb' }}>
|
|
281
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 200 }}>{t.elementId}</span>
|
|
282
|
+
<span>{t.count}</span>
|
|
283
|
+
</div>
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
<Toggle label="热力图模式" checked={settings.heatmapMode} onChange={v => update({ heatmapMode: v })} />
|
|
289
|
+
<Toggle label="采集埋点" checked={settings.trackingEnabled} onChange={v => update({ trackingEnabled: v })} />
|
|
290
|
+
<Toggle label="鼠标热力图" checked={settings.trackMouse} onChange={v => update({ trackMouse: v })} />
|
|
291
|
+
<Toggle label="上传需授权" checked={settings.consentRequired} onChange={v => update({ consentRequired: v })} />
|
|
292
|
+
|
|
293
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
294
|
+
<label style={{ fontSize: 11, color: '#aaa' }}>上传地址(留空 = 本地收集器)</label>
|
|
295
|
+
<input
|
|
296
|
+
value={settings.uploadEndpoint}
|
|
297
|
+
onChange={e => update({ uploadEndpoint: e.target.value })}
|
|
298
|
+
placeholder={destination}
|
|
299
|
+
style={{ fontSize: 11, padding: '4px 6px', background: '#141519', color: '#ddd', border: '1px solid #3a3c44', borderRadius: 5 }}
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
{uploadMsg && <div style={{ fontSize: 11, color: uploadMsg.startsWith('已上传') ? '#8e8' : '#e88' }}>{uploadMsg}</div>}
|
|
304
|
+
|
|
305
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
306
|
+
<button onClick={onUploadClick} style={btn('#3a82f7', '#fff', true)}>上传数据</button>
|
|
307
|
+
<button onClick={() => { clientStore.clear(); setTick(v => v + 1) }} style={btn('#2a2b30', '#ccc')}>清除本地</button>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
{consentOpen && (
|
|
313
|
+
<ConsentModal
|
|
314
|
+
destination={destination}
|
|
315
|
+
count={clientStore.all().length}
|
|
316
|
+
onCancel={() => setConsentOpen(false)}
|
|
317
|
+
onAgree={onAgree}
|
|
318
|
+
/>
|
|
319
|
+
)}
|
|
320
|
+
</>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function Row({ label, value }: { label: string; value: string }): ReactNode {
|
|
325
|
+
return (
|
|
326
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
327
|
+
<span style={{ color: '#999' }}>{label}</span>
|
|
328
|
+
<span style={{ fontWeight: 600 }}>{value}</span>
|
|
329
|
+
</div>
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function Toggle({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }): ReactNode {
|
|
334
|
+
return (
|
|
335
|
+
<label style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}>
|
|
336
|
+
<span>{label}</span>
|
|
337
|
+
<input type="checkbox" checked={checked} onChange={e => onChange(e.target.checked)} />
|
|
338
|
+
</label>
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function btn(bg: string, color: string, primary = false): React.CSSProperties {
|
|
343
|
+
return {
|
|
344
|
+
flex: primary ? 1.4 : 1,
|
|
345
|
+
background: bg,
|
|
346
|
+
color,
|
|
347
|
+
border: '1px solid #4a4c55',
|
|
348
|
+
borderRadius: 6,
|
|
349
|
+
padding: '6px 10px',
|
|
350
|
+
fontSize: 12,
|
|
351
|
+
cursor: 'pointer',
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-heatmap — browser 半部。
|
|
3
|
+
*
|
|
4
|
+
* 1) 启动全局埋点采集引擎(document 级监听,卸载时全部移除);
|
|
5
|
+
* 2) 把热力图覆盖层(含统计面板 + 上传授权弹窗)注册进 `shell.overlay`。
|
|
6
|
+
*
|
|
7
|
+
* 数据只存在本地,热力图展示「本地 / 本人」数据,无需授权;上传动作才触发授权弹窗。
|
|
8
|
+
*/
|
|
9
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
10
|
+
// 仅引入类型,把 `shell.overlay` 等 SlotMap 键合并进类型系统(type-only,构建时被擦除)。
|
|
11
|
+
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
|
12
|
+
|
|
13
|
+
import { createTracker } from './tracker.ts'
|
|
14
|
+
import HeatmapOverlay from './HeatmapOverlay.tsx'
|
|
15
|
+
|
|
16
|
+
export const inject = ['slots']
|
|
17
|
+
|
|
18
|
+
export function apply(ctx: ClientContext): void {
|
|
19
|
+
// 埋点采集引擎:dispose 时移除所有 document/window 监听并 flush。
|
|
20
|
+
const tracker = createTracker()
|
|
21
|
+
ctx.effect(() => () => tracker.dispose(), 'dsh-heatmap: tracker')
|
|
22
|
+
|
|
23
|
+
// 热力图覆盖层(list slot,additive,不遮挡其他 shell.overlay 条目)。
|
|
24
|
+
ctx.slots.register(
|
|
25
|
+
{ name: 'shell.overlay', id: 'dsh-heatmap-overlay', order: 1000 },
|
|
26
|
+
HeatmapOverlay,
|
|
27
|
+
)
|
|
28
|
+
}
|