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