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