@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
|
@@ -26,7 +26,7 @@ var Loamly = (() => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
// src/config.ts
|
|
29
|
-
var VERSION = "2.1.
|
|
29
|
+
var VERSION = "2.1.1";
|
|
30
30
|
var DEFAULT_CONFIG = {
|
|
31
31
|
apiHost: "https://app.loamly.ai",
|
|
32
32
|
endpoints: {
|
|
@@ -862,11 +862,12 @@ var Loamly = (() => {
|
|
|
862
862
|
/**
|
|
863
863
|
* Add event to queue
|
|
864
864
|
*/
|
|
865
|
-
push(type, payload) {
|
|
865
|
+
push(type, payload, headers) {
|
|
866
866
|
const event = {
|
|
867
867
|
id: this.generateId(),
|
|
868
868
|
type,
|
|
869
869
|
payload,
|
|
870
|
+
headers,
|
|
870
871
|
timestamp: Date.now(),
|
|
871
872
|
retries: 0
|
|
872
873
|
};
|
|
@@ -895,21 +896,25 @@ var Loamly = (() => {
|
|
|
895
896
|
*/
|
|
896
897
|
flushBeacon() {
|
|
897
898
|
if (this.queue.length === 0) return true;
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
JSON.stringify(
|
|
907
|
-
|
|
908
|
-
|
|
899
|
+
const baseUrl = this.config.apiKey ? `${this.endpoint}?api_key=${encodeURIComponent(this.config.apiKey)}` : this.endpoint;
|
|
900
|
+
let allSent = true;
|
|
901
|
+
for (const event of this.queue) {
|
|
902
|
+
const payload = {
|
|
903
|
+
...event.payload,
|
|
904
|
+
_queue_id: event.id,
|
|
905
|
+
_queue_timestamp: event.timestamp
|
|
906
|
+
};
|
|
907
|
+
const success = navigator.sendBeacon?.(baseUrl, JSON.stringify(payload)) ?? false;
|
|
908
|
+
if (!success) {
|
|
909
|
+
allSent = false;
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (allSent) {
|
|
909
914
|
this.queue = [];
|
|
910
915
|
this.clearStorage();
|
|
911
916
|
}
|
|
912
|
-
return
|
|
917
|
+
return allSent;
|
|
913
918
|
}
|
|
914
919
|
/**
|
|
915
920
|
* Get current queue length
|
|
@@ -936,24 +941,38 @@ var Loamly = (() => {
|
|
|
936
941
|
}
|
|
937
942
|
async sendBatch(events) {
|
|
938
943
|
if (events.length === 0) return;
|
|
939
|
-
const payload = {
|
|
940
|
-
events: events.map((e) => ({
|
|
941
|
-
type: e.type,
|
|
942
|
-
...e.payload,
|
|
943
|
-
_queue_id: e.id,
|
|
944
|
-
_queue_timestamp: e.timestamp
|
|
945
|
-
})),
|
|
946
|
-
batch: true
|
|
947
|
-
};
|
|
948
944
|
try {
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
945
|
+
const results = await Promise.allSettled(
|
|
946
|
+
events.map(async (event) => {
|
|
947
|
+
const response = await fetch(this.endpoint, {
|
|
948
|
+
method: "POST",
|
|
949
|
+
headers: {
|
|
950
|
+
"Content-Type": "application/json",
|
|
951
|
+
...event.headers || {}
|
|
952
|
+
},
|
|
953
|
+
body: JSON.stringify({
|
|
954
|
+
...event.payload,
|
|
955
|
+
_queue_id: event.id,
|
|
956
|
+
_queue_timestamp: event.timestamp
|
|
957
|
+
})
|
|
958
|
+
});
|
|
959
|
+
if (!response.ok) {
|
|
960
|
+
throw new Error(`HTTP ${response.status}`);
|
|
961
|
+
}
|
|
962
|
+
})
|
|
963
|
+
);
|
|
964
|
+
const failedEvents = events.filter((_, index) => results[index]?.status === "rejected");
|
|
965
|
+
if (failedEvents.length > 0) {
|
|
966
|
+
for (const event of failedEvents) {
|
|
967
|
+
if (event.retries < this.config.maxRetries) {
|
|
968
|
+
event.retries++;
|
|
969
|
+
this.queue.push(event);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const delay = this.config.retryDelayMs * Math.pow(2, failedEvents[0].retries - 1);
|
|
973
|
+
setTimeout(() => this.flush(), delay);
|
|
956
974
|
}
|
|
975
|
+
return;
|
|
957
976
|
} catch (error) {
|
|
958
977
|
for (const event of events) {
|
|
959
978
|
if (event.retries < this.config.maxRetries) {
|
|
@@ -1006,6 +1025,7 @@ var Loamly = (() => {
|
|
|
1006
1025
|
constructor(sessionId2, visitorId2, version, config2 = {}) {
|
|
1007
1026
|
this.intervalId = null;
|
|
1008
1027
|
this.isVisible = true;
|
|
1028
|
+
this.isFocused = true;
|
|
1009
1029
|
this.currentScrollDepth = 0;
|
|
1010
1030
|
this.ping = async () => {
|
|
1011
1031
|
const data = this.getData();
|
|
@@ -1024,6 +1044,12 @@ var Loamly = (() => {
|
|
|
1024
1044
|
this.handleVisibilityChange = () => {
|
|
1025
1045
|
this.isVisible = document.visibilityState === "visible";
|
|
1026
1046
|
};
|
|
1047
|
+
this.handleFocusChange = () => {
|
|
1048
|
+
this.isFocused = typeof document.hasFocus === "function" ? document.hasFocus() : true;
|
|
1049
|
+
if (this.intervalId && this.isVisible && this.isFocused) {
|
|
1050
|
+
this.ping();
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1027
1053
|
this.handleScroll = () => {
|
|
1028
1054
|
const scrollPercent = Math.round(
|
|
1029
1055
|
(window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100
|
|
@@ -1036,12 +1062,16 @@ var Loamly = (() => {
|
|
|
1036
1062
|
this.visitorId = visitorId2;
|
|
1037
1063
|
this.version = version;
|
|
1038
1064
|
this.pageLoadTime = Date.now();
|
|
1065
|
+
this.isVisible = document.visibilityState === "visible";
|
|
1066
|
+
this.isFocused = typeof document.hasFocus === "function" ? document.hasFocus() : true;
|
|
1039
1067
|
this.config = {
|
|
1040
1068
|
interval: DEFAULT_CONFIG.pingInterval,
|
|
1041
1069
|
endpoint: "",
|
|
1042
1070
|
...config2
|
|
1043
1071
|
};
|
|
1044
1072
|
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1073
|
+
window.addEventListener("focus", this.handleFocusChange);
|
|
1074
|
+
window.addEventListener("blur", this.handleFocusChange);
|
|
1045
1075
|
window.addEventListener("scroll", this.handleScroll, { passive: true });
|
|
1046
1076
|
}
|
|
1047
1077
|
/**
|
|
@@ -1050,11 +1080,13 @@ var Loamly = (() => {
|
|
|
1050
1080
|
start() {
|
|
1051
1081
|
if (this.intervalId) return;
|
|
1052
1082
|
this.intervalId = setInterval(() => {
|
|
1053
|
-
if (this.isVisible) {
|
|
1083
|
+
if (this.isVisible && this.isFocused) {
|
|
1054
1084
|
this.ping();
|
|
1055
1085
|
}
|
|
1056
1086
|
}, this.config.interval);
|
|
1057
|
-
this.
|
|
1087
|
+
if (this.isVisible && this.isFocused) {
|
|
1088
|
+
this.ping();
|
|
1089
|
+
}
|
|
1058
1090
|
}
|
|
1059
1091
|
/**
|
|
1060
1092
|
* Stop the ping service
|
|
@@ -1065,6 +1097,8 @@ var Loamly = (() => {
|
|
|
1065
1097
|
this.intervalId = null;
|
|
1066
1098
|
}
|
|
1067
1099
|
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1100
|
+
window.removeEventListener("focus", this.handleFocusChange);
|
|
1101
|
+
window.removeEventListener("blur", this.handleFocusChange);
|
|
1068
1102
|
window.removeEventListener("scroll", this.handleScroll);
|
|
1069
1103
|
}
|
|
1070
1104
|
/**
|
|
@@ -1085,7 +1119,7 @@ var Loamly = (() => {
|
|
|
1085
1119
|
url: window.location.href,
|
|
1086
1120
|
time_on_page_ms: Date.now() - this.pageLoadTime,
|
|
1087
1121
|
scroll_depth: this.currentScrollDepth,
|
|
1088
|
-
is_active: this.isVisible,
|
|
1122
|
+
is_active: this.isVisible && this.isFocused,
|
|
1089
1123
|
tracker_version: this.version
|
|
1090
1124
|
};
|
|
1091
1125
|
}
|
|
@@ -1392,7 +1426,8 @@ var Loamly = (() => {
|
|
|
1392
1426
|
form_id: formId,
|
|
1393
1427
|
form_type: this.detectFormType(form),
|
|
1394
1428
|
time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
|
|
1395
|
-
is_conversion: true
|
|
1429
|
+
is_conversion: true,
|
|
1430
|
+
submit_source: "submit"
|
|
1396
1431
|
});
|
|
1397
1432
|
};
|
|
1398
1433
|
this.handleClick = (e) => {
|
|
@@ -1407,7 +1442,8 @@ var Loamly = (() => {
|
|
|
1407
1442
|
form_id: formId,
|
|
1408
1443
|
form_type: "hubspot",
|
|
1409
1444
|
time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
|
|
1410
|
-
is_conversion: true
|
|
1445
|
+
is_conversion: true,
|
|
1446
|
+
submit_source: "click"
|
|
1411
1447
|
});
|
|
1412
1448
|
}
|
|
1413
1449
|
}
|
|
@@ -1416,7 +1452,8 @@ var Loamly = (() => {
|
|
|
1416
1452
|
event_type: "form_submit",
|
|
1417
1453
|
form_id: "typeform_embed",
|
|
1418
1454
|
form_type: "typeform",
|
|
1419
|
-
is_conversion: true
|
|
1455
|
+
is_conversion: true,
|
|
1456
|
+
submit_source: "click"
|
|
1420
1457
|
});
|
|
1421
1458
|
}
|
|
1422
1459
|
};
|
|
@@ -1518,7 +1555,8 @@ var Loamly = (() => {
|
|
|
1518
1555
|
event_type: "form_success",
|
|
1519
1556
|
form_id: "page_conversion",
|
|
1520
1557
|
form_type: "unknown",
|
|
1521
|
-
is_conversion: true
|
|
1558
|
+
is_conversion: true,
|
|
1559
|
+
submit_source: "thank_you"
|
|
1522
1560
|
});
|
|
1523
1561
|
break;
|
|
1524
1562
|
}
|
|
@@ -1742,8 +1780,10 @@ var Loamly = (() => {
|
|
|
1742
1780
|
var debugMode = false;
|
|
1743
1781
|
var visitorId = null;
|
|
1744
1782
|
var sessionId = null;
|
|
1783
|
+
var workspaceId = null;
|
|
1745
1784
|
var navigationTiming = null;
|
|
1746
1785
|
var aiDetection = null;
|
|
1786
|
+
var pageStartTime = null;
|
|
1747
1787
|
var behavioralClassifier = null;
|
|
1748
1788
|
var behavioralMLResult = null;
|
|
1749
1789
|
var focusBlurAnalyzer = null;
|
|
@@ -1763,6 +1803,28 @@ var Loamly = (() => {
|
|
|
1763
1803
|
function endpoint(path) {
|
|
1764
1804
|
return `${config.apiHost}${path}`;
|
|
1765
1805
|
}
|
|
1806
|
+
function buildHeaders(idempotencyKey) {
|
|
1807
|
+
const headers = {
|
|
1808
|
+
"Content-Type": "application/json"
|
|
1809
|
+
};
|
|
1810
|
+
if (config.apiKey) {
|
|
1811
|
+
headers["X-Loamly-Api-Key"] = config.apiKey;
|
|
1812
|
+
}
|
|
1813
|
+
if (idempotencyKey) {
|
|
1814
|
+
headers["X-Idempotency-Key"] = idempotencyKey;
|
|
1815
|
+
}
|
|
1816
|
+
return headers;
|
|
1817
|
+
}
|
|
1818
|
+
function buildBeaconUrl(path) {
|
|
1819
|
+
if (!config.apiKey) return path;
|
|
1820
|
+
const url = new URL(path, config.apiHost);
|
|
1821
|
+
url.searchParams.set("api_key", config.apiKey);
|
|
1822
|
+
return url.toString();
|
|
1823
|
+
}
|
|
1824
|
+
function buildIdempotencyKey(prefix) {
|
|
1825
|
+
const base = sessionId || visitorId || "unknown";
|
|
1826
|
+
return `${prefix}:${base}:${Date.now()}`;
|
|
1827
|
+
}
|
|
1766
1828
|
function init(userConfig = {}) {
|
|
1767
1829
|
if (initialized) {
|
|
1768
1830
|
log("Already initialized");
|
|
@@ -1773,7 +1835,11 @@ var Loamly = (() => {
|
|
|
1773
1835
|
...userConfig,
|
|
1774
1836
|
apiHost: userConfig.apiHost || DEFAULT_CONFIG.apiHost
|
|
1775
1837
|
};
|
|
1838
|
+
workspaceId = userConfig.workspaceId ?? null;
|
|
1776
1839
|
debugMode = userConfig.debug ?? false;
|
|
1840
|
+
if (config.apiKey && !workspaceId) {
|
|
1841
|
+
log("Workspace ID missing. Behavioral events require workspaceId.");
|
|
1842
|
+
}
|
|
1777
1843
|
const features = {
|
|
1778
1844
|
scroll: true,
|
|
1779
1845
|
time: true,
|
|
@@ -1791,13 +1857,11 @@ var Loamly = (() => {
|
|
|
1791
1857
|
log("Features:", features);
|
|
1792
1858
|
visitorId = getVisitorId();
|
|
1793
1859
|
log("Visitor ID:", visitorId);
|
|
1794
|
-
const session = getSessionId();
|
|
1795
|
-
sessionId = session.sessionId;
|
|
1796
|
-
log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
|
|
1797
1860
|
if (features.eventQueue) {
|
|
1798
1861
|
eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
1799
1862
|
batchSize: DEFAULT_CONFIG.batchSize,
|
|
1800
|
-
batchTimeout: DEFAULT_CONFIG.batchTimeout
|
|
1863
|
+
batchTimeout: DEFAULT_CONFIG.batchTimeout,
|
|
1864
|
+
apiKey: config.apiKey
|
|
1801
1865
|
});
|
|
1802
1866
|
}
|
|
1803
1867
|
navigationTiming = detectNavigationType();
|
|
@@ -1807,37 +1871,40 @@ var Loamly = (() => {
|
|
|
1807
1871
|
log("AI detected:", aiDetection);
|
|
1808
1872
|
}
|
|
1809
1873
|
initialized = true;
|
|
1810
|
-
if (!userConfig.disableAutoPageview) {
|
|
1811
|
-
pageview();
|
|
1812
|
-
}
|
|
1813
|
-
if (!userConfig.disableBehavioral) {
|
|
1814
|
-
setupAdvancedBehavioralTracking(features);
|
|
1815
|
-
}
|
|
1816
|
-
if (features.behavioralML) {
|
|
1817
|
-
behavioralClassifier = new BehavioralClassifier(1e4);
|
|
1818
|
-
behavioralClassifier.setOnClassify(handleBehavioralClassification);
|
|
1819
|
-
setupBehavioralMLTracking();
|
|
1820
|
-
}
|
|
1821
|
-
if (features.focusBlur) {
|
|
1822
|
-
focusBlurAnalyzer = new FocusBlurAnalyzer();
|
|
1823
|
-
focusBlurAnalyzer.initTracking();
|
|
1824
|
-
setTimeout(() => {
|
|
1825
|
-
if (focusBlurAnalyzer) {
|
|
1826
|
-
handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
|
|
1827
|
-
}
|
|
1828
|
-
}, 5e3);
|
|
1829
|
-
}
|
|
1830
1874
|
if (features.agentic) {
|
|
1831
1875
|
agenticAnalyzer = new AgenticBrowserAnalyzer();
|
|
1832
1876
|
agenticAnalyzer.init();
|
|
1833
1877
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1878
|
+
void initializeSession().finally(() => {
|
|
1879
|
+
void registerServiceWorker();
|
|
1880
|
+
if (!userConfig.disableAutoPageview) {
|
|
1881
|
+
pageview();
|
|
1882
|
+
}
|
|
1883
|
+
if (!userConfig.disableBehavioral) {
|
|
1884
|
+
setupAdvancedBehavioralTracking(features);
|
|
1885
|
+
}
|
|
1886
|
+
if (features.behavioralML) {
|
|
1887
|
+
behavioralClassifier = new BehavioralClassifier(1e4);
|
|
1888
|
+
behavioralClassifier.setOnClassify(handleBehavioralClassification);
|
|
1889
|
+
setupBehavioralMLTracking();
|
|
1890
|
+
}
|
|
1891
|
+
if (features.focusBlur) {
|
|
1892
|
+
focusBlurAnalyzer = new FocusBlurAnalyzer();
|
|
1893
|
+
focusBlurAnalyzer.initTracking();
|
|
1894
|
+
setTimeout(() => {
|
|
1895
|
+
if (focusBlurAnalyzer) {
|
|
1896
|
+
handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
|
|
1897
|
+
}
|
|
1898
|
+
}, 5e3);
|
|
1899
|
+
}
|
|
1900
|
+
if (features.ping && visitorId && sessionId) {
|
|
1901
|
+
pingService = new PingService(sessionId, visitorId, VERSION, {
|
|
1902
|
+
interval: DEFAULT_CONFIG.pingInterval,
|
|
1903
|
+
endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping)
|
|
1904
|
+
});
|
|
1905
|
+
pingService.start();
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1841
1908
|
spaRouter = new SPARouter({
|
|
1842
1909
|
onNavigate: handleSPANavigation
|
|
1843
1910
|
});
|
|
@@ -1846,6 +1913,78 @@ var Loamly = (() => {
|
|
|
1846
1913
|
reportHealth("initialized");
|
|
1847
1914
|
log("Initialization complete");
|
|
1848
1915
|
}
|
|
1916
|
+
async function registerServiceWorker() {
|
|
1917
|
+
if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return;
|
|
1918
|
+
if (!config.apiKey || !workspaceId) return;
|
|
1919
|
+
try {
|
|
1920
|
+
const swUrl = new URL("/tracker/loamly-sw.js", window.location.origin);
|
|
1921
|
+
swUrl.searchParams.set("workspace_id", workspaceId);
|
|
1922
|
+
swUrl.searchParams.set("api_key", config.apiKey);
|
|
1923
|
+
const registration = await navigator.serviceWorker.register(swUrl.toString(), { scope: "/" });
|
|
1924
|
+
registration.addEventListener("updatefound", () => {
|
|
1925
|
+
const installing = registration.installing;
|
|
1926
|
+
installing?.addEventListener("statechange", () => {
|
|
1927
|
+
if (installing.state === "activated") {
|
|
1928
|
+
installing.postMessage({ type: "SKIP_WAITING" });
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
});
|
|
1932
|
+
setInterval(() => {
|
|
1933
|
+
registration.update().catch(() => {
|
|
1934
|
+
});
|
|
1935
|
+
}, 24 * 60 * 60 * 1e3);
|
|
1936
|
+
} catch {
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
async function initializeSession() {
|
|
1940
|
+
const now = Date.now();
|
|
1941
|
+
pageStartTime = now;
|
|
1942
|
+
try {
|
|
1943
|
+
const storedSession = sessionStorage.getItem("loamly_session");
|
|
1944
|
+
const storedStart = sessionStorage.getItem("loamly_start");
|
|
1945
|
+
const sessionTimeout = config.sessionTimeout ?? DEFAULT_CONFIG.sessionTimeout;
|
|
1946
|
+
if (storedSession && storedStart) {
|
|
1947
|
+
const startTime = parseInt(storedStart, 10);
|
|
1948
|
+
const elapsed = now - startTime;
|
|
1949
|
+
if (elapsed > 0 && elapsed < sessionTimeout) {
|
|
1950
|
+
sessionId = storedSession;
|
|
1951
|
+
log("Session ID:", sessionId, "(existing)");
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
} catch {
|
|
1956
|
+
}
|
|
1957
|
+
if (config.apiKey && workspaceId && visitorId) {
|
|
1958
|
+
try {
|
|
1959
|
+
const response = await safeFetch(endpoint(DEFAULT_CONFIG.endpoints.session), {
|
|
1960
|
+
method: "POST",
|
|
1961
|
+
headers: buildHeaders(),
|
|
1962
|
+
body: JSON.stringify({
|
|
1963
|
+
workspace_id: workspaceId,
|
|
1964
|
+
visitor_id: visitorId
|
|
1965
|
+
})
|
|
1966
|
+
});
|
|
1967
|
+
if (response?.ok) {
|
|
1968
|
+
const data = await response.json();
|
|
1969
|
+
sessionId = data.session_id || sessionId;
|
|
1970
|
+
const startTime = data.start_time || now;
|
|
1971
|
+
if (sessionId) {
|
|
1972
|
+
try {
|
|
1973
|
+
sessionStorage.setItem("loamly_session", sessionId);
|
|
1974
|
+
sessionStorage.setItem("loamly_start", String(startTime));
|
|
1975
|
+
} catch {
|
|
1976
|
+
}
|
|
1977
|
+
log("Session ID:", sessionId, "(server)");
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
} catch {
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
const session = getSessionId();
|
|
1985
|
+
sessionId = session.sessionId;
|
|
1986
|
+
log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
|
|
1987
|
+
}
|
|
1849
1988
|
function setupAdvancedBehavioralTracking(features) {
|
|
1850
1989
|
if (features.scroll) {
|
|
1851
1990
|
scrollTracker = new ScrollTracker({
|
|
@@ -1853,8 +1992,8 @@ var Loamly = (() => {
|
|
|
1853
1992
|
onChunkReached: (event) => {
|
|
1854
1993
|
log("Scroll chunk:", event.chunk);
|
|
1855
1994
|
queueEvent("scroll_depth", {
|
|
1856
|
-
|
|
1857
|
-
|
|
1995
|
+
scroll_depth: Math.round(event.depth / 100 * 100) / 100,
|
|
1996
|
+
milestone: Math.round(event.chunk / 100 * 100) / 100,
|
|
1858
1997
|
time_to_reach_ms: event.time_to_reach_ms
|
|
1859
1998
|
});
|
|
1860
1999
|
}
|
|
@@ -1868,10 +2007,8 @@ var Loamly = (() => {
|
|
|
1868
2007
|
onUpdate: (event) => {
|
|
1869
2008
|
if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
|
|
1870
2009
|
queueEvent("time_spent", {
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
idle_time_ms: event.idle_time_ms,
|
|
1874
|
-
is_engaged: event.is_engaged
|
|
2010
|
+
visible_time_ms: event.total_time_ms,
|
|
2011
|
+
page_start_time: pageStartTime || Date.now()
|
|
1875
2012
|
});
|
|
1876
2013
|
}
|
|
1877
2014
|
}
|
|
@@ -1882,13 +2019,19 @@ var Loamly = (() => {
|
|
|
1882
2019
|
formTracker = new FormTracker({
|
|
1883
2020
|
onFormEvent: (event) => {
|
|
1884
2021
|
log("Form event:", event.event_type, event.form_id);
|
|
1885
|
-
|
|
2022
|
+
const isSubmitEvent = event.event_type === "form_submit";
|
|
2023
|
+
const isSuccessEvent = event.event_type === "form_success";
|
|
2024
|
+
const normalizedEventType = isSubmitEvent || isSuccessEvent ? "form_submit" : "form_focus";
|
|
2025
|
+
const submitSource = event.submit_source || (isSuccessEvent ? "thank_you" : isSubmitEvent ? "submit" : null);
|
|
2026
|
+
queueEvent(normalizedEventType, {
|
|
1886
2027
|
form_id: event.form_id,
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2028
|
+
form_provider: event.form_type || "unknown",
|
|
2029
|
+
form_field_type: event.field_type || null,
|
|
2030
|
+
form_field_name: event.field_name || null,
|
|
2031
|
+
form_event_type: event.event_type,
|
|
2032
|
+
submit_source: submitSource,
|
|
2033
|
+
is_inferred: isSuccessEvent,
|
|
2034
|
+
time_to_submit_seconds: event.time_to_submit_ms ? Math.round(event.time_to_submit_ms / 1e3) : null
|
|
1892
2035
|
});
|
|
1893
2036
|
}
|
|
1894
2037
|
});
|
|
@@ -1909,7 +2052,7 @@ var Loamly = (() => {
|
|
|
1909
2052
|
if (link && link.href) {
|
|
1910
2053
|
const isExternal = link.hostname !== window.location.hostname;
|
|
1911
2054
|
queueEvent("click", {
|
|
1912
|
-
|
|
2055
|
+
element_type: "link",
|
|
1913
2056
|
href: truncateText(link.href, 200),
|
|
1914
2057
|
text: truncateText(link.textContent || "", 100),
|
|
1915
2058
|
is_external: isExternal
|
|
@@ -1919,15 +2062,32 @@ var Loamly = (() => {
|
|
|
1919
2062
|
}
|
|
1920
2063
|
function queueEvent(eventType, data) {
|
|
1921
2064
|
if (!eventQueue) return;
|
|
1922
|
-
|
|
2065
|
+
if (!config.apiKey) {
|
|
2066
|
+
log("Missing apiKey, behavioral event skipped:", eventType);
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
if (!workspaceId) {
|
|
2070
|
+
log("Missing workspaceId, behavioral event skipped:", eventType);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
if (!sessionId) {
|
|
2074
|
+
log("Missing sessionId, behavioral event skipped:", eventType);
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
const idempotencyKey = buildIdempotencyKey(eventType);
|
|
2078
|
+
const payload = {
|
|
1923
2079
|
visitor_id: visitorId,
|
|
1924
2080
|
session_id: sessionId,
|
|
1925
2081
|
event_type: eventType,
|
|
1926
|
-
|
|
1927
|
-
|
|
2082
|
+
event_data: data,
|
|
2083
|
+
page_url: window.location.href,
|
|
2084
|
+
page_path: window.location.pathname,
|
|
1928
2085
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1929
|
-
tracker_version: VERSION
|
|
1930
|
-
|
|
2086
|
+
tracker_version: VERSION,
|
|
2087
|
+
idempotency_key: idempotencyKey
|
|
2088
|
+
};
|
|
2089
|
+
payload.workspace_id = workspaceId;
|
|
2090
|
+
eventQueue.push(eventType, payload, buildHeaders(idempotencyKey));
|
|
1931
2091
|
}
|
|
1932
2092
|
function handleSPANavigation(event) {
|
|
1933
2093
|
log("SPA navigation:", event.navigation_type, event.to_url);
|
|
@@ -1955,34 +2115,42 @@ var Loamly = (() => {
|
|
|
1955
2115
|
}
|
|
1956
2116
|
function setupUnloadHandlers() {
|
|
1957
2117
|
const handleUnload = () => {
|
|
2118
|
+
if (!workspaceId || !config.apiKey || !sessionId) return;
|
|
1958
2119
|
const scrollEvent = scrollTracker?.getFinalEvent();
|
|
1959
2120
|
if (scrollEvent) {
|
|
1960
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
2121
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
|
|
2122
|
+
workspace_id: workspaceId,
|
|
1961
2123
|
visitor_id: visitorId,
|
|
1962
2124
|
session_id: sessionId,
|
|
1963
2125
|
event_type: "scroll_depth_final",
|
|
1964
|
-
|
|
1965
|
-
|
|
2126
|
+
event_data: {
|
|
2127
|
+
scroll_depth: Math.round(scrollEvent.depth / 100 * 100) / 100,
|
|
2128
|
+
milestone: Math.round(scrollEvent.chunk / 100 * 100) / 100,
|
|
2129
|
+
time_to_reach_ms: scrollEvent.time_to_reach_ms
|
|
2130
|
+
},
|
|
2131
|
+
page_url: window.location.href,
|
|
2132
|
+
page_path: window.location.pathname,
|
|
2133
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2134
|
+
tracker_version: VERSION,
|
|
2135
|
+
idempotency_key: buildIdempotencyKey("scroll_depth_final")
|
|
1966
2136
|
});
|
|
1967
2137
|
}
|
|
1968
2138
|
const timeEvent = timeTracker?.getFinalMetrics();
|
|
1969
2139
|
if (timeEvent) {
|
|
1970
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
1971
|
-
|
|
1972
|
-
session_id: sessionId,
|
|
1973
|
-
event_type: "time_spent_final",
|
|
1974
|
-
data: timeEvent,
|
|
1975
|
-
url: window.location.href
|
|
1976
|
-
});
|
|
1977
|
-
}
|
|
1978
|
-
const agenticResult = agenticAnalyzer?.getResult();
|
|
1979
|
-
if (agenticResult && agenticResult.agenticProbability > 0) {
|
|
1980
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
2140
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
|
|
2141
|
+
workspace_id: workspaceId,
|
|
1981
2142
|
visitor_id: visitorId,
|
|
1982
2143
|
session_id: sessionId,
|
|
1983
|
-
event_type: "
|
|
1984
|
-
|
|
1985
|
-
|
|
2144
|
+
event_type: "time_spent",
|
|
2145
|
+
event_data: {
|
|
2146
|
+
visible_time_ms: timeEvent.total_time_ms,
|
|
2147
|
+
page_start_time: pageStartTime || Date.now()
|
|
2148
|
+
},
|
|
2149
|
+
page_url: window.location.href,
|
|
2150
|
+
page_path: window.location.pathname,
|
|
2151
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2152
|
+
tracker_version: VERSION,
|
|
2153
|
+
idempotency_key: buildIdempotencyKey("time_spent")
|
|
1986
2154
|
});
|
|
1987
2155
|
}
|
|
1988
2156
|
eventQueue?.flushBeacon();
|
|
@@ -2005,30 +2173,55 @@ var Loamly = (() => {
|
|
|
2005
2173
|
log("Not initialized, call init() first");
|
|
2006
2174
|
return;
|
|
2007
2175
|
}
|
|
2176
|
+
if (!config.apiKey) {
|
|
2177
|
+
log("Missing apiKey, pageview skipped");
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2008
2180
|
const url = customUrl || window.location.href;
|
|
2181
|
+
const utmParams = extractUTMParams(url);
|
|
2182
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2183
|
+
const idempotencyKey = buildIdempotencyKey("visit");
|
|
2184
|
+
const agenticResult = agenticAnalyzer?.getResult();
|
|
2185
|
+
const pagePath = (() => {
|
|
2186
|
+
try {
|
|
2187
|
+
return new URL(url).pathname;
|
|
2188
|
+
} catch {
|
|
2189
|
+
return window.location.pathname;
|
|
2190
|
+
}
|
|
2191
|
+
})();
|
|
2009
2192
|
const payload = {
|
|
2010
2193
|
visitor_id: visitorId,
|
|
2011
2194
|
session_id: sessionId,
|
|
2012
|
-
url,
|
|
2195
|
+
page_url: url,
|
|
2196
|
+
page_path: pagePath,
|
|
2013
2197
|
referrer: document.referrer || null,
|
|
2014
2198
|
title: document.title || null,
|
|
2015
|
-
utm_source:
|
|
2016
|
-
utm_medium:
|
|
2017
|
-
utm_campaign:
|
|
2199
|
+
utm_source: utmParams.utm_source || null,
|
|
2200
|
+
utm_medium: utmParams.utm_medium || null,
|
|
2201
|
+
utm_campaign: utmParams.utm_campaign || null,
|
|
2202
|
+
utm_term: utmParams.utm_term || null,
|
|
2203
|
+
utm_content: utmParams.utm_content || null,
|
|
2018
2204
|
user_agent: navigator.userAgent,
|
|
2019
2205
|
screen_width: window.screen?.width,
|
|
2020
2206
|
screen_height: window.screen?.height,
|
|
2021
2207
|
language: navigator.language,
|
|
2022
2208
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
2023
2209
|
tracker_version: VERSION,
|
|
2210
|
+
event_type: "pageview",
|
|
2211
|
+
event_data: null,
|
|
2212
|
+
timestamp,
|
|
2024
2213
|
navigation_timing: navigationTiming,
|
|
2025
2214
|
ai_platform: aiDetection?.platform || null,
|
|
2026
|
-
is_ai_referrer: aiDetection?.isAI || false
|
|
2215
|
+
is_ai_referrer: aiDetection?.isAI || false,
|
|
2216
|
+
agentic_detection: agenticResult || null
|
|
2027
2217
|
};
|
|
2218
|
+
if (workspaceId) {
|
|
2219
|
+
payload.workspace_id = workspaceId;
|
|
2220
|
+
}
|
|
2028
2221
|
log("Pageview:", payload);
|
|
2029
2222
|
safeFetch(endpoint(DEFAULT_CONFIG.endpoints.visit), {
|
|
2030
2223
|
method: "POST",
|
|
2031
|
-
headers:
|
|
2224
|
+
headers: buildHeaders(idempotencyKey),
|
|
2032
2225
|
body: JSON.stringify(payload)
|
|
2033
2226
|
});
|
|
2034
2227
|
}
|
|
@@ -2037,6 +2230,11 @@ var Loamly = (() => {
|
|
|
2037
2230
|
log("Not initialized, call init() first");
|
|
2038
2231
|
return;
|
|
2039
2232
|
}
|
|
2233
|
+
if (!config.apiKey) {
|
|
2234
|
+
log("Missing apiKey, event skipped:", eventName);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
const idempotencyKey = buildIdempotencyKey(`event:${eventName}`);
|
|
2040
2238
|
const payload = {
|
|
2041
2239
|
visitor_id: visitorId,
|
|
2042
2240
|
session_id: sessionId,
|
|
@@ -2045,14 +2243,19 @@ var Loamly = (() => {
|
|
|
2045
2243
|
properties: options.properties || {},
|
|
2046
2244
|
revenue: options.revenue,
|
|
2047
2245
|
currency: options.currency || "USD",
|
|
2048
|
-
|
|
2246
|
+
page_url: window.location.href,
|
|
2247
|
+
referrer: document.referrer || null,
|
|
2049
2248
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2050
|
-
tracker_version: VERSION
|
|
2249
|
+
tracker_version: VERSION,
|
|
2250
|
+
idempotency_key: idempotencyKey
|
|
2051
2251
|
};
|
|
2252
|
+
if (workspaceId) {
|
|
2253
|
+
payload.workspace_id = workspaceId;
|
|
2254
|
+
}
|
|
2052
2255
|
log("Event:", eventName, payload);
|
|
2053
2256
|
safeFetch(endpoint("/api/ingest/event"), {
|
|
2054
2257
|
method: "POST",
|
|
2055
|
-
headers:
|
|
2258
|
+
headers: buildHeaders(idempotencyKey),
|
|
2056
2259
|
body: JSON.stringify(payload)
|
|
2057
2260
|
});
|
|
2058
2261
|
}
|
|
@@ -2064,17 +2267,26 @@ var Loamly = (() => {
|
|
|
2064
2267
|
log("Not initialized, call init() first");
|
|
2065
2268
|
return;
|
|
2066
2269
|
}
|
|
2270
|
+
if (!config.apiKey) {
|
|
2271
|
+
log("Missing apiKey, identify skipped");
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2067
2274
|
log("Identify:", userId, traits);
|
|
2275
|
+
const idempotencyKey = buildIdempotencyKey("identify");
|
|
2068
2276
|
const payload = {
|
|
2069
2277
|
visitor_id: visitorId,
|
|
2070
2278
|
session_id: sessionId,
|
|
2071
2279
|
user_id: userId,
|
|
2072
2280
|
traits,
|
|
2073
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2281
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2282
|
+
idempotency_key: idempotencyKey
|
|
2074
2283
|
};
|
|
2284
|
+
if (workspaceId) {
|
|
2285
|
+
payload.workspace_id = workspaceId;
|
|
2286
|
+
}
|
|
2075
2287
|
safeFetch(endpoint("/api/ingest/identify"), {
|
|
2076
2288
|
method: "POST",
|
|
2077
|
-
headers:
|
|
2289
|
+
headers: buildHeaders(idempotencyKey),
|
|
2078
2290
|
body: JSON.stringify(payload)
|
|
2079
2291
|
});
|
|
2080
2292
|
}
|
|
@@ -2205,14 +2417,15 @@ var Loamly = (() => {
|
|
|
2205
2417
|
return initialized;
|
|
2206
2418
|
}
|
|
2207
2419
|
function reportHealth(status, errorMessage) {
|
|
2208
|
-
if (!config.apiKey) return;
|
|
2209
2420
|
try {
|
|
2210
2421
|
const healthData = {
|
|
2211
|
-
workspace_id:
|
|
2422
|
+
workspace_id: workspaceId,
|
|
2423
|
+
visitor_id: visitorId,
|
|
2424
|
+
session_id: sessionId,
|
|
2212
2425
|
status,
|
|
2213
2426
|
error_message: errorMessage || null,
|
|
2214
|
-
|
|
2215
|
-
|
|
2427
|
+
tracker_version: VERSION,
|
|
2428
|
+
page_url: typeof window !== "undefined" ? window.location.href : null,
|
|
2216
2429
|
user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
|
2217
2430
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2218
2431
|
features: {
|
|
@@ -2308,15 +2521,16 @@ var Loamly = (() => {
|
|
|
2308
2521
|
}
|
|
2309
2522
|
async function resolveWorkspaceConfig(domain) {
|
|
2310
2523
|
try {
|
|
2311
|
-
const response = await fetch(`${DEFAULT_CONFIG.apiHost}${DEFAULT_CONFIG.endpoints.resolve}?
|
|
2524
|
+
const response = await fetch(`${DEFAULT_CONFIG.apiHost}${DEFAULT_CONFIG.endpoints.resolve}?d=${encodeURIComponent(domain)}`);
|
|
2312
2525
|
if (!response.ok) {
|
|
2313
2526
|
console.warn("[Loamly] Failed to resolve workspace for domain:", domain);
|
|
2314
2527
|
return null;
|
|
2315
2528
|
}
|
|
2316
2529
|
const data = await response.json();
|
|
2317
|
-
if (data.workspace_id) {
|
|
2530
|
+
if (data.workspace_id && data.public_key) {
|
|
2318
2531
|
return {
|
|
2319
|
-
apiKey: data.
|
|
2532
|
+
apiKey: data.public_key,
|
|
2533
|
+
workspaceId: data.workspace_id,
|
|
2320
2534
|
apiHost: DEFAULT_CONFIG.apiHost
|
|
2321
2535
|
};
|
|
2322
2536
|
}
|
|
@@ -2334,6 +2548,9 @@ var Loamly = (() => {
|
|
|
2334
2548
|
if (script.dataset.apiKey) {
|
|
2335
2549
|
config2.apiKey = script.dataset.apiKey;
|
|
2336
2550
|
}
|
|
2551
|
+
if (script.dataset.workspaceId) {
|
|
2552
|
+
config2.workspaceId = script.dataset.workspaceId;
|
|
2553
|
+
}
|
|
2337
2554
|
if (script.dataset.apiHost) {
|
|
2338
2555
|
config2.apiHost = script.dataset.apiHost;
|
|
2339
2556
|
}
|
|
@@ -2346,7 +2563,7 @@ var Loamly = (() => {
|
|
|
2346
2563
|
if (script.dataset.disableBehavioral === "true") {
|
|
2347
2564
|
config2.disableBehavioral = true;
|
|
2348
2565
|
}
|
|
2349
|
-
if (config2.apiKey) {
|
|
2566
|
+
if (config2.apiKey || config2.workspaceId) {
|
|
2350
2567
|
return config2;
|
|
2351
2568
|
}
|
|
2352
2569
|
}
|
|
@@ -2367,6 +2584,16 @@ var Loamly = (() => {
|
|
|
2367
2584
|
loamly.init(dataConfig);
|
|
2368
2585
|
return;
|
|
2369
2586
|
}
|
|
2587
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
2588
|
+
const apiKeyParam = urlParams.get("api_key");
|
|
2589
|
+
const workspaceIdParam = urlParams.get("workspace_id");
|
|
2590
|
+
if (apiKeyParam || workspaceIdParam) {
|
|
2591
|
+
loamly.init({
|
|
2592
|
+
apiKey: apiKeyParam || void 0,
|
|
2593
|
+
workspaceId: workspaceIdParam || void 0
|
|
2594
|
+
});
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2370
2597
|
const currentDomain = window.location.hostname;
|
|
2371
2598
|
if (currentDomain && currentDomain !== "localhost") {
|
|
2372
2599
|
const resolvedConfig = await resolveWorkspaceConfig(currentDomain);
|