@loamly/tracker 2.1.0 → 2.1.1
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 +4 -3
- package/dist/index.cjs +332 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.mjs +332 -119
- package/dist/index.mjs.map +1 -1
- package/dist/loamly.iife.global.js +349 -122
- package/dist/loamly.iife.global.js.map +1 -1
- package/dist/loamly.iife.min.global.js +1 -1
- package/dist/loamly.iife.min.global.js.map +1 -1
- package/package.json +1 -1
- package/src/behavioral/form-tracker.ts +5 -0
- package/src/browser.ts +25 -6
- package/src/config.ts +1 -1
- package/src/core.ts +313 -101
- package/src/index.ts +1 -1
- package/src/infrastructure/event-queue.ts +58 -32
- package/src/infrastructure/ping.ts +19 -3
- package/src/types.ts +3 -0
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
|
-
var VERSION = "2.1.
|
|
2
|
+
var VERSION = "2.1.1";
|
|
3
3
|
var DEFAULT_CONFIG = {
|
|
4
4
|
apiHost: "https://app.loamly.ai",
|
|
5
5
|
endpoints: {
|
|
@@ -858,11 +858,12 @@ var EventQueue = class {
|
|
|
858
858
|
/**
|
|
859
859
|
* Add event to queue
|
|
860
860
|
*/
|
|
861
|
-
push(type, payload) {
|
|
861
|
+
push(type, payload, headers) {
|
|
862
862
|
const event = {
|
|
863
863
|
id: this.generateId(),
|
|
864
864
|
type,
|
|
865
865
|
payload,
|
|
866
|
+
headers,
|
|
866
867
|
timestamp: Date.now(),
|
|
867
868
|
retries: 0
|
|
868
869
|
};
|
|
@@ -891,21 +892,25 @@ var EventQueue = class {
|
|
|
891
892
|
*/
|
|
892
893
|
flushBeacon() {
|
|
893
894
|
if (this.queue.length === 0) return true;
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
JSON.stringify(
|
|
903
|
-
|
|
904
|
-
|
|
895
|
+
const baseUrl = this.config.apiKey ? `${this.endpoint}?api_key=${encodeURIComponent(this.config.apiKey)}` : this.endpoint;
|
|
896
|
+
let allSent = true;
|
|
897
|
+
for (const event of this.queue) {
|
|
898
|
+
const payload = {
|
|
899
|
+
...event.payload,
|
|
900
|
+
_queue_id: event.id,
|
|
901
|
+
_queue_timestamp: event.timestamp
|
|
902
|
+
};
|
|
903
|
+
const success = navigator.sendBeacon?.(baseUrl, JSON.stringify(payload)) ?? false;
|
|
904
|
+
if (!success) {
|
|
905
|
+
allSent = false;
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (allSent) {
|
|
905
910
|
this.queue = [];
|
|
906
911
|
this.clearStorage();
|
|
907
912
|
}
|
|
908
|
-
return
|
|
913
|
+
return allSent;
|
|
909
914
|
}
|
|
910
915
|
/**
|
|
911
916
|
* Get current queue length
|
|
@@ -932,24 +937,38 @@ var EventQueue = class {
|
|
|
932
937
|
}
|
|
933
938
|
async sendBatch(events) {
|
|
934
939
|
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
940
|
try {
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
941
|
+
const results = await Promise.allSettled(
|
|
942
|
+
events.map(async (event) => {
|
|
943
|
+
const response = await fetch(this.endpoint, {
|
|
944
|
+
method: "POST",
|
|
945
|
+
headers: {
|
|
946
|
+
"Content-Type": "application/json",
|
|
947
|
+
...event.headers || {}
|
|
948
|
+
},
|
|
949
|
+
body: JSON.stringify({
|
|
950
|
+
...event.payload,
|
|
951
|
+
_queue_id: event.id,
|
|
952
|
+
_queue_timestamp: event.timestamp
|
|
953
|
+
})
|
|
954
|
+
});
|
|
955
|
+
if (!response.ok) {
|
|
956
|
+
throw new Error(`HTTP ${response.status}`);
|
|
957
|
+
}
|
|
958
|
+
})
|
|
959
|
+
);
|
|
960
|
+
const failedEvents = events.filter((_, index) => results[index]?.status === "rejected");
|
|
961
|
+
if (failedEvents.length > 0) {
|
|
962
|
+
for (const event of failedEvents) {
|
|
963
|
+
if (event.retries < this.config.maxRetries) {
|
|
964
|
+
event.retries++;
|
|
965
|
+
this.queue.push(event);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const delay = this.config.retryDelayMs * Math.pow(2, failedEvents[0].retries - 1);
|
|
969
|
+
setTimeout(() => this.flush(), delay);
|
|
952
970
|
}
|
|
971
|
+
return;
|
|
953
972
|
} catch (error) {
|
|
954
973
|
for (const event of events) {
|
|
955
974
|
if (event.retries < this.config.maxRetries) {
|
|
@@ -1002,6 +1021,7 @@ var PingService = class {
|
|
|
1002
1021
|
constructor(sessionId2, visitorId2, version, config2 = {}) {
|
|
1003
1022
|
this.intervalId = null;
|
|
1004
1023
|
this.isVisible = true;
|
|
1024
|
+
this.isFocused = true;
|
|
1005
1025
|
this.currentScrollDepth = 0;
|
|
1006
1026
|
this.ping = async () => {
|
|
1007
1027
|
const data = this.getData();
|
|
@@ -1020,6 +1040,12 @@ var PingService = class {
|
|
|
1020
1040
|
this.handleVisibilityChange = () => {
|
|
1021
1041
|
this.isVisible = document.visibilityState === "visible";
|
|
1022
1042
|
};
|
|
1043
|
+
this.handleFocusChange = () => {
|
|
1044
|
+
this.isFocused = typeof document.hasFocus === "function" ? document.hasFocus() : true;
|
|
1045
|
+
if (this.intervalId && this.isVisible && this.isFocused) {
|
|
1046
|
+
this.ping();
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1023
1049
|
this.handleScroll = () => {
|
|
1024
1050
|
const scrollPercent = Math.round(
|
|
1025
1051
|
(window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100
|
|
@@ -1032,12 +1058,16 @@ var PingService = class {
|
|
|
1032
1058
|
this.visitorId = visitorId2;
|
|
1033
1059
|
this.version = version;
|
|
1034
1060
|
this.pageLoadTime = Date.now();
|
|
1061
|
+
this.isVisible = document.visibilityState === "visible";
|
|
1062
|
+
this.isFocused = typeof document.hasFocus === "function" ? document.hasFocus() : true;
|
|
1035
1063
|
this.config = {
|
|
1036
1064
|
interval: DEFAULT_CONFIG.pingInterval,
|
|
1037
1065
|
endpoint: "",
|
|
1038
1066
|
...config2
|
|
1039
1067
|
};
|
|
1040
1068
|
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1069
|
+
window.addEventListener("focus", this.handleFocusChange);
|
|
1070
|
+
window.addEventListener("blur", this.handleFocusChange);
|
|
1041
1071
|
window.addEventListener("scroll", this.handleScroll, { passive: true });
|
|
1042
1072
|
}
|
|
1043
1073
|
/**
|
|
@@ -1046,11 +1076,13 @@ var PingService = class {
|
|
|
1046
1076
|
start() {
|
|
1047
1077
|
if (this.intervalId) return;
|
|
1048
1078
|
this.intervalId = setInterval(() => {
|
|
1049
|
-
if (this.isVisible) {
|
|
1079
|
+
if (this.isVisible && this.isFocused) {
|
|
1050
1080
|
this.ping();
|
|
1051
1081
|
}
|
|
1052
1082
|
}, this.config.interval);
|
|
1053
|
-
this.
|
|
1083
|
+
if (this.isVisible && this.isFocused) {
|
|
1084
|
+
this.ping();
|
|
1085
|
+
}
|
|
1054
1086
|
}
|
|
1055
1087
|
/**
|
|
1056
1088
|
* Stop the ping service
|
|
@@ -1061,6 +1093,8 @@ var PingService = class {
|
|
|
1061
1093
|
this.intervalId = null;
|
|
1062
1094
|
}
|
|
1063
1095
|
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1096
|
+
window.removeEventListener("focus", this.handleFocusChange);
|
|
1097
|
+
window.removeEventListener("blur", this.handleFocusChange);
|
|
1064
1098
|
window.removeEventListener("scroll", this.handleScroll);
|
|
1065
1099
|
}
|
|
1066
1100
|
/**
|
|
@@ -1081,7 +1115,7 @@ var PingService = class {
|
|
|
1081
1115
|
url: window.location.href,
|
|
1082
1116
|
time_on_page_ms: Date.now() - this.pageLoadTime,
|
|
1083
1117
|
scroll_depth: this.currentScrollDepth,
|
|
1084
|
-
is_active: this.isVisible,
|
|
1118
|
+
is_active: this.isVisible && this.isFocused,
|
|
1085
1119
|
tracker_version: this.version
|
|
1086
1120
|
};
|
|
1087
1121
|
}
|
|
@@ -1388,7 +1422,8 @@ var FormTracker = class {
|
|
|
1388
1422
|
form_id: formId,
|
|
1389
1423
|
form_type: this.detectFormType(form),
|
|
1390
1424
|
time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
|
|
1391
|
-
is_conversion: true
|
|
1425
|
+
is_conversion: true,
|
|
1426
|
+
submit_source: "submit"
|
|
1392
1427
|
});
|
|
1393
1428
|
};
|
|
1394
1429
|
this.handleClick = (e) => {
|
|
@@ -1403,7 +1438,8 @@ var FormTracker = class {
|
|
|
1403
1438
|
form_id: formId,
|
|
1404
1439
|
form_type: "hubspot",
|
|
1405
1440
|
time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
|
|
1406
|
-
is_conversion: true
|
|
1441
|
+
is_conversion: true,
|
|
1442
|
+
submit_source: "click"
|
|
1407
1443
|
});
|
|
1408
1444
|
}
|
|
1409
1445
|
}
|
|
@@ -1412,7 +1448,8 @@ var FormTracker = class {
|
|
|
1412
1448
|
event_type: "form_submit",
|
|
1413
1449
|
form_id: "typeform_embed",
|
|
1414
1450
|
form_type: "typeform",
|
|
1415
|
-
is_conversion: true
|
|
1451
|
+
is_conversion: true,
|
|
1452
|
+
submit_source: "click"
|
|
1416
1453
|
});
|
|
1417
1454
|
}
|
|
1418
1455
|
};
|
|
@@ -1514,7 +1551,8 @@ var FormTracker = class {
|
|
|
1514
1551
|
event_type: "form_success",
|
|
1515
1552
|
form_id: "page_conversion",
|
|
1516
1553
|
form_type: "unknown",
|
|
1517
|
-
is_conversion: true
|
|
1554
|
+
is_conversion: true,
|
|
1555
|
+
submit_source: "thank_you"
|
|
1518
1556
|
});
|
|
1519
1557
|
break;
|
|
1520
1558
|
}
|
|
@@ -1738,8 +1776,10 @@ var initialized = false;
|
|
|
1738
1776
|
var debugMode = false;
|
|
1739
1777
|
var visitorId = null;
|
|
1740
1778
|
var sessionId = null;
|
|
1779
|
+
var workspaceId = null;
|
|
1741
1780
|
var navigationTiming = null;
|
|
1742
1781
|
var aiDetection = null;
|
|
1782
|
+
var pageStartTime = null;
|
|
1743
1783
|
var behavioralClassifier = null;
|
|
1744
1784
|
var behavioralMLResult = null;
|
|
1745
1785
|
var focusBlurAnalyzer = null;
|
|
@@ -1759,6 +1799,28 @@ function log(...args) {
|
|
|
1759
1799
|
function endpoint(path) {
|
|
1760
1800
|
return `${config.apiHost}${path}`;
|
|
1761
1801
|
}
|
|
1802
|
+
function buildHeaders(idempotencyKey) {
|
|
1803
|
+
const headers = {
|
|
1804
|
+
"Content-Type": "application/json"
|
|
1805
|
+
};
|
|
1806
|
+
if (config.apiKey) {
|
|
1807
|
+
headers["X-Loamly-Api-Key"] = config.apiKey;
|
|
1808
|
+
}
|
|
1809
|
+
if (idempotencyKey) {
|
|
1810
|
+
headers["X-Idempotency-Key"] = idempotencyKey;
|
|
1811
|
+
}
|
|
1812
|
+
return headers;
|
|
1813
|
+
}
|
|
1814
|
+
function buildBeaconUrl(path) {
|
|
1815
|
+
if (!config.apiKey) return path;
|
|
1816
|
+
const url = new URL(path, config.apiHost);
|
|
1817
|
+
url.searchParams.set("api_key", config.apiKey);
|
|
1818
|
+
return url.toString();
|
|
1819
|
+
}
|
|
1820
|
+
function buildIdempotencyKey(prefix) {
|
|
1821
|
+
const base = sessionId || visitorId || "unknown";
|
|
1822
|
+
return `${prefix}:${base}:${Date.now()}`;
|
|
1823
|
+
}
|
|
1762
1824
|
function init(userConfig = {}) {
|
|
1763
1825
|
if (initialized) {
|
|
1764
1826
|
log("Already initialized");
|
|
@@ -1769,7 +1831,11 @@ function init(userConfig = {}) {
|
|
|
1769
1831
|
...userConfig,
|
|
1770
1832
|
apiHost: userConfig.apiHost || DEFAULT_CONFIG.apiHost
|
|
1771
1833
|
};
|
|
1834
|
+
workspaceId = userConfig.workspaceId ?? null;
|
|
1772
1835
|
debugMode = userConfig.debug ?? false;
|
|
1836
|
+
if (config.apiKey && !workspaceId) {
|
|
1837
|
+
log("Workspace ID missing. Behavioral events require workspaceId.");
|
|
1838
|
+
}
|
|
1773
1839
|
const features = {
|
|
1774
1840
|
scroll: true,
|
|
1775
1841
|
time: true,
|
|
@@ -1787,13 +1853,11 @@ function init(userConfig = {}) {
|
|
|
1787
1853
|
log("Features:", features);
|
|
1788
1854
|
visitorId = getVisitorId();
|
|
1789
1855
|
log("Visitor ID:", visitorId);
|
|
1790
|
-
const session = getSessionId();
|
|
1791
|
-
sessionId = session.sessionId;
|
|
1792
|
-
log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
|
|
1793
1856
|
if (features.eventQueue) {
|
|
1794
1857
|
eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
1795
1858
|
batchSize: DEFAULT_CONFIG.batchSize,
|
|
1796
|
-
batchTimeout: DEFAULT_CONFIG.batchTimeout
|
|
1859
|
+
batchTimeout: DEFAULT_CONFIG.batchTimeout,
|
|
1860
|
+
apiKey: config.apiKey
|
|
1797
1861
|
});
|
|
1798
1862
|
}
|
|
1799
1863
|
navigationTiming = detectNavigationType();
|
|
@@ -1803,37 +1867,40 @@ function init(userConfig = {}) {
|
|
|
1803
1867
|
log("AI detected:", aiDetection);
|
|
1804
1868
|
}
|
|
1805
1869
|
initialized = true;
|
|
1806
|
-
if (!userConfig.disableAutoPageview) {
|
|
1807
|
-
pageview();
|
|
1808
|
-
}
|
|
1809
|
-
if (!userConfig.disableBehavioral) {
|
|
1810
|
-
setupAdvancedBehavioralTracking(features);
|
|
1811
|
-
}
|
|
1812
|
-
if (features.behavioralML) {
|
|
1813
|
-
behavioralClassifier = new BehavioralClassifier(1e4);
|
|
1814
|
-
behavioralClassifier.setOnClassify(handleBehavioralClassification);
|
|
1815
|
-
setupBehavioralMLTracking();
|
|
1816
|
-
}
|
|
1817
|
-
if (features.focusBlur) {
|
|
1818
|
-
focusBlurAnalyzer = new FocusBlurAnalyzer();
|
|
1819
|
-
focusBlurAnalyzer.initTracking();
|
|
1820
|
-
setTimeout(() => {
|
|
1821
|
-
if (focusBlurAnalyzer) {
|
|
1822
|
-
handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
|
|
1823
|
-
}
|
|
1824
|
-
}, 5e3);
|
|
1825
|
-
}
|
|
1826
1870
|
if (features.agentic) {
|
|
1827
1871
|
agenticAnalyzer = new AgenticBrowserAnalyzer();
|
|
1828
1872
|
agenticAnalyzer.init();
|
|
1829
1873
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1874
|
+
void initializeSession().finally(() => {
|
|
1875
|
+
void registerServiceWorker();
|
|
1876
|
+
if (!userConfig.disableAutoPageview) {
|
|
1877
|
+
pageview();
|
|
1878
|
+
}
|
|
1879
|
+
if (!userConfig.disableBehavioral) {
|
|
1880
|
+
setupAdvancedBehavioralTracking(features);
|
|
1881
|
+
}
|
|
1882
|
+
if (features.behavioralML) {
|
|
1883
|
+
behavioralClassifier = new BehavioralClassifier(1e4);
|
|
1884
|
+
behavioralClassifier.setOnClassify(handleBehavioralClassification);
|
|
1885
|
+
setupBehavioralMLTracking();
|
|
1886
|
+
}
|
|
1887
|
+
if (features.focusBlur) {
|
|
1888
|
+
focusBlurAnalyzer = new FocusBlurAnalyzer();
|
|
1889
|
+
focusBlurAnalyzer.initTracking();
|
|
1890
|
+
setTimeout(() => {
|
|
1891
|
+
if (focusBlurAnalyzer) {
|
|
1892
|
+
handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
|
|
1893
|
+
}
|
|
1894
|
+
}, 5e3);
|
|
1895
|
+
}
|
|
1896
|
+
if (features.ping && visitorId && sessionId) {
|
|
1897
|
+
pingService = new PingService(sessionId, visitorId, VERSION, {
|
|
1898
|
+
interval: DEFAULT_CONFIG.pingInterval,
|
|
1899
|
+
endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping)
|
|
1900
|
+
});
|
|
1901
|
+
pingService.start();
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1837
1904
|
spaRouter = new SPARouter({
|
|
1838
1905
|
onNavigate: handleSPANavigation
|
|
1839
1906
|
});
|
|
@@ -1842,6 +1909,78 @@ function init(userConfig = {}) {
|
|
|
1842
1909
|
reportHealth("initialized");
|
|
1843
1910
|
log("Initialization complete");
|
|
1844
1911
|
}
|
|
1912
|
+
async function registerServiceWorker() {
|
|
1913
|
+
if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return;
|
|
1914
|
+
if (!config.apiKey || !workspaceId) return;
|
|
1915
|
+
try {
|
|
1916
|
+
const swUrl = new URL("/tracker/loamly-sw.js", window.location.origin);
|
|
1917
|
+
swUrl.searchParams.set("workspace_id", workspaceId);
|
|
1918
|
+
swUrl.searchParams.set("api_key", config.apiKey);
|
|
1919
|
+
const registration = await navigator.serviceWorker.register(swUrl.toString(), { scope: "/" });
|
|
1920
|
+
registration.addEventListener("updatefound", () => {
|
|
1921
|
+
const installing = registration.installing;
|
|
1922
|
+
installing?.addEventListener("statechange", () => {
|
|
1923
|
+
if (installing.state === "activated") {
|
|
1924
|
+
installing.postMessage({ type: "SKIP_WAITING" });
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
});
|
|
1928
|
+
setInterval(() => {
|
|
1929
|
+
registration.update().catch(() => {
|
|
1930
|
+
});
|
|
1931
|
+
}, 24 * 60 * 60 * 1e3);
|
|
1932
|
+
} catch {
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
async function initializeSession() {
|
|
1936
|
+
const now = Date.now();
|
|
1937
|
+
pageStartTime = now;
|
|
1938
|
+
try {
|
|
1939
|
+
const storedSession = sessionStorage.getItem("loamly_session");
|
|
1940
|
+
const storedStart = sessionStorage.getItem("loamly_start");
|
|
1941
|
+
const sessionTimeout = config.sessionTimeout ?? DEFAULT_CONFIG.sessionTimeout;
|
|
1942
|
+
if (storedSession && storedStart) {
|
|
1943
|
+
const startTime = parseInt(storedStart, 10);
|
|
1944
|
+
const elapsed = now - startTime;
|
|
1945
|
+
if (elapsed > 0 && elapsed < sessionTimeout) {
|
|
1946
|
+
sessionId = storedSession;
|
|
1947
|
+
log("Session ID:", sessionId, "(existing)");
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
} catch {
|
|
1952
|
+
}
|
|
1953
|
+
if (config.apiKey && workspaceId && visitorId) {
|
|
1954
|
+
try {
|
|
1955
|
+
const response = await safeFetch(endpoint(DEFAULT_CONFIG.endpoints.session), {
|
|
1956
|
+
method: "POST",
|
|
1957
|
+
headers: buildHeaders(),
|
|
1958
|
+
body: JSON.stringify({
|
|
1959
|
+
workspace_id: workspaceId,
|
|
1960
|
+
visitor_id: visitorId
|
|
1961
|
+
})
|
|
1962
|
+
});
|
|
1963
|
+
if (response?.ok) {
|
|
1964
|
+
const data = await response.json();
|
|
1965
|
+
sessionId = data.session_id || sessionId;
|
|
1966
|
+
const startTime = data.start_time || now;
|
|
1967
|
+
if (sessionId) {
|
|
1968
|
+
try {
|
|
1969
|
+
sessionStorage.setItem("loamly_session", sessionId);
|
|
1970
|
+
sessionStorage.setItem("loamly_start", String(startTime));
|
|
1971
|
+
} catch {
|
|
1972
|
+
}
|
|
1973
|
+
log("Session ID:", sessionId, "(server)");
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
} catch {
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
const session = getSessionId();
|
|
1981
|
+
sessionId = session.sessionId;
|
|
1982
|
+
log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
|
|
1983
|
+
}
|
|
1845
1984
|
function setupAdvancedBehavioralTracking(features) {
|
|
1846
1985
|
if (features.scroll) {
|
|
1847
1986
|
scrollTracker = new ScrollTracker({
|
|
@@ -1849,8 +1988,8 @@ function setupAdvancedBehavioralTracking(features) {
|
|
|
1849
1988
|
onChunkReached: (event) => {
|
|
1850
1989
|
log("Scroll chunk:", event.chunk);
|
|
1851
1990
|
queueEvent("scroll_depth", {
|
|
1852
|
-
|
|
1853
|
-
|
|
1991
|
+
scroll_depth: Math.round(event.depth / 100 * 100) / 100,
|
|
1992
|
+
milestone: Math.round(event.chunk / 100 * 100) / 100,
|
|
1854
1993
|
time_to_reach_ms: event.time_to_reach_ms
|
|
1855
1994
|
});
|
|
1856
1995
|
}
|
|
@@ -1864,10 +2003,8 @@ function setupAdvancedBehavioralTracking(features) {
|
|
|
1864
2003
|
onUpdate: (event) => {
|
|
1865
2004
|
if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
|
|
1866
2005
|
queueEvent("time_spent", {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
idle_time_ms: event.idle_time_ms,
|
|
1870
|
-
is_engaged: event.is_engaged
|
|
2006
|
+
visible_time_ms: event.total_time_ms,
|
|
2007
|
+
page_start_time: pageStartTime || Date.now()
|
|
1871
2008
|
});
|
|
1872
2009
|
}
|
|
1873
2010
|
}
|
|
@@ -1878,13 +2015,19 @@ function setupAdvancedBehavioralTracking(features) {
|
|
|
1878
2015
|
formTracker = new FormTracker({
|
|
1879
2016
|
onFormEvent: (event) => {
|
|
1880
2017
|
log("Form event:", event.event_type, event.form_id);
|
|
1881
|
-
|
|
2018
|
+
const isSubmitEvent = event.event_type === "form_submit";
|
|
2019
|
+
const isSuccessEvent = event.event_type === "form_success";
|
|
2020
|
+
const normalizedEventType = isSubmitEvent || isSuccessEvent ? "form_submit" : "form_focus";
|
|
2021
|
+
const submitSource = event.submit_source || (isSuccessEvent ? "thank_you" : isSubmitEvent ? "submit" : null);
|
|
2022
|
+
queueEvent(normalizedEventType, {
|
|
1882
2023
|
form_id: event.form_id,
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
2024
|
+
form_provider: event.form_type || "unknown",
|
|
2025
|
+
form_field_type: event.field_type || null,
|
|
2026
|
+
form_field_name: event.field_name || null,
|
|
2027
|
+
form_event_type: event.event_type,
|
|
2028
|
+
submit_source: submitSource,
|
|
2029
|
+
is_inferred: isSuccessEvent,
|
|
2030
|
+
time_to_submit_seconds: event.time_to_submit_ms ? Math.round(event.time_to_submit_ms / 1e3) : null
|
|
1888
2031
|
});
|
|
1889
2032
|
}
|
|
1890
2033
|
});
|
|
@@ -1905,7 +2048,7 @@ function setupAdvancedBehavioralTracking(features) {
|
|
|
1905
2048
|
if (link && link.href) {
|
|
1906
2049
|
const isExternal = link.hostname !== window.location.hostname;
|
|
1907
2050
|
queueEvent("click", {
|
|
1908
|
-
|
|
2051
|
+
element_type: "link",
|
|
1909
2052
|
href: truncateText(link.href, 200),
|
|
1910
2053
|
text: truncateText(link.textContent || "", 100),
|
|
1911
2054
|
is_external: isExternal
|
|
@@ -1915,15 +2058,32 @@ function setupAdvancedBehavioralTracking(features) {
|
|
|
1915
2058
|
}
|
|
1916
2059
|
function queueEvent(eventType, data) {
|
|
1917
2060
|
if (!eventQueue) return;
|
|
1918
|
-
|
|
2061
|
+
if (!config.apiKey) {
|
|
2062
|
+
log("Missing apiKey, behavioral event skipped:", eventType);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (!workspaceId) {
|
|
2066
|
+
log("Missing workspaceId, behavioral event skipped:", eventType);
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
if (!sessionId) {
|
|
2070
|
+
log("Missing sessionId, behavioral event skipped:", eventType);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
const idempotencyKey = buildIdempotencyKey(eventType);
|
|
2074
|
+
const payload = {
|
|
1919
2075
|
visitor_id: visitorId,
|
|
1920
2076
|
session_id: sessionId,
|
|
1921
2077
|
event_type: eventType,
|
|
1922
|
-
|
|
1923
|
-
|
|
2078
|
+
event_data: data,
|
|
2079
|
+
page_url: window.location.href,
|
|
2080
|
+
page_path: window.location.pathname,
|
|
1924
2081
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1925
|
-
tracker_version: VERSION
|
|
1926
|
-
|
|
2082
|
+
tracker_version: VERSION,
|
|
2083
|
+
idempotency_key: idempotencyKey
|
|
2084
|
+
};
|
|
2085
|
+
payload.workspace_id = workspaceId;
|
|
2086
|
+
eventQueue.push(eventType, payload, buildHeaders(idempotencyKey));
|
|
1927
2087
|
}
|
|
1928
2088
|
function handleSPANavigation(event) {
|
|
1929
2089
|
log("SPA navigation:", event.navigation_type, event.to_url);
|
|
@@ -1951,34 +2111,42 @@ function handleSPANavigation(event) {
|
|
|
1951
2111
|
}
|
|
1952
2112
|
function setupUnloadHandlers() {
|
|
1953
2113
|
const handleUnload = () => {
|
|
2114
|
+
if (!workspaceId || !config.apiKey || !sessionId) return;
|
|
1954
2115
|
const scrollEvent = scrollTracker?.getFinalEvent();
|
|
1955
2116
|
if (scrollEvent) {
|
|
1956
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
2117
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
|
|
2118
|
+
workspace_id: workspaceId,
|
|
1957
2119
|
visitor_id: visitorId,
|
|
1958
2120
|
session_id: sessionId,
|
|
1959
2121
|
event_type: "scroll_depth_final",
|
|
1960
|
-
|
|
1961
|
-
|
|
2122
|
+
event_data: {
|
|
2123
|
+
scroll_depth: Math.round(scrollEvent.depth / 100 * 100) / 100,
|
|
2124
|
+
milestone: Math.round(scrollEvent.chunk / 100 * 100) / 100,
|
|
2125
|
+
time_to_reach_ms: scrollEvent.time_to_reach_ms
|
|
2126
|
+
},
|
|
2127
|
+
page_url: window.location.href,
|
|
2128
|
+
page_path: window.location.pathname,
|
|
2129
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2130
|
+
tracker_version: VERSION,
|
|
2131
|
+
idempotency_key: buildIdempotencyKey("scroll_depth_final")
|
|
1962
2132
|
});
|
|
1963
2133
|
}
|
|
1964
2134
|
const timeEvent = timeTracker?.getFinalMetrics();
|
|
1965
2135
|
if (timeEvent) {
|
|
1966
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
2136
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
|
|
2137
|
+
workspace_id: workspaceId,
|
|
1967
2138
|
visitor_id: visitorId,
|
|
1968
2139
|
session_id: sessionId,
|
|
1969
|
-
event_type: "
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
event_type: "agentic_detection",
|
|
1980
|
-
data: agenticResult,
|
|
1981
|
-
url: window.location.href
|
|
2140
|
+
event_type: "time_spent",
|
|
2141
|
+
event_data: {
|
|
2142
|
+
visible_time_ms: timeEvent.total_time_ms,
|
|
2143
|
+
page_start_time: pageStartTime || Date.now()
|
|
2144
|
+
},
|
|
2145
|
+
page_url: window.location.href,
|
|
2146
|
+
page_path: window.location.pathname,
|
|
2147
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2148
|
+
tracker_version: VERSION,
|
|
2149
|
+
idempotency_key: buildIdempotencyKey("time_spent")
|
|
1982
2150
|
});
|
|
1983
2151
|
}
|
|
1984
2152
|
eventQueue?.flushBeacon();
|
|
@@ -2001,30 +2169,55 @@ function pageview(customUrl) {
|
|
|
2001
2169
|
log("Not initialized, call init() first");
|
|
2002
2170
|
return;
|
|
2003
2171
|
}
|
|
2172
|
+
if (!config.apiKey) {
|
|
2173
|
+
log("Missing apiKey, pageview skipped");
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2004
2176
|
const url = customUrl || window.location.href;
|
|
2177
|
+
const utmParams = extractUTMParams(url);
|
|
2178
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2179
|
+
const idempotencyKey = buildIdempotencyKey("visit");
|
|
2180
|
+
const agenticResult = agenticAnalyzer?.getResult();
|
|
2181
|
+
const pagePath = (() => {
|
|
2182
|
+
try {
|
|
2183
|
+
return new URL(url).pathname;
|
|
2184
|
+
} catch {
|
|
2185
|
+
return window.location.pathname;
|
|
2186
|
+
}
|
|
2187
|
+
})();
|
|
2005
2188
|
const payload = {
|
|
2006
2189
|
visitor_id: visitorId,
|
|
2007
2190
|
session_id: sessionId,
|
|
2008
|
-
url,
|
|
2191
|
+
page_url: url,
|
|
2192
|
+
page_path: pagePath,
|
|
2009
2193
|
referrer: document.referrer || null,
|
|
2010
2194
|
title: document.title || null,
|
|
2011
|
-
utm_source:
|
|
2012
|
-
utm_medium:
|
|
2013
|
-
utm_campaign:
|
|
2195
|
+
utm_source: utmParams.utm_source || null,
|
|
2196
|
+
utm_medium: utmParams.utm_medium || null,
|
|
2197
|
+
utm_campaign: utmParams.utm_campaign || null,
|
|
2198
|
+
utm_term: utmParams.utm_term || null,
|
|
2199
|
+
utm_content: utmParams.utm_content || null,
|
|
2014
2200
|
user_agent: navigator.userAgent,
|
|
2015
2201
|
screen_width: window.screen?.width,
|
|
2016
2202
|
screen_height: window.screen?.height,
|
|
2017
2203
|
language: navigator.language,
|
|
2018
2204
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
2019
2205
|
tracker_version: VERSION,
|
|
2206
|
+
event_type: "pageview",
|
|
2207
|
+
event_data: null,
|
|
2208
|
+
timestamp,
|
|
2020
2209
|
navigation_timing: navigationTiming,
|
|
2021
2210
|
ai_platform: aiDetection?.platform || null,
|
|
2022
|
-
is_ai_referrer: aiDetection?.isAI || false
|
|
2211
|
+
is_ai_referrer: aiDetection?.isAI || false,
|
|
2212
|
+
agentic_detection: agenticResult || null
|
|
2023
2213
|
};
|
|
2214
|
+
if (workspaceId) {
|
|
2215
|
+
payload.workspace_id = workspaceId;
|
|
2216
|
+
}
|
|
2024
2217
|
log("Pageview:", payload);
|
|
2025
2218
|
safeFetch(endpoint(DEFAULT_CONFIG.endpoints.visit), {
|
|
2026
2219
|
method: "POST",
|
|
2027
|
-
headers:
|
|
2220
|
+
headers: buildHeaders(idempotencyKey),
|
|
2028
2221
|
body: JSON.stringify(payload)
|
|
2029
2222
|
});
|
|
2030
2223
|
}
|
|
@@ -2033,6 +2226,11 @@ function track(eventName, options = {}) {
|
|
|
2033
2226
|
log("Not initialized, call init() first");
|
|
2034
2227
|
return;
|
|
2035
2228
|
}
|
|
2229
|
+
if (!config.apiKey) {
|
|
2230
|
+
log("Missing apiKey, event skipped:", eventName);
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
const idempotencyKey = buildIdempotencyKey(`event:${eventName}`);
|
|
2036
2234
|
const payload = {
|
|
2037
2235
|
visitor_id: visitorId,
|
|
2038
2236
|
session_id: sessionId,
|
|
@@ -2041,14 +2239,19 @@ function track(eventName, options = {}) {
|
|
|
2041
2239
|
properties: options.properties || {},
|
|
2042
2240
|
revenue: options.revenue,
|
|
2043
2241
|
currency: options.currency || "USD",
|
|
2044
|
-
|
|
2242
|
+
page_url: window.location.href,
|
|
2243
|
+
referrer: document.referrer || null,
|
|
2045
2244
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2046
|
-
tracker_version: VERSION
|
|
2245
|
+
tracker_version: VERSION,
|
|
2246
|
+
idempotency_key: idempotencyKey
|
|
2047
2247
|
};
|
|
2248
|
+
if (workspaceId) {
|
|
2249
|
+
payload.workspace_id = workspaceId;
|
|
2250
|
+
}
|
|
2048
2251
|
log("Event:", eventName, payload);
|
|
2049
2252
|
safeFetch(endpoint("/api/ingest/event"), {
|
|
2050
2253
|
method: "POST",
|
|
2051
|
-
headers:
|
|
2254
|
+
headers: buildHeaders(idempotencyKey),
|
|
2052
2255
|
body: JSON.stringify(payload)
|
|
2053
2256
|
});
|
|
2054
2257
|
}
|
|
@@ -2060,17 +2263,26 @@ function identify(userId, traits = {}) {
|
|
|
2060
2263
|
log("Not initialized, call init() first");
|
|
2061
2264
|
return;
|
|
2062
2265
|
}
|
|
2266
|
+
if (!config.apiKey) {
|
|
2267
|
+
log("Missing apiKey, identify skipped");
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2063
2270
|
log("Identify:", userId, traits);
|
|
2271
|
+
const idempotencyKey = buildIdempotencyKey("identify");
|
|
2064
2272
|
const payload = {
|
|
2065
2273
|
visitor_id: visitorId,
|
|
2066
2274
|
session_id: sessionId,
|
|
2067
2275
|
user_id: userId,
|
|
2068
2276
|
traits,
|
|
2069
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2277
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2278
|
+
idempotency_key: idempotencyKey
|
|
2070
2279
|
};
|
|
2280
|
+
if (workspaceId) {
|
|
2281
|
+
payload.workspace_id = workspaceId;
|
|
2282
|
+
}
|
|
2071
2283
|
safeFetch(endpoint("/api/ingest/identify"), {
|
|
2072
2284
|
method: "POST",
|
|
2073
|
-
headers:
|
|
2285
|
+
headers: buildHeaders(idempotencyKey),
|
|
2074
2286
|
body: JSON.stringify(payload)
|
|
2075
2287
|
});
|
|
2076
2288
|
}
|
|
@@ -2201,14 +2413,15 @@ function isTrackerInitialized() {
|
|
|
2201
2413
|
return initialized;
|
|
2202
2414
|
}
|
|
2203
2415
|
function reportHealth(status, errorMessage) {
|
|
2204
|
-
if (!config.apiKey) return;
|
|
2205
2416
|
try {
|
|
2206
2417
|
const healthData = {
|
|
2207
|
-
workspace_id:
|
|
2418
|
+
workspace_id: workspaceId,
|
|
2419
|
+
visitor_id: visitorId,
|
|
2420
|
+
session_id: sessionId,
|
|
2208
2421
|
status,
|
|
2209
2422
|
error_message: errorMessage || null,
|
|
2210
|
-
|
|
2211
|
-
|
|
2423
|
+
tracker_version: VERSION,
|
|
2424
|
+
page_url: typeof window !== "undefined" ? window.location.href : null,
|
|
2212
2425
|
user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
|
2213
2426
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2214
2427
|
features: {
|
|
@@ -2326,7 +2539,7 @@ export {
|
|
|
2326
2539
|
* See what AI tells your customers — and track when they click.
|
|
2327
2540
|
*
|
|
2328
2541
|
* @module @loamly/tracker
|
|
2329
|
-
* @version 1.
|
|
2542
|
+
* @version 2.1.0
|
|
2330
2543
|
* @license MIT
|
|
2331
2544
|
* @see https://github.com/loamly/loamly
|
|
2332
2545
|
* @see https://loamly.ai
|