@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/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
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 = "
|
|
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
|
-
|
|
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
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
|
848
|
-
if (!
|
|
849
|
-
|
|
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
|
-
|
|
856
|
-
|
|
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
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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:
|
|
1957
|
+
data: scrollEvent,
|
|
943
1958
|
url: window.location.href
|
|
944
1959
|
});
|
|
945
1960
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
|
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
|
-
|
|
982
|
-
|
|
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("
|
|
988
|
-
safeFetch(endpoint(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|