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