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