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