@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/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.0.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 events = this.queue.map((e) => ({
930
- type: e.type,
931
- ...e.payload,
932
- _queue_id: e.id,
933
- _queue_timestamp: e.timestamp
934
- }));
935
- const success = navigator.sendBeacon?.(
936
- this.endpoint,
937
- JSON.stringify({ events, beacon: true })
938
- ) ?? false;
939
- if (success) {
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 success;
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 response = await fetch(this.endpoint, {
981
- method: "POST",
982
- headers: { "Content-Type": "application/json" },
983
- body: JSON.stringify(payload)
984
- });
985
- if (!response.ok) {
986
- throw new Error(`HTTP ${response.status}`);
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.ping();
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
- const session = getSessionId();
1812
- sessionId = session.sessionId;
1813
- log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
1814
- eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1815
- batchSize: DEFAULT_CONFIG.batchSize,
1816
- batchTimeout: DEFAULT_CONFIG.batchTimeout
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 (!userConfig.disableAutoPageview) {
1826
- pageview();
1827
- }
1828
- if (!userConfig.disableBehavioral) {
1829
- setupAdvancedBehavioralTracking();
1830
- }
1831
- behavioralClassifier = new BehavioralClassifier(1e4);
1832
- behavioralClassifier.setOnClassify(handleBehavioralClassification);
1833
- setupBehavioralMLTracking();
1834
- focusBlurAnalyzer = new FocusBlurAnalyzer();
1835
- focusBlurAnalyzer.initTracking();
1836
- setTimeout(() => {
1837
- if (focusBlurAnalyzer) {
1838
- handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
1839
- }
1840
- }, 5e3);
1841
- agenticAnalyzer = new AgenticBrowserAnalyzer();
1842
- agenticAnalyzer.init();
1843
- if (visitorId && sessionId) {
1844
- pingService = new PingService(sessionId, visitorId, VERSION, {
1845
- interval: DEFAULT_CONFIG.pingInterval,
1846
- endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping)
1847
- });
1848
- pingService.start();
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 setupAdvancedBehavioralTracking() {
1859
- scrollTracker = new ScrollTracker({
1860
- chunks: [30, 60, 90, 100],
1861
- onChunkReached: (event) => {
1862
- log("Scroll chunk:", event.chunk);
1863
- queueEvent("scroll_depth", {
1864
- depth: event.depth,
1865
- chunk: event.chunk,
1866
- time_to_reach_ms: event.time_to_reach_ms
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
- scrollTracker.start();
1871
- timeTracker = new TimeTracker({
1872
- updateIntervalMs: 1e4,
1873
- // Report every 10 seconds
1874
- onUpdate: (event) => {
1875
- if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
1876
- queueEvent("time_spent", {
1877
- active_time_ms: event.active_time_ms,
1878
- total_time_ms: event.total_time_ms,
1879
- idle_time_ms: event.idle_time_ms,
1880
- is_engaged: event.is_engaged
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
- timeTracker.start();
1886
- formTracker = new FormTracker({
1887
- onFormEvent: (event) => {
1888
- log("Form event:", event.event_type, event.form_id);
1889
- queueEvent(event.event_type, {
1890
- form_id: event.form_id,
1891
- form_type: event.form_type,
1892
- field_name: event.field_name,
1893
- field_type: event.field_type,
1894
- time_to_submit_ms: event.time_to_submit_ms,
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
- formTracker.start();
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
- element: "link",
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
- eventQueue.push(eventType, {
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
- ...data,
1921
- url: window.location.href,
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
- data: scrollEvent,
1959
- url: window.location.href
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
- visitor_id: visitorId,
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: "agentic_detection",
1978
- data: agenticResult,
1979
- url: window.location.href
2175
+ event_type: "time_spent",
2176
+ event_data: {
2177
+ visible_time_ms: timeEvent.total_time_ms,
2178
+ page_start_time: pageStartTime || Date.now()
2179
+ },
2180
+ page_url: window.location.href,
2181
+ page_path: window.location.pathname,
2182
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2183
+ tracker_version: VERSION,
2184
+ idempotency_key: buildIdempotencyKey("time_spent")
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: extractUTMParams(url).utm_source || null,
2010
- utm_medium: extractUTMParams(url).utm_medium || null,
2011
- utm_campaign: extractUTMParams(url).utm_campaign || null,
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: { "Content-Type": "application/json" },
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
- url: window.location.href,
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: { "Content-Type": "application/json" },
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: { "Content-Type": "application/json" },
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: config.apiKey,
2453
+ workspace_id: workspaceId,
2454
+ visitor_id: visitorId,
2455
+ session_id: sessionId,
2206
2456
  status,
2207
2457
  error_message: errorMessage || null,
2208
- version: VERSION,
2209
- url: typeof window !== "undefined" ? window.location.href : null,
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.8.0
2577
+ * @version 2.1.0
2328
2578
  * @license MIT
2329
2579
  * @see https://github.com/loamly/loamly
2330
2580
  * @see https://loamly.ai