@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,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"]}