@loamly/tracker 1.7.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.7.0";
37
+ var VERSION = "1.8.0";
36
38
  var DEFAULT_CONFIG = {
37
39
  apiHost: "https://app.loamly.ai",
38
40
  endpoints: {
@@ -525,6 +527,151 @@ var BehavioralClassifier = class {
525
527
  }
526
528
  };
527
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
+
528
675
  // src/utils.ts
529
676
  function generateUUID() {
530
677
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
@@ -612,6 +759,8 @@ var navigationTiming = null;
612
759
  var aiDetection = null;
613
760
  var behavioralClassifier = null;
614
761
  var behavioralMLResult = null;
762
+ var focusBlurAnalyzer = null;
763
+ var focusBlurResult = null;
615
764
  function log(...args) {
616
765
  if (debugMode) {
617
766
  console.log("[Loamly]", ...args);
@@ -654,6 +803,13 @@ function init(userConfig = {}) {
654
803
  behavioralClassifier = new BehavioralClassifier(1e4);
655
804
  behavioralClassifier.setOnClassify(handleBehavioralClassification);
656
805
  setupBehavioralMLTracking();
806
+ focusBlurAnalyzer = new FocusBlurAnalyzer();
807
+ focusBlurAnalyzer.initTracking();
808
+ setTimeout(() => {
809
+ if (focusBlurAnalyzer) {
810
+ handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
811
+ }
812
+ }, 5e3);
657
813
  log("Initialization complete");
658
814
  }
659
815
  function pageview(customUrl) {
@@ -907,7 +1063,8 @@ function handleBehavioralClassification(result) {
907
1063
  signals: result.signals,
908
1064
  session_duration_ms: result.sessionDurationMs,
909
1065
  navigation_timing: navigationTiming,
910
- ai_detection: aiDetection
1066
+ ai_detection: aiDetection,
1067
+ focus_blur: focusBlurResult
911
1068
  });
912
1069
  if (result.classification === "ai_influenced" && result.confidence >= 0.7) {
913
1070
  aiDetection = {
@@ -918,6 +1075,32 @@ function handleBehavioralClassification(result) {
918
1075
  log("AI detection updated from behavioral ML:", aiDetection);
919
1076
  }
920
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
+ }
921
1104
  function getCurrentSessionId() {
922
1105
  return sessionId;
923
1106
  }
@@ -933,6 +1116,9 @@ function getNavigationTimingResult() {
933
1116
  function getBehavioralMLResult() {
934
1117
  return behavioralMLResult;
935
1118
  }
1119
+ function getFocusBlurResult() {
1120
+ return focusBlurResult;
1121
+ }
936
1122
  function isTrackerInitialized() {
937
1123
  return initialized;
938
1124
  }
@@ -946,6 +1132,8 @@ function reset() {
946
1132
  aiDetection = null;
947
1133
  behavioralClassifier = null;
948
1134
  behavioralMLResult = null;
1135
+ focusBlurAnalyzer = null;
1136
+ focusBlurResult = null;
949
1137
  try {
950
1138
  sessionStorage.removeItem("loamly_session");
951
1139
  sessionStorage.removeItem("loamly_start");
@@ -967,20 +1155,246 @@ var loamly = {
967
1155
  getAIDetection: getAIDetectionResult,
968
1156
  getNavigationTiming: getNavigationTimingResult,
969
1157
  getBehavioralML: getBehavioralMLResult,
1158
+ getFocusBlur: getFocusBlurResult,
970
1159
  isInitialized: isTrackerInitialized,
971
1160
  reset,
972
1161
  debug: setDebug
973
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
+ }
974
1364
  // Annotate the CommonJS export names for ESM import in node:
975
1365
  0 && (module.exports = {
976
1366
  AI_BOT_PATTERNS,
977
1367
  AI_PLATFORMS,
1368
+ AgenticBrowserAnalyzer,
978
1369
  VERSION,
1370
+ createAgenticAnalyzer,
979
1371
  detectAIFromReferrer,
980
1372
  detectAIFromUTM,
981
1373
  detectNavigationType,
982
1374
  loamly
983
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
+ */
984
1398
  /**
985
1399
  * Loamly Tracker
986
1400
  *
@@ -988,7 +1402,9 @@ var loamly = {
988
1402
  * See what AI tells your customers — and track when they click.
989
1403
  *
990
1404
  * @module @loamly/tracker
1405
+ * @version 1.8.0
991
1406
  * @license MIT
1407
+ * @see https://github.com/loamly/loamly
992
1408
  * @see https://loamly.ai
993
1409
  */
994
1410
  //# sourceMappingURL=index.cjs.map