@loamly/tracker 2.0.2 → 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 +27 -3
- package/dist/index.cjs +395 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.mjs +395 -145
- package/dist/index.mjs.map +1 -1
- package/dist/loamly.iife.global.js +412 -148
- 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 +400 -136
- 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 +28 -0
|
@@ -26,7 +26,7 @@ var Loamly = (() => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
// src/config.ts
|
|
29
|
-
var VERSION = "2.
|
|
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,17 +1835,35 @@ 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
|
+
}
|
|
1843
|
+
const features = {
|
|
1844
|
+
scroll: true,
|
|
1845
|
+
time: true,
|
|
1846
|
+
forms: true,
|
|
1847
|
+
spa: true,
|
|
1848
|
+
behavioralML: true,
|
|
1849
|
+
focusBlur: true,
|
|
1850
|
+
agentic: true,
|
|
1851
|
+
eventQueue: true,
|
|
1852
|
+
ping: false,
|
|
1853
|
+
// Opt-in only
|
|
1854
|
+
...userConfig.features
|
|
1855
|
+
};
|
|
1777
1856
|
log("Initializing Loamly Tracker v" + VERSION);
|
|
1857
|
+
log("Features:", features);
|
|
1778
1858
|
visitorId = getVisitorId();
|
|
1779
1859
|
log("Visitor ID:", visitorId);
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
}
|
|
1860
|
+
if (features.eventQueue) {
|
|
1861
|
+
eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
1862
|
+
batchSize: DEFAULT_CONFIG.batchSize,
|
|
1863
|
+
batchTimeout: DEFAULT_CONFIG.batchTimeout,
|
|
1864
|
+
apiKey: config.apiKey
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1787
1867
|
navigationTiming = detectNavigationType();
|
|
1788
1868
|
log("Navigation timing:", navigationTiming);
|
|
1789
1869
|
aiDetection = detectAIFromReferrer(document.referrer) || detectAIFromUTM(window.location.href);
|
|
@@ -1791,31 +1871,40 @@ var Loamly = (() => {
|
|
|
1791
1871
|
log("AI detected:", aiDetection);
|
|
1792
1872
|
}
|
|
1793
1873
|
initialized = true;
|
|
1794
|
-
if (
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
if (
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1874
|
+
if (features.agentic) {
|
|
1875
|
+
agenticAnalyzer = new AgenticBrowserAnalyzer();
|
|
1876
|
+
agenticAnalyzer.init();
|
|
1877
|
+
}
|
|
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
|
+
});
|
|
1819
1908
|
spaRouter = new SPARouter({
|
|
1820
1909
|
onNavigate: handleSPANavigation
|
|
1821
1910
|
});
|
|
@@ -1824,55 +1913,146 @@ var Loamly = (() => {
|
|
|
1824
1913
|
reportHealth("initialized");
|
|
1825
1914
|
log("Initialization complete");
|
|
1826
1915
|
}
|
|
1827
|
-
function
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
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
|
+
}
|
|
1836
1930
|
});
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
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;
|
|
1851
1953
|
}
|
|
1852
1954
|
}
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
is_conversion: event.is_conversion
|
|
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
|
+
})
|
|
1865
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 {
|
|
1866
1982
|
}
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1983
|
+
}
|
|
1984
|
+
const session = getSessionId();
|
|
1985
|
+
sessionId = session.sessionId;
|
|
1986
|
+
log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
|
|
1987
|
+
}
|
|
1988
|
+
function setupAdvancedBehavioralTracking(features) {
|
|
1989
|
+
if (features.scroll) {
|
|
1990
|
+
scrollTracker = new ScrollTracker({
|
|
1991
|
+
chunks: [30, 60, 90, 100],
|
|
1992
|
+
onChunkReached: (event) => {
|
|
1993
|
+
log("Scroll chunk:", event.chunk);
|
|
1994
|
+
queueEvent("scroll_depth", {
|
|
1995
|
+
scroll_depth: Math.round(event.depth / 100 * 100) / 100,
|
|
1996
|
+
milestone: Math.round(event.chunk / 100 * 100) / 100,
|
|
1997
|
+
time_to_reach_ms: event.time_to_reach_ms
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
2001
|
+
scrollTracker.start();
|
|
2002
|
+
}
|
|
2003
|
+
if (features.time) {
|
|
2004
|
+
timeTracker = new TimeTracker({
|
|
2005
|
+
updateIntervalMs: 1e4,
|
|
2006
|
+
// Report every 10 seconds
|
|
2007
|
+
onUpdate: (event) => {
|
|
2008
|
+
if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
|
|
2009
|
+
queueEvent("time_spent", {
|
|
2010
|
+
visible_time_ms: event.total_time_ms,
|
|
2011
|
+
page_start_time: pageStartTime || Date.now()
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
2016
|
+
timeTracker.start();
|
|
2017
|
+
}
|
|
2018
|
+
if (features.forms) {
|
|
2019
|
+
formTracker = new FormTracker({
|
|
2020
|
+
onFormEvent: (event) => {
|
|
2021
|
+
log("Form event:", event.event_type, event.form_id);
|
|
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, {
|
|
2027
|
+
form_id: event.form_id,
|
|
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
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
formTracker.start();
|
|
2039
|
+
}
|
|
2040
|
+
if (features.spa) {
|
|
2041
|
+
spaRouter = new SPARouter({
|
|
2042
|
+
onNavigate: (event) => {
|
|
2043
|
+
log("SPA navigation:", event.navigation_type);
|
|
2044
|
+
pageview(event.to_url);
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
spaRouter.start();
|
|
2048
|
+
}
|
|
1869
2049
|
document.addEventListener("click", (e) => {
|
|
1870
2050
|
const target = e.target;
|
|
1871
2051
|
const link = target.closest("a");
|
|
1872
2052
|
if (link && link.href) {
|
|
1873
2053
|
const isExternal = link.hostname !== window.location.hostname;
|
|
1874
2054
|
queueEvent("click", {
|
|
1875
|
-
|
|
2055
|
+
element_type: "link",
|
|
1876
2056
|
href: truncateText(link.href, 200),
|
|
1877
2057
|
text: truncateText(link.textContent || "", 100),
|
|
1878
2058
|
is_external: isExternal
|
|
@@ -1882,15 +2062,32 @@ var Loamly = (() => {
|
|
|
1882
2062
|
}
|
|
1883
2063
|
function queueEvent(eventType, data) {
|
|
1884
2064
|
if (!eventQueue) return;
|
|
1885
|
-
|
|
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 = {
|
|
1886
2079
|
visitor_id: visitorId,
|
|
1887
2080
|
session_id: sessionId,
|
|
1888
2081
|
event_type: eventType,
|
|
1889
|
-
|
|
1890
|
-
|
|
2082
|
+
event_data: data,
|
|
2083
|
+
page_url: window.location.href,
|
|
2084
|
+
page_path: window.location.pathname,
|
|
1891
2085
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1892
|
-
tracker_version: VERSION
|
|
1893
|
-
|
|
2086
|
+
tracker_version: VERSION,
|
|
2087
|
+
idempotency_key: idempotencyKey
|
|
2088
|
+
};
|
|
2089
|
+
payload.workspace_id = workspaceId;
|
|
2090
|
+
eventQueue.push(eventType, payload, buildHeaders(idempotencyKey));
|
|
1894
2091
|
}
|
|
1895
2092
|
function handleSPANavigation(event) {
|
|
1896
2093
|
log("SPA navigation:", event.navigation_type, event.to_url);
|
|
@@ -1918,34 +2115,42 @@ var Loamly = (() => {
|
|
|
1918
2115
|
}
|
|
1919
2116
|
function setupUnloadHandlers() {
|
|
1920
2117
|
const handleUnload = () => {
|
|
2118
|
+
if (!workspaceId || !config.apiKey || !sessionId) return;
|
|
1921
2119
|
const scrollEvent = scrollTracker?.getFinalEvent();
|
|
1922
2120
|
if (scrollEvent) {
|
|
1923
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
2121
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
|
|
2122
|
+
workspace_id: workspaceId,
|
|
1924
2123
|
visitor_id: visitorId,
|
|
1925
2124
|
session_id: sessionId,
|
|
1926
2125
|
event_type: "scroll_depth_final",
|
|
1927
|
-
|
|
1928
|
-
|
|
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")
|
|
1929
2136
|
});
|
|
1930
2137
|
}
|
|
1931
2138
|
const timeEvent = timeTracker?.getFinalMetrics();
|
|
1932
2139
|
if (timeEvent) {
|
|
1933
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
2140
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
|
|
2141
|
+
workspace_id: workspaceId,
|
|
1934
2142
|
visitor_id: visitorId,
|
|
1935
2143
|
session_id: sessionId,
|
|
1936
|
-
event_type: "
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
event_type: "agentic_detection",
|
|
1947
|
-
data: agenticResult,
|
|
1948
|
-
url: window.location.href
|
|
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")
|
|
1949
2154
|
});
|
|
1950
2155
|
}
|
|
1951
2156
|
eventQueue?.flushBeacon();
|
|
@@ -1968,30 +2173,55 @@ var Loamly = (() => {
|
|
|
1968
2173
|
log("Not initialized, call init() first");
|
|
1969
2174
|
return;
|
|
1970
2175
|
}
|
|
2176
|
+
if (!config.apiKey) {
|
|
2177
|
+
log("Missing apiKey, pageview skipped");
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
1971
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
|
+
})();
|
|
1972
2192
|
const payload = {
|
|
1973
2193
|
visitor_id: visitorId,
|
|
1974
2194
|
session_id: sessionId,
|
|
1975
|
-
url,
|
|
2195
|
+
page_url: url,
|
|
2196
|
+
page_path: pagePath,
|
|
1976
2197
|
referrer: document.referrer || null,
|
|
1977
2198
|
title: document.title || null,
|
|
1978
|
-
utm_source:
|
|
1979
|
-
utm_medium:
|
|
1980
|
-
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,
|
|
1981
2204
|
user_agent: navigator.userAgent,
|
|
1982
2205
|
screen_width: window.screen?.width,
|
|
1983
2206
|
screen_height: window.screen?.height,
|
|
1984
2207
|
language: navigator.language,
|
|
1985
2208
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
1986
2209
|
tracker_version: VERSION,
|
|
2210
|
+
event_type: "pageview",
|
|
2211
|
+
event_data: null,
|
|
2212
|
+
timestamp,
|
|
1987
2213
|
navigation_timing: navigationTiming,
|
|
1988
2214
|
ai_platform: aiDetection?.platform || null,
|
|
1989
|
-
is_ai_referrer: aiDetection?.isAI || false
|
|
2215
|
+
is_ai_referrer: aiDetection?.isAI || false,
|
|
2216
|
+
agentic_detection: agenticResult || null
|
|
1990
2217
|
};
|
|
2218
|
+
if (workspaceId) {
|
|
2219
|
+
payload.workspace_id = workspaceId;
|
|
2220
|
+
}
|
|
1991
2221
|
log("Pageview:", payload);
|
|
1992
2222
|
safeFetch(endpoint(DEFAULT_CONFIG.endpoints.visit), {
|
|
1993
2223
|
method: "POST",
|
|
1994
|
-
headers:
|
|
2224
|
+
headers: buildHeaders(idempotencyKey),
|
|
1995
2225
|
body: JSON.stringify(payload)
|
|
1996
2226
|
});
|
|
1997
2227
|
}
|
|
@@ -2000,6 +2230,11 @@ var Loamly = (() => {
|
|
|
2000
2230
|
log("Not initialized, call init() first");
|
|
2001
2231
|
return;
|
|
2002
2232
|
}
|
|
2233
|
+
if (!config.apiKey) {
|
|
2234
|
+
log("Missing apiKey, event skipped:", eventName);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
const idempotencyKey = buildIdempotencyKey(`event:${eventName}`);
|
|
2003
2238
|
const payload = {
|
|
2004
2239
|
visitor_id: visitorId,
|
|
2005
2240
|
session_id: sessionId,
|
|
@@ -2008,14 +2243,19 @@ var Loamly = (() => {
|
|
|
2008
2243
|
properties: options.properties || {},
|
|
2009
2244
|
revenue: options.revenue,
|
|
2010
2245
|
currency: options.currency || "USD",
|
|
2011
|
-
|
|
2246
|
+
page_url: window.location.href,
|
|
2247
|
+
referrer: document.referrer || null,
|
|
2012
2248
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2013
|
-
tracker_version: VERSION
|
|
2249
|
+
tracker_version: VERSION,
|
|
2250
|
+
idempotency_key: idempotencyKey
|
|
2014
2251
|
};
|
|
2252
|
+
if (workspaceId) {
|
|
2253
|
+
payload.workspace_id = workspaceId;
|
|
2254
|
+
}
|
|
2015
2255
|
log("Event:", eventName, payload);
|
|
2016
2256
|
safeFetch(endpoint("/api/ingest/event"), {
|
|
2017
2257
|
method: "POST",
|
|
2018
|
-
headers:
|
|
2258
|
+
headers: buildHeaders(idempotencyKey),
|
|
2019
2259
|
body: JSON.stringify(payload)
|
|
2020
2260
|
});
|
|
2021
2261
|
}
|
|
@@ -2027,17 +2267,26 @@ var Loamly = (() => {
|
|
|
2027
2267
|
log("Not initialized, call init() first");
|
|
2028
2268
|
return;
|
|
2029
2269
|
}
|
|
2270
|
+
if (!config.apiKey) {
|
|
2271
|
+
log("Missing apiKey, identify skipped");
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2030
2274
|
log("Identify:", userId, traits);
|
|
2275
|
+
const idempotencyKey = buildIdempotencyKey("identify");
|
|
2031
2276
|
const payload = {
|
|
2032
2277
|
visitor_id: visitorId,
|
|
2033
2278
|
session_id: sessionId,
|
|
2034
2279
|
user_id: userId,
|
|
2035
2280
|
traits,
|
|
2036
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2281
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2282
|
+
idempotency_key: idempotencyKey
|
|
2037
2283
|
};
|
|
2284
|
+
if (workspaceId) {
|
|
2285
|
+
payload.workspace_id = workspaceId;
|
|
2286
|
+
}
|
|
2038
2287
|
safeFetch(endpoint("/api/ingest/identify"), {
|
|
2039
2288
|
method: "POST",
|
|
2040
|
-
headers:
|
|
2289
|
+
headers: buildHeaders(idempotencyKey),
|
|
2041
2290
|
body: JSON.stringify(payload)
|
|
2042
2291
|
});
|
|
2043
2292
|
}
|
|
@@ -2168,14 +2417,15 @@ var Loamly = (() => {
|
|
|
2168
2417
|
return initialized;
|
|
2169
2418
|
}
|
|
2170
2419
|
function reportHealth(status, errorMessage) {
|
|
2171
|
-
if (!config.apiKey) return;
|
|
2172
2420
|
try {
|
|
2173
2421
|
const healthData = {
|
|
2174
|
-
workspace_id:
|
|
2422
|
+
workspace_id: workspaceId,
|
|
2423
|
+
visitor_id: visitorId,
|
|
2424
|
+
session_id: sessionId,
|
|
2175
2425
|
status,
|
|
2176
2426
|
error_message: errorMessage || null,
|
|
2177
|
-
|
|
2178
|
-
|
|
2427
|
+
tracker_version: VERSION,
|
|
2428
|
+
page_url: typeof window !== "undefined" ? window.location.href : null,
|
|
2179
2429
|
user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
|
2180
2430
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2181
2431
|
features: {
|
|
@@ -2271,15 +2521,16 @@ var Loamly = (() => {
|
|
|
2271
2521
|
}
|
|
2272
2522
|
async function resolveWorkspaceConfig(domain) {
|
|
2273
2523
|
try {
|
|
2274
|
-
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)}`);
|
|
2275
2525
|
if (!response.ok) {
|
|
2276
2526
|
console.warn("[Loamly] Failed to resolve workspace for domain:", domain);
|
|
2277
2527
|
return null;
|
|
2278
2528
|
}
|
|
2279
2529
|
const data = await response.json();
|
|
2280
|
-
if (data.workspace_id) {
|
|
2530
|
+
if (data.workspace_id && data.public_key) {
|
|
2281
2531
|
return {
|
|
2282
|
-
apiKey: data.
|
|
2532
|
+
apiKey: data.public_key,
|
|
2533
|
+
workspaceId: data.workspace_id,
|
|
2283
2534
|
apiHost: DEFAULT_CONFIG.apiHost
|
|
2284
2535
|
};
|
|
2285
2536
|
}
|
|
@@ -2297,6 +2548,9 @@ var Loamly = (() => {
|
|
|
2297
2548
|
if (script.dataset.apiKey) {
|
|
2298
2549
|
config2.apiKey = script.dataset.apiKey;
|
|
2299
2550
|
}
|
|
2551
|
+
if (script.dataset.workspaceId) {
|
|
2552
|
+
config2.workspaceId = script.dataset.workspaceId;
|
|
2553
|
+
}
|
|
2300
2554
|
if (script.dataset.apiHost) {
|
|
2301
2555
|
config2.apiHost = script.dataset.apiHost;
|
|
2302
2556
|
}
|
|
@@ -2309,7 +2563,7 @@ var Loamly = (() => {
|
|
|
2309
2563
|
if (script.dataset.disableBehavioral === "true") {
|
|
2310
2564
|
config2.disableBehavioral = true;
|
|
2311
2565
|
}
|
|
2312
|
-
if (config2.apiKey) {
|
|
2566
|
+
if (config2.apiKey || config2.workspaceId) {
|
|
2313
2567
|
return config2;
|
|
2314
2568
|
}
|
|
2315
2569
|
}
|
|
@@ -2330,6 +2584,16 @@ var Loamly = (() => {
|
|
|
2330
2584
|
loamly.init(dataConfig);
|
|
2331
2585
|
return;
|
|
2332
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
|
+
}
|
|
2333
2597
|
const currentDomain = window.location.hostname;
|
|
2334
2598
|
if (currentDomain && currentDomain !== "localhost") {
|
|
2335
2599
|
const resolvedConfig = await resolveWorkspaceConfig(currentDomain);
|