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