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