@loamly/tracker 1.8.0 → 2.0.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 +146 -47
- package/dist/index.cjs +1244 -357
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +78 -65
- package/dist/index.d.ts +78 -65
- package/dist/index.mjs +1244 -357
- package/dist/index.mjs.map +1 -1
- package/dist/loamly.iife.global.js +1291 -140
- package/dist/loamly.iife.global.js.map +1 -1
- package/dist/loamly.iife.min.global.js +16 -1
- package/dist/loamly.iife.min.global.js.map +1 -1
- package/package.json +2 -2
- package/src/behavioral/form-tracker.ts +325 -0
- package/src/behavioral/index.ts +9 -0
- package/src/behavioral/scroll-tracker.ts +163 -0
- package/src/behavioral/time-tracker.ts +174 -0
- package/src/browser.ts +127 -36
- package/src/config.ts +1 -1
- package/src/core.ts +278 -156
- package/src/infrastructure/event-queue.ts +225 -0
- package/src/infrastructure/index.ts +8 -0
- package/src/infrastructure/ping.ts +149 -0
- package/src/spa/index.ts +7 -0
- package/src/spa/router.ts +147 -0
package/src/core.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Loamly Tracker Core
|
|
3
3
|
*
|
|
4
|
-
* Cookie-free, privacy-first analytics with AI traffic detection.
|
|
4
|
+
* Cookie-free, privacy-first analytics with comprehensive AI traffic detection.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Navigation Timing API (paste vs click detection)
|
|
8
|
+
* - Behavioral ML Classifier (mouse, scroll, interaction patterns)
|
|
9
|
+
* - Focus/Blur Sequence Analysis (copy-paste detection)
|
|
10
|
+
* - Agentic Browser Detection (Comet, CDP, teleporting clicks)
|
|
11
|
+
* - Advanced Scroll Tracking (30% chunk reporting)
|
|
12
|
+
* - Universal Form Tracking (HubSpot, Typeform, native)
|
|
13
|
+
* - SPA Navigation Support (History API hooks)
|
|
14
|
+
* - Event Queue with Retry (offline support)
|
|
15
|
+
* - Real-time Ping (heartbeat)
|
|
5
16
|
*
|
|
6
17
|
* @module @loamly/tracker
|
|
7
18
|
*/
|
|
@@ -17,6 +28,16 @@ import {
|
|
|
17
28
|
FocusBlurAnalyzer,
|
|
18
29
|
type FocusBlurResult
|
|
19
30
|
} from './detection/focus-blur'
|
|
31
|
+
import {
|
|
32
|
+
AgenticBrowserAnalyzer,
|
|
33
|
+
type AgenticDetectionResult
|
|
34
|
+
} from './detection/agentic-browser'
|
|
35
|
+
import { EventQueue } from './infrastructure/event-queue'
|
|
36
|
+
import { PingService } from './infrastructure/ping'
|
|
37
|
+
import { ScrollTracker, type ScrollEvent } from './behavioral/scroll-tracker'
|
|
38
|
+
import { TimeTracker, type TimeEvent } from './behavioral/time-tracker'
|
|
39
|
+
import { FormTracker, type FormEvent } from './behavioral/form-tracker'
|
|
40
|
+
import { SPARouter, type NavigationEvent } from './spa/router'
|
|
20
41
|
import {
|
|
21
42
|
getVisitorId,
|
|
22
43
|
getSessionId,
|
|
@@ -41,13 +62,27 @@ let initialized = false
|
|
|
41
62
|
let debugMode = false
|
|
42
63
|
let visitorId: string | null = null
|
|
43
64
|
let sessionId: string | null = null
|
|
44
|
-
let sessionStartTime: number | null = null
|
|
45
65
|
let navigationTiming: NavigationTiming | null = null
|
|
46
66
|
let aiDetection: AIDetectionResult | null = null
|
|
67
|
+
|
|
68
|
+
// Detection modules
|
|
47
69
|
let behavioralClassifier: BehavioralClassifier | null = null
|
|
48
70
|
let behavioralMLResult: BehavioralMLResult | null = null
|
|
49
71
|
let focusBlurAnalyzer: FocusBlurAnalyzer | null = null
|
|
50
72
|
let focusBlurResult: FocusBlurMLResult | null = null
|
|
73
|
+
let agenticAnalyzer: AgenticBrowserAnalyzer | null = null
|
|
74
|
+
|
|
75
|
+
// Infrastructure modules
|
|
76
|
+
let eventQueue: EventQueue | null = null
|
|
77
|
+
let pingService: PingService | null = null
|
|
78
|
+
|
|
79
|
+
// Behavioral tracking modules
|
|
80
|
+
let scrollTracker: ScrollTracker | null = null
|
|
81
|
+
let timeTracker: TimeTracker | null = null
|
|
82
|
+
let formTracker: FormTracker | null = null
|
|
83
|
+
|
|
84
|
+
// SPA navigation
|
|
85
|
+
let spaRouter: SPARouter | null = null
|
|
51
86
|
|
|
52
87
|
/**
|
|
53
88
|
* Debug logger
|
|
@@ -91,14 +126,19 @@ function init(userConfig: LoamlyConfig = {}): void {
|
|
|
91
126
|
// Get/create session
|
|
92
127
|
const session = getSessionId()
|
|
93
128
|
sessionId = session.sessionId
|
|
94
|
-
sessionStartTime = Date.now()
|
|
95
129
|
log('Session ID:', sessionId, session.isNew ? '(new)' : '(existing)')
|
|
96
130
|
|
|
131
|
+
// Initialize event queue with batching
|
|
132
|
+
eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
133
|
+
batchSize: DEFAULT_CONFIG.batchSize,
|
|
134
|
+
batchTimeout: DEFAULT_CONFIG.batchTimeout,
|
|
135
|
+
})
|
|
136
|
+
|
|
97
137
|
// Detect navigation timing (paste vs click)
|
|
98
138
|
navigationTiming = detectNavigationType()
|
|
99
139
|
log('Navigation timing:', navigationTiming)
|
|
100
140
|
|
|
101
|
-
// Detect AI from referrer
|
|
141
|
+
// Detect AI from referrer/UTM
|
|
102
142
|
aiDetection = detectAIFromReferrer(document.referrer) || detectAIFromUTM(window.location.href)
|
|
103
143
|
if (aiDetection) {
|
|
104
144
|
log('AI detected:', aiDetection)
|
|
@@ -113,7 +153,7 @@ function init(userConfig: LoamlyConfig = {}): void {
|
|
|
113
153
|
|
|
114
154
|
// Set up behavioral tracking unless disabled
|
|
115
155
|
if (!userConfig.disableBehavioral) {
|
|
116
|
-
|
|
156
|
+
setupAdvancedBehavioralTracking()
|
|
117
157
|
}
|
|
118
158
|
|
|
119
159
|
// Initialize behavioral ML classifier (LOA-180)
|
|
@@ -132,9 +172,214 @@ function init(userConfig: LoamlyConfig = {}): void {
|
|
|
132
172
|
}
|
|
133
173
|
}, 5000)
|
|
134
174
|
|
|
175
|
+
// Initialize agentic browser detection (LOA-187)
|
|
176
|
+
agenticAnalyzer = new AgenticBrowserAnalyzer()
|
|
177
|
+
agenticAnalyzer.init()
|
|
178
|
+
|
|
179
|
+
// Set up ping service
|
|
180
|
+
if (visitorId && sessionId) {
|
|
181
|
+
pingService = new PingService(sessionId, visitorId, VERSION, {
|
|
182
|
+
interval: DEFAULT_CONFIG.pingInterval,
|
|
183
|
+
endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping),
|
|
184
|
+
})
|
|
185
|
+
pingService.start()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Set up SPA navigation tracking
|
|
189
|
+
spaRouter = new SPARouter({
|
|
190
|
+
onNavigate: handleSPANavigation,
|
|
191
|
+
})
|
|
192
|
+
spaRouter.start()
|
|
193
|
+
|
|
194
|
+
// Set up unload handlers
|
|
195
|
+
setupUnloadHandlers()
|
|
196
|
+
|
|
135
197
|
log('Initialization complete')
|
|
136
198
|
}
|
|
137
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Set up advanced behavioral tracking with new modules
|
|
202
|
+
*/
|
|
203
|
+
function setupAdvancedBehavioralTracking(): void {
|
|
204
|
+
// Scroll tracker with 30% chunks
|
|
205
|
+
scrollTracker = new ScrollTracker({
|
|
206
|
+
chunks: [30, 60, 90, 100],
|
|
207
|
+
onChunkReached: (event: ScrollEvent) => {
|
|
208
|
+
log('Scroll chunk:', event.chunk)
|
|
209
|
+
queueEvent('scroll_depth', {
|
|
210
|
+
depth: event.depth,
|
|
211
|
+
chunk: event.chunk,
|
|
212
|
+
time_to_reach_ms: event.time_to_reach_ms,
|
|
213
|
+
})
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
scrollTracker.start()
|
|
217
|
+
|
|
218
|
+
// Time tracker
|
|
219
|
+
timeTracker = new TimeTracker({
|
|
220
|
+
updateIntervalMs: 10000, // Report every 10 seconds
|
|
221
|
+
onUpdate: (event: TimeEvent) => {
|
|
222
|
+
if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
|
|
223
|
+
queueEvent('time_spent', {
|
|
224
|
+
active_time_ms: event.active_time_ms,
|
|
225
|
+
total_time_ms: event.total_time_ms,
|
|
226
|
+
idle_time_ms: event.idle_time_ms,
|
|
227
|
+
is_engaged: event.is_engaged,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
timeTracker.start()
|
|
233
|
+
|
|
234
|
+
// Form tracker with universal support
|
|
235
|
+
formTracker = new FormTracker({
|
|
236
|
+
onFormEvent: (event: FormEvent) => {
|
|
237
|
+
log('Form event:', event.event_type, event.form_id)
|
|
238
|
+
queueEvent(event.event_type, {
|
|
239
|
+
form_id: event.form_id,
|
|
240
|
+
form_type: event.form_type,
|
|
241
|
+
field_name: event.field_name,
|
|
242
|
+
field_type: event.field_type,
|
|
243
|
+
time_to_submit_ms: event.time_to_submit_ms,
|
|
244
|
+
is_conversion: event.is_conversion,
|
|
245
|
+
})
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
formTracker.start()
|
|
249
|
+
|
|
250
|
+
// Click tracking for links (basic)
|
|
251
|
+
document.addEventListener('click', (e) => {
|
|
252
|
+
const target = e.target as HTMLElement
|
|
253
|
+
const link = target.closest('a')
|
|
254
|
+
|
|
255
|
+
if (link && link.href) {
|
|
256
|
+
const isExternal = link.hostname !== window.location.hostname
|
|
257
|
+
queueEvent('click', {
|
|
258
|
+
element: 'link',
|
|
259
|
+
href: truncateText(link.href, 200),
|
|
260
|
+
text: truncateText(link.textContent || '', 100),
|
|
261
|
+
is_external: isExternal,
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Queue an event for batched sending
|
|
269
|
+
*/
|
|
270
|
+
function queueEvent(eventType: string, data: Record<string, unknown>): void {
|
|
271
|
+
if (!eventQueue) return
|
|
272
|
+
|
|
273
|
+
eventQueue.push(eventType, {
|
|
274
|
+
visitor_id: visitorId,
|
|
275
|
+
session_id: sessionId,
|
|
276
|
+
event_type: eventType,
|
|
277
|
+
...data,
|
|
278
|
+
url: window.location.href,
|
|
279
|
+
timestamp: new Date().toISOString(),
|
|
280
|
+
tracker_version: VERSION,
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Handle SPA navigation
|
|
286
|
+
*/
|
|
287
|
+
function handleSPANavigation(event: NavigationEvent): void {
|
|
288
|
+
log('SPA navigation:', event.navigation_type, event.to_url)
|
|
289
|
+
|
|
290
|
+
// Flush pending events before navigation
|
|
291
|
+
eventQueue?.flush()
|
|
292
|
+
|
|
293
|
+
// Update ping service
|
|
294
|
+
pingService?.updateScrollDepth(0)
|
|
295
|
+
|
|
296
|
+
// Reset scroll tracker for new page
|
|
297
|
+
scrollTracker?.stop()
|
|
298
|
+
scrollTracker = new ScrollTracker({
|
|
299
|
+
chunks: [30, 60, 90, 100],
|
|
300
|
+
onChunkReached: (scrollEvent: ScrollEvent) => {
|
|
301
|
+
queueEvent('scroll_depth', {
|
|
302
|
+
depth: scrollEvent.depth,
|
|
303
|
+
chunk: scrollEvent.chunk,
|
|
304
|
+
time_to_reach_ms: scrollEvent.time_to_reach_ms,
|
|
305
|
+
})
|
|
306
|
+
},
|
|
307
|
+
})
|
|
308
|
+
scrollTracker.start()
|
|
309
|
+
|
|
310
|
+
// Track the virtual pageview
|
|
311
|
+
pageview(event.to_url)
|
|
312
|
+
|
|
313
|
+
// Queue navigation event
|
|
314
|
+
queueEvent('spa_navigation', {
|
|
315
|
+
from_url: event.from_url,
|
|
316
|
+
to_url: event.to_url,
|
|
317
|
+
navigation_type: event.navigation_type,
|
|
318
|
+
time_on_previous_page_ms: event.time_on_previous_page_ms,
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Set up handlers for page unload
|
|
324
|
+
*/
|
|
325
|
+
function setupUnloadHandlers(): void {
|
|
326
|
+
const handleUnload = (): void => {
|
|
327
|
+
// Get final scroll depth
|
|
328
|
+
const scrollEvent = scrollTracker?.getFinalEvent()
|
|
329
|
+
if (scrollEvent) {
|
|
330
|
+
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
331
|
+
visitor_id: visitorId,
|
|
332
|
+
session_id: sessionId,
|
|
333
|
+
event_type: 'scroll_depth_final',
|
|
334
|
+
data: scrollEvent,
|
|
335
|
+
url: window.location.href,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Get final time metrics
|
|
340
|
+
const timeEvent = timeTracker?.getFinalMetrics()
|
|
341
|
+
if (timeEvent) {
|
|
342
|
+
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
343
|
+
visitor_id: visitorId,
|
|
344
|
+
session_id: sessionId,
|
|
345
|
+
event_type: 'time_spent_final',
|
|
346
|
+
data: timeEvent,
|
|
347
|
+
url: window.location.href,
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Get agentic detection result
|
|
352
|
+
const agenticResult = agenticAnalyzer?.getResult()
|
|
353
|
+
if (agenticResult && agenticResult.agenticProbability > 0) {
|
|
354
|
+
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
355
|
+
visitor_id: visitorId,
|
|
356
|
+
session_id: sessionId,
|
|
357
|
+
event_type: 'agentic_detection',
|
|
358
|
+
data: agenticResult,
|
|
359
|
+
url: window.location.href,
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Flush event queue
|
|
364
|
+
eventQueue?.flushBeacon()
|
|
365
|
+
|
|
366
|
+
// Force classify behavioral ML if not done
|
|
367
|
+
if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
|
|
368
|
+
const result = behavioralClassifier.forceClassify()
|
|
369
|
+
if (result) {
|
|
370
|
+
handleBehavioralClassification(result)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
window.addEventListener('beforeunload', handleUnload)
|
|
376
|
+
document.addEventListener('visibilitychange', () => {
|
|
377
|
+
if (document.visibilityState === 'hidden') {
|
|
378
|
+
handleUnload()
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
|
|
138
383
|
/**
|
|
139
384
|
* Track a page view
|
|
140
385
|
*/
|
|
@@ -238,143 +483,8 @@ function identify(userId: string, traits: Record<string, unknown> = {}): void {
|
|
|
238
483
|
})
|
|
239
484
|
}
|
|
240
485
|
|
|
241
|
-
/**
|
|
242
|
-
* Set up behavioral tracking (scroll, time spent, etc.)
|
|
243
|
-
*/
|
|
244
|
-
function setupBehavioralTracking(): void {
|
|
245
|
-
let maxScrollDepth = 0
|
|
246
|
-
let lastScrollUpdate = 0
|
|
247
|
-
let lastTimeUpdate = Date.now()
|
|
248
|
-
|
|
249
|
-
// Scroll tracking with requestAnimationFrame throttling
|
|
250
|
-
let scrollTicking = false
|
|
251
|
-
|
|
252
|
-
window.addEventListener('scroll', () => {
|
|
253
|
-
if (!scrollTicking) {
|
|
254
|
-
requestAnimationFrame(() => {
|
|
255
|
-
const scrollPercent = Math.round(
|
|
256
|
-
((window.scrollY + window.innerHeight) / document.documentElement.scrollHeight) * 100
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
if (scrollPercent > maxScrollDepth) {
|
|
260
|
-
maxScrollDepth = scrollPercent
|
|
261
|
-
|
|
262
|
-
// Report at milestones (25%, 50%, 75%, 100%)
|
|
263
|
-
const milestones = [25, 50, 75, 100]
|
|
264
|
-
for (const milestone of milestones) {
|
|
265
|
-
if (scrollPercent >= milestone && lastScrollUpdate < milestone) {
|
|
266
|
-
lastScrollUpdate = milestone
|
|
267
|
-
sendBehavioralEvent('scroll_depth', { depth: milestone })
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
scrollTicking = false
|
|
273
|
-
})
|
|
274
|
-
scrollTicking = true
|
|
275
|
-
}
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
// Time spent tracking (every 5 seconds minimum)
|
|
279
|
-
const trackTimeSpent = (): void => {
|
|
280
|
-
const now = Date.now()
|
|
281
|
-
const delta = now - lastTimeUpdate
|
|
282
|
-
|
|
283
|
-
if (delta >= DEFAULT_CONFIG.timeSpentThresholdMs) {
|
|
284
|
-
lastTimeUpdate = now
|
|
285
|
-
sendBehavioralEvent('time_spent', {
|
|
286
|
-
seconds: Math.round(delta / 1000),
|
|
287
|
-
total_seconds: Math.round((now - (sessionStartTime || now)) / 1000)
|
|
288
|
-
})
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Track on visibility change
|
|
293
|
-
document.addEventListener('visibilitychange', () => {
|
|
294
|
-
if (document.visibilityState === 'hidden') {
|
|
295
|
-
trackTimeSpent()
|
|
296
|
-
}
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
// Track on page unload
|
|
300
|
-
window.addEventListener('beforeunload', () => {
|
|
301
|
-
trackTimeSpent()
|
|
302
|
-
|
|
303
|
-
// Send final scroll depth
|
|
304
|
-
if (maxScrollDepth > 0) {
|
|
305
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
306
|
-
visitor_id: visitorId,
|
|
307
|
-
session_id: sessionId,
|
|
308
|
-
event_type: 'scroll_depth_final',
|
|
309
|
-
data: { depth: maxScrollDepth },
|
|
310
|
-
url: window.location.href,
|
|
311
|
-
})
|
|
312
|
-
}
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
// Form interaction tracking
|
|
316
|
-
document.addEventListener('focusin', (e) => {
|
|
317
|
-
const target = e.target as HTMLElement
|
|
318
|
-
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') {
|
|
319
|
-
sendBehavioralEvent('form_focus', {
|
|
320
|
-
field_type: target.tagName.toLowerCase(),
|
|
321
|
-
field_name: (target as HTMLInputElement).name || (target as HTMLInputElement).id || 'unknown',
|
|
322
|
-
})
|
|
323
|
-
}
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
// Form submit tracking
|
|
327
|
-
document.addEventListener('submit', (e) => {
|
|
328
|
-
const form = e.target as HTMLFormElement
|
|
329
|
-
sendBehavioralEvent('form_submit', {
|
|
330
|
-
form_id: form.id || form.name || 'unknown',
|
|
331
|
-
form_action: form.action ? new URL(form.action).pathname : 'unknown',
|
|
332
|
-
})
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
// Click tracking for links
|
|
336
|
-
document.addEventListener('click', (e) => {
|
|
337
|
-
const target = e.target as HTMLElement
|
|
338
|
-
const link = target.closest('a')
|
|
339
|
-
|
|
340
|
-
if (link && link.href) {
|
|
341
|
-
const isExternal = link.hostname !== window.location.hostname
|
|
342
|
-
sendBehavioralEvent('click', {
|
|
343
|
-
element: 'link',
|
|
344
|
-
href: truncateText(link.href, 200),
|
|
345
|
-
text: truncateText(link.textContent || '', 100),
|
|
346
|
-
is_external: isExternal,
|
|
347
|
-
})
|
|
348
|
-
}
|
|
349
|
-
})
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Send a behavioral event
|
|
354
|
-
*/
|
|
355
|
-
function sendBehavioralEvent(eventType: string, data: Record<string, unknown>): void {
|
|
356
|
-
const payload = {
|
|
357
|
-
visitor_id: visitorId,
|
|
358
|
-
session_id: sessionId,
|
|
359
|
-
event_type: eventType,
|
|
360
|
-
data,
|
|
361
|
-
url: window.location.href,
|
|
362
|
-
timestamp: new Date().toISOString(),
|
|
363
|
-
tracker_version: VERSION,
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
log('Behavioral:', eventType, data)
|
|
367
|
-
|
|
368
|
-
safeFetch(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
369
|
-
method: 'POST',
|
|
370
|
-
headers: { 'Content-Type': 'application/json' },
|
|
371
|
-
body: JSON.stringify(payload),
|
|
372
|
-
})
|
|
373
|
-
}
|
|
374
|
-
|
|
375
486
|
/**
|
|
376
487
|
* Set up behavioral ML signal collection (LOA-180)
|
|
377
|
-
* Collects mouse, scroll, and interaction signals for Naive Bayes classification
|
|
378
488
|
*/
|
|
379
489
|
function setupBehavioralMLTracking(): void {
|
|
380
490
|
if (!behavioralClassifier) return
|
|
@@ -427,16 +537,6 @@ function setupBehavioralMLTracking(): void {
|
|
|
427
537
|
}
|
|
428
538
|
}, { passive: true })
|
|
429
539
|
|
|
430
|
-
// Force classification on page unload
|
|
431
|
-
window.addEventListener('beforeunload', () => {
|
|
432
|
-
if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
|
|
433
|
-
const result = behavioralClassifier.forceClassify()
|
|
434
|
-
if (result) {
|
|
435
|
-
handleBehavioralClassification(result)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
})
|
|
439
|
-
|
|
440
540
|
// Also try to classify after 30 seconds as backup
|
|
441
541
|
setTimeout(() => {
|
|
442
542
|
if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
|
|
@@ -462,7 +562,7 @@ function handleBehavioralClassification(result: BehavioralClassificationResult):
|
|
|
462
562
|
}
|
|
463
563
|
|
|
464
564
|
// Send to backend
|
|
465
|
-
|
|
565
|
+
queueEvent('ml_classification', {
|
|
466
566
|
classification: result.classification,
|
|
467
567
|
human_probability: result.humanProbability,
|
|
468
568
|
ai_probability: result.aiProbability,
|
|
@@ -500,7 +600,7 @@ function handleFocusBlurAnalysis(result: FocusBlurResult): void {
|
|
|
500
600
|
}
|
|
501
601
|
|
|
502
602
|
// Send to backend
|
|
503
|
-
|
|
603
|
+
queueEvent('focus_blur_analysis', {
|
|
504
604
|
nav_type: result.nav_type,
|
|
505
605
|
confidence: result.confidence,
|
|
506
606
|
signals: result.signals,
|
|
@@ -564,6 +664,13 @@ function getFocusBlurResult(): FocusBlurMLResult | null {
|
|
|
564
664
|
return focusBlurResult
|
|
565
665
|
}
|
|
566
666
|
|
|
667
|
+
/**
|
|
668
|
+
* Get agentic browser detection result
|
|
669
|
+
*/
|
|
670
|
+
function getAgenticResult(): AgenticDetectionResult | null {
|
|
671
|
+
return agenticAnalyzer?.getResult() || null
|
|
672
|
+
}
|
|
673
|
+
|
|
567
674
|
/**
|
|
568
675
|
* Check if initialized
|
|
569
676
|
*/
|
|
@@ -576,16 +683,32 @@ function isTrackerInitialized(): boolean {
|
|
|
576
683
|
*/
|
|
577
684
|
function reset(): void {
|
|
578
685
|
log('Resetting tracker')
|
|
686
|
+
|
|
687
|
+
// Stop all services
|
|
688
|
+
pingService?.stop()
|
|
689
|
+
scrollTracker?.stop()
|
|
690
|
+
timeTracker?.stop()
|
|
691
|
+
formTracker?.stop()
|
|
692
|
+
spaRouter?.stop()
|
|
693
|
+
agenticAnalyzer?.destroy()
|
|
694
|
+
|
|
695
|
+
// Reset state
|
|
579
696
|
initialized = false
|
|
580
697
|
visitorId = null
|
|
581
698
|
sessionId = null
|
|
582
|
-
sessionStartTime = null
|
|
583
699
|
navigationTiming = null
|
|
584
700
|
aiDetection = null
|
|
585
701
|
behavioralClassifier = null
|
|
586
702
|
behavioralMLResult = null
|
|
587
703
|
focusBlurAnalyzer = null
|
|
588
704
|
focusBlurResult = null
|
|
705
|
+
agenticAnalyzer = null
|
|
706
|
+
eventQueue = null
|
|
707
|
+
pingService = null
|
|
708
|
+
scrollTracker = null
|
|
709
|
+
timeTracker = null
|
|
710
|
+
formTracker = null
|
|
711
|
+
spaRouter = null
|
|
589
712
|
|
|
590
713
|
try {
|
|
591
714
|
sessionStorage.removeItem('loamly_session')
|
|
@@ -606,7 +729,7 @@ function setDebug(enabled: boolean): void {
|
|
|
606
729
|
/**
|
|
607
730
|
* The Loamly Tracker instance
|
|
608
731
|
*/
|
|
609
|
-
export const loamly: LoamlyTracker = {
|
|
732
|
+
export const loamly: LoamlyTracker & { getAgentic: () => AgenticDetectionResult | null } = {
|
|
610
733
|
init,
|
|
611
734
|
pageview,
|
|
612
735
|
track,
|
|
@@ -618,11 +741,10 @@ export const loamly: LoamlyTracker = {
|
|
|
618
741
|
getNavigationTiming: getNavigationTimingResult,
|
|
619
742
|
getBehavioralML: getBehavioralMLResult,
|
|
620
743
|
getFocusBlur: getFocusBlurResult,
|
|
744
|
+
getAgentic: getAgenticResult,
|
|
621
745
|
isInitialized: isTrackerInitialized,
|
|
622
746
|
reset,
|
|
623
747
|
debug: setDebug,
|
|
624
748
|
}
|
|
625
749
|
|
|
626
750
|
export default loamly
|
|
627
|
-
|
|
628
|
-
|