@loamly/tracker 1.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,7 @@ var Loamly = (() => {
26
26
  });
27
27
 
28
28
  // src/config.ts
29
- var VERSION = "1.8.0";
29
+ var VERSION = "2.0.0";
30
30
  var DEFAULT_CONFIG = {
31
31
  apiHost: "https://app.loamly.ai",
32
32
  endpoints: {
@@ -652,6 +652,1014 @@ var Loamly = (() => {
652
652
  }
653
653
  };
654
654
 
655
+ // src/detection/agentic-browser.ts
656
+ var CometDetector = class {
657
+ constructor() {
658
+ this.detected = false;
659
+ this.checkComplete = false;
660
+ this.observer = null;
661
+ }
662
+ /**
663
+ * Initialize detection
664
+ * @param timeout - Max time to observe for Comet DOM (default: 5s)
665
+ */
666
+ init(timeout = 5e3) {
667
+ if (typeof document === "undefined") return;
668
+ this.check();
669
+ if (!this.detected && document.body) {
670
+ this.observer = new MutationObserver(() => this.check());
671
+ this.observer.observe(document.body, { childList: true, subtree: true });
672
+ setTimeout(() => {
673
+ if (this.observer && !this.detected) {
674
+ this.observer.disconnect();
675
+ this.observer = null;
676
+ this.checkComplete = true;
677
+ }
678
+ }, timeout);
679
+ }
680
+ }
681
+ check() {
682
+ if (document.querySelector(".pplx-agent-overlay-stop-button")) {
683
+ this.detected = true;
684
+ this.checkComplete = true;
685
+ if (this.observer) {
686
+ this.observer.disconnect();
687
+ this.observer = null;
688
+ }
689
+ }
690
+ }
691
+ isDetected() {
692
+ return this.detected;
693
+ }
694
+ isCheckComplete() {
695
+ return this.checkComplete;
696
+ }
697
+ destroy() {
698
+ if (this.observer) {
699
+ this.observer.disconnect();
700
+ this.observer = null;
701
+ }
702
+ }
703
+ };
704
+ var MouseAnalyzer = class {
705
+ /**
706
+ * @param teleportThreshold - Distance in pixels to consider a teleport (default: 500)
707
+ */
708
+ constructor(teleportThreshold = 500) {
709
+ this.lastX = -1;
710
+ this.lastY = -1;
711
+ this.teleportingClicks = 0;
712
+ this.totalMovements = 0;
713
+ this.handleMove = (e) => {
714
+ this.totalMovements++;
715
+ this.lastX = e.clientX;
716
+ this.lastY = e.clientY;
717
+ };
718
+ this.handleClick = (e) => {
719
+ if (this.lastX !== -1 && this.lastY !== -1) {
720
+ const dx = Math.abs(e.clientX - this.lastX);
721
+ const dy = Math.abs(e.clientY - this.lastY);
722
+ if (dx > this.teleportThreshold || dy > this.teleportThreshold) {
723
+ this.teleportingClicks++;
724
+ }
725
+ }
726
+ this.lastX = e.clientX;
727
+ this.lastY = e.clientY;
728
+ };
729
+ this.teleportThreshold = teleportThreshold;
730
+ }
731
+ /**
732
+ * Initialize mouse tracking
733
+ */
734
+ init() {
735
+ if (typeof document === "undefined") return;
736
+ document.addEventListener("mousemove", this.handleMove, { passive: true });
737
+ document.addEventListener("mousedown", this.handleClick, { passive: true });
738
+ }
739
+ getPatterns() {
740
+ return {
741
+ teleportingClicks: this.teleportingClicks,
742
+ totalMovements: this.totalMovements
743
+ };
744
+ }
745
+ destroy() {
746
+ if (typeof document === "undefined") return;
747
+ document.removeEventListener("mousemove", this.handleMove);
748
+ document.removeEventListener("mousedown", this.handleClick);
749
+ }
750
+ };
751
+ var CDPDetector = class {
752
+ constructor() {
753
+ this.detected = false;
754
+ }
755
+ /**
756
+ * Run detection checks
757
+ */
758
+ detect() {
759
+ if (typeof navigator === "undefined") return false;
760
+ if (navigator.webdriver) {
761
+ this.detected = true;
762
+ return true;
763
+ }
764
+ if (typeof window !== "undefined") {
765
+ const win = window;
766
+ const automationProps = [
767
+ "__webdriver_evaluate",
768
+ "__selenium_evaluate",
769
+ "__webdriver_script_function",
770
+ "__webdriver_script_func",
771
+ "__webdriver_script_fn",
772
+ "__fxdriver_evaluate",
773
+ "__driver_unwrapped",
774
+ "__webdriver_unwrapped",
775
+ "__driver_evaluate",
776
+ "__selenium_unwrapped",
777
+ "__fxdriver_unwrapped"
778
+ ];
779
+ for (const prop of automationProps) {
780
+ if (prop in win) {
781
+ this.detected = true;
782
+ return true;
783
+ }
784
+ }
785
+ }
786
+ return false;
787
+ }
788
+ isDetected() {
789
+ return this.detected;
790
+ }
791
+ };
792
+ var AgenticBrowserAnalyzer = class {
793
+ constructor() {
794
+ this.initialized = false;
795
+ this.cometDetector = new CometDetector();
796
+ this.mouseAnalyzer = new MouseAnalyzer();
797
+ this.cdpDetector = new CDPDetector();
798
+ }
799
+ /**
800
+ * Initialize all detectors
801
+ */
802
+ init() {
803
+ if (this.initialized) return;
804
+ this.initialized = true;
805
+ this.cometDetector.init();
806
+ this.mouseAnalyzer.init();
807
+ this.cdpDetector.detect();
808
+ }
809
+ /**
810
+ * Get current detection result
811
+ */
812
+ getResult() {
813
+ const signals = [];
814
+ let probability = 0;
815
+ if (this.cometDetector.isDetected()) {
816
+ signals.push("comet_dom_detected");
817
+ probability = Math.max(probability, 0.85);
818
+ }
819
+ if (this.cdpDetector.isDetected()) {
820
+ signals.push("cdp_detected");
821
+ probability = Math.max(probability, 0.92);
822
+ }
823
+ const mousePatterns = this.mouseAnalyzer.getPatterns();
824
+ if (mousePatterns.teleportingClicks > 0) {
825
+ signals.push(`teleporting_clicks:${mousePatterns.teleportingClicks}`);
826
+ probability = Math.max(probability, 0.78);
827
+ }
828
+ return {
829
+ cometDOMDetected: this.cometDetector.isDetected(),
830
+ cdpDetected: this.cdpDetector.isDetected(),
831
+ mousePatterns,
832
+ agenticProbability: probability,
833
+ signals
834
+ };
835
+ }
836
+ /**
837
+ * Cleanup resources
838
+ */
839
+ destroy() {
840
+ this.cometDetector.destroy();
841
+ this.mouseAnalyzer.destroy();
842
+ }
843
+ };
844
+
845
+ // src/infrastructure/event-queue.ts
846
+ var DEFAULT_QUEUE_CONFIG = {
847
+ batchSize: DEFAULT_CONFIG.batchSize,
848
+ batchTimeout: DEFAULT_CONFIG.batchTimeout,
849
+ maxRetries: 3,
850
+ retryDelayMs: 1e3,
851
+ storageKey: "_loamly_queue"
852
+ };
853
+ var EventQueue = class {
854
+ constructor(endpoint2, config2 = {}) {
855
+ this.queue = [];
856
+ this.batchTimer = null;
857
+ this.isFlushing = false;
858
+ this.endpoint = endpoint2;
859
+ this.config = { ...DEFAULT_QUEUE_CONFIG, ...config2 };
860
+ this.loadFromStorage();
861
+ }
862
+ /**
863
+ * Add event to queue
864
+ */
865
+ push(type, payload) {
866
+ const event = {
867
+ id: this.generateId(),
868
+ type,
869
+ payload,
870
+ timestamp: Date.now(),
871
+ retries: 0
872
+ };
873
+ this.queue.push(event);
874
+ this.saveToStorage();
875
+ this.scheduleBatch();
876
+ }
877
+ /**
878
+ * Force flush all events immediately
879
+ */
880
+ async flush() {
881
+ if (this.isFlushing || this.queue.length === 0) return;
882
+ this.isFlushing = true;
883
+ this.clearBatchTimer();
884
+ try {
885
+ const events = [...this.queue];
886
+ this.queue = [];
887
+ await this.sendBatch(events);
888
+ } finally {
889
+ this.isFlushing = false;
890
+ this.saveToStorage();
891
+ }
892
+ }
893
+ /**
894
+ * Flush using sendBeacon (for unload events)
895
+ */
896
+ flushBeacon() {
897
+ if (this.queue.length === 0) return true;
898
+ const events = this.queue.map((e) => ({
899
+ type: e.type,
900
+ ...e.payload,
901
+ _queue_id: e.id,
902
+ _queue_timestamp: e.timestamp
903
+ }));
904
+ const success = navigator.sendBeacon?.(
905
+ this.endpoint,
906
+ JSON.stringify({ events, beacon: true })
907
+ ) ?? false;
908
+ if (success) {
909
+ this.queue = [];
910
+ this.clearStorage();
911
+ }
912
+ return success;
913
+ }
914
+ /**
915
+ * Get current queue length
916
+ */
917
+ get length() {
918
+ return this.queue.length;
919
+ }
920
+ scheduleBatch() {
921
+ if (this.batchTimer) return;
922
+ if (this.queue.length >= this.config.batchSize) {
923
+ this.flush();
924
+ return;
925
+ }
926
+ this.batchTimer = setTimeout(() => {
927
+ this.batchTimer = null;
928
+ this.flush();
929
+ }, this.config.batchTimeout);
930
+ }
931
+ clearBatchTimer() {
932
+ if (this.batchTimer) {
933
+ clearTimeout(this.batchTimer);
934
+ this.batchTimer = null;
935
+ }
936
+ }
937
+ async sendBatch(events) {
938
+ if (events.length === 0) return;
939
+ const payload = {
940
+ events: events.map((e) => ({
941
+ type: e.type,
942
+ ...e.payload,
943
+ _queue_id: e.id,
944
+ _queue_timestamp: e.timestamp
945
+ })),
946
+ batch: true
947
+ };
948
+ try {
949
+ const response = await fetch(this.endpoint, {
950
+ method: "POST",
951
+ headers: { "Content-Type": "application/json" },
952
+ body: JSON.stringify(payload)
953
+ });
954
+ if (!response.ok) {
955
+ throw new Error(`HTTP ${response.status}`);
956
+ }
957
+ } catch (error) {
958
+ for (const event of events) {
959
+ if (event.retries < this.config.maxRetries) {
960
+ event.retries++;
961
+ this.queue.push(event);
962
+ }
963
+ }
964
+ if (this.queue.length > 0) {
965
+ const delay = this.config.retryDelayMs * Math.pow(2, events[0].retries - 1);
966
+ setTimeout(() => this.flush(), delay);
967
+ }
968
+ }
969
+ }
970
+ loadFromStorage() {
971
+ try {
972
+ const stored = localStorage.getItem(this.config.storageKey);
973
+ if (stored) {
974
+ const parsed = JSON.parse(stored);
975
+ if (Array.isArray(parsed)) {
976
+ const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
977
+ this.queue = parsed.filter((e) => e.timestamp > cutoff);
978
+ }
979
+ }
980
+ } catch {
981
+ }
982
+ }
983
+ saveToStorage() {
984
+ try {
985
+ if (this.queue.length > 0) {
986
+ localStorage.setItem(this.config.storageKey, JSON.stringify(this.queue));
987
+ } else {
988
+ this.clearStorage();
989
+ }
990
+ } catch {
991
+ }
992
+ }
993
+ clearStorage() {
994
+ try {
995
+ localStorage.removeItem(this.config.storageKey);
996
+ } catch {
997
+ }
998
+ }
999
+ generateId() {
1000
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
1001
+ }
1002
+ };
1003
+
1004
+ // src/infrastructure/ping.ts
1005
+ var PingService = class {
1006
+ constructor(sessionId2, visitorId2, version, config2 = {}) {
1007
+ this.intervalId = null;
1008
+ this.isVisible = true;
1009
+ this.currentScrollDepth = 0;
1010
+ this.ping = async () => {
1011
+ const data = this.getData();
1012
+ this.config.onPing?.(data);
1013
+ if (this.config.endpoint) {
1014
+ try {
1015
+ await fetch(this.config.endpoint, {
1016
+ method: "POST",
1017
+ headers: { "Content-Type": "application/json" },
1018
+ body: JSON.stringify(data)
1019
+ });
1020
+ } catch {
1021
+ }
1022
+ }
1023
+ };
1024
+ this.handleVisibilityChange = () => {
1025
+ this.isVisible = document.visibilityState === "visible";
1026
+ };
1027
+ this.handleScroll = () => {
1028
+ const scrollPercent = Math.round(
1029
+ (window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100
1030
+ );
1031
+ if (scrollPercent > this.currentScrollDepth) {
1032
+ this.currentScrollDepth = Math.min(scrollPercent, 100);
1033
+ }
1034
+ };
1035
+ this.sessionId = sessionId2;
1036
+ this.visitorId = visitorId2;
1037
+ this.version = version;
1038
+ this.pageLoadTime = Date.now();
1039
+ this.config = {
1040
+ interval: DEFAULT_CONFIG.pingInterval,
1041
+ endpoint: "",
1042
+ ...config2
1043
+ };
1044
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
1045
+ window.addEventListener("scroll", this.handleScroll, { passive: true });
1046
+ }
1047
+ /**
1048
+ * Start the ping service
1049
+ */
1050
+ start() {
1051
+ if (this.intervalId) return;
1052
+ this.intervalId = setInterval(() => {
1053
+ if (this.isVisible) {
1054
+ this.ping();
1055
+ }
1056
+ }, this.config.interval);
1057
+ this.ping();
1058
+ }
1059
+ /**
1060
+ * Stop the ping service
1061
+ */
1062
+ stop() {
1063
+ if (this.intervalId) {
1064
+ clearInterval(this.intervalId);
1065
+ this.intervalId = null;
1066
+ }
1067
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
1068
+ window.removeEventListener("scroll", this.handleScroll);
1069
+ }
1070
+ /**
1071
+ * Update scroll depth (called by external scroll tracker)
1072
+ */
1073
+ updateScrollDepth(depth) {
1074
+ if (depth > this.currentScrollDepth) {
1075
+ this.currentScrollDepth = depth;
1076
+ }
1077
+ }
1078
+ /**
1079
+ * Get current ping data
1080
+ */
1081
+ getData() {
1082
+ return {
1083
+ session_id: this.sessionId,
1084
+ visitor_id: this.visitorId,
1085
+ url: window.location.href,
1086
+ time_on_page_ms: Date.now() - this.pageLoadTime,
1087
+ scroll_depth: this.currentScrollDepth,
1088
+ is_active: this.isVisible,
1089
+ tracker_version: this.version
1090
+ };
1091
+ }
1092
+ };
1093
+
1094
+ // src/behavioral/scroll-tracker.ts
1095
+ var DEFAULT_CHUNKS = [30, 60, 90, 100];
1096
+ var ScrollTracker = class {
1097
+ constructor(config2 = {}) {
1098
+ this.maxDepth = 0;
1099
+ this.reportedChunks = /* @__PURE__ */ new Set();
1100
+ this.ticking = false;
1101
+ this.isVisible = true;
1102
+ this.handleScroll = () => {
1103
+ if (!this.ticking && this.isVisible) {
1104
+ requestAnimationFrame(() => {
1105
+ this.checkScrollDepth();
1106
+ this.ticking = false;
1107
+ });
1108
+ this.ticking = true;
1109
+ }
1110
+ };
1111
+ this.handleVisibility = () => {
1112
+ this.isVisible = document.visibilityState === "visible";
1113
+ };
1114
+ this.config = {
1115
+ chunks: DEFAULT_CHUNKS,
1116
+ ...config2
1117
+ };
1118
+ this.startTime = Date.now();
1119
+ }
1120
+ /**
1121
+ * Start tracking scroll depth
1122
+ */
1123
+ start() {
1124
+ window.addEventListener("scroll", this.handleScroll, { passive: true });
1125
+ document.addEventListener("visibilitychange", this.handleVisibility);
1126
+ this.checkScrollDepth();
1127
+ }
1128
+ /**
1129
+ * Stop tracking
1130
+ */
1131
+ stop() {
1132
+ window.removeEventListener("scroll", this.handleScroll);
1133
+ document.removeEventListener("visibilitychange", this.handleVisibility);
1134
+ }
1135
+ /**
1136
+ * Get current max scroll depth
1137
+ */
1138
+ getMaxDepth() {
1139
+ return this.maxDepth;
1140
+ }
1141
+ /**
1142
+ * Get reported chunks
1143
+ */
1144
+ getReportedChunks() {
1145
+ return Array.from(this.reportedChunks).sort((a, b) => a - b);
1146
+ }
1147
+ /**
1148
+ * Get final scroll event (for unload)
1149
+ */
1150
+ getFinalEvent() {
1151
+ const docHeight = document.documentElement.scrollHeight;
1152
+ const viewportHeight = window.innerHeight;
1153
+ return {
1154
+ depth: this.maxDepth,
1155
+ chunk: this.getChunkForDepth(this.maxDepth),
1156
+ time_to_reach_ms: Date.now() - this.startTime,
1157
+ total_height: docHeight,
1158
+ viewport_height: viewportHeight
1159
+ };
1160
+ }
1161
+ checkScrollDepth() {
1162
+ const scrollY = window.scrollY;
1163
+ const viewportHeight = window.innerHeight;
1164
+ const docHeight = document.documentElement.scrollHeight;
1165
+ if (docHeight <= viewportHeight) {
1166
+ this.updateDepth(100);
1167
+ return;
1168
+ }
1169
+ const scrollableHeight = docHeight - viewportHeight;
1170
+ const currentDepth = Math.min(100, Math.round(scrollY / scrollableHeight * 100));
1171
+ this.updateDepth(currentDepth);
1172
+ }
1173
+ updateDepth(depth) {
1174
+ if (depth <= this.maxDepth) return;
1175
+ this.maxDepth = depth;
1176
+ this.config.onDepthChange?.(depth);
1177
+ for (const chunk of this.config.chunks) {
1178
+ if (depth >= chunk && !this.reportedChunks.has(chunk)) {
1179
+ this.reportedChunks.add(chunk);
1180
+ this.reportChunk(chunk);
1181
+ }
1182
+ }
1183
+ }
1184
+ reportChunk(chunk) {
1185
+ const docHeight = document.documentElement.scrollHeight;
1186
+ const viewportHeight = window.innerHeight;
1187
+ const event = {
1188
+ depth: this.maxDepth,
1189
+ chunk,
1190
+ time_to_reach_ms: Date.now() - this.startTime,
1191
+ total_height: docHeight,
1192
+ viewport_height: viewportHeight
1193
+ };
1194
+ this.config.onChunkReached?.(event);
1195
+ }
1196
+ getChunkForDepth(depth) {
1197
+ const chunks = this.config.chunks.sort((a, b) => b - a);
1198
+ for (const chunk of chunks) {
1199
+ if (depth >= chunk) return chunk;
1200
+ }
1201
+ return 0;
1202
+ }
1203
+ };
1204
+
1205
+ // src/behavioral/time-tracker.ts
1206
+ var DEFAULT_CONFIG2 = {
1207
+ idleThresholdMs: 3e4,
1208
+ // 30 seconds
1209
+ updateIntervalMs: 5e3
1210
+ // 5 seconds
1211
+ };
1212
+ var TimeTracker = class {
1213
+ constructor(config2 = {}) {
1214
+ this.activeTime = 0;
1215
+ this.idleTime = 0;
1216
+ this.isVisible = true;
1217
+ this.isIdle = false;
1218
+ this.updateInterval = null;
1219
+ this.idleCheckInterval = null;
1220
+ this.handleVisibility = () => {
1221
+ const wasVisible = this.isVisible;
1222
+ this.isVisible = document.visibilityState === "visible";
1223
+ if (wasVisible && !this.isVisible) {
1224
+ this.updateTimes();
1225
+ } else if (!wasVisible && this.isVisible) {
1226
+ this.lastUpdateTime = Date.now();
1227
+ this.lastActivityTime = Date.now();
1228
+ }
1229
+ };
1230
+ this.handleActivity = () => {
1231
+ const now = Date.now();
1232
+ if (this.isIdle) {
1233
+ this.isIdle = false;
1234
+ }
1235
+ this.lastActivityTime = now;
1236
+ };
1237
+ this.config = { ...DEFAULT_CONFIG2, ...config2 };
1238
+ this.startTime = Date.now();
1239
+ this.lastActivityTime = this.startTime;
1240
+ this.lastUpdateTime = this.startTime;
1241
+ }
1242
+ /**
1243
+ * Start tracking time
1244
+ */
1245
+ start() {
1246
+ document.addEventListener("visibilitychange", this.handleVisibility);
1247
+ const activityEvents = ["mousemove", "keydown", "scroll", "click", "touchstart"];
1248
+ activityEvents.forEach((event) => {
1249
+ document.addEventListener(event, this.handleActivity, { passive: true });
1250
+ });
1251
+ this.updateInterval = setInterval(() => {
1252
+ this.update();
1253
+ }, this.config.updateIntervalMs);
1254
+ this.idleCheckInterval = setInterval(() => {
1255
+ this.checkIdle();
1256
+ }, 1e3);
1257
+ }
1258
+ /**
1259
+ * Stop tracking
1260
+ */
1261
+ stop() {
1262
+ document.removeEventListener("visibilitychange", this.handleVisibility);
1263
+ const activityEvents = ["mousemove", "keydown", "scroll", "click", "touchstart"];
1264
+ activityEvents.forEach((event) => {
1265
+ document.removeEventListener(event, this.handleActivity);
1266
+ });
1267
+ if (this.updateInterval) {
1268
+ clearInterval(this.updateInterval);
1269
+ this.updateInterval = null;
1270
+ }
1271
+ if (this.idleCheckInterval) {
1272
+ clearInterval(this.idleCheckInterval);
1273
+ this.idleCheckInterval = null;
1274
+ }
1275
+ }
1276
+ /**
1277
+ * Get current time metrics
1278
+ */
1279
+ getMetrics() {
1280
+ this.updateTimes();
1281
+ return {
1282
+ active_time_ms: this.activeTime,
1283
+ total_time_ms: Date.now() - this.startTime,
1284
+ idle_time_ms: this.idleTime,
1285
+ is_engaged: !this.isIdle && this.isVisible
1286
+ };
1287
+ }
1288
+ /**
1289
+ * Get final metrics (for unload)
1290
+ */
1291
+ getFinalMetrics() {
1292
+ this.updateTimes();
1293
+ return this.getMetrics();
1294
+ }
1295
+ checkIdle() {
1296
+ const now = Date.now();
1297
+ const timeSinceActivity = now - this.lastActivityTime;
1298
+ if (!this.isIdle && timeSinceActivity >= this.config.idleThresholdMs) {
1299
+ this.isIdle = true;
1300
+ }
1301
+ }
1302
+ updateTimes() {
1303
+ const now = Date.now();
1304
+ const elapsed = now - this.lastUpdateTime;
1305
+ if (this.isVisible) {
1306
+ if (this.isIdle) {
1307
+ this.idleTime += elapsed;
1308
+ } else {
1309
+ this.activeTime += elapsed;
1310
+ }
1311
+ }
1312
+ this.lastUpdateTime = now;
1313
+ }
1314
+ update() {
1315
+ if (!this.isVisible) return;
1316
+ this.updateTimes();
1317
+ this.config.onUpdate?.(this.getMetrics());
1318
+ }
1319
+ };
1320
+
1321
+ // src/behavioral/form-tracker.ts
1322
+ var DEFAULT_CONFIG3 = {
1323
+ sensitiveFields: [
1324
+ "password",
1325
+ "pwd",
1326
+ "pass",
1327
+ "credit",
1328
+ "card",
1329
+ "cvv",
1330
+ "cvc",
1331
+ "ssn",
1332
+ "social",
1333
+ "secret",
1334
+ "token",
1335
+ "key"
1336
+ ],
1337
+ trackableFields: [
1338
+ "email",
1339
+ "name",
1340
+ "phone",
1341
+ "company",
1342
+ "first",
1343
+ "last",
1344
+ "city",
1345
+ "country"
1346
+ ],
1347
+ thankYouPatterns: [
1348
+ /thank[-_]?you/i,
1349
+ /success/i,
1350
+ /confirmation/i,
1351
+ /submitted/i,
1352
+ /complete/i
1353
+ ]
1354
+ };
1355
+ var FormTracker = class {
1356
+ constructor(config2 = {}) {
1357
+ this.formStartTimes = /* @__PURE__ */ new Map();
1358
+ this.interactedForms = /* @__PURE__ */ new Set();
1359
+ this.mutationObserver = null;
1360
+ this.handleFocusIn = (e) => {
1361
+ const target = e.target;
1362
+ if (!this.isFormField(target)) return;
1363
+ const form = target.closest("form");
1364
+ const formId = this.getFormId(form || target);
1365
+ if (!this.formStartTimes.has(formId)) {
1366
+ this.formStartTimes.set(formId, Date.now());
1367
+ this.interactedForms.add(formId);
1368
+ this.emitEvent({
1369
+ event_type: "form_start",
1370
+ form_id: formId,
1371
+ form_type: this.detectFormType(form || target)
1372
+ });
1373
+ }
1374
+ const fieldName = this.getFieldName(target);
1375
+ if (fieldName && !this.isSensitiveField(fieldName)) {
1376
+ this.emitEvent({
1377
+ event_type: "form_field",
1378
+ form_id: formId,
1379
+ form_type: this.detectFormType(form || target),
1380
+ field_name: this.sanitizeFieldName(fieldName),
1381
+ field_type: target.type || target.tagName.toLowerCase()
1382
+ });
1383
+ }
1384
+ };
1385
+ this.handleSubmit = (e) => {
1386
+ const form = e.target;
1387
+ if (!form || form.tagName !== "FORM") return;
1388
+ const formId = this.getFormId(form);
1389
+ const startTime = this.formStartTimes.get(formId);
1390
+ this.emitEvent({
1391
+ event_type: "form_submit",
1392
+ form_id: formId,
1393
+ form_type: this.detectFormType(form),
1394
+ time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
1395
+ is_conversion: true
1396
+ });
1397
+ };
1398
+ this.handleClick = (e) => {
1399
+ const target = e.target;
1400
+ if (target.closest(".hs-button") || target.closest('[type="submit"]')) {
1401
+ const form = target.closest("form");
1402
+ if (form && form.classList.contains("hs-form")) {
1403
+ const formId = this.getFormId(form);
1404
+ const startTime = this.formStartTimes.get(formId);
1405
+ this.emitEvent({
1406
+ event_type: "form_submit",
1407
+ form_id: formId,
1408
+ form_type: "hubspot",
1409
+ time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
1410
+ is_conversion: true
1411
+ });
1412
+ }
1413
+ }
1414
+ if (target.closest('[data-qa="submit-button"]')) {
1415
+ this.emitEvent({
1416
+ event_type: "form_submit",
1417
+ form_id: "typeform_embed",
1418
+ form_type: "typeform",
1419
+ is_conversion: true
1420
+ });
1421
+ }
1422
+ };
1423
+ this.config = {
1424
+ ...DEFAULT_CONFIG3,
1425
+ ...config2,
1426
+ sensitiveFields: [
1427
+ ...DEFAULT_CONFIG3.sensitiveFields,
1428
+ ...config2.sensitiveFields || []
1429
+ ]
1430
+ };
1431
+ }
1432
+ /**
1433
+ * Start tracking forms
1434
+ */
1435
+ start() {
1436
+ document.addEventListener("focusin", this.handleFocusIn, { passive: true });
1437
+ document.addEventListener("submit", this.handleSubmit);
1438
+ document.addEventListener("click", this.handleClick, { passive: true });
1439
+ this.startMutationObserver();
1440
+ this.checkThankYouPage();
1441
+ this.scanForEmbeddedForms();
1442
+ }
1443
+ /**
1444
+ * Stop tracking
1445
+ */
1446
+ stop() {
1447
+ document.removeEventListener("focusin", this.handleFocusIn);
1448
+ document.removeEventListener("submit", this.handleSubmit);
1449
+ document.removeEventListener("click", this.handleClick);
1450
+ this.mutationObserver?.disconnect();
1451
+ }
1452
+ /**
1453
+ * Get forms that had interaction
1454
+ */
1455
+ getInteractedForms() {
1456
+ return Array.from(this.interactedForms);
1457
+ }
1458
+ startMutationObserver() {
1459
+ this.mutationObserver = new MutationObserver((mutations) => {
1460
+ for (const mutation of mutations) {
1461
+ for (const node of mutation.addedNodes) {
1462
+ if (node instanceof HTMLElement) {
1463
+ if (node.classList?.contains("hs-form") || node.querySelector?.(".hs-form")) {
1464
+ this.trackEmbeddedForm(node, "hubspot");
1465
+ }
1466
+ if (node.classList?.contains("typeform-widget") || node.querySelector?.("[data-tf-widget]")) {
1467
+ this.trackEmbeddedForm(node, "typeform");
1468
+ }
1469
+ if (node.classList?.contains("jotform-form") || node.querySelector?.(".jotform-form")) {
1470
+ this.trackEmbeddedForm(node, "jotform");
1471
+ }
1472
+ if (node.classList?.contains("gform_wrapper") || node.querySelector?.(".gform_wrapper")) {
1473
+ this.trackEmbeddedForm(node, "gravity");
1474
+ }
1475
+ }
1476
+ }
1477
+ }
1478
+ });
1479
+ this.mutationObserver.observe(document.body, {
1480
+ childList: true,
1481
+ subtree: true
1482
+ });
1483
+ }
1484
+ scanForEmbeddedForms() {
1485
+ document.querySelectorAll(".hs-form").forEach((form) => {
1486
+ this.trackEmbeddedForm(form, "hubspot");
1487
+ });
1488
+ document.querySelectorAll("[data-tf-widget], .typeform-widget").forEach((form) => {
1489
+ this.trackEmbeddedForm(form, "typeform");
1490
+ });
1491
+ document.querySelectorAll(".jotform-form").forEach((form) => {
1492
+ this.trackEmbeddedForm(form, "jotform");
1493
+ });
1494
+ document.querySelectorAll(".gform_wrapper").forEach((form) => {
1495
+ this.trackEmbeddedForm(form, "gravity");
1496
+ });
1497
+ }
1498
+ trackEmbeddedForm(element, type) {
1499
+ const formId = `${type}_${this.getFormId(element)}`;
1500
+ element.addEventListener("focusin", () => {
1501
+ if (!this.formStartTimes.has(formId)) {
1502
+ this.formStartTimes.set(formId, Date.now());
1503
+ this.interactedForms.add(formId);
1504
+ this.emitEvent({
1505
+ event_type: "form_start",
1506
+ form_id: formId,
1507
+ form_type: type
1508
+ });
1509
+ }
1510
+ }, { passive: true });
1511
+ }
1512
+ checkThankYouPage() {
1513
+ const url = window.location.href.toLowerCase();
1514
+ const title = document.title.toLowerCase();
1515
+ for (const pattern of this.config.thankYouPatterns) {
1516
+ if (pattern.test(url) || pattern.test(title)) {
1517
+ this.emitEvent({
1518
+ event_type: "form_success",
1519
+ form_id: "page_conversion",
1520
+ form_type: "unknown",
1521
+ is_conversion: true
1522
+ });
1523
+ break;
1524
+ }
1525
+ }
1526
+ }
1527
+ isFormField(element) {
1528
+ const tagName = element.tagName;
1529
+ return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
1530
+ }
1531
+ getFormId(element) {
1532
+ if (!element) return "unknown";
1533
+ return element.id || element.getAttribute("name") || element.getAttribute("data-form-id") || "form_" + Math.random().toString(36).substring(2, 8);
1534
+ }
1535
+ getFieldName(input) {
1536
+ return input.name || input.id || input.getAttribute("data-name") || "";
1537
+ }
1538
+ isSensitiveField(fieldName) {
1539
+ const lowerName = fieldName.toLowerCase();
1540
+ return this.config.sensitiveFields.some((sensitive) => lowerName.includes(sensitive));
1541
+ }
1542
+ sanitizeFieldName(fieldName) {
1543
+ return fieldName.replace(/[0-9]+/g, "*").substring(0, 50);
1544
+ }
1545
+ detectFormType(element) {
1546
+ if (element.classList.contains("hs-form") || element.closest(".hs-form")) {
1547
+ return "hubspot";
1548
+ }
1549
+ if (element.classList.contains("typeform-widget") || element.closest("[data-tf-widget]")) {
1550
+ return "typeform";
1551
+ }
1552
+ if (element.classList.contains("jotform-form") || element.closest(".jotform-form")) {
1553
+ return "jotform";
1554
+ }
1555
+ if (element.classList.contains("gform_wrapper") || element.closest(".gform_wrapper")) {
1556
+ return "gravity";
1557
+ }
1558
+ if (element.tagName === "FORM") {
1559
+ return "native";
1560
+ }
1561
+ return "unknown";
1562
+ }
1563
+ emitEvent(event) {
1564
+ this.config.onFormEvent?.(event);
1565
+ }
1566
+ };
1567
+
1568
+ // src/spa/router.ts
1569
+ var SPARouter = class {
1570
+ constructor(config2 = {}) {
1571
+ this.originalPushState = null;
1572
+ this.originalReplaceState = null;
1573
+ this.handleStateChange = (type) => {
1574
+ const newUrl = window.location.href;
1575
+ if (newUrl !== this.currentUrl) {
1576
+ this.emitNavigation(newUrl, type);
1577
+ }
1578
+ };
1579
+ this.handlePopState = () => {
1580
+ const newUrl = window.location.href;
1581
+ if (newUrl !== this.currentUrl) {
1582
+ this.emitNavigation(newUrl, "pop");
1583
+ }
1584
+ };
1585
+ this.handleHashChange = () => {
1586
+ const newUrl = window.location.href;
1587
+ if (newUrl !== this.currentUrl) {
1588
+ this.emitNavigation(newUrl, "hash");
1589
+ }
1590
+ };
1591
+ this.config = config2;
1592
+ this.currentUrl = window.location.href;
1593
+ this.pageEnterTime = Date.now();
1594
+ }
1595
+ /**
1596
+ * Start listening for navigation events
1597
+ */
1598
+ start() {
1599
+ this.patchHistoryAPI();
1600
+ window.addEventListener("popstate", this.handlePopState);
1601
+ if (!this.config.ignoreHashChange) {
1602
+ window.addEventListener("hashchange", this.handleHashChange);
1603
+ }
1604
+ }
1605
+ /**
1606
+ * Stop listening and restore original methods
1607
+ */
1608
+ stop() {
1609
+ if (this.originalPushState) {
1610
+ history.pushState = this.originalPushState;
1611
+ }
1612
+ if (this.originalReplaceState) {
1613
+ history.replaceState = this.originalReplaceState;
1614
+ }
1615
+ window.removeEventListener("popstate", this.handlePopState);
1616
+ window.removeEventListener("hashchange", this.handleHashChange);
1617
+ }
1618
+ /**
1619
+ * Manually trigger a navigation event (for custom routers)
1620
+ */
1621
+ navigate(url, type = "push") {
1622
+ this.emitNavigation(url, type);
1623
+ }
1624
+ /**
1625
+ * Get current URL
1626
+ */
1627
+ getCurrentUrl() {
1628
+ return this.currentUrl;
1629
+ }
1630
+ /**
1631
+ * Get time on current page
1632
+ */
1633
+ getTimeOnPage() {
1634
+ return Date.now() - this.pageEnterTime;
1635
+ }
1636
+ patchHistoryAPI() {
1637
+ this.originalPushState = history.pushState.bind(history);
1638
+ this.originalReplaceState = history.replaceState.bind(history);
1639
+ history.pushState = (...args) => {
1640
+ const result = this.originalPushState(...args);
1641
+ this.handleStateChange("push");
1642
+ return result;
1643
+ };
1644
+ history.replaceState = (...args) => {
1645
+ const result = this.originalReplaceState(...args);
1646
+ this.handleStateChange("replace");
1647
+ return result;
1648
+ };
1649
+ }
1650
+ emitNavigation(toUrl, type) {
1651
+ const event = {
1652
+ from_url: this.currentUrl,
1653
+ to_url: toUrl,
1654
+ navigation_type: type,
1655
+ time_on_previous_page_ms: Date.now() - this.pageEnterTime
1656
+ };
1657
+ this.currentUrl = toUrl;
1658
+ this.pageEnterTime = Date.now();
1659
+ this.config.onNavigate?.(event);
1660
+ }
1661
+ };
1662
+
655
1663
  // src/utils.ts
656
1664
  function generateUUID() {
657
1665
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
@@ -734,13 +1742,19 @@ var Loamly = (() => {
734
1742
  var debugMode = false;
735
1743
  var visitorId = null;
736
1744
  var sessionId = null;
737
- var sessionStartTime = null;
738
1745
  var navigationTiming = null;
739
1746
  var aiDetection = null;
740
1747
  var behavioralClassifier = null;
741
1748
  var behavioralMLResult = null;
742
1749
  var focusBlurAnalyzer = null;
743
1750
  var focusBlurResult = null;
1751
+ var agenticAnalyzer = null;
1752
+ var eventQueue = null;
1753
+ var pingService = null;
1754
+ var scrollTracker = null;
1755
+ var timeTracker = null;
1756
+ var formTracker = null;
1757
+ var spaRouter = null;
744
1758
  function log(...args) {
745
1759
  if (debugMode) {
746
1760
  console.log("[Loamly]", ...args);
@@ -765,8 +1779,11 @@ var Loamly = (() => {
765
1779
  log("Visitor ID:", visitorId);
766
1780
  const session = getSessionId();
767
1781
  sessionId = session.sessionId;
768
- sessionStartTime = Date.now();
769
1782
  log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
1783
+ eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1784
+ batchSize: DEFAULT_CONFIG.batchSize,
1785
+ batchTimeout: DEFAULT_CONFIG.batchTimeout
1786
+ });
770
1787
  navigationTiming = detectNavigationType();
771
1788
  log("Navigation timing:", navigationTiming);
772
1789
  aiDetection = detectAIFromReferrer(document.referrer) || detectAIFromUTM(window.location.href);
@@ -778,7 +1795,7 @@ var Loamly = (() => {
778
1795
  pageview();
779
1796
  }
780
1797
  if (!userConfig.disableBehavioral) {
781
- setupBehavioralTracking();
1798
+ setupAdvancedBehavioralTracking();
782
1799
  }
783
1800
  behavioralClassifier = new BehavioralClassifier(1e4);
784
1801
  behavioralClassifier.setOnClassify(handleBehavioralClassification);
@@ -790,8 +1807,161 @@ var Loamly = (() => {
790
1807
  handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
791
1808
  }
792
1809
  }, 5e3);
1810
+ agenticAnalyzer = new AgenticBrowserAnalyzer();
1811
+ agenticAnalyzer.init();
1812
+ if (visitorId && sessionId) {
1813
+ pingService = new PingService(sessionId, visitorId, VERSION, {
1814
+ interval: DEFAULT_CONFIG.pingInterval,
1815
+ endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping)
1816
+ });
1817
+ pingService.start();
1818
+ }
1819
+ spaRouter = new SPARouter({
1820
+ onNavigate: handleSPANavigation
1821
+ });
1822
+ spaRouter.start();
1823
+ setupUnloadHandlers();
793
1824
  log("Initialization complete");
794
1825
  }
1826
+ function setupAdvancedBehavioralTracking() {
1827
+ scrollTracker = new ScrollTracker({
1828
+ chunks: [30, 60, 90, 100],
1829
+ onChunkReached: (event) => {
1830
+ log("Scroll chunk:", event.chunk);
1831
+ queueEvent("scroll_depth", {
1832
+ depth: event.depth,
1833
+ chunk: event.chunk,
1834
+ time_to_reach_ms: event.time_to_reach_ms
1835
+ });
1836
+ }
1837
+ });
1838
+ scrollTracker.start();
1839
+ timeTracker = new TimeTracker({
1840
+ updateIntervalMs: 1e4,
1841
+ // Report every 10 seconds
1842
+ onUpdate: (event) => {
1843
+ if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
1844
+ queueEvent("time_spent", {
1845
+ active_time_ms: event.active_time_ms,
1846
+ total_time_ms: event.total_time_ms,
1847
+ idle_time_ms: event.idle_time_ms,
1848
+ is_engaged: event.is_engaged
1849
+ });
1850
+ }
1851
+ }
1852
+ });
1853
+ timeTracker.start();
1854
+ formTracker = new FormTracker({
1855
+ onFormEvent: (event) => {
1856
+ log("Form event:", event.event_type, event.form_id);
1857
+ queueEvent(event.event_type, {
1858
+ form_id: event.form_id,
1859
+ form_type: event.form_type,
1860
+ field_name: event.field_name,
1861
+ field_type: event.field_type,
1862
+ time_to_submit_ms: event.time_to_submit_ms,
1863
+ is_conversion: event.is_conversion
1864
+ });
1865
+ }
1866
+ });
1867
+ formTracker.start();
1868
+ document.addEventListener("click", (e) => {
1869
+ const target = e.target;
1870
+ const link = target.closest("a");
1871
+ if (link && link.href) {
1872
+ const isExternal = link.hostname !== window.location.hostname;
1873
+ queueEvent("click", {
1874
+ element: "link",
1875
+ href: truncateText(link.href, 200),
1876
+ text: truncateText(link.textContent || "", 100),
1877
+ is_external: isExternal
1878
+ });
1879
+ }
1880
+ });
1881
+ }
1882
+ function queueEvent(eventType, data) {
1883
+ if (!eventQueue) return;
1884
+ eventQueue.push(eventType, {
1885
+ visitor_id: visitorId,
1886
+ session_id: sessionId,
1887
+ event_type: eventType,
1888
+ ...data,
1889
+ url: window.location.href,
1890
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1891
+ tracker_version: VERSION
1892
+ });
1893
+ }
1894
+ function handleSPANavigation(event) {
1895
+ log("SPA navigation:", event.navigation_type, event.to_url);
1896
+ eventQueue?.flush();
1897
+ pingService?.updateScrollDepth(0);
1898
+ scrollTracker?.stop();
1899
+ scrollTracker = new ScrollTracker({
1900
+ chunks: [30, 60, 90, 100],
1901
+ onChunkReached: (scrollEvent) => {
1902
+ queueEvent("scroll_depth", {
1903
+ depth: scrollEvent.depth,
1904
+ chunk: scrollEvent.chunk,
1905
+ time_to_reach_ms: scrollEvent.time_to_reach_ms
1906
+ });
1907
+ }
1908
+ });
1909
+ scrollTracker.start();
1910
+ pageview(event.to_url);
1911
+ queueEvent("spa_navigation", {
1912
+ from_url: event.from_url,
1913
+ to_url: event.to_url,
1914
+ navigation_type: event.navigation_type,
1915
+ time_on_previous_page_ms: event.time_on_previous_page_ms
1916
+ });
1917
+ }
1918
+ function setupUnloadHandlers() {
1919
+ const handleUnload = () => {
1920
+ const scrollEvent = scrollTracker?.getFinalEvent();
1921
+ if (scrollEvent) {
1922
+ sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1923
+ visitor_id: visitorId,
1924
+ session_id: sessionId,
1925
+ event_type: "scroll_depth_final",
1926
+ data: scrollEvent,
1927
+ url: window.location.href
1928
+ });
1929
+ }
1930
+ const timeEvent = timeTracker?.getFinalMetrics();
1931
+ if (timeEvent) {
1932
+ sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1933
+ visitor_id: visitorId,
1934
+ session_id: sessionId,
1935
+ event_type: "time_spent_final",
1936
+ data: timeEvent,
1937
+ url: window.location.href
1938
+ });
1939
+ }
1940
+ const agenticResult = agenticAnalyzer?.getResult();
1941
+ if (agenticResult && agenticResult.agenticProbability > 0) {
1942
+ sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1943
+ visitor_id: visitorId,
1944
+ session_id: sessionId,
1945
+ event_type: "agentic_detection",
1946
+ data: agenticResult,
1947
+ url: window.location.href
1948
+ });
1949
+ }
1950
+ eventQueue?.flushBeacon();
1951
+ if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
1952
+ const result = behavioralClassifier.forceClassify();
1953
+ if (result) {
1954
+ handleBehavioralClassification(result);
1955
+ }
1956
+ }
1957
+ };
1958
+ window.addEventListener("beforeunload", handleUnload);
1959
+ document.addEventListener("visibilitychange", () => {
1960
+ if (document.visibilityState === "hidden") {
1961
+ handleUnload();
1962
+ }
1963
+ });
1964
+ }
795
1965
  function pageview(customUrl) {
796
1966
  if (!initialized) {
797
1967
  log("Not initialized, call init() first");
@@ -870,107 +2040,6 @@ var Loamly = (() => {
870
2040
  body: JSON.stringify(payload)
871
2041
  });
872
2042
  }
873
- function setupBehavioralTracking() {
874
- let maxScrollDepth = 0;
875
- let lastScrollUpdate = 0;
876
- let lastTimeUpdate = Date.now();
877
- let scrollTicking = false;
878
- window.addEventListener("scroll", () => {
879
- if (!scrollTicking) {
880
- requestAnimationFrame(() => {
881
- const scrollPercent = Math.round(
882
- (window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100
883
- );
884
- if (scrollPercent > maxScrollDepth) {
885
- maxScrollDepth = scrollPercent;
886
- const milestones = [25, 50, 75, 100];
887
- for (const milestone of milestones) {
888
- if (scrollPercent >= milestone && lastScrollUpdate < milestone) {
889
- lastScrollUpdate = milestone;
890
- sendBehavioralEvent("scroll_depth", { depth: milestone });
891
- }
892
- }
893
- }
894
- scrollTicking = false;
895
- });
896
- scrollTicking = true;
897
- }
898
- });
899
- const trackTimeSpent = () => {
900
- const now = Date.now();
901
- const delta = now - lastTimeUpdate;
902
- if (delta >= DEFAULT_CONFIG.timeSpentThresholdMs) {
903
- lastTimeUpdate = now;
904
- sendBehavioralEvent("time_spent", {
905
- seconds: Math.round(delta / 1e3),
906
- total_seconds: Math.round((now - (sessionStartTime || now)) / 1e3)
907
- });
908
- }
909
- };
910
- document.addEventListener("visibilitychange", () => {
911
- if (document.visibilityState === "hidden") {
912
- trackTimeSpent();
913
- }
914
- });
915
- window.addEventListener("beforeunload", () => {
916
- trackTimeSpent();
917
- if (maxScrollDepth > 0) {
918
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
919
- visitor_id: visitorId,
920
- session_id: sessionId,
921
- event_type: "scroll_depth_final",
922
- data: { depth: maxScrollDepth },
923
- url: window.location.href
924
- });
925
- }
926
- });
927
- document.addEventListener("focusin", (e) => {
928
- const target = e.target;
929
- if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT") {
930
- sendBehavioralEvent("form_focus", {
931
- field_type: target.tagName.toLowerCase(),
932
- field_name: target.name || target.id || "unknown"
933
- });
934
- }
935
- });
936
- document.addEventListener("submit", (e) => {
937
- const form = e.target;
938
- sendBehavioralEvent("form_submit", {
939
- form_id: form.id || form.name || "unknown",
940
- form_action: form.action ? new URL(form.action).pathname : "unknown"
941
- });
942
- });
943
- document.addEventListener("click", (e) => {
944
- const target = e.target;
945
- const link = target.closest("a");
946
- if (link && link.href) {
947
- const isExternal = link.hostname !== window.location.hostname;
948
- sendBehavioralEvent("click", {
949
- element: "link",
950
- href: truncateText(link.href, 200),
951
- text: truncateText(link.textContent || "", 100),
952
- is_external: isExternal
953
- });
954
- }
955
- });
956
- }
957
- function sendBehavioralEvent(eventType, data) {
958
- const payload = {
959
- visitor_id: visitorId,
960
- session_id: sessionId,
961
- event_type: eventType,
962
- data,
963
- url: window.location.href,
964
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
965
- tracker_version: VERSION
966
- };
967
- log("Behavioral:", eventType, data);
968
- safeFetch(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
969
- method: "POST",
970
- headers: { "Content-Type": "application/json" },
971
- body: JSON.stringify(payload)
972
- });
973
- }
974
2043
  function setupBehavioralMLTracking() {
975
2044
  if (!behavioralClassifier) return;
976
2045
  let mouseSampleCount = 0;
@@ -1011,14 +2080,6 @@ var Loamly = (() => {
1011
2080
  }
1012
2081
  }
1013
2082
  }, { passive: true });
1014
- window.addEventListener("beforeunload", () => {
1015
- if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
1016
- const result = behavioralClassifier.forceClassify();
1017
- if (result) {
1018
- handleBehavioralClassification(result);
1019
- }
1020
- }
1021
- });
1022
2083
  setTimeout(() => {
1023
2084
  if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
1024
2085
  behavioralClassifier.forceClassify();
@@ -1035,7 +2096,7 @@ var Loamly = (() => {
1035
2096
  signals: result.signals,
1036
2097
  sessionDurationMs: result.sessionDurationMs
1037
2098
  };
1038
- sendBehavioralEvent("ml_classification", {
2099
+ queueEvent("ml_classification", {
1039
2100
  classification: result.classification,
1040
2101
  human_probability: result.humanProbability,
1041
2102
  ai_probability: result.aiProbability,
@@ -1063,7 +2124,7 @@ var Loamly = (() => {
1063
2124
  signals: result.signals,
1064
2125
  timeToFirstInteractionMs: result.time_to_first_interaction_ms
1065
2126
  };
1066
- sendBehavioralEvent("focus_blur_analysis", {
2127
+ queueEvent("focus_blur_analysis", {
1067
2128
  nav_type: result.nav_type,
1068
2129
  confidence: result.confidence,
1069
2130
  signals: result.signals,
@@ -1099,21 +2160,36 @@ var Loamly = (() => {
1099
2160
  function getFocusBlurResult() {
1100
2161
  return focusBlurResult;
1101
2162
  }
2163
+ function getAgenticResult() {
2164
+ return agenticAnalyzer?.getResult() || null;
2165
+ }
1102
2166
  function isTrackerInitialized() {
1103
2167
  return initialized;
1104
2168
  }
1105
2169
  function reset() {
1106
2170
  log("Resetting tracker");
2171
+ pingService?.stop();
2172
+ scrollTracker?.stop();
2173
+ timeTracker?.stop();
2174
+ formTracker?.stop();
2175
+ spaRouter?.stop();
2176
+ agenticAnalyzer?.destroy();
1107
2177
  initialized = false;
1108
2178
  visitorId = null;
1109
2179
  sessionId = null;
1110
- sessionStartTime = null;
1111
2180
  navigationTiming = null;
1112
2181
  aiDetection = null;
1113
2182
  behavioralClassifier = null;
1114
2183
  behavioralMLResult = null;
1115
2184
  focusBlurAnalyzer = null;
1116
2185
  focusBlurResult = null;
2186
+ agenticAnalyzer = null;
2187
+ eventQueue = null;
2188
+ pingService = null;
2189
+ scrollTracker = null;
2190
+ timeTracker = null;
2191
+ formTracker = null;
2192
+ spaRouter = null;
1117
2193
  try {
1118
2194
  sessionStorage.removeItem("loamly_session");
1119
2195
  sessionStorage.removeItem("loamly_start");
@@ -1136,50 +2212,110 @@ var Loamly = (() => {
1136
2212
  getNavigationTiming: getNavigationTimingResult,
1137
2213
  getBehavioralML: getBehavioralMLResult,
1138
2214
  getFocusBlur: getFocusBlurResult,
2215
+ getAgentic: getAgenticResult,
1139
2216
  isInitialized: isTrackerInitialized,
1140
2217
  reset,
1141
2218
  debug: setDebug
1142
2219
  };
1143
2220
 
1144
2221
  // src/browser.ts
1145
- function autoInit() {
2222
+ function extractDomainFromScriptUrl() {
1146
2223
  const scripts = document.getElementsByTagName("script");
1147
- let scriptTag = null;
1148
2224
  for (const script of scripts) {
1149
- if (script.src.includes("loamly") || script.dataset.loamly !== void 0) {
1150
- scriptTag = script;
1151
- break;
2225
+ const src = script.src;
2226
+ if (src.includes("t.js") || src.includes("loamly")) {
2227
+ try {
2228
+ const url = new URL(src);
2229
+ const domain = url.searchParams.get("d");
2230
+ if (domain) return domain;
2231
+ } catch {
2232
+ }
1152
2233
  }
1153
2234
  }
1154
- if (!scriptTag) {
1155
- return;
1156
- }
1157
- const config2 = {};
1158
- if (scriptTag.dataset.apiKey) {
1159
- config2.apiKey = scriptTag.dataset.apiKey;
1160
- }
1161
- if (scriptTag.dataset.apiHost) {
1162
- config2.apiHost = scriptTag.dataset.apiHost;
2235
+ return null;
2236
+ }
2237
+ async function resolveWorkspaceConfig(domain) {
2238
+ try {
2239
+ const response = await fetch(`${DEFAULT_CONFIG.apiHost}${DEFAULT_CONFIG.endpoints.resolve}?domain=${encodeURIComponent(domain)}`);
2240
+ if (!response.ok) {
2241
+ console.warn("[Loamly] Failed to resolve workspace for domain:", domain);
2242
+ return null;
2243
+ }
2244
+ const data = await response.json();
2245
+ if (data.workspace_id) {
2246
+ return {
2247
+ apiKey: data.workspace_api_key,
2248
+ apiHost: DEFAULT_CONFIG.apiHost
2249
+ };
2250
+ }
2251
+ return null;
2252
+ } catch (error) {
2253
+ console.warn("[Loamly] Error resolving workspace:", error);
2254
+ return null;
1163
2255
  }
1164
- if (scriptTag.dataset.debug === "true") {
1165
- config2.debug = true;
2256
+ }
2257
+ function extractConfigFromDataAttributes() {
2258
+ const scripts = document.getElementsByTagName("script");
2259
+ for (const script of scripts) {
2260
+ if (script.src.includes("loamly") || script.dataset.loamly !== void 0) {
2261
+ const config2 = {};
2262
+ if (script.dataset.apiKey) {
2263
+ config2.apiKey = script.dataset.apiKey;
2264
+ }
2265
+ if (script.dataset.apiHost) {
2266
+ config2.apiHost = script.dataset.apiHost;
2267
+ }
2268
+ if (script.dataset.debug === "true") {
2269
+ config2.debug = true;
2270
+ }
2271
+ if (script.dataset.disableAutoPageview === "true") {
2272
+ config2.disableAutoPageview = true;
2273
+ }
2274
+ if (script.dataset.disableBehavioral === "true") {
2275
+ config2.disableBehavioral = true;
2276
+ }
2277
+ if (config2.apiKey) {
2278
+ return config2;
2279
+ }
2280
+ }
1166
2281
  }
1167
- if (scriptTag.dataset.disableAutoPageview === "true") {
1168
- config2.disableAutoPageview = true;
2282
+ return null;
2283
+ }
2284
+ async function autoInit() {
2285
+ const domain = extractDomainFromScriptUrl();
2286
+ if (domain) {
2287
+ const resolvedConfig = await resolveWorkspaceConfig(domain);
2288
+ if (resolvedConfig) {
2289
+ loamly.init(resolvedConfig);
2290
+ return;
2291
+ }
1169
2292
  }
1170
- if (scriptTag.dataset.disableBehavioral === "true") {
1171
- config2.disableBehavioral = true;
2293
+ const dataConfig = extractConfigFromDataAttributes();
2294
+ if (dataConfig) {
2295
+ loamly.init(dataConfig);
2296
+ return;
1172
2297
  }
1173
- if (config2.apiKey || scriptTag.dataset.loamly !== void 0) {
1174
- loamly.init(config2);
2298
+ const currentDomain = window.location.hostname;
2299
+ if (currentDomain && currentDomain !== "localhost") {
2300
+ const resolvedConfig = await resolveWorkspaceConfig(currentDomain);
2301
+ if (resolvedConfig) {
2302
+ loamly.init(resolvedConfig);
2303
+ return;
2304
+ }
1175
2305
  }
1176
2306
  }
1177
2307
  if (typeof document !== "undefined") {
1178
2308
  if (document.readyState === "loading") {
1179
- document.addEventListener("DOMContentLoaded", autoInit);
2309
+ document.addEventListener("DOMContentLoaded", () => {
2310
+ if (typeof requestIdleCallback !== "undefined") {
2311
+ requestIdleCallback(() => autoInit());
2312
+ } else {
2313
+ setTimeout(autoInit, 0);
2314
+ }
2315
+ });
1180
2316
  } else {
1181
2317
  if (typeof requestIdleCallback !== "undefined") {
1182
- requestIdleCallback(autoInit);
2318
+ requestIdleCallback(() => autoInit());
1183
2319
  } else {
1184
2320
  setTimeout(autoInit, 0);
1185
2321
  }
@@ -1195,4 +2331,19 @@ var Loamly = (() => {
1195
2331
  * @license MIT
1196
2332
  * @see https://github.com/loamly/loamly
1197
2333
  */
2334
+ /**
2335
+ * Agentic Browser Detection
2336
+ *
2337
+ * LOA-187: Detects AI agentic browsers like Perplexity Comet, ChatGPT Atlas,
2338
+ * and other automated browsing agents.
2339
+ *
2340
+ * Detection methods:
2341
+ * - DOM fingerprinting (Perplexity Comet overlay)
2342
+ * - Mouse movement patterns (teleporting clicks)
2343
+ * - CDP (Chrome DevTools Protocol) automation fingerprint
2344
+ * - navigator.webdriver detection
2345
+ *
2346
+ * @module @loamly/tracker/detection/agentic-browser
2347
+ * @license MIT
2348
+ */
1198
2349
  //# sourceMappingURL=loamly.iife.global.js.map