@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
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.
|
|
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,17 +1866,35 @@ 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
|
+
}
|
|
1874
|
+
const features = {
|
|
1875
|
+
scroll: true,
|
|
1876
|
+
time: true,
|
|
1877
|
+
forms: true,
|
|
1878
|
+
spa: true,
|
|
1879
|
+
behavioralML: true,
|
|
1880
|
+
focusBlur: true,
|
|
1881
|
+
agentic: true,
|
|
1882
|
+
eventQueue: true,
|
|
1883
|
+
ping: false,
|
|
1884
|
+
// Opt-in only
|
|
1885
|
+
...userConfig.features
|
|
1886
|
+
};
|
|
1808
1887
|
log("Initializing Loamly Tracker v" + VERSION);
|
|
1888
|
+
log("Features:", features);
|
|
1809
1889
|
visitorId = getVisitorId();
|
|
1810
1890
|
log("Visitor ID:", visitorId);
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
}
|
|
1891
|
+
if (features.eventQueue) {
|
|
1892
|
+
eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
1893
|
+
batchSize: DEFAULT_CONFIG.batchSize,
|
|
1894
|
+
batchTimeout: DEFAULT_CONFIG.batchTimeout,
|
|
1895
|
+
apiKey: config.apiKey
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1818
1898
|
navigationTiming = detectNavigationType();
|
|
1819
1899
|
log("Navigation timing:", navigationTiming);
|
|
1820
1900
|
aiDetection = detectAIFromReferrer(document.referrer) || detectAIFromUTM(window.location.href);
|
|
@@ -1822,31 +1902,40 @@ function init(userConfig = {}) {
|
|
|
1822
1902
|
log("AI detected:", aiDetection);
|
|
1823
1903
|
}
|
|
1824
1904
|
initialized = true;
|
|
1825
|
-
if (
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
if (
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1905
|
+
if (features.agentic) {
|
|
1906
|
+
agenticAnalyzer = new AgenticBrowserAnalyzer();
|
|
1907
|
+
agenticAnalyzer.init();
|
|
1908
|
+
}
|
|
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
|
+
});
|
|
1850
1939
|
spaRouter = new SPARouter({
|
|
1851
1940
|
onNavigate: handleSPANavigation
|
|
1852
1941
|
});
|
|
@@ -1855,55 +1944,146 @@ function init(userConfig = {}) {
|
|
|
1855
1944
|
reportHealth("initialized");
|
|
1856
1945
|
log("Initialization complete");
|
|
1857
1946
|
}
|
|
1858
|
-
function
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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
|
+
}
|
|
1867
1961
|
});
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
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;
|
|
1882
1984
|
}
|
|
1883
1985
|
}
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
is_conversion: event.is_conversion
|
|
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
|
+
})
|
|
1896
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 {
|
|
1897
2013
|
}
|
|
1898
|
-
}
|
|
1899
|
-
|
|
2014
|
+
}
|
|
2015
|
+
const session = getSessionId();
|
|
2016
|
+
sessionId = session.sessionId;
|
|
2017
|
+
log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
|
|
2018
|
+
}
|
|
2019
|
+
function setupAdvancedBehavioralTracking(features) {
|
|
2020
|
+
if (features.scroll) {
|
|
2021
|
+
scrollTracker = new ScrollTracker({
|
|
2022
|
+
chunks: [30, 60, 90, 100],
|
|
2023
|
+
onChunkReached: (event) => {
|
|
2024
|
+
log("Scroll chunk:", event.chunk);
|
|
2025
|
+
queueEvent("scroll_depth", {
|
|
2026
|
+
scroll_depth: Math.round(event.depth / 100 * 100) / 100,
|
|
2027
|
+
milestone: Math.round(event.chunk / 100 * 100) / 100,
|
|
2028
|
+
time_to_reach_ms: event.time_to_reach_ms
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
scrollTracker.start();
|
|
2033
|
+
}
|
|
2034
|
+
if (features.time) {
|
|
2035
|
+
timeTracker = new TimeTracker({
|
|
2036
|
+
updateIntervalMs: 1e4,
|
|
2037
|
+
// Report every 10 seconds
|
|
2038
|
+
onUpdate: (event) => {
|
|
2039
|
+
if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
|
|
2040
|
+
queueEvent("time_spent", {
|
|
2041
|
+
visible_time_ms: event.total_time_ms,
|
|
2042
|
+
page_start_time: pageStartTime || Date.now()
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
timeTracker.start();
|
|
2048
|
+
}
|
|
2049
|
+
if (features.forms) {
|
|
2050
|
+
formTracker = new FormTracker({
|
|
2051
|
+
onFormEvent: (event) => {
|
|
2052
|
+
log("Form event:", event.event_type, event.form_id);
|
|
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, {
|
|
2058
|
+
form_id: event.form_id,
|
|
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
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
formTracker.start();
|
|
2070
|
+
}
|
|
2071
|
+
if (features.spa) {
|
|
2072
|
+
spaRouter = new SPARouter({
|
|
2073
|
+
onNavigate: (event) => {
|
|
2074
|
+
log("SPA navigation:", event.navigation_type);
|
|
2075
|
+
pageview(event.to_url);
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
spaRouter.start();
|
|
2079
|
+
}
|
|
1900
2080
|
document.addEventListener("click", (e) => {
|
|
1901
2081
|
const target = e.target;
|
|
1902
2082
|
const link = target.closest("a");
|
|
1903
2083
|
if (link && link.href) {
|
|
1904
2084
|
const isExternal = link.hostname !== window.location.hostname;
|
|
1905
2085
|
queueEvent("click", {
|
|
1906
|
-
|
|
2086
|
+
element_type: "link",
|
|
1907
2087
|
href: truncateText(link.href, 200),
|
|
1908
2088
|
text: truncateText(link.textContent || "", 100),
|
|
1909
2089
|
is_external: isExternal
|
|
@@ -1913,15 +2093,32 @@ function setupAdvancedBehavioralTracking() {
|
|
|
1913
2093
|
}
|
|
1914
2094
|
function queueEvent(eventType, data) {
|
|
1915
2095
|
if (!eventQueue) return;
|
|
1916
|
-
|
|
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 = {
|
|
1917
2110
|
visitor_id: visitorId,
|
|
1918
2111
|
session_id: sessionId,
|
|
1919
2112
|
event_type: eventType,
|
|
1920
|
-
|
|
1921
|
-
|
|
2113
|
+
event_data: data,
|
|
2114
|
+
page_url: window.location.href,
|
|
2115
|
+
page_path: window.location.pathname,
|
|
1922
2116
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1923
|
-
tracker_version: VERSION
|
|
1924
|
-
|
|
2117
|
+
tracker_version: VERSION,
|
|
2118
|
+
idempotency_key: idempotencyKey
|
|
2119
|
+
};
|
|
2120
|
+
payload.workspace_id = workspaceId;
|
|
2121
|
+
eventQueue.push(eventType, payload, buildHeaders(idempotencyKey));
|
|
1925
2122
|
}
|
|
1926
2123
|
function handleSPANavigation(event) {
|
|
1927
2124
|
log("SPA navigation:", event.navigation_type, event.to_url);
|
|
@@ -1949,34 +2146,42 @@ function handleSPANavigation(event) {
|
|
|
1949
2146
|
}
|
|
1950
2147
|
function setupUnloadHandlers() {
|
|
1951
2148
|
const handleUnload = () => {
|
|
2149
|
+
if (!workspaceId || !config.apiKey || !sessionId) return;
|
|
1952
2150
|
const scrollEvent = scrollTracker?.getFinalEvent();
|
|
1953
2151
|
if (scrollEvent) {
|
|
1954
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
2152
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
|
|
2153
|
+
workspace_id: workspaceId,
|
|
1955
2154
|
visitor_id: visitorId,
|
|
1956
2155
|
session_id: sessionId,
|
|
1957
2156
|
event_type: "scroll_depth_final",
|
|
1958
|
-
|
|
1959
|
-
|
|
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")
|
|
1960
2167
|
});
|
|
1961
2168
|
}
|
|
1962
2169
|
const timeEvent = timeTracker?.getFinalMetrics();
|
|
1963
2170
|
if (timeEvent) {
|
|
1964
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
1965
|
-
|
|
1966
|
-
session_id: sessionId,
|
|
1967
|
-
event_type: "time_spent_final",
|
|
1968
|
-
data: timeEvent,
|
|
1969
|
-
url: window.location.href
|
|
1970
|
-
});
|
|
1971
|
-
}
|
|
1972
|
-
const agenticResult = agenticAnalyzer?.getResult();
|
|
1973
|
-
if (agenticResult && agenticResult.agenticProbability > 0) {
|
|
1974
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
2171
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
|
|
2172
|
+
workspace_id: workspaceId,
|
|
1975
2173
|
visitor_id: visitorId,
|
|
1976
2174
|
session_id: sessionId,
|
|
1977
|
-
event_type: "
|
|
1978
|
-
|
|
1979
|
-
|
|
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")
|
|
1980
2185
|
});
|
|
1981
2186
|
}
|
|
1982
2187
|
eventQueue?.flushBeacon();
|
|
@@ -1999,30 +2204,55 @@ function pageview(customUrl) {
|
|
|
1999
2204
|
log("Not initialized, call init() first");
|
|
2000
2205
|
return;
|
|
2001
2206
|
}
|
|
2207
|
+
if (!config.apiKey) {
|
|
2208
|
+
log("Missing apiKey, pageview skipped");
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2002
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
|
+
})();
|
|
2003
2223
|
const payload = {
|
|
2004
2224
|
visitor_id: visitorId,
|
|
2005
2225
|
session_id: sessionId,
|
|
2006
|
-
url,
|
|
2226
|
+
page_url: url,
|
|
2227
|
+
page_path: pagePath,
|
|
2007
2228
|
referrer: document.referrer || null,
|
|
2008
2229
|
title: document.title || null,
|
|
2009
|
-
utm_source:
|
|
2010
|
-
utm_medium:
|
|
2011
|
-
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,
|
|
2012
2235
|
user_agent: navigator.userAgent,
|
|
2013
2236
|
screen_width: window.screen?.width,
|
|
2014
2237
|
screen_height: window.screen?.height,
|
|
2015
2238
|
language: navigator.language,
|
|
2016
2239
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
2017
2240
|
tracker_version: VERSION,
|
|
2241
|
+
event_type: "pageview",
|
|
2242
|
+
event_data: null,
|
|
2243
|
+
timestamp,
|
|
2018
2244
|
navigation_timing: navigationTiming,
|
|
2019
2245
|
ai_platform: aiDetection?.platform || null,
|
|
2020
|
-
is_ai_referrer: aiDetection?.isAI || false
|
|
2246
|
+
is_ai_referrer: aiDetection?.isAI || false,
|
|
2247
|
+
agentic_detection: agenticResult || null
|
|
2021
2248
|
};
|
|
2249
|
+
if (workspaceId) {
|
|
2250
|
+
payload.workspace_id = workspaceId;
|
|
2251
|
+
}
|
|
2022
2252
|
log("Pageview:", payload);
|
|
2023
2253
|
safeFetch(endpoint(DEFAULT_CONFIG.endpoints.visit), {
|
|
2024
2254
|
method: "POST",
|
|
2025
|
-
headers:
|
|
2255
|
+
headers: buildHeaders(idempotencyKey),
|
|
2026
2256
|
body: JSON.stringify(payload)
|
|
2027
2257
|
});
|
|
2028
2258
|
}
|
|
@@ -2031,6 +2261,11 @@ function track(eventName, options = {}) {
|
|
|
2031
2261
|
log("Not initialized, call init() first");
|
|
2032
2262
|
return;
|
|
2033
2263
|
}
|
|
2264
|
+
if (!config.apiKey) {
|
|
2265
|
+
log("Missing apiKey, event skipped:", eventName);
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
const idempotencyKey = buildIdempotencyKey(`event:${eventName}`);
|
|
2034
2269
|
const payload = {
|
|
2035
2270
|
visitor_id: visitorId,
|
|
2036
2271
|
session_id: sessionId,
|
|
@@ -2039,14 +2274,19 @@ function track(eventName, options = {}) {
|
|
|
2039
2274
|
properties: options.properties || {},
|
|
2040
2275
|
revenue: options.revenue,
|
|
2041
2276
|
currency: options.currency || "USD",
|
|
2042
|
-
|
|
2277
|
+
page_url: window.location.href,
|
|
2278
|
+
referrer: document.referrer || null,
|
|
2043
2279
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2044
|
-
tracker_version: VERSION
|
|
2280
|
+
tracker_version: VERSION,
|
|
2281
|
+
idempotency_key: idempotencyKey
|
|
2045
2282
|
};
|
|
2283
|
+
if (workspaceId) {
|
|
2284
|
+
payload.workspace_id = workspaceId;
|
|
2285
|
+
}
|
|
2046
2286
|
log("Event:", eventName, payload);
|
|
2047
2287
|
safeFetch(endpoint("/api/ingest/event"), {
|
|
2048
2288
|
method: "POST",
|
|
2049
|
-
headers:
|
|
2289
|
+
headers: buildHeaders(idempotencyKey),
|
|
2050
2290
|
body: JSON.stringify(payload)
|
|
2051
2291
|
});
|
|
2052
2292
|
}
|
|
@@ -2058,17 +2298,26 @@ function identify(userId, traits = {}) {
|
|
|
2058
2298
|
log("Not initialized, call init() first");
|
|
2059
2299
|
return;
|
|
2060
2300
|
}
|
|
2301
|
+
if (!config.apiKey) {
|
|
2302
|
+
log("Missing apiKey, identify skipped");
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2061
2305
|
log("Identify:", userId, traits);
|
|
2306
|
+
const idempotencyKey = buildIdempotencyKey("identify");
|
|
2062
2307
|
const payload = {
|
|
2063
2308
|
visitor_id: visitorId,
|
|
2064
2309
|
session_id: sessionId,
|
|
2065
2310
|
user_id: userId,
|
|
2066
2311
|
traits,
|
|
2067
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2312
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2313
|
+
idempotency_key: idempotencyKey
|
|
2068
2314
|
};
|
|
2315
|
+
if (workspaceId) {
|
|
2316
|
+
payload.workspace_id = workspaceId;
|
|
2317
|
+
}
|
|
2069
2318
|
safeFetch(endpoint("/api/ingest/identify"), {
|
|
2070
2319
|
method: "POST",
|
|
2071
|
-
headers:
|
|
2320
|
+
headers: buildHeaders(idempotencyKey),
|
|
2072
2321
|
body: JSON.stringify(payload)
|
|
2073
2322
|
});
|
|
2074
2323
|
}
|
|
@@ -2199,14 +2448,15 @@ function isTrackerInitialized() {
|
|
|
2199
2448
|
return initialized;
|
|
2200
2449
|
}
|
|
2201
2450
|
function reportHealth(status, errorMessage) {
|
|
2202
|
-
if (!config.apiKey) return;
|
|
2203
2451
|
try {
|
|
2204
2452
|
const healthData = {
|
|
2205
|
-
workspace_id:
|
|
2453
|
+
workspace_id: workspaceId,
|
|
2454
|
+
visitor_id: visitorId,
|
|
2455
|
+
session_id: sessionId,
|
|
2206
2456
|
status,
|
|
2207
2457
|
error_message: errorMessage || null,
|
|
2208
|
-
|
|
2209
|
-
|
|
2458
|
+
tracker_version: VERSION,
|
|
2459
|
+
page_url: typeof window !== "undefined" ? window.location.href : null,
|
|
2210
2460
|
user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
|
2211
2461
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2212
2462
|
features: {
|
|
@@ -2324,7 +2574,7 @@ var loamly = {
|
|
|
2324
2574
|
* See what AI tells your customers — and track when they click.
|
|
2325
2575
|
*
|
|
2326
2576
|
* @module @loamly/tracker
|
|
2327
|
-
* @version 1.
|
|
2577
|
+
* @version 2.1.0
|
|
2328
2578
|
* @license MIT
|
|
2329
2579
|
* @see https://github.com/loamly/loamly
|
|
2330
2580
|
* @see https://loamly.ai
|