@loamly/tracker 1.6.0 → 1.8.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/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/config.ts
2
- var VERSION = "1.6.0";
2
+ var VERSION = "1.8.0";
3
3
  var DEFAULT_CONFIG = {
4
4
  apiHost: "https://app.loamly.ai",
5
5
  endpoints: {
@@ -176,6 +176,467 @@ function detectAIFromUTM(url) {
176
176
  }
177
177
  }
178
178
 
179
+ // src/detection/behavioral-classifier.ts
180
+ var NAIVE_BAYES_WEIGHTS = {
181
+ human: {
182
+ time_to_first_click_delayed: 0.85,
183
+ time_to_first_click_normal: 0.75,
184
+ time_to_first_click_fast: 0.5,
185
+ time_to_first_click_immediate: 0.25,
186
+ scroll_speed_variable: 0.8,
187
+ scroll_speed_erratic: 0.7,
188
+ scroll_speed_uniform: 0.35,
189
+ scroll_speed_none: 0.45,
190
+ nav_timing_click: 0.75,
191
+ nav_timing_unknown: 0.55,
192
+ nav_timing_paste: 0.35,
193
+ has_referrer: 0.7,
194
+ no_referrer: 0.45,
195
+ homepage_landing: 0.65,
196
+ deep_landing: 0.5,
197
+ mouse_movement_curved: 0.9,
198
+ mouse_movement_linear: 0.3,
199
+ mouse_movement_none: 0.4,
200
+ form_fill_normal: 0.85,
201
+ form_fill_fast: 0.6,
202
+ form_fill_instant: 0.2,
203
+ focus_blur_normal: 0.75,
204
+ focus_blur_rapid: 0.45
205
+ },
206
+ ai_influenced: {
207
+ time_to_first_click_immediate: 0.75,
208
+ time_to_first_click_fast: 0.55,
209
+ time_to_first_click_normal: 0.4,
210
+ time_to_first_click_delayed: 0.35,
211
+ scroll_speed_none: 0.55,
212
+ scroll_speed_uniform: 0.7,
213
+ scroll_speed_variable: 0.35,
214
+ scroll_speed_erratic: 0.4,
215
+ nav_timing_paste: 0.75,
216
+ nav_timing_unknown: 0.5,
217
+ nav_timing_click: 0.35,
218
+ no_referrer: 0.65,
219
+ has_referrer: 0.4,
220
+ deep_landing: 0.6,
221
+ homepage_landing: 0.45,
222
+ mouse_movement_none: 0.6,
223
+ mouse_movement_linear: 0.75,
224
+ mouse_movement_curved: 0.25,
225
+ form_fill_instant: 0.8,
226
+ form_fill_fast: 0.55,
227
+ form_fill_normal: 0.3,
228
+ focus_blur_rapid: 0.6,
229
+ focus_blur_normal: 0.4
230
+ }
231
+ };
232
+ var PRIORS = {
233
+ human: 0.85,
234
+ ai_influenced: 0.15
235
+ };
236
+ var DEFAULT_WEIGHT = 0.5;
237
+ var BehavioralClassifier = class {
238
+ /**
239
+ * Create a new classifier
240
+ * @param minSessionTimeMs Minimum session time before classification (default: 10s)
241
+ */
242
+ constructor(minSessionTimeMs = 1e4) {
243
+ this.classified = false;
244
+ this.result = null;
245
+ this.onClassify = null;
246
+ this.minSessionTime = minSessionTimeMs;
247
+ this.data = {
248
+ firstClickTime: null,
249
+ scrollEvents: [],
250
+ mouseEvents: [],
251
+ formEvents: [],
252
+ focusBlurEvents: [],
253
+ startTime: Date.now()
254
+ };
255
+ }
256
+ /**
257
+ * Set callback for when classification completes
258
+ */
259
+ setOnClassify(callback) {
260
+ this.onClassify = callback;
261
+ }
262
+ /**
263
+ * Record a click event
264
+ */
265
+ recordClick() {
266
+ if (this.data.firstClickTime === null) {
267
+ this.data.firstClickTime = Date.now();
268
+ }
269
+ this.checkAndClassify();
270
+ }
271
+ /**
272
+ * Record a scroll event
273
+ */
274
+ recordScroll(position) {
275
+ this.data.scrollEvents.push({ time: Date.now(), position });
276
+ if (this.data.scrollEvents.length > 50) {
277
+ this.data.scrollEvents = this.data.scrollEvents.slice(-50);
278
+ }
279
+ this.checkAndClassify();
280
+ }
281
+ /**
282
+ * Record mouse movement
283
+ */
284
+ recordMouse(x, y) {
285
+ this.data.mouseEvents.push({ time: Date.now(), x, y });
286
+ if (this.data.mouseEvents.length > 100) {
287
+ this.data.mouseEvents = this.data.mouseEvents.slice(-100);
288
+ }
289
+ this.checkAndClassify();
290
+ }
291
+ /**
292
+ * Record form field interaction start
293
+ */
294
+ recordFormStart(fieldId) {
295
+ const existing = this.data.formEvents.find((e) => e.fieldId === fieldId && e.endTime === 0);
296
+ if (!existing) {
297
+ this.data.formEvents.push({ fieldId, startTime: Date.now(), endTime: 0 });
298
+ }
299
+ }
300
+ /**
301
+ * Record form field interaction end
302
+ */
303
+ recordFormEnd(fieldId) {
304
+ const event = this.data.formEvents.find((e) => e.fieldId === fieldId && e.endTime === 0);
305
+ if (event) {
306
+ event.endTime = Date.now();
307
+ }
308
+ this.checkAndClassify();
309
+ }
310
+ /**
311
+ * Record focus/blur event
312
+ */
313
+ recordFocusBlur(type) {
314
+ this.data.focusBlurEvents.push({ type, time: Date.now() });
315
+ if (this.data.focusBlurEvents.length > 20) {
316
+ this.data.focusBlurEvents = this.data.focusBlurEvents.slice(-20);
317
+ }
318
+ }
319
+ /**
320
+ * Check if we have enough data and classify
321
+ */
322
+ checkAndClassify() {
323
+ if (this.classified) return;
324
+ const sessionDuration = Date.now() - this.data.startTime;
325
+ if (sessionDuration < this.minSessionTime) return;
326
+ const hasData = this.data.scrollEvents.length >= 2 || this.data.mouseEvents.length >= 5 || this.data.firstClickTime !== null;
327
+ if (!hasData) return;
328
+ this.classify();
329
+ }
330
+ /**
331
+ * Force classification (for beforeunload)
332
+ */
333
+ forceClassify() {
334
+ if (this.classified) return this.result;
335
+ return this.classify();
336
+ }
337
+ /**
338
+ * Perform classification
339
+ */
340
+ classify() {
341
+ const sessionDuration = Date.now() - this.data.startTime;
342
+ const signals = this.extractSignals();
343
+ let humanLogProb = Math.log(PRIORS.human);
344
+ let aiLogProb = Math.log(PRIORS.ai_influenced);
345
+ for (const signal of signals) {
346
+ const humanWeight = NAIVE_BAYES_WEIGHTS.human[signal] ?? DEFAULT_WEIGHT;
347
+ const aiWeight = NAIVE_BAYES_WEIGHTS.ai_influenced[signal] ?? DEFAULT_WEIGHT;
348
+ humanLogProb += Math.log(humanWeight);
349
+ aiLogProb += Math.log(aiWeight);
350
+ }
351
+ const maxLog = Math.max(humanLogProb, aiLogProb);
352
+ const humanExp = Math.exp(humanLogProb - maxLog);
353
+ const aiExp = Math.exp(aiLogProb - maxLog);
354
+ const total = humanExp + aiExp;
355
+ const humanProbability = humanExp / total;
356
+ const aiProbability = aiExp / total;
357
+ let classification;
358
+ let confidence;
359
+ if (humanProbability > 0.6) {
360
+ classification = "human";
361
+ confidence = humanProbability;
362
+ } else if (aiProbability > 0.6) {
363
+ classification = "ai_influenced";
364
+ confidence = aiProbability;
365
+ } else {
366
+ classification = "uncertain";
367
+ confidence = Math.max(humanProbability, aiProbability);
368
+ }
369
+ this.result = {
370
+ classification,
371
+ humanProbability,
372
+ aiProbability,
373
+ confidence,
374
+ signals,
375
+ timestamp: Date.now(),
376
+ sessionDurationMs: sessionDuration
377
+ };
378
+ this.classified = true;
379
+ if (this.onClassify) {
380
+ this.onClassify(this.result);
381
+ }
382
+ return this.result;
383
+ }
384
+ /**
385
+ * Extract behavioral signals from collected data
386
+ */
387
+ extractSignals() {
388
+ const signals = [];
389
+ if (this.data.firstClickTime !== null) {
390
+ const timeToClick = this.data.firstClickTime - this.data.startTime;
391
+ if (timeToClick < 500) {
392
+ signals.push("time_to_first_click_immediate");
393
+ } else if (timeToClick < 2e3) {
394
+ signals.push("time_to_first_click_fast");
395
+ } else if (timeToClick < 1e4) {
396
+ signals.push("time_to_first_click_normal");
397
+ } else {
398
+ signals.push("time_to_first_click_delayed");
399
+ }
400
+ }
401
+ if (this.data.scrollEvents.length === 0) {
402
+ signals.push("scroll_speed_none");
403
+ } else if (this.data.scrollEvents.length >= 3) {
404
+ const scrollDeltas = [];
405
+ for (let i = 1; i < this.data.scrollEvents.length; i++) {
406
+ const delta = this.data.scrollEvents[i].time - this.data.scrollEvents[i - 1].time;
407
+ scrollDeltas.push(delta);
408
+ }
409
+ const mean = scrollDeltas.reduce((a, b) => a + b, 0) / scrollDeltas.length;
410
+ const variance = scrollDeltas.reduce((sum, d) => sum + Math.pow(d - mean, 2), 0) / scrollDeltas.length;
411
+ const stdDev = Math.sqrt(variance);
412
+ const cv = mean > 0 ? stdDev / mean : 0;
413
+ if (cv < 0.2) {
414
+ signals.push("scroll_speed_uniform");
415
+ } else if (cv < 0.6) {
416
+ signals.push("scroll_speed_variable");
417
+ } else {
418
+ signals.push("scroll_speed_erratic");
419
+ }
420
+ }
421
+ if (this.data.mouseEvents.length === 0) {
422
+ signals.push("mouse_movement_none");
423
+ } else if (this.data.mouseEvents.length >= 10) {
424
+ const n = Math.min(this.data.mouseEvents.length, 20);
425
+ const recentMouse = this.data.mouseEvents.slice(-n);
426
+ let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
427
+ for (const event of recentMouse) {
428
+ sumX += event.x;
429
+ sumY += event.y;
430
+ sumXY += event.x * event.y;
431
+ sumX2 += event.x * event.x;
432
+ }
433
+ const denominator = n * sumX2 - sumX * sumX;
434
+ const slope = denominator !== 0 ? (n * sumXY - sumX * sumY) / denominator : 0;
435
+ const intercept = (sumY - slope * sumX) / n;
436
+ let ssRes = 0, ssTot = 0;
437
+ const yMean = sumY / n;
438
+ for (const event of recentMouse) {
439
+ const yPred = slope * event.x + intercept;
440
+ ssRes += Math.pow(event.y - yPred, 2);
441
+ ssTot += Math.pow(event.y - yMean, 2);
442
+ }
443
+ const r2 = ssTot !== 0 ? 1 - ssRes / ssTot : 0;
444
+ if (r2 > 0.95) {
445
+ signals.push("mouse_movement_linear");
446
+ } else {
447
+ signals.push("mouse_movement_curved");
448
+ }
449
+ }
450
+ const completedForms = this.data.formEvents.filter((e) => e.endTime > 0);
451
+ if (completedForms.length > 0) {
452
+ const avgFillTime = completedForms.reduce((sum, e) => sum + (e.endTime - e.startTime), 0) / completedForms.length;
453
+ if (avgFillTime < 100) {
454
+ signals.push("form_fill_instant");
455
+ } else if (avgFillTime < 500) {
456
+ signals.push("form_fill_fast");
457
+ } else {
458
+ signals.push("form_fill_normal");
459
+ }
460
+ }
461
+ if (this.data.focusBlurEvents.length >= 4) {
462
+ const recentFB = this.data.focusBlurEvents.slice(-10);
463
+ const intervals = [];
464
+ for (let i = 1; i < recentFB.length; i++) {
465
+ intervals.push(recentFB[i].time - recentFB[i - 1].time);
466
+ }
467
+ const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
468
+ if (avgInterval < 1e3) {
469
+ signals.push("focus_blur_rapid");
470
+ } else {
471
+ signals.push("focus_blur_normal");
472
+ }
473
+ }
474
+ return signals;
475
+ }
476
+ /**
477
+ * Add context signals (set by tracker from external data)
478
+ */
479
+ addContextSignal(_signal) {
480
+ }
481
+ /**
482
+ * Get current result (null if not yet classified)
483
+ */
484
+ getResult() {
485
+ return this.result;
486
+ }
487
+ /**
488
+ * Check if classification has been performed
489
+ */
490
+ hasClassified() {
491
+ return this.classified;
492
+ }
493
+ };
494
+
495
+ // src/detection/focus-blur.ts
496
+ var FocusBlurAnalyzer = class {
497
+ constructor() {
498
+ this.sequence = [];
499
+ this.firstInteractionTime = null;
500
+ this.analyzed = false;
501
+ this.result = null;
502
+ this.pageLoadTime = performance.now();
503
+ }
504
+ /**
505
+ * Initialize event tracking
506
+ * Must be called after DOM is ready
507
+ */
508
+ initTracking() {
509
+ document.addEventListener("focus", (e) => {
510
+ this.recordEvent("focus", e.target);
511
+ }, true);
512
+ document.addEventListener("blur", (e) => {
513
+ this.recordEvent("blur", e.target);
514
+ }, true);
515
+ window.addEventListener("focus", () => {
516
+ this.recordEvent("window_focus", null);
517
+ });
518
+ window.addEventListener("blur", () => {
519
+ this.recordEvent("window_blur", null);
520
+ });
521
+ const recordFirstInteraction = () => {
522
+ if (this.firstInteractionTime === null) {
523
+ this.firstInteractionTime = performance.now();
524
+ }
525
+ };
526
+ document.addEventListener("click", recordFirstInteraction, { once: true, passive: true });
527
+ document.addEventListener("keydown", recordFirstInteraction, { once: true, passive: true });
528
+ }
529
+ /**
530
+ * Record a focus/blur event
531
+ */
532
+ recordEvent(type, target) {
533
+ const event = {
534
+ type,
535
+ target: target?.tagName || "WINDOW",
536
+ timestamp: performance.now()
537
+ };
538
+ this.sequence.push(event);
539
+ if (this.sequence.length > 20) {
540
+ this.sequence = this.sequence.slice(-20);
541
+ }
542
+ }
543
+ /**
544
+ * Analyze the focus/blur sequence for paste patterns
545
+ */
546
+ analyze() {
547
+ if (this.analyzed && this.result) {
548
+ return this.result;
549
+ }
550
+ const signals = [];
551
+ let confidence = 0;
552
+ const earlyEvents = this.sequence.filter((e) => e.timestamp < this.pageLoadTime + 500);
553
+ const hasEarlyWindowFocus = earlyEvents.some((e) => e.type === "window_focus");
554
+ if (hasEarlyWindowFocus) {
555
+ signals.push("early_window_focus");
556
+ confidence += 0.15;
557
+ }
558
+ const hasEarlyBodyFocus = earlyEvents.some(
559
+ (e) => e.type === "focus" && e.target === "BODY"
560
+ );
561
+ if (hasEarlyBodyFocus) {
562
+ signals.push("early_body_focus");
563
+ confidence += 0.15;
564
+ }
565
+ const hasLinkFocus = this.sequence.some(
566
+ (e) => e.type === "focus" && e.target === "A"
567
+ );
568
+ if (!hasLinkFocus) {
569
+ signals.push("no_link_focus");
570
+ confidence += 0.1;
571
+ }
572
+ const firstFocus = this.sequence.find((e) => e.type === "focus");
573
+ if (firstFocus && (firstFocus.target === "BODY" || firstFocus.target === "HTML")) {
574
+ signals.push("first_focus_body");
575
+ confidence += 0.1;
576
+ }
577
+ const windowEvents = this.sequence.filter(
578
+ (e) => e.type === "window_focus" || e.type === "window_blur"
579
+ );
580
+ if (windowEvents.length <= 2) {
581
+ signals.push("minimal_window_switches");
582
+ confidence += 0.05;
583
+ }
584
+ if (this.firstInteractionTime !== null) {
585
+ const timeToInteraction = this.firstInteractionTime - this.pageLoadTime;
586
+ if (timeToInteraction > 3e3) {
587
+ signals.push("delayed_first_interaction");
588
+ confidence += 0.1;
589
+ }
590
+ }
591
+ confidence = Math.min(confidence, 0.65);
592
+ let navType;
593
+ if (confidence >= 0.35) {
594
+ navType = "likely_paste";
595
+ } else if (signals.length === 0) {
596
+ navType = "unknown";
597
+ } else {
598
+ navType = "likely_click";
599
+ }
600
+ this.result = {
601
+ nav_type: navType,
602
+ confidence,
603
+ signals,
604
+ sequence: this.sequence.slice(-10),
605
+ time_to_first_interaction_ms: this.firstInteractionTime ? Math.round(this.firstInteractionTime - this.pageLoadTime) : null
606
+ };
607
+ this.analyzed = true;
608
+ return this.result;
609
+ }
610
+ /**
611
+ * Get current result (analyze if not done)
612
+ */
613
+ getResult() {
614
+ return this.analyze();
615
+ }
616
+ /**
617
+ * Check if analysis has been performed
618
+ */
619
+ hasAnalyzed() {
620
+ return this.analyzed;
621
+ }
622
+ /**
623
+ * Get the raw sequence for debugging
624
+ */
625
+ getSequence() {
626
+ return [...this.sequence];
627
+ }
628
+ /**
629
+ * Reset the analyzer
630
+ */
631
+ reset() {
632
+ this.sequence = [];
633
+ this.pageLoadTime = performance.now();
634
+ this.firstInteractionTime = null;
635
+ this.analyzed = false;
636
+ this.result = null;
637
+ }
638
+ };
639
+
179
640
  // src/utils.ts
180
641
  function generateUUID() {
181
642
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
@@ -261,6 +722,10 @@ var sessionId = null;
261
722
  var sessionStartTime = null;
262
723
  var navigationTiming = null;
263
724
  var aiDetection = null;
725
+ var behavioralClassifier = null;
726
+ var behavioralMLResult = null;
727
+ var focusBlurAnalyzer = null;
728
+ var focusBlurResult = null;
264
729
  function log(...args) {
265
730
  if (debugMode) {
266
731
  console.log("[Loamly]", ...args);
@@ -300,6 +765,16 @@ function init(userConfig = {}) {
300
765
  if (!userConfig.disableBehavioral) {
301
766
  setupBehavioralTracking();
302
767
  }
768
+ behavioralClassifier = new BehavioralClassifier(1e4);
769
+ behavioralClassifier.setOnClassify(handleBehavioralClassification);
770
+ setupBehavioralMLTracking();
771
+ focusBlurAnalyzer = new FocusBlurAnalyzer();
772
+ focusBlurAnalyzer.initTracking();
773
+ setTimeout(() => {
774
+ if (focusBlurAnalyzer) {
775
+ handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
776
+ }
777
+ }, 5e3);
303
778
  log("Initialization complete");
304
779
  }
305
780
  function pageview(customUrl) {
@@ -481,6 +956,116 @@ function sendBehavioralEvent(eventType, data) {
481
956
  body: JSON.stringify(payload)
482
957
  });
483
958
  }
959
+ function setupBehavioralMLTracking() {
960
+ if (!behavioralClassifier) return;
961
+ let mouseSampleCount = 0;
962
+ document.addEventListener("mousemove", (e) => {
963
+ mouseSampleCount++;
964
+ if (mouseSampleCount % 10 === 0 && behavioralClassifier) {
965
+ behavioralClassifier.recordMouse(e.clientX, e.clientY);
966
+ }
967
+ }, { passive: true });
968
+ document.addEventListener("click", () => {
969
+ if (behavioralClassifier) {
970
+ behavioralClassifier.recordClick();
971
+ }
972
+ }, { passive: true });
973
+ let lastScrollY = 0;
974
+ document.addEventListener("scroll", () => {
975
+ const currentY = window.scrollY;
976
+ if (Math.abs(currentY - lastScrollY) > 50 && behavioralClassifier) {
977
+ lastScrollY = currentY;
978
+ behavioralClassifier.recordScroll(currentY);
979
+ }
980
+ }, { passive: true });
981
+ document.addEventListener("focusin", (e) => {
982
+ if (behavioralClassifier) {
983
+ behavioralClassifier.recordFocusBlur("focus");
984
+ const target = e.target;
985
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
986
+ behavioralClassifier.recordFormStart(target.id || target.getAttribute("name") || "unknown");
987
+ }
988
+ }
989
+ }, { passive: true });
990
+ document.addEventListener("focusout", (e) => {
991
+ if (behavioralClassifier) {
992
+ behavioralClassifier.recordFocusBlur("blur");
993
+ const target = e.target;
994
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
995
+ behavioralClassifier.recordFormEnd(target.id || target.getAttribute("name") || "unknown");
996
+ }
997
+ }
998
+ }, { passive: true });
999
+ window.addEventListener("beforeunload", () => {
1000
+ if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
1001
+ const result = behavioralClassifier.forceClassify();
1002
+ if (result) {
1003
+ handleBehavioralClassification(result);
1004
+ }
1005
+ }
1006
+ });
1007
+ setTimeout(() => {
1008
+ if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
1009
+ behavioralClassifier.forceClassify();
1010
+ }
1011
+ }, 3e4);
1012
+ }
1013
+ function handleBehavioralClassification(result) {
1014
+ log("Behavioral ML classification:", result);
1015
+ behavioralMLResult = {
1016
+ classification: result.classification,
1017
+ humanProbability: result.humanProbability,
1018
+ aiProbability: result.aiProbability,
1019
+ confidence: result.confidence,
1020
+ signals: result.signals,
1021
+ sessionDurationMs: result.sessionDurationMs
1022
+ };
1023
+ sendBehavioralEvent("ml_classification", {
1024
+ classification: result.classification,
1025
+ human_probability: result.humanProbability,
1026
+ ai_probability: result.aiProbability,
1027
+ confidence: result.confidence,
1028
+ signals: result.signals,
1029
+ session_duration_ms: result.sessionDurationMs,
1030
+ navigation_timing: navigationTiming,
1031
+ ai_detection: aiDetection,
1032
+ focus_blur: focusBlurResult
1033
+ });
1034
+ if (result.classification === "ai_influenced" && result.confidence >= 0.7) {
1035
+ aiDetection = {
1036
+ isAI: true,
1037
+ confidence: result.confidence,
1038
+ method: "behavioral"
1039
+ };
1040
+ log("AI detection updated from behavioral ML:", aiDetection);
1041
+ }
1042
+ }
1043
+ function handleFocusBlurAnalysis(result) {
1044
+ log("Focus/blur analysis:", result);
1045
+ focusBlurResult = {
1046
+ navType: result.nav_type,
1047
+ confidence: result.confidence,
1048
+ signals: result.signals,
1049
+ timeToFirstInteractionMs: result.time_to_first_interaction_ms
1050
+ };
1051
+ sendBehavioralEvent("focus_blur_analysis", {
1052
+ nav_type: result.nav_type,
1053
+ confidence: result.confidence,
1054
+ signals: result.signals,
1055
+ time_to_first_interaction_ms: result.time_to_first_interaction_ms,
1056
+ sequence_length: result.sequence.length
1057
+ });
1058
+ if (result.nav_type === "likely_paste" && result.confidence >= 0.4) {
1059
+ if (!aiDetection || aiDetection.confidence < result.confidence) {
1060
+ aiDetection = {
1061
+ isAI: true,
1062
+ confidence: result.confidence,
1063
+ method: "behavioral"
1064
+ };
1065
+ log("AI detection updated from focus/blur analysis:", aiDetection);
1066
+ }
1067
+ }
1068
+ }
484
1069
  function getCurrentSessionId() {
485
1070
  return sessionId;
486
1071
  }
@@ -493,6 +1078,12 @@ function getAIDetectionResult() {
493
1078
  function getNavigationTimingResult() {
494
1079
  return navigationTiming;
495
1080
  }
1081
+ function getBehavioralMLResult() {
1082
+ return behavioralMLResult;
1083
+ }
1084
+ function getFocusBlurResult() {
1085
+ return focusBlurResult;
1086
+ }
496
1087
  function isTrackerInitialized() {
497
1088
  return initialized;
498
1089
  }
@@ -504,6 +1095,10 @@ function reset() {
504
1095
  sessionStartTime = null;
505
1096
  navigationTiming = null;
506
1097
  aiDetection = null;
1098
+ behavioralClassifier = null;
1099
+ behavioralMLResult = null;
1100
+ focusBlurAnalyzer = null;
1101
+ focusBlurResult = null;
507
1102
  try {
508
1103
  sessionStorage.removeItem("loamly_session");
509
1104
  sessionStorage.removeItem("loamly_start");
@@ -524,20 +1119,247 @@ var loamly = {
524
1119
  getVisitorId: getCurrentVisitorId,
525
1120
  getAIDetection: getAIDetectionResult,
526
1121
  getNavigationTiming: getNavigationTimingResult,
1122
+ getBehavioralML: getBehavioralMLResult,
1123
+ getFocusBlur: getFocusBlurResult,
527
1124
  isInitialized: isTrackerInitialized,
528
1125
  reset,
529
1126
  debug: setDebug
530
1127
  };
1128
+
1129
+ // src/detection/agentic-browser.ts
1130
+ var CometDetector = class {
1131
+ constructor() {
1132
+ this.detected = false;
1133
+ this.checkComplete = false;
1134
+ this.observer = null;
1135
+ }
1136
+ /**
1137
+ * Initialize detection
1138
+ * @param timeout - Max time to observe for Comet DOM (default: 5s)
1139
+ */
1140
+ init(timeout = 5e3) {
1141
+ if (typeof document === "undefined") return;
1142
+ this.check();
1143
+ if (!this.detected && document.body) {
1144
+ this.observer = new MutationObserver(() => this.check());
1145
+ this.observer.observe(document.body, { childList: true, subtree: true });
1146
+ setTimeout(() => {
1147
+ if (this.observer && !this.detected) {
1148
+ this.observer.disconnect();
1149
+ this.observer = null;
1150
+ this.checkComplete = true;
1151
+ }
1152
+ }, timeout);
1153
+ }
1154
+ }
1155
+ check() {
1156
+ if (document.querySelector(".pplx-agent-overlay-stop-button")) {
1157
+ this.detected = true;
1158
+ this.checkComplete = true;
1159
+ if (this.observer) {
1160
+ this.observer.disconnect();
1161
+ this.observer = null;
1162
+ }
1163
+ }
1164
+ }
1165
+ isDetected() {
1166
+ return this.detected;
1167
+ }
1168
+ isCheckComplete() {
1169
+ return this.checkComplete;
1170
+ }
1171
+ destroy() {
1172
+ if (this.observer) {
1173
+ this.observer.disconnect();
1174
+ this.observer = null;
1175
+ }
1176
+ }
1177
+ };
1178
+ var MouseAnalyzer = class {
1179
+ /**
1180
+ * @param teleportThreshold - Distance in pixels to consider a teleport (default: 500)
1181
+ */
1182
+ constructor(teleportThreshold = 500) {
1183
+ this.lastX = -1;
1184
+ this.lastY = -1;
1185
+ this.teleportingClicks = 0;
1186
+ this.totalMovements = 0;
1187
+ this.handleMove = (e) => {
1188
+ this.totalMovements++;
1189
+ this.lastX = e.clientX;
1190
+ this.lastY = e.clientY;
1191
+ };
1192
+ this.handleClick = (e) => {
1193
+ if (this.lastX !== -1 && this.lastY !== -1) {
1194
+ const dx = Math.abs(e.clientX - this.lastX);
1195
+ const dy = Math.abs(e.clientY - this.lastY);
1196
+ if (dx > this.teleportThreshold || dy > this.teleportThreshold) {
1197
+ this.teleportingClicks++;
1198
+ }
1199
+ }
1200
+ this.lastX = e.clientX;
1201
+ this.lastY = e.clientY;
1202
+ };
1203
+ this.teleportThreshold = teleportThreshold;
1204
+ }
1205
+ /**
1206
+ * Initialize mouse tracking
1207
+ */
1208
+ init() {
1209
+ if (typeof document === "undefined") return;
1210
+ document.addEventListener("mousemove", this.handleMove, { passive: true });
1211
+ document.addEventListener("mousedown", this.handleClick, { passive: true });
1212
+ }
1213
+ getPatterns() {
1214
+ return {
1215
+ teleportingClicks: this.teleportingClicks,
1216
+ totalMovements: this.totalMovements
1217
+ };
1218
+ }
1219
+ destroy() {
1220
+ if (typeof document === "undefined") return;
1221
+ document.removeEventListener("mousemove", this.handleMove);
1222
+ document.removeEventListener("mousedown", this.handleClick);
1223
+ }
1224
+ };
1225
+ var CDPDetector = class {
1226
+ constructor() {
1227
+ this.detected = false;
1228
+ }
1229
+ /**
1230
+ * Run detection checks
1231
+ */
1232
+ detect() {
1233
+ if (typeof navigator === "undefined") return false;
1234
+ if (navigator.webdriver) {
1235
+ this.detected = true;
1236
+ return true;
1237
+ }
1238
+ if (typeof window !== "undefined") {
1239
+ const win = window;
1240
+ const automationProps = [
1241
+ "__webdriver_evaluate",
1242
+ "__selenium_evaluate",
1243
+ "__webdriver_script_function",
1244
+ "__webdriver_script_func",
1245
+ "__webdriver_script_fn",
1246
+ "__fxdriver_evaluate",
1247
+ "__driver_unwrapped",
1248
+ "__webdriver_unwrapped",
1249
+ "__driver_evaluate",
1250
+ "__selenium_unwrapped",
1251
+ "__fxdriver_unwrapped"
1252
+ ];
1253
+ for (const prop of automationProps) {
1254
+ if (prop in win) {
1255
+ this.detected = true;
1256
+ return true;
1257
+ }
1258
+ }
1259
+ }
1260
+ return false;
1261
+ }
1262
+ isDetected() {
1263
+ return this.detected;
1264
+ }
1265
+ };
1266
+ var AgenticBrowserAnalyzer = class {
1267
+ constructor() {
1268
+ this.initialized = false;
1269
+ this.cometDetector = new CometDetector();
1270
+ this.mouseAnalyzer = new MouseAnalyzer();
1271
+ this.cdpDetector = new CDPDetector();
1272
+ }
1273
+ /**
1274
+ * Initialize all detectors
1275
+ */
1276
+ init() {
1277
+ if (this.initialized) return;
1278
+ this.initialized = true;
1279
+ this.cometDetector.init();
1280
+ this.mouseAnalyzer.init();
1281
+ this.cdpDetector.detect();
1282
+ }
1283
+ /**
1284
+ * Get current detection result
1285
+ */
1286
+ getResult() {
1287
+ const signals = [];
1288
+ let probability = 0;
1289
+ if (this.cometDetector.isDetected()) {
1290
+ signals.push("comet_dom_detected");
1291
+ probability = Math.max(probability, 0.85);
1292
+ }
1293
+ if (this.cdpDetector.isDetected()) {
1294
+ signals.push("cdp_detected");
1295
+ probability = Math.max(probability, 0.92);
1296
+ }
1297
+ const mousePatterns = this.mouseAnalyzer.getPatterns();
1298
+ if (mousePatterns.teleportingClicks > 0) {
1299
+ signals.push(`teleporting_clicks:${mousePatterns.teleportingClicks}`);
1300
+ probability = Math.max(probability, 0.78);
1301
+ }
1302
+ return {
1303
+ cometDOMDetected: this.cometDetector.isDetected(),
1304
+ cdpDetected: this.cdpDetector.isDetected(),
1305
+ mousePatterns,
1306
+ agenticProbability: probability,
1307
+ signals
1308
+ };
1309
+ }
1310
+ /**
1311
+ * Cleanup resources
1312
+ */
1313
+ destroy() {
1314
+ this.cometDetector.destroy();
1315
+ this.mouseAnalyzer.destroy();
1316
+ }
1317
+ };
1318
+ function createAgenticAnalyzer() {
1319
+ const analyzer = new AgenticBrowserAnalyzer();
1320
+ if (typeof document !== "undefined") {
1321
+ if (document.readyState === "loading") {
1322
+ document.addEventListener("DOMContentLoaded", () => analyzer.init());
1323
+ } else {
1324
+ analyzer.init();
1325
+ }
1326
+ }
1327
+ return analyzer;
1328
+ }
531
1329
  export {
532
1330
  AI_BOT_PATTERNS,
533
1331
  AI_PLATFORMS,
1332
+ AgenticBrowserAnalyzer,
534
1333
  VERSION,
1334
+ createAgenticAnalyzer,
535
1335
  loamly as default,
536
1336
  detectAIFromReferrer,
537
1337
  detectAIFromUTM,
538
1338
  detectNavigationType,
539
1339
  loamly
540
1340
  };
1341
+ /**
1342
+ * Loamly Tracker Configuration
1343
+ *
1344
+ * @module @loamly/tracker
1345
+ * @license MIT
1346
+ * @see https://github.com/loamly/loamly
1347
+ */
1348
+ /**
1349
+ * Agentic Browser Detection
1350
+ *
1351
+ * LOA-187: Detects AI agentic browsers like Perplexity Comet, ChatGPT Atlas,
1352
+ * and other automated browsing agents.
1353
+ *
1354
+ * Detection methods:
1355
+ * - DOM fingerprinting (Perplexity Comet overlay)
1356
+ * - Mouse movement patterns (teleporting clicks)
1357
+ * - CDP (Chrome DevTools Protocol) automation fingerprint
1358
+ * - navigator.webdriver detection
1359
+ *
1360
+ * @module @loamly/tracker/detection/agentic-browser
1361
+ * @license MIT
1362
+ */
541
1363
  /**
542
1364
  * Loamly Tracker
543
1365
  *
@@ -545,7 +1367,9 @@ export {
545
1367
  * See what AI tells your customers — and track when they click.
546
1368
  *
547
1369
  * @module @loamly/tracker
1370
+ * @version 1.8.0
548
1371
  * @license MIT
1372
+ * @see https://github.com/loamly/loamly
549
1373
  * @see https://loamly.ai
550
1374
  */
551
1375
  //# sourceMappingURL=index.mjs.map