@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.cjs
CHANGED
|
@@ -34,7 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
module.exports = __toCommonJS(index_exports);
|
|
35
35
|
|
|
36
36
|
// src/config.ts
|
|
37
|
-
var VERSION = "2.1.
|
|
37
|
+
var VERSION = "2.1.1";
|
|
38
38
|
var DEFAULT_CONFIG = {
|
|
39
39
|
apiHost: "https://app.loamly.ai",
|
|
40
40
|
endpoints: {
|
|
@@ -893,11 +893,12 @@ var EventQueue = class {
|
|
|
893
893
|
/**
|
|
894
894
|
* Add event to queue
|
|
895
895
|
*/
|
|
896
|
-
push(type, payload) {
|
|
896
|
+
push(type, payload, headers) {
|
|
897
897
|
const event = {
|
|
898
898
|
id: this.generateId(),
|
|
899
899
|
type,
|
|
900
900
|
payload,
|
|
901
|
+
headers,
|
|
901
902
|
timestamp: Date.now(),
|
|
902
903
|
retries: 0
|
|
903
904
|
};
|
|
@@ -926,21 +927,25 @@ var EventQueue = class {
|
|
|
926
927
|
*/
|
|
927
928
|
flushBeacon() {
|
|
928
929
|
if (this.queue.length === 0) return true;
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
JSON.stringify(
|
|
938
|
-
|
|
939
|
-
|
|
930
|
+
const baseUrl = this.config.apiKey ? `${this.endpoint}?api_key=${encodeURIComponent(this.config.apiKey)}` : this.endpoint;
|
|
931
|
+
let allSent = true;
|
|
932
|
+
for (const event of this.queue) {
|
|
933
|
+
const payload = {
|
|
934
|
+
...event.payload,
|
|
935
|
+
_queue_id: event.id,
|
|
936
|
+
_queue_timestamp: event.timestamp
|
|
937
|
+
};
|
|
938
|
+
const success = navigator.sendBeacon?.(baseUrl, JSON.stringify(payload)) ?? false;
|
|
939
|
+
if (!success) {
|
|
940
|
+
allSent = false;
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (allSent) {
|
|
940
945
|
this.queue = [];
|
|
941
946
|
this.clearStorage();
|
|
942
947
|
}
|
|
943
|
-
return
|
|
948
|
+
return allSent;
|
|
944
949
|
}
|
|
945
950
|
/**
|
|
946
951
|
* Get current queue length
|
|
@@ -967,24 +972,38 @@ var EventQueue = class {
|
|
|
967
972
|
}
|
|
968
973
|
async sendBatch(events) {
|
|
969
974
|
if (events.length === 0) return;
|
|
970
|
-
const payload = {
|
|
971
|
-
events: events.map((e) => ({
|
|
972
|
-
type: e.type,
|
|
973
|
-
...e.payload,
|
|
974
|
-
_queue_id: e.id,
|
|
975
|
-
_queue_timestamp: e.timestamp
|
|
976
|
-
})),
|
|
977
|
-
batch: true
|
|
978
|
-
};
|
|
979
975
|
try {
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
976
|
+
const results = await Promise.allSettled(
|
|
977
|
+
events.map(async (event) => {
|
|
978
|
+
const response = await fetch(this.endpoint, {
|
|
979
|
+
method: "POST",
|
|
980
|
+
headers: {
|
|
981
|
+
"Content-Type": "application/json",
|
|
982
|
+
...event.headers || {}
|
|
983
|
+
},
|
|
984
|
+
body: JSON.stringify({
|
|
985
|
+
...event.payload,
|
|
986
|
+
_queue_id: event.id,
|
|
987
|
+
_queue_timestamp: event.timestamp
|
|
988
|
+
})
|
|
989
|
+
});
|
|
990
|
+
if (!response.ok) {
|
|
991
|
+
throw new Error(`HTTP ${response.status}`);
|
|
992
|
+
}
|
|
993
|
+
})
|
|
994
|
+
);
|
|
995
|
+
const failedEvents = events.filter((_, index) => results[index]?.status === "rejected");
|
|
996
|
+
if (failedEvents.length > 0) {
|
|
997
|
+
for (const event of failedEvents) {
|
|
998
|
+
if (event.retries < this.config.maxRetries) {
|
|
999
|
+
event.retries++;
|
|
1000
|
+
this.queue.push(event);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const delay = this.config.retryDelayMs * Math.pow(2, failedEvents[0].retries - 1);
|
|
1004
|
+
setTimeout(() => this.flush(), delay);
|
|
987
1005
|
}
|
|
1006
|
+
return;
|
|
988
1007
|
} catch (error) {
|
|
989
1008
|
for (const event of events) {
|
|
990
1009
|
if (event.retries < this.config.maxRetries) {
|
|
@@ -1037,6 +1056,7 @@ var PingService = class {
|
|
|
1037
1056
|
constructor(sessionId2, visitorId2, version, config2 = {}) {
|
|
1038
1057
|
this.intervalId = null;
|
|
1039
1058
|
this.isVisible = true;
|
|
1059
|
+
this.isFocused = true;
|
|
1040
1060
|
this.currentScrollDepth = 0;
|
|
1041
1061
|
this.ping = async () => {
|
|
1042
1062
|
const data = this.getData();
|
|
@@ -1055,6 +1075,12 @@ var PingService = class {
|
|
|
1055
1075
|
this.handleVisibilityChange = () => {
|
|
1056
1076
|
this.isVisible = document.visibilityState === "visible";
|
|
1057
1077
|
};
|
|
1078
|
+
this.handleFocusChange = () => {
|
|
1079
|
+
this.isFocused = typeof document.hasFocus === "function" ? document.hasFocus() : true;
|
|
1080
|
+
if (this.intervalId && this.isVisible && this.isFocused) {
|
|
1081
|
+
this.ping();
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1058
1084
|
this.handleScroll = () => {
|
|
1059
1085
|
const scrollPercent = Math.round(
|
|
1060
1086
|
(window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100
|
|
@@ -1067,12 +1093,16 @@ var PingService = class {
|
|
|
1067
1093
|
this.visitorId = visitorId2;
|
|
1068
1094
|
this.version = version;
|
|
1069
1095
|
this.pageLoadTime = Date.now();
|
|
1096
|
+
this.isVisible = document.visibilityState === "visible";
|
|
1097
|
+
this.isFocused = typeof document.hasFocus === "function" ? document.hasFocus() : true;
|
|
1070
1098
|
this.config = {
|
|
1071
1099
|
interval: DEFAULT_CONFIG.pingInterval,
|
|
1072
1100
|
endpoint: "",
|
|
1073
1101
|
...config2
|
|
1074
1102
|
};
|
|
1075
1103
|
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1104
|
+
window.addEventListener("focus", this.handleFocusChange);
|
|
1105
|
+
window.addEventListener("blur", this.handleFocusChange);
|
|
1076
1106
|
window.addEventListener("scroll", this.handleScroll, { passive: true });
|
|
1077
1107
|
}
|
|
1078
1108
|
/**
|
|
@@ -1081,11 +1111,13 @@ var PingService = class {
|
|
|
1081
1111
|
start() {
|
|
1082
1112
|
if (this.intervalId) return;
|
|
1083
1113
|
this.intervalId = setInterval(() => {
|
|
1084
|
-
if (this.isVisible) {
|
|
1114
|
+
if (this.isVisible && this.isFocused) {
|
|
1085
1115
|
this.ping();
|
|
1086
1116
|
}
|
|
1087
1117
|
}, this.config.interval);
|
|
1088
|
-
this.
|
|
1118
|
+
if (this.isVisible && this.isFocused) {
|
|
1119
|
+
this.ping();
|
|
1120
|
+
}
|
|
1089
1121
|
}
|
|
1090
1122
|
/**
|
|
1091
1123
|
* Stop the ping service
|
|
@@ -1096,6 +1128,8 @@ var PingService = class {
|
|
|
1096
1128
|
this.intervalId = null;
|
|
1097
1129
|
}
|
|
1098
1130
|
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1131
|
+
window.removeEventListener("focus", this.handleFocusChange);
|
|
1132
|
+
window.removeEventListener("blur", this.handleFocusChange);
|
|
1099
1133
|
window.removeEventListener("scroll", this.handleScroll);
|
|
1100
1134
|
}
|
|
1101
1135
|
/**
|
|
@@ -1116,7 +1150,7 @@ var PingService = class {
|
|
|
1116
1150
|
url: window.location.href,
|
|
1117
1151
|
time_on_page_ms: Date.now() - this.pageLoadTime,
|
|
1118
1152
|
scroll_depth: this.currentScrollDepth,
|
|
1119
|
-
is_active: this.isVisible,
|
|
1153
|
+
is_active: this.isVisible && this.isFocused,
|
|
1120
1154
|
tracker_version: this.version
|
|
1121
1155
|
};
|
|
1122
1156
|
}
|
|
@@ -1423,7 +1457,8 @@ var FormTracker = class {
|
|
|
1423
1457
|
form_id: formId,
|
|
1424
1458
|
form_type: this.detectFormType(form),
|
|
1425
1459
|
time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
|
|
1426
|
-
is_conversion: true
|
|
1460
|
+
is_conversion: true,
|
|
1461
|
+
submit_source: "submit"
|
|
1427
1462
|
});
|
|
1428
1463
|
};
|
|
1429
1464
|
this.handleClick = (e) => {
|
|
@@ -1438,7 +1473,8 @@ var FormTracker = class {
|
|
|
1438
1473
|
form_id: formId,
|
|
1439
1474
|
form_type: "hubspot",
|
|
1440
1475
|
time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
|
|
1441
|
-
is_conversion: true
|
|
1476
|
+
is_conversion: true,
|
|
1477
|
+
submit_source: "click"
|
|
1442
1478
|
});
|
|
1443
1479
|
}
|
|
1444
1480
|
}
|
|
@@ -1447,7 +1483,8 @@ var FormTracker = class {
|
|
|
1447
1483
|
event_type: "form_submit",
|
|
1448
1484
|
form_id: "typeform_embed",
|
|
1449
1485
|
form_type: "typeform",
|
|
1450
|
-
is_conversion: true
|
|
1486
|
+
is_conversion: true,
|
|
1487
|
+
submit_source: "click"
|
|
1451
1488
|
});
|
|
1452
1489
|
}
|
|
1453
1490
|
};
|
|
@@ -1549,7 +1586,8 @@ var FormTracker = class {
|
|
|
1549
1586
|
event_type: "form_success",
|
|
1550
1587
|
form_id: "page_conversion",
|
|
1551
1588
|
form_type: "unknown",
|
|
1552
|
-
is_conversion: true
|
|
1589
|
+
is_conversion: true,
|
|
1590
|
+
submit_source: "thank_you"
|
|
1553
1591
|
});
|
|
1554
1592
|
break;
|
|
1555
1593
|
}
|
|
@@ -1773,8 +1811,10 @@ var initialized = false;
|
|
|
1773
1811
|
var debugMode = false;
|
|
1774
1812
|
var visitorId = null;
|
|
1775
1813
|
var sessionId = null;
|
|
1814
|
+
var workspaceId = null;
|
|
1776
1815
|
var navigationTiming = null;
|
|
1777
1816
|
var aiDetection = null;
|
|
1817
|
+
var pageStartTime = null;
|
|
1778
1818
|
var behavioralClassifier = null;
|
|
1779
1819
|
var behavioralMLResult = null;
|
|
1780
1820
|
var focusBlurAnalyzer = null;
|
|
@@ -1794,6 +1834,28 @@ function log(...args) {
|
|
|
1794
1834
|
function endpoint(path) {
|
|
1795
1835
|
return `${config.apiHost}${path}`;
|
|
1796
1836
|
}
|
|
1837
|
+
function buildHeaders(idempotencyKey) {
|
|
1838
|
+
const headers = {
|
|
1839
|
+
"Content-Type": "application/json"
|
|
1840
|
+
};
|
|
1841
|
+
if (config.apiKey) {
|
|
1842
|
+
headers["X-Loamly-Api-Key"] = config.apiKey;
|
|
1843
|
+
}
|
|
1844
|
+
if (idempotencyKey) {
|
|
1845
|
+
headers["X-Idempotency-Key"] = idempotencyKey;
|
|
1846
|
+
}
|
|
1847
|
+
return headers;
|
|
1848
|
+
}
|
|
1849
|
+
function buildBeaconUrl(path) {
|
|
1850
|
+
if (!config.apiKey) return path;
|
|
1851
|
+
const url = new URL(path, config.apiHost);
|
|
1852
|
+
url.searchParams.set("api_key", config.apiKey);
|
|
1853
|
+
return url.toString();
|
|
1854
|
+
}
|
|
1855
|
+
function buildIdempotencyKey(prefix) {
|
|
1856
|
+
const base = sessionId || visitorId || "unknown";
|
|
1857
|
+
return `${prefix}:${base}:${Date.now()}`;
|
|
1858
|
+
}
|
|
1797
1859
|
function init(userConfig = {}) {
|
|
1798
1860
|
if (initialized) {
|
|
1799
1861
|
log("Already initialized");
|
|
@@ -1804,7 +1866,11 @@ function init(userConfig = {}) {
|
|
|
1804
1866
|
...userConfig,
|
|
1805
1867
|
apiHost: userConfig.apiHost || DEFAULT_CONFIG.apiHost
|
|
1806
1868
|
};
|
|
1869
|
+
workspaceId = userConfig.workspaceId ?? null;
|
|
1807
1870
|
debugMode = userConfig.debug ?? false;
|
|
1871
|
+
if (config.apiKey && !workspaceId) {
|
|
1872
|
+
log("Workspace ID missing. Behavioral events require workspaceId.");
|
|
1873
|
+
}
|
|
1808
1874
|
const features = {
|
|
1809
1875
|
scroll: true,
|
|
1810
1876
|
time: true,
|
|
@@ -1822,13 +1888,11 @@ function init(userConfig = {}) {
|
|
|
1822
1888
|
log("Features:", features);
|
|
1823
1889
|
visitorId = getVisitorId();
|
|
1824
1890
|
log("Visitor ID:", visitorId);
|
|
1825
|
-
const session = getSessionId();
|
|
1826
|
-
sessionId = session.sessionId;
|
|
1827
|
-
log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
|
|
1828
1891
|
if (features.eventQueue) {
|
|
1829
1892
|
eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
1830
1893
|
batchSize: DEFAULT_CONFIG.batchSize,
|
|
1831
|
-
batchTimeout: DEFAULT_CONFIG.batchTimeout
|
|
1894
|
+
batchTimeout: DEFAULT_CONFIG.batchTimeout,
|
|
1895
|
+
apiKey: config.apiKey
|
|
1832
1896
|
});
|
|
1833
1897
|
}
|
|
1834
1898
|
navigationTiming = detectNavigationType();
|
|
@@ -1838,37 +1902,40 @@ function init(userConfig = {}) {
|
|
|
1838
1902
|
log("AI detected:", aiDetection);
|
|
1839
1903
|
}
|
|
1840
1904
|
initialized = true;
|
|
1841
|
-
if (!userConfig.disableAutoPageview) {
|
|
1842
|
-
pageview();
|
|
1843
|
-
}
|
|
1844
|
-
if (!userConfig.disableBehavioral) {
|
|
1845
|
-
setupAdvancedBehavioralTracking(features);
|
|
1846
|
-
}
|
|
1847
|
-
if (features.behavioralML) {
|
|
1848
|
-
behavioralClassifier = new BehavioralClassifier(1e4);
|
|
1849
|
-
behavioralClassifier.setOnClassify(handleBehavioralClassification);
|
|
1850
|
-
setupBehavioralMLTracking();
|
|
1851
|
-
}
|
|
1852
|
-
if (features.focusBlur) {
|
|
1853
|
-
focusBlurAnalyzer = new FocusBlurAnalyzer();
|
|
1854
|
-
focusBlurAnalyzer.initTracking();
|
|
1855
|
-
setTimeout(() => {
|
|
1856
|
-
if (focusBlurAnalyzer) {
|
|
1857
|
-
handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
|
|
1858
|
-
}
|
|
1859
|
-
}, 5e3);
|
|
1860
|
-
}
|
|
1861
1905
|
if (features.agentic) {
|
|
1862
1906
|
agenticAnalyzer = new AgenticBrowserAnalyzer();
|
|
1863
1907
|
agenticAnalyzer.init();
|
|
1864
1908
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1909
|
+
void initializeSession().finally(() => {
|
|
1910
|
+
void registerServiceWorker();
|
|
1911
|
+
if (!userConfig.disableAutoPageview) {
|
|
1912
|
+
pageview();
|
|
1913
|
+
}
|
|
1914
|
+
if (!userConfig.disableBehavioral) {
|
|
1915
|
+
setupAdvancedBehavioralTracking(features);
|
|
1916
|
+
}
|
|
1917
|
+
if (features.behavioralML) {
|
|
1918
|
+
behavioralClassifier = new BehavioralClassifier(1e4);
|
|
1919
|
+
behavioralClassifier.setOnClassify(handleBehavioralClassification);
|
|
1920
|
+
setupBehavioralMLTracking();
|
|
1921
|
+
}
|
|
1922
|
+
if (features.focusBlur) {
|
|
1923
|
+
focusBlurAnalyzer = new FocusBlurAnalyzer();
|
|
1924
|
+
focusBlurAnalyzer.initTracking();
|
|
1925
|
+
setTimeout(() => {
|
|
1926
|
+
if (focusBlurAnalyzer) {
|
|
1927
|
+
handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
|
|
1928
|
+
}
|
|
1929
|
+
}, 5e3);
|
|
1930
|
+
}
|
|
1931
|
+
if (features.ping && visitorId && sessionId) {
|
|
1932
|
+
pingService = new PingService(sessionId, visitorId, VERSION, {
|
|
1933
|
+
interval: DEFAULT_CONFIG.pingInterval,
|
|
1934
|
+
endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping)
|
|
1935
|
+
});
|
|
1936
|
+
pingService.start();
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1872
1939
|
spaRouter = new SPARouter({
|
|
1873
1940
|
onNavigate: handleSPANavigation
|
|
1874
1941
|
});
|
|
@@ -1877,6 +1944,78 @@ function init(userConfig = {}) {
|
|
|
1877
1944
|
reportHealth("initialized");
|
|
1878
1945
|
log("Initialization complete");
|
|
1879
1946
|
}
|
|
1947
|
+
async function registerServiceWorker() {
|
|
1948
|
+
if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return;
|
|
1949
|
+
if (!config.apiKey || !workspaceId) return;
|
|
1950
|
+
try {
|
|
1951
|
+
const swUrl = new URL("/tracker/loamly-sw.js", window.location.origin);
|
|
1952
|
+
swUrl.searchParams.set("workspace_id", workspaceId);
|
|
1953
|
+
swUrl.searchParams.set("api_key", config.apiKey);
|
|
1954
|
+
const registration = await navigator.serviceWorker.register(swUrl.toString(), { scope: "/" });
|
|
1955
|
+
registration.addEventListener("updatefound", () => {
|
|
1956
|
+
const installing = registration.installing;
|
|
1957
|
+
installing?.addEventListener("statechange", () => {
|
|
1958
|
+
if (installing.state === "activated") {
|
|
1959
|
+
installing.postMessage({ type: "SKIP_WAITING" });
|
|
1960
|
+
}
|
|
1961
|
+
});
|
|
1962
|
+
});
|
|
1963
|
+
setInterval(() => {
|
|
1964
|
+
registration.update().catch(() => {
|
|
1965
|
+
});
|
|
1966
|
+
}, 24 * 60 * 60 * 1e3);
|
|
1967
|
+
} catch {
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
async function initializeSession() {
|
|
1971
|
+
const now = Date.now();
|
|
1972
|
+
pageStartTime = now;
|
|
1973
|
+
try {
|
|
1974
|
+
const storedSession = sessionStorage.getItem("loamly_session");
|
|
1975
|
+
const storedStart = sessionStorage.getItem("loamly_start");
|
|
1976
|
+
const sessionTimeout = config.sessionTimeout ?? DEFAULT_CONFIG.sessionTimeout;
|
|
1977
|
+
if (storedSession && storedStart) {
|
|
1978
|
+
const startTime = parseInt(storedStart, 10);
|
|
1979
|
+
const elapsed = now - startTime;
|
|
1980
|
+
if (elapsed > 0 && elapsed < sessionTimeout) {
|
|
1981
|
+
sessionId = storedSession;
|
|
1982
|
+
log("Session ID:", sessionId, "(existing)");
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
} catch {
|
|
1987
|
+
}
|
|
1988
|
+
if (config.apiKey && workspaceId && visitorId) {
|
|
1989
|
+
try {
|
|
1990
|
+
const response = await safeFetch(endpoint(DEFAULT_CONFIG.endpoints.session), {
|
|
1991
|
+
method: "POST",
|
|
1992
|
+
headers: buildHeaders(),
|
|
1993
|
+
body: JSON.stringify({
|
|
1994
|
+
workspace_id: workspaceId,
|
|
1995
|
+
visitor_id: visitorId
|
|
1996
|
+
})
|
|
1997
|
+
});
|
|
1998
|
+
if (response?.ok) {
|
|
1999
|
+
const data = await response.json();
|
|
2000
|
+
sessionId = data.session_id || sessionId;
|
|
2001
|
+
const startTime = data.start_time || now;
|
|
2002
|
+
if (sessionId) {
|
|
2003
|
+
try {
|
|
2004
|
+
sessionStorage.setItem("loamly_session", sessionId);
|
|
2005
|
+
sessionStorage.setItem("loamly_start", String(startTime));
|
|
2006
|
+
} catch {
|
|
2007
|
+
}
|
|
2008
|
+
log("Session ID:", sessionId, "(server)");
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
} catch {
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
const session = getSessionId();
|
|
2016
|
+
sessionId = session.sessionId;
|
|
2017
|
+
log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
|
|
2018
|
+
}
|
|
1880
2019
|
function setupAdvancedBehavioralTracking(features) {
|
|
1881
2020
|
if (features.scroll) {
|
|
1882
2021
|
scrollTracker = new ScrollTracker({
|
|
@@ -1884,8 +2023,8 @@ function setupAdvancedBehavioralTracking(features) {
|
|
|
1884
2023
|
onChunkReached: (event) => {
|
|
1885
2024
|
log("Scroll chunk:", event.chunk);
|
|
1886
2025
|
queueEvent("scroll_depth", {
|
|
1887
|
-
|
|
1888
|
-
|
|
2026
|
+
scroll_depth: Math.round(event.depth / 100 * 100) / 100,
|
|
2027
|
+
milestone: Math.round(event.chunk / 100 * 100) / 100,
|
|
1889
2028
|
time_to_reach_ms: event.time_to_reach_ms
|
|
1890
2029
|
});
|
|
1891
2030
|
}
|
|
@@ -1899,10 +2038,8 @@ function setupAdvancedBehavioralTracking(features) {
|
|
|
1899
2038
|
onUpdate: (event) => {
|
|
1900
2039
|
if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
|
|
1901
2040
|
queueEvent("time_spent", {
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
idle_time_ms: event.idle_time_ms,
|
|
1905
|
-
is_engaged: event.is_engaged
|
|
2041
|
+
visible_time_ms: event.total_time_ms,
|
|
2042
|
+
page_start_time: pageStartTime || Date.now()
|
|
1906
2043
|
});
|
|
1907
2044
|
}
|
|
1908
2045
|
}
|
|
@@ -1913,13 +2050,19 @@ function setupAdvancedBehavioralTracking(features) {
|
|
|
1913
2050
|
formTracker = new FormTracker({
|
|
1914
2051
|
onFormEvent: (event) => {
|
|
1915
2052
|
log("Form event:", event.event_type, event.form_id);
|
|
1916
|
-
|
|
2053
|
+
const isSubmitEvent = event.event_type === "form_submit";
|
|
2054
|
+
const isSuccessEvent = event.event_type === "form_success";
|
|
2055
|
+
const normalizedEventType = isSubmitEvent || isSuccessEvent ? "form_submit" : "form_focus";
|
|
2056
|
+
const submitSource = event.submit_source || (isSuccessEvent ? "thank_you" : isSubmitEvent ? "submit" : null);
|
|
2057
|
+
queueEvent(normalizedEventType, {
|
|
1917
2058
|
form_id: event.form_id,
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2059
|
+
form_provider: event.form_type || "unknown",
|
|
2060
|
+
form_field_type: event.field_type || null,
|
|
2061
|
+
form_field_name: event.field_name || null,
|
|
2062
|
+
form_event_type: event.event_type,
|
|
2063
|
+
submit_source: submitSource,
|
|
2064
|
+
is_inferred: isSuccessEvent,
|
|
2065
|
+
time_to_submit_seconds: event.time_to_submit_ms ? Math.round(event.time_to_submit_ms / 1e3) : null
|
|
1923
2066
|
});
|
|
1924
2067
|
}
|
|
1925
2068
|
});
|
|
@@ -1940,7 +2083,7 @@ function setupAdvancedBehavioralTracking(features) {
|
|
|
1940
2083
|
if (link && link.href) {
|
|
1941
2084
|
const isExternal = link.hostname !== window.location.hostname;
|
|
1942
2085
|
queueEvent("click", {
|
|
1943
|
-
|
|
2086
|
+
element_type: "link",
|
|
1944
2087
|
href: truncateText(link.href, 200),
|
|
1945
2088
|
text: truncateText(link.textContent || "", 100),
|
|
1946
2089
|
is_external: isExternal
|
|
@@ -1950,15 +2093,32 @@ function setupAdvancedBehavioralTracking(features) {
|
|
|
1950
2093
|
}
|
|
1951
2094
|
function queueEvent(eventType, data) {
|
|
1952
2095
|
if (!eventQueue) return;
|
|
1953
|
-
|
|
2096
|
+
if (!config.apiKey) {
|
|
2097
|
+
log("Missing apiKey, behavioral event skipped:", eventType);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (!workspaceId) {
|
|
2101
|
+
log("Missing workspaceId, behavioral event skipped:", eventType);
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
if (!sessionId) {
|
|
2105
|
+
log("Missing sessionId, behavioral event skipped:", eventType);
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
const idempotencyKey = buildIdempotencyKey(eventType);
|
|
2109
|
+
const payload = {
|
|
1954
2110
|
visitor_id: visitorId,
|
|
1955
2111
|
session_id: sessionId,
|
|
1956
2112
|
event_type: eventType,
|
|
1957
|
-
|
|
1958
|
-
|
|
2113
|
+
event_data: data,
|
|
2114
|
+
page_url: window.location.href,
|
|
2115
|
+
page_path: window.location.pathname,
|
|
1959
2116
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1960
|
-
tracker_version: VERSION
|
|
1961
|
-
|
|
2117
|
+
tracker_version: VERSION,
|
|
2118
|
+
idempotency_key: idempotencyKey
|
|
2119
|
+
};
|
|
2120
|
+
payload.workspace_id = workspaceId;
|
|
2121
|
+
eventQueue.push(eventType, payload, buildHeaders(idempotencyKey));
|
|
1962
2122
|
}
|
|
1963
2123
|
function handleSPANavigation(event) {
|
|
1964
2124
|
log("SPA navigation:", event.navigation_type, event.to_url);
|
|
@@ -1986,34 +2146,42 @@ function handleSPANavigation(event) {
|
|
|
1986
2146
|
}
|
|
1987
2147
|
function setupUnloadHandlers() {
|
|
1988
2148
|
const handleUnload = () => {
|
|
2149
|
+
if (!workspaceId || !config.apiKey || !sessionId) return;
|
|
1989
2150
|
const scrollEvent = scrollTracker?.getFinalEvent();
|
|
1990
2151
|
if (scrollEvent) {
|
|
1991
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
2152
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
|
|
2153
|
+
workspace_id: workspaceId,
|
|
1992
2154
|
visitor_id: visitorId,
|
|
1993
2155
|
session_id: sessionId,
|
|
1994
2156
|
event_type: "scroll_depth_final",
|
|
1995
|
-
|
|
1996
|
-
|
|
2157
|
+
event_data: {
|
|
2158
|
+
scroll_depth: Math.round(scrollEvent.depth / 100 * 100) / 100,
|
|
2159
|
+
milestone: Math.round(scrollEvent.chunk / 100 * 100) / 100,
|
|
2160
|
+
time_to_reach_ms: scrollEvent.time_to_reach_ms
|
|
2161
|
+
},
|
|
2162
|
+
page_url: window.location.href,
|
|
2163
|
+
page_path: window.location.pathname,
|
|
2164
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2165
|
+
tracker_version: VERSION,
|
|
2166
|
+
idempotency_key: buildIdempotencyKey("scroll_depth_final")
|
|
1997
2167
|
});
|
|
1998
2168
|
}
|
|
1999
2169
|
const timeEvent = timeTracker?.getFinalMetrics();
|
|
2000
2170
|
if (timeEvent) {
|
|
2001
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
2171
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
|
|
2172
|
+
workspace_id: workspaceId,
|
|
2002
2173
|
visitor_id: visitorId,
|
|
2003
2174
|
session_id: sessionId,
|
|
2004
|
-
event_type: "
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
event_type: "agentic_detection",
|
|
2015
|
-
data: agenticResult,
|
|
2016
|
-
url: window.location.href
|
|
2175
|
+
event_type: "time_spent",
|
|
2176
|
+
event_data: {
|
|
2177
|
+
visible_time_ms: timeEvent.total_time_ms,
|
|
2178
|
+
page_start_time: pageStartTime || Date.now()
|
|
2179
|
+
},
|
|
2180
|
+
page_url: window.location.href,
|
|
2181
|
+
page_path: window.location.pathname,
|
|
2182
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2183
|
+
tracker_version: VERSION,
|
|
2184
|
+
idempotency_key: buildIdempotencyKey("time_spent")
|
|
2017
2185
|
});
|
|
2018
2186
|
}
|
|
2019
2187
|
eventQueue?.flushBeacon();
|
|
@@ -2036,30 +2204,55 @@ function pageview(customUrl) {
|
|
|
2036
2204
|
log("Not initialized, call init() first");
|
|
2037
2205
|
return;
|
|
2038
2206
|
}
|
|
2207
|
+
if (!config.apiKey) {
|
|
2208
|
+
log("Missing apiKey, pageview skipped");
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2039
2211
|
const url = customUrl || window.location.href;
|
|
2212
|
+
const utmParams = extractUTMParams(url);
|
|
2213
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2214
|
+
const idempotencyKey = buildIdempotencyKey("visit");
|
|
2215
|
+
const agenticResult = agenticAnalyzer?.getResult();
|
|
2216
|
+
const pagePath = (() => {
|
|
2217
|
+
try {
|
|
2218
|
+
return new URL(url).pathname;
|
|
2219
|
+
} catch {
|
|
2220
|
+
return window.location.pathname;
|
|
2221
|
+
}
|
|
2222
|
+
})();
|
|
2040
2223
|
const payload = {
|
|
2041
2224
|
visitor_id: visitorId,
|
|
2042
2225
|
session_id: sessionId,
|
|
2043
|
-
url,
|
|
2226
|
+
page_url: url,
|
|
2227
|
+
page_path: pagePath,
|
|
2044
2228
|
referrer: document.referrer || null,
|
|
2045
2229
|
title: document.title || null,
|
|
2046
|
-
utm_source:
|
|
2047
|
-
utm_medium:
|
|
2048
|
-
utm_campaign:
|
|
2230
|
+
utm_source: utmParams.utm_source || null,
|
|
2231
|
+
utm_medium: utmParams.utm_medium || null,
|
|
2232
|
+
utm_campaign: utmParams.utm_campaign || null,
|
|
2233
|
+
utm_term: utmParams.utm_term || null,
|
|
2234
|
+
utm_content: utmParams.utm_content || null,
|
|
2049
2235
|
user_agent: navigator.userAgent,
|
|
2050
2236
|
screen_width: window.screen?.width,
|
|
2051
2237
|
screen_height: window.screen?.height,
|
|
2052
2238
|
language: navigator.language,
|
|
2053
2239
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
2054
2240
|
tracker_version: VERSION,
|
|
2241
|
+
event_type: "pageview",
|
|
2242
|
+
event_data: null,
|
|
2243
|
+
timestamp,
|
|
2055
2244
|
navigation_timing: navigationTiming,
|
|
2056
2245
|
ai_platform: aiDetection?.platform || null,
|
|
2057
|
-
is_ai_referrer: aiDetection?.isAI || false
|
|
2246
|
+
is_ai_referrer: aiDetection?.isAI || false,
|
|
2247
|
+
agentic_detection: agenticResult || null
|
|
2058
2248
|
};
|
|
2249
|
+
if (workspaceId) {
|
|
2250
|
+
payload.workspace_id = workspaceId;
|
|
2251
|
+
}
|
|
2059
2252
|
log("Pageview:", payload);
|
|
2060
2253
|
safeFetch(endpoint(DEFAULT_CONFIG.endpoints.visit), {
|
|
2061
2254
|
method: "POST",
|
|
2062
|
-
headers:
|
|
2255
|
+
headers: buildHeaders(idempotencyKey),
|
|
2063
2256
|
body: JSON.stringify(payload)
|
|
2064
2257
|
});
|
|
2065
2258
|
}
|
|
@@ -2068,6 +2261,11 @@ function track(eventName, options = {}) {
|
|
|
2068
2261
|
log("Not initialized, call init() first");
|
|
2069
2262
|
return;
|
|
2070
2263
|
}
|
|
2264
|
+
if (!config.apiKey) {
|
|
2265
|
+
log("Missing apiKey, event skipped:", eventName);
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
const idempotencyKey = buildIdempotencyKey(`event:${eventName}`);
|
|
2071
2269
|
const payload = {
|
|
2072
2270
|
visitor_id: visitorId,
|
|
2073
2271
|
session_id: sessionId,
|
|
@@ -2076,14 +2274,19 @@ function track(eventName, options = {}) {
|
|
|
2076
2274
|
properties: options.properties || {},
|
|
2077
2275
|
revenue: options.revenue,
|
|
2078
2276
|
currency: options.currency || "USD",
|
|
2079
|
-
|
|
2277
|
+
page_url: window.location.href,
|
|
2278
|
+
referrer: document.referrer || null,
|
|
2080
2279
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2081
|
-
tracker_version: VERSION
|
|
2280
|
+
tracker_version: VERSION,
|
|
2281
|
+
idempotency_key: idempotencyKey
|
|
2082
2282
|
};
|
|
2283
|
+
if (workspaceId) {
|
|
2284
|
+
payload.workspace_id = workspaceId;
|
|
2285
|
+
}
|
|
2083
2286
|
log("Event:", eventName, payload);
|
|
2084
2287
|
safeFetch(endpoint("/api/ingest/event"), {
|
|
2085
2288
|
method: "POST",
|
|
2086
|
-
headers:
|
|
2289
|
+
headers: buildHeaders(idempotencyKey),
|
|
2087
2290
|
body: JSON.stringify(payload)
|
|
2088
2291
|
});
|
|
2089
2292
|
}
|
|
@@ -2095,17 +2298,26 @@ function identify(userId, traits = {}) {
|
|
|
2095
2298
|
log("Not initialized, call init() first");
|
|
2096
2299
|
return;
|
|
2097
2300
|
}
|
|
2301
|
+
if (!config.apiKey) {
|
|
2302
|
+
log("Missing apiKey, identify skipped");
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2098
2305
|
log("Identify:", userId, traits);
|
|
2306
|
+
const idempotencyKey = buildIdempotencyKey("identify");
|
|
2099
2307
|
const payload = {
|
|
2100
2308
|
visitor_id: visitorId,
|
|
2101
2309
|
session_id: sessionId,
|
|
2102
2310
|
user_id: userId,
|
|
2103
2311
|
traits,
|
|
2104
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2312
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2313
|
+
idempotency_key: idempotencyKey
|
|
2105
2314
|
};
|
|
2315
|
+
if (workspaceId) {
|
|
2316
|
+
payload.workspace_id = workspaceId;
|
|
2317
|
+
}
|
|
2106
2318
|
safeFetch(endpoint("/api/ingest/identify"), {
|
|
2107
2319
|
method: "POST",
|
|
2108
|
-
headers:
|
|
2320
|
+
headers: buildHeaders(idempotencyKey),
|
|
2109
2321
|
body: JSON.stringify(payload)
|
|
2110
2322
|
});
|
|
2111
2323
|
}
|
|
@@ -2236,14 +2448,15 @@ function isTrackerInitialized() {
|
|
|
2236
2448
|
return initialized;
|
|
2237
2449
|
}
|
|
2238
2450
|
function reportHealth(status, errorMessage) {
|
|
2239
|
-
if (!config.apiKey) return;
|
|
2240
2451
|
try {
|
|
2241
2452
|
const healthData = {
|
|
2242
|
-
workspace_id:
|
|
2453
|
+
workspace_id: workspaceId,
|
|
2454
|
+
visitor_id: visitorId,
|
|
2455
|
+
session_id: sessionId,
|
|
2243
2456
|
status,
|
|
2244
2457
|
error_message: errorMessage || null,
|
|
2245
|
-
|
|
2246
|
-
|
|
2458
|
+
tracker_version: VERSION,
|
|
2459
|
+
page_url: typeof window !== "undefined" ? window.location.href : null,
|
|
2247
2460
|
user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
|
2248
2461
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2249
2462
|
features: {
|
|
@@ -2361,7 +2574,7 @@ var loamly = {
|
|
|
2361
2574
|
* See what AI tells your customers — and track when they click.
|
|
2362
2575
|
*
|
|
2363
2576
|
* @module @loamly/tracker
|
|
2364
|
-
* @version 1.
|
|
2577
|
+
* @version 2.1.0
|
|
2365
2578
|
* @license MIT
|
|
2366
2579
|
* @see https://github.com/loamly/loamly
|
|
2367
2580
|
* @see https://loamly.ai
|