@neowhale/storefront 0.2.32 → 0.2.34
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/dist/chunk-AKWSW7DW.js +329 -0
- package/dist/chunk-AKWSW7DW.js.map +1 -0
- package/dist/chunk-HOQKBNYB.cjs +331 -0
- package/dist/chunk-HOQKBNYB.cjs.map +1 -0
- package/dist/landing.global.js +1323 -0
- package/dist/react/index.cjs +132 -334
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +2 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +131 -333
- package/dist/react/index.js.map +1 -1
- package/dist/tracker-34GTXU54.js +3 -0
- package/dist/tracker-34GTXU54.js.map +1 -0
- package/dist/tracker-WYKTEQ4F.cjs +12 -0
- package/dist/tracker-WYKTEQ4F.cjs.map +1 -0
- package/package.json +4 -1
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// src/behavioral/tracker.ts
|
|
2
|
+
var SCROLL_MILESTONES = [25, 50, 75, 100];
|
|
3
|
+
var TIME_MILESTONES = [30, 60, 120, 300];
|
|
4
|
+
var MOUSE_THROTTLE_MS = 200;
|
|
5
|
+
var MOUSE_BUFFER_MAX = 100;
|
|
6
|
+
var RAGE_CLICK_COUNT = 3;
|
|
7
|
+
var RAGE_CLICK_RADIUS = 50;
|
|
8
|
+
var RAGE_CLICK_WINDOW_MS = 2e3;
|
|
9
|
+
var MAX_CLICK_HISTORY = 10;
|
|
10
|
+
var BehavioralTracker = class {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.buffer = [];
|
|
13
|
+
this.pageUrl = "";
|
|
14
|
+
this.pagePath = "";
|
|
15
|
+
this.flushTimer = null;
|
|
16
|
+
this.scrollMilestones = /* @__PURE__ */ new Set();
|
|
17
|
+
this.timeMilestones = /* @__PURE__ */ new Set();
|
|
18
|
+
this.timeTimers = [];
|
|
19
|
+
this.exitIntentFired = false;
|
|
20
|
+
this.startTime = 0;
|
|
21
|
+
this.clickHistory = [];
|
|
22
|
+
this.mouseBuffer = [];
|
|
23
|
+
this.lastMouseTime = 0;
|
|
24
|
+
this.listeners = [];
|
|
25
|
+
this.observer = null;
|
|
26
|
+
this.sentinels = [];
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Event handlers (arrow functions for stable `this`)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
this.handleClick = (e) => {
|
|
31
|
+
const me = e;
|
|
32
|
+
const target = me.target;
|
|
33
|
+
if (!target) return;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const x = me.clientX;
|
|
36
|
+
const y = me.clientY;
|
|
37
|
+
this.clickHistory.push({ x, y, t: now });
|
|
38
|
+
if (this.clickHistory.length > MAX_CLICK_HISTORY) {
|
|
39
|
+
this.clickHistory.shift();
|
|
40
|
+
}
|
|
41
|
+
const tag = target.tagName?.toLowerCase() ?? "";
|
|
42
|
+
const rawText = target.textContent ?? "";
|
|
43
|
+
const text = rawText.trim().slice(0, 50);
|
|
44
|
+
this.push({
|
|
45
|
+
data_type: "click",
|
|
46
|
+
data: {
|
|
47
|
+
tag,
|
|
48
|
+
text,
|
|
49
|
+
selector: this.getSelector(target),
|
|
50
|
+
x,
|
|
51
|
+
y,
|
|
52
|
+
timestamp: now
|
|
53
|
+
},
|
|
54
|
+
page_url: this.pageUrl,
|
|
55
|
+
page_path: this.pagePath
|
|
56
|
+
});
|
|
57
|
+
this.detectRageClick(x, y, now);
|
|
58
|
+
};
|
|
59
|
+
this.handleMouseMove = (e) => {
|
|
60
|
+
const me = e;
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
if (now - this.lastMouseTime < MOUSE_THROTTLE_MS) return;
|
|
63
|
+
this.lastMouseTime = now;
|
|
64
|
+
this.mouseBuffer.push({ x: me.clientX, y: me.clientY, t: now });
|
|
65
|
+
if (this.mouseBuffer.length > MOUSE_BUFFER_MAX) {
|
|
66
|
+
this.mouseBuffer.shift();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
this.handleMouseOut = (e) => {
|
|
70
|
+
const me = e;
|
|
71
|
+
if (this.exitIntentFired) return;
|
|
72
|
+
if (me.clientY > 0) return;
|
|
73
|
+
if (me.relatedTarget !== null) return;
|
|
74
|
+
this.exitIntentFired = true;
|
|
75
|
+
this.push({
|
|
76
|
+
data_type: "exit_intent",
|
|
77
|
+
data: {
|
|
78
|
+
time_on_page_ms: Date.now() - this.startTime,
|
|
79
|
+
timestamp: Date.now()
|
|
80
|
+
},
|
|
81
|
+
page_url: this.pageUrl,
|
|
82
|
+
page_path: this.pagePath
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
this.handleCopy = () => {
|
|
86
|
+
const selection = window.getSelection();
|
|
87
|
+
const length = selection?.toString().length ?? 0;
|
|
88
|
+
this.push({
|
|
89
|
+
data_type: "copy",
|
|
90
|
+
data: {
|
|
91
|
+
text_length: length,
|
|
92
|
+
timestamp: Date.now()
|
|
93
|
+
},
|
|
94
|
+
page_url: this.pageUrl,
|
|
95
|
+
page_path: this.pagePath
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
this.handleVisibilityChange = () => {
|
|
99
|
+
if (document.visibilityState !== "hidden") return;
|
|
100
|
+
const timeSpent = Date.now() - this.startTime;
|
|
101
|
+
this.push({
|
|
102
|
+
data_type: "page_exit",
|
|
103
|
+
data: {
|
|
104
|
+
time_spent_ms: timeSpent,
|
|
105
|
+
timestamp: Date.now()
|
|
106
|
+
},
|
|
107
|
+
page_url: this.pageUrl,
|
|
108
|
+
page_path: this.pagePath
|
|
109
|
+
});
|
|
110
|
+
this.flushMouseBuffer();
|
|
111
|
+
this.flush();
|
|
112
|
+
};
|
|
113
|
+
this.config = {
|
|
114
|
+
sendBatch: config.sendBatch,
|
|
115
|
+
sessionId: config.sessionId,
|
|
116
|
+
visitorId: config.visitorId,
|
|
117
|
+
flushIntervalMs: config.flushIntervalMs ?? 1e4,
|
|
118
|
+
maxBufferSize: config.maxBufferSize ?? 500
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
start() {
|
|
122
|
+
this.startTime = Date.now();
|
|
123
|
+
this.addListener(document, "click", this.handleClick);
|
|
124
|
+
this.addListener(document, "mousemove", this.handleMouseMove);
|
|
125
|
+
this.addListener(document, "mouseout", this.handleMouseOut);
|
|
126
|
+
this.addListener(document, "copy", this.handleCopy);
|
|
127
|
+
this.addListener(document, "visibilitychange", this.handleVisibilityChange);
|
|
128
|
+
this.setupScrollTracking();
|
|
129
|
+
this.setupTimeMilestones();
|
|
130
|
+
this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
|
|
131
|
+
}
|
|
132
|
+
stop() {
|
|
133
|
+
for (const [target, event, handler] of this.listeners) {
|
|
134
|
+
target.removeEventListener(event, handler, { capture: true });
|
|
135
|
+
}
|
|
136
|
+
this.listeners = [];
|
|
137
|
+
if (this.flushTimer !== null) {
|
|
138
|
+
clearInterval(this.flushTimer);
|
|
139
|
+
this.flushTimer = null;
|
|
140
|
+
}
|
|
141
|
+
this.clearTimeMilestones();
|
|
142
|
+
this.cleanupScrollTracking();
|
|
143
|
+
this.flushMouseBuffer();
|
|
144
|
+
this.flush();
|
|
145
|
+
}
|
|
146
|
+
setPageContext(url, path) {
|
|
147
|
+
this.flushMouseBuffer();
|
|
148
|
+
this.flush();
|
|
149
|
+
this.pageUrl = url;
|
|
150
|
+
this.pagePath = path;
|
|
151
|
+
this.scrollMilestones.clear();
|
|
152
|
+
this.timeMilestones.clear();
|
|
153
|
+
this.exitIntentFired = false;
|
|
154
|
+
this.startTime = Date.now();
|
|
155
|
+
this.clickHistory = [];
|
|
156
|
+
this.clearTimeMilestones();
|
|
157
|
+
this.cleanupScrollTracking();
|
|
158
|
+
this.setupTimeMilestones();
|
|
159
|
+
requestAnimationFrame(() => this.setupScrollTracking());
|
|
160
|
+
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Buffer management
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
push(event) {
|
|
165
|
+
this.buffer.push(event);
|
|
166
|
+
if (this.buffer.length >= this.config.maxBufferSize) {
|
|
167
|
+
this.flush();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
flush() {
|
|
171
|
+
if (this.buffer.length === 0) return;
|
|
172
|
+
const batch = {
|
|
173
|
+
session_id: this.config.sessionId,
|
|
174
|
+
visitor_id: this.config.visitorId,
|
|
175
|
+
events: this.buffer
|
|
176
|
+
};
|
|
177
|
+
this.buffer = [];
|
|
178
|
+
this.config.sendBatch(batch).catch(() => {
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
addListener(target, event, handler) {
|
|
182
|
+
target.addEventListener(event, handler, { passive: true, capture: true });
|
|
183
|
+
this.listeners.push([target, event, handler]);
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Scroll tracking with IntersectionObserver
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
setupScrollTracking() {
|
|
189
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
190
|
+
this.observer = new IntersectionObserver(
|
|
191
|
+
(entries) => {
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (!entry.isIntersecting) continue;
|
|
194
|
+
const milestone = Number(entry.target.getAttribute("data-scroll-milestone"));
|
|
195
|
+
if (isNaN(milestone) || this.scrollMilestones.has(milestone)) continue;
|
|
196
|
+
this.scrollMilestones.add(milestone);
|
|
197
|
+
this.push({
|
|
198
|
+
data_type: "scroll_depth",
|
|
199
|
+
data: {
|
|
200
|
+
depth_percent: milestone,
|
|
201
|
+
timestamp: Date.now()
|
|
202
|
+
},
|
|
203
|
+
page_url: this.pageUrl,
|
|
204
|
+
page_path: this.pagePath
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{ threshold: 0 }
|
|
209
|
+
);
|
|
210
|
+
const docHeight = document.documentElement.scrollHeight;
|
|
211
|
+
for (const pct of SCROLL_MILESTONES) {
|
|
212
|
+
const sentinel = document.createElement("div");
|
|
213
|
+
sentinel.setAttribute("data-scroll-milestone", String(pct));
|
|
214
|
+
sentinel.style.position = "absolute";
|
|
215
|
+
sentinel.style.left = "0";
|
|
216
|
+
sentinel.style.width = "1px";
|
|
217
|
+
sentinel.style.height = "1px";
|
|
218
|
+
sentinel.style.pointerEvents = "none";
|
|
219
|
+
sentinel.style.opacity = "0";
|
|
220
|
+
sentinel.style.top = `${docHeight * pct / 100 - 1}px`;
|
|
221
|
+
document.body.appendChild(sentinel);
|
|
222
|
+
this.sentinels.push(sentinel);
|
|
223
|
+
this.observer.observe(sentinel);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
cleanupScrollTracking() {
|
|
227
|
+
if (this.observer) {
|
|
228
|
+
this.observer.disconnect();
|
|
229
|
+
this.observer = null;
|
|
230
|
+
}
|
|
231
|
+
for (const sentinel of this.sentinels) {
|
|
232
|
+
sentinel.remove();
|
|
233
|
+
}
|
|
234
|
+
this.sentinels = [];
|
|
235
|
+
}
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Time milestones
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
setupTimeMilestones() {
|
|
240
|
+
for (const seconds of TIME_MILESTONES) {
|
|
241
|
+
const timer = setTimeout(() => {
|
|
242
|
+
if (this.timeMilestones.has(seconds)) return;
|
|
243
|
+
this.timeMilestones.add(seconds);
|
|
244
|
+
this.push({
|
|
245
|
+
data_type: "time_on_page",
|
|
246
|
+
data: {
|
|
247
|
+
milestone_seconds: seconds,
|
|
248
|
+
timestamp: Date.now()
|
|
249
|
+
},
|
|
250
|
+
page_url: this.pageUrl,
|
|
251
|
+
page_path: this.pagePath
|
|
252
|
+
});
|
|
253
|
+
}, seconds * 1e3);
|
|
254
|
+
this.timeTimers.push(timer);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
clearTimeMilestones() {
|
|
258
|
+
for (const timer of this.timeTimers) {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
}
|
|
261
|
+
this.timeTimers = [];
|
|
262
|
+
}
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Rage click detection
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
detectRageClick(x, y, now) {
|
|
267
|
+
const windowStart = now - RAGE_CLICK_WINDOW_MS;
|
|
268
|
+
const nearby = this.clickHistory.filter((c) => {
|
|
269
|
+
if (c.t < windowStart) return false;
|
|
270
|
+
const dx = c.x - x;
|
|
271
|
+
const dy = c.y - y;
|
|
272
|
+
return Math.sqrt(dx * dx + dy * dy) <= RAGE_CLICK_RADIUS;
|
|
273
|
+
});
|
|
274
|
+
if (nearby.length >= RAGE_CLICK_COUNT) {
|
|
275
|
+
this.push({
|
|
276
|
+
data_type: "rage_click",
|
|
277
|
+
data: {
|
|
278
|
+
x,
|
|
279
|
+
y,
|
|
280
|
+
click_count: nearby.length,
|
|
281
|
+
timestamp: now
|
|
282
|
+
},
|
|
283
|
+
page_url: this.pageUrl,
|
|
284
|
+
page_path: this.pagePath
|
|
285
|
+
});
|
|
286
|
+
this.clickHistory = [];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Mouse buffer flush
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
flushMouseBuffer() {
|
|
293
|
+
if (this.mouseBuffer.length === 0) return;
|
|
294
|
+
this.push({
|
|
295
|
+
data_type: "mouse_movement",
|
|
296
|
+
data: {
|
|
297
|
+
points: [...this.mouseBuffer],
|
|
298
|
+
timestamp: Date.now()
|
|
299
|
+
},
|
|
300
|
+
page_url: this.pageUrl,
|
|
301
|
+
page_path: this.pagePath
|
|
302
|
+
});
|
|
303
|
+
this.mouseBuffer = [];
|
|
304
|
+
}
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// CSS selector helper
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
getSelector(el) {
|
|
309
|
+
const parts = [];
|
|
310
|
+
let current = el;
|
|
311
|
+
let depth = 0;
|
|
312
|
+
while (current && depth < 3) {
|
|
313
|
+
let segment = current.tagName.toLowerCase();
|
|
314
|
+
if (current.id) {
|
|
315
|
+
segment += `#${current.id}`;
|
|
316
|
+
} else if (current.classList.length > 0) {
|
|
317
|
+
segment += `.${Array.from(current.classList).join(".")}`;
|
|
318
|
+
}
|
|
319
|
+
parts.unshift(segment);
|
|
320
|
+
current = current.parentElement;
|
|
321
|
+
depth++;
|
|
322
|
+
}
|
|
323
|
+
return parts.join(" > ");
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
export { BehavioralTracker };
|
|
328
|
+
//# sourceMappingURL=chunk-AKWSW7DW.js.map
|
|
329
|
+
//# sourceMappingURL=chunk-AKWSW7DW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/behavioral/tracker.ts"],"names":[],"mappings":"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","file":"chunk-AKWSW7DW.js","sourcesContent":["import type { BehavioralEvent, BehavioralBatch } from './types.js'\n\ninterface BehavioralTrackerConfig {\n sendBatch: (batch: BehavioralBatch) => Promise<void>\n sessionId: string\n visitorId: string\n flushIntervalMs?: number\n maxBufferSize?: number\n}\n\nconst SCROLL_MILESTONES = [25, 50, 75, 100] as const\nconst TIME_MILESTONES = [30, 60, 120, 300] as const\nconst MOUSE_THROTTLE_MS = 200\nconst MOUSE_BUFFER_MAX = 100\nconst RAGE_CLICK_COUNT = 3\nconst RAGE_CLICK_RADIUS = 50\nconst RAGE_CLICK_WINDOW_MS = 2000\nconst MAX_CLICK_HISTORY = 10\n\nexport class BehavioralTracker {\n private buffer: BehavioralEvent[] = []\n private config: Required<BehavioralTrackerConfig>\n private pageUrl = ''\n private pagePath = ''\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private scrollMilestones = new Set<number>()\n private timeMilestones = new Set<number>()\n private timeTimers: ReturnType<typeof setTimeout>[] = []\n private exitIntentFired = false\n private startTime = 0\n private clickHistory: Array<{ x: number; y: number; t: number }> = []\n private mouseBuffer: Array<{ x: number; y: number; t: number }> = []\n private lastMouseTime = 0\n private listeners: Array<[EventTarget, string, EventListener]> = []\n private observer: IntersectionObserver | null = null\n private sentinels: HTMLElement[] = []\n\n constructor(config: BehavioralTrackerConfig) {\n this.config = {\n sendBatch: config.sendBatch,\n sessionId: config.sessionId,\n visitorId: config.visitorId,\n flushIntervalMs: config.flushIntervalMs ?? 10000,\n maxBufferSize: config.maxBufferSize ?? 500,\n }\n }\n\n start(): void {\n this.startTime = Date.now()\n\n this.addListener(document, 'click', this.handleClick)\n this.addListener(document, 'mousemove', this.handleMouseMove)\n this.addListener(document, 'mouseout', this.handleMouseOut)\n this.addListener(document, 'copy', this.handleCopy)\n this.addListener(document, 'visibilitychange', this.handleVisibilityChange)\n\n this.setupScrollTracking()\n this.setupTimeMilestones()\n\n this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs)\n }\n\n stop(): void {\n for (const [target, event, handler] of this.listeners) {\n target.removeEventListener(event, handler, { capture: true } as EventListenerOptions)\n }\n this.listeners = []\n\n if (this.flushTimer !== null) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n this.clearTimeMilestones()\n this.cleanupScrollTracking()\n\n // Final flush with any remaining mouse data\n this.flushMouseBuffer()\n this.flush()\n }\n\n setPageContext(url: string, path: string): void {\n // Flush remaining data from previous page\n this.flushMouseBuffer()\n this.flush()\n\n this.pageUrl = url\n this.pagePath = path\n this.scrollMilestones.clear()\n this.timeMilestones.clear()\n this.exitIntentFired = false\n this.startTime = Date.now()\n this.clickHistory = []\n\n this.clearTimeMilestones()\n this.cleanupScrollTracking()\n\n this.setupTimeMilestones()\n // Delay scroll tracking setup to let the new page render\n requestAnimationFrame(() => this.setupScrollTracking())\n }\n\n // ---------------------------------------------------------------------------\n // Buffer management\n // ---------------------------------------------------------------------------\n\n private push(event: BehavioralEvent): void {\n this.buffer.push(event)\n if (this.buffer.length >= this.config.maxBufferSize) {\n this.flush()\n }\n }\n\n private flush(): void {\n if (this.buffer.length === 0) return\n\n const batch: BehavioralBatch = {\n session_id: this.config.sessionId,\n visitor_id: this.config.visitorId,\n events: this.buffer,\n }\n this.buffer = []\n\n this.config.sendBatch(batch).catch(() => {\n // sendBatch implementation handles retries and beacon fallback\n })\n }\n\n private addListener(target: EventTarget, event: string, handler: EventListener): void {\n target.addEventListener(event, handler, { passive: true, capture: true })\n this.listeners.push([target, event, handler])\n }\n\n // ---------------------------------------------------------------------------\n // Event handlers (arrow functions for stable `this`)\n // ---------------------------------------------------------------------------\n\n private handleClick = (e: Event): void => {\n const me = e as MouseEvent\n const target = me.target as Element | null\n if (!target) return\n\n const now = Date.now()\n const x = me.clientX\n const y = me.clientY\n\n // Record click for rage detection\n this.clickHistory.push({ x, y, t: now })\n if (this.clickHistory.length > MAX_CLICK_HISTORY) {\n this.clickHistory.shift()\n }\n\n // Standard click event\n const tag = target.tagName?.toLowerCase() ?? ''\n const rawText = (target as HTMLElement).textContent ?? ''\n const text = rawText.trim().slice(0, 50)\n\n this.push({\n data_type: 'click',\n data: {\n tag,\n text,\n selector: this.getSelector(target),\n x,\n y,\n timestamp: now,\n },\n page_url: this.pageUrl,\n page_path: this.pagePath,\n })\n\n // Rage click detection\n this.detectRageClick(x, y, now)\n }\n\n private handleMouseMove = (e: Event): void => {\n const me = e as MouseEvent\n const now = Date.now()\n\n if (now - this.lastMouseTime < MOUSE_THROTTLE_MS) return\n this.lastMouseTime = now\n\n this.mouseBuffer.push({ x: me.clientX, y: me.clientY, t: now })\n if (this.mouseBuffer.length > MOUSE_BUFFER_MAX) {\n this.mouseBuffer.shift()\n }\n }\n\n private handleMouseOut = (e: Event): void => {\n const me = e as MouseEvent\n if (this.exitIntentFired) return\n if (me.clientY > 0) return\n // Only fire when mouse actually leaves the document element (viewport top)\n if (me.relatedTarget !== null) return\n\n this.exitIntentFired = true\n this.push({\n data_type: 'exit_intent',\n data: {\n time_on_page_ms: Date.now() - this.startTime,\n timestamp: Date.now(),\n },\n page_url: this.pageUrl,\n page_path: this.pagePath,\n })\n }\n\n private handleCopy = (): void => {\n const selection = window.getSelection()\n const length = selection?.toString().length ?? 0\n\n this.push({\n data_type: 'copy',\n data: {\n text_length: length,\n timestamp: Date.now(),\n },\n page_url: this.pageUrl,\n page_path: this.pagePath,\n })\n }\n\n private handleVisibilityChange = (): void => {\n if (document.visibilityState !== 'hidden') return\n\n const timeSpent = Date.now() - this.startTime\n\n this.push({\n data_type: 'page_exit',\n data: {\n time_spent_ms: timeSpent,\n timestamp: Date.now(),\n },\n page_url: this.pageUrl,\n page_path: this.pagePath,\n })\n\n // Flush immediately — page may be closing\n this.flushMouseBuffer()\n this.flush()\n }\n\n // ---------------------------------------------------------------------------\n // Scroll tracking with IntersectionObserver\n // ---------------------------------------------------------------------------\n\n private setupScrollTracking(): void {\n if (typeof IntersectionObserver === 'undefined') return\n\n this.observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue\n const milestone = Number(entry.target.getAttribute('data-scroll-milestone'))\n if (isNaN(milestone) || this.scrollMilestones.has(milestone)) continue\n\n this.scrollMilestones.add(milestone)\n this.push({\n data_type: 'scroll_depth',\n data: {\n depth_percent: milestone,\n timestamp: Date.now(),\n },\n page_url: this.pageUrl,\n page_path: this.pagePath,\n })\n }\n },\n { threshold: 0 },\n )\n\n const docHeight = document.documentElement.scrollHeight\n for (const pct of SCROLL_MILESTONES) {\n const sentinel = document.createElement('div')\n sentinel.setAttribute('data-scroll-milestone', String(pct))\n sentinel.style.position = 'absolute'\n sentinel.style.left = '0'\n sentinel.style.width = '1px'\n sentinel.style.height = '1px'\n sentinel.style.pointerEvents = 'none'\n sentinel.style.opacity = '0'\n sentinel.style.top = `${(docHeight * pct) / 100 - 1}px`\n document.body.appendChild(sentinel)\n this.sentinels.push(sentinel)\n this.observer.observe(sentinel)\n }\n }\n\n private cleanupScrollTracking(): void {\n if (this.observer) {\n this.observer.disconnect()\n this.observer = null\n }\n for (const sentinel of this.sentinels) {\n sentinel.remove()\n }\n this.sentinels = []\n }\n\n // ---------------------------------------------------------------------------\n // Time milestones\n // ---------------------------------------------------------------------------\n\n private setupTimeMilestones(): void {\n for (const seconds of TIME_MILESTONES) {\n const timer = setTimeout(() => {\n if (this.timeMilestones.has(seconds)) return\n this.timeMilestones.add(seconds)\n this.push({\n data_type: 'time_on_page',\n data: {\n milestone_seconds: seconds,\n timestamp: Date.now(),\n },\n page_url: this.pageUrl,\n page_path: this.pagePath,\n })\n }, seconds * 1000)\n this.timeTimers.push(timer)\n }\n }\n\n private clearTimeMilestones(): void {\n for (const timer of this.timeTimers) {\n clearTimeout(timer)\n }\n this.timeTimers = []\n }\n\n // ---------------------------------------------------------------------------\n // Rage click detection\n // ---------------------------------------------------------------------------\n\n private detectRageClick(x: number, y: number, now: number): void {\n const windowStart = now - RAGE_CLICK_WINDOW_MS\n const nearby = this.clickHistory.filter((c) => {\n if (c.t < windowStart) return false\n const dx = c.x - x\n const dy = c.y - y\n return Math.sqrt(dx * dx + dy * dy) <= RAGE_CLICK_RADIUS\n })\n\n if (nearby.length >= RAGE_CLICK_COUNT) {\n this.push({\n data_type: 'rage_click',\n data: {\n x,\n y,\n click_count: nearby.length,\n timestamp: now,\n },\n page_url: this.pageUrl,\n page_path: this.pagePath,\n })\n // Reset history to avoid repeat detections for the same burst\n this.clickHistory = []\n }\n }\n\n // ---------------------------------------------------------------------------\n // Mouse buffer flush\n // ---------------------------------------------------------------------------\n\n private flushMouseBuffer(): void {\n if (this.mouseBuffer.length === 0) return\n\n this.push({\n data_type: 'mouse_movement',\n data: {\n points: [...this.mouseBuffer],\n timestamp: Date.now(),\n },\n page_url: this.pageUrl,\n page_path: this.pagePath,\n })\n this.mouseBuffer = []\n }\n\n // ---------------------------------------------------------------------------\n // CSS selector helper\n // ---------------------------------------------------------------------------\n\n private getSelector(el: Element): string {\n const parts: string[] = []\n let current: Element | null = el\n let depth = 0\n\n while (current && depth < 3) {\n let segment = current.tagName.toLowerCase()\n\n if (current.id) {\n segment += `#${current.id}`\n } else if (current.classList.length > 0) {\n segment += `.${Array.from(current.classList).join('.')}`\n }\n\n parts.unshift(segment)\n current = current.parentElement\n depth++\n }\n\n return parts.join(' > ')\n }\n}\n"]}
|