@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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/config.ts
2
- var VERSION = "1.7.0";
2
+ var VERSION = "1.8.0";
3
3
  var DEFAULT_CONFIG = {
4
4
  apiHost: "https://app.loamly.ai",
5
5
  endpoints: {
@@ -492,6 +492,151 @@ var BehavioralClassifier = class {
492
492
  }
493
493
  };
494
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
+
495
640
  // src/utils.ts
496
641
  function generateUUID() {
497
642
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
@@ -579,6 +724,8 @@ var navigationTiming = null;
579
724
  var aiDetection = null;
580
725
  var behavioralClassifier = null;
581
726
  var behavioralMLResult = null;
727
+ var focusBlurAnalyzer = null;
728
+ var focusBlurResult = null;
582
729
  function log(...args) {
583
730
  if (debugMode) {
584
731
  console.log("[Loamly]", ...args);
@@ -621,6 +768,13 @@ function init(userConfig = {}) {
621
768
  behavioralClassifier = new BehavioralClassifier(1e4);
622
769
  behavioralClassifier.setOnClassify(handleBehavioralClassification);
623
770
  setupBehavioralMLTracking();
771
+ focusBlurAnalyzer = new FocusBlurAnalyzer();
772
+ focusBlurAnalyzer.initTracking();
773
+ setTimeout(() => {
774
+ if (focusBlurAnalyzer) {
775
+ handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
776
+ }
777
+ }, 5e3);
624
778
  log("Initialization complete");
625
779
  }
626
780
  function pageview(customUrl) {
@@ -874,7 +1028,8 @@ function handleBehavioralClassification(result) {
874
1028
  signals: result.signals,
875
1029
  session_duration_ms: result.sessionDurationMs,
876
1030
  navigation_timing: navigationTiming,
877
- ai_detection: aiDetection
1031
+ ai_detection: aiDetection,
1032
+ focus_blur: focusBlurResult
878
1033
  });
879
1034
  if (result.classification === "ai_influenced" && result.confidence >= 0.7) {
880
1035
  aiDetection = {
@@ -885,6 +1040,32 @@ function handleBehavioralClassification(result) {
885
1040
  log("AI detection updated from behavioral ML:", aiDetection);
886
1041
  }
887
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
+ }
888
1069
  function getCurrentSessionId() {
889
1070
  return sessionId;
890
1071
  }
@@ -900,6 +1081,9 @@ function getNavigationTimingResult() {
900
1081
  function getBehavioralMLResult() {
901
1082
  return behavioralMLResult;
902
1083
  }
1084
+ function getFocusBlurResult() {
1085
+ return focusBlurResult;
1086
+ }
903
1087
  function isTrackerInitialized() {
904
1088
  return initialized;
905
1089
  }
@@ -913,6 +1097,8 @@ function reset() {
913
1097
  aiDetection = null;
914
1098
  behavioralClassifier = null;
915
1099
  behavioralMLResult = null;
1100
+ focusBlurAnalyzer = null;
1101
+ focusBlurResult = null;
916
1102
  try {
917
1103
  sessionStorage.removeItem("loamly_session");
918
1104
  sessionStorage.removeItem("loamly_start");
@@ -934,20 +1120,246 @@ var loamly = {
934
1120
  getAIDetection: getAIDetectionResult,
935
1121
  getNavigationTiming: getNavigationTimingResult,
936
1122
  getBehavioralML: getBehavioralMLResult,
1123
+ getFocusBlur: getFocusBlurResult,
937
1124
  isInitialized: isTrackerInitialized,
938
1125
  reset,
939
1126
  debug: setDebug
940
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
+ }
941
1329
  export {
942
1330
  AI_BOT_PATTERNS,
943
1331
  AI_PLATFORMS,
1332
+ AgenticBrowserAnalyzer,
944
1333
  VERSION,
1334
+ createAgenticAnalyzer,
945
1335
  loamly as default,
946
1336
  detectAIFromReferrer,
947
1337
  detectAIFromUTM,
948
1338
  detectNavigationType,
949
1339
  loamly
950
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
+ */
951
1363
  /**
952
1364
  * Loamly Tracker
953
1365
  *
@@ -955,7 +1367,9 @@ export {
955
1367
  * See what AI tells your customers — and track when they click.
956
1368
  *
957
1369
  * @module @loamly/tracker
1370
+ * @version 1.8.0
958
1371
  * @license MIT
1372
+ * @see https://github.com/loamly/loamly
959
1373
  * @see https://loamly.ai
960
1374
  */
961
1375
  //# sourceMappingURL=index.mjs.map