@loamly/tracker 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1.0";
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,7 +1866,11 @@ 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
+ }
1808
1874
  const features = {
1809
1875
  scroll: true,
1810
1876
  time: true,
@@ -1822,13 +1888,11 @@ function init(userConfig = {}) {
1822
1888
  log("Features:", features);
1823
1889
  visitorId = getVisitorId();
1824
1890
  log("Visitor ID:", visitorId);
1825
- const session = getSessionId();
1826
- sessionId = session.sessionId;
1827
- log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
1828
1891
  if (features.eventQueue) {
1829
1892
  eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1830
1893
  batchSize: DEFAULT_CONFIG.batchSize,
1831
- batchTimeout: DEFAULT_CONFIG.batchTimeout
1894
+ batchTimeout: DEFAULT_CONFIG.batchTimeout,
1895
+ apiKey: config.apiKey
1832
1896
  });
1833
1897
  }
1834
1898
  navigationTiming = detectNavigationType();
@@ -1838,37 +1902,40 @@ function init(userConfig = {}) {
1838
1902
  log("AI detected:", aiDetection);
1839
1903
  }
1840
1904
  initialized = true;
1841
- if (!userConfig.disableAutoPageview) {
1842
- pageview();
1843
- }
1844
- if (!userConfig.disableBehavioral) {
1845
- setupAdvancedBehavioralTracking(features);
1846
- }
1847
- if (features.behavioralML) {
1848
- behavioralClassifier = new BehavioralClassifier(1e4);
1849
- behavioralClassifier.setOnClassify(handleBehavioralClassification);
1850
- setupBehavioralMLTracking();
1851
- }
1852
- if (features.focusBlur) {
1853
- focusBlurAnalyzer = new FocusBlurAnalyzer();
1854
- focusBlurAnalyzer.initTracking();
1855
- setTimeout(() => {
1856
- if (focusBlurAnalyzer) {
1857
- handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
1858
- }
1859
- }, 5e3);
1860
- }
1861
1905
  if (features.agentic) {
1862
1906
  agenticAnalyzer = new AgenticBrowserAnalyzer();
1863
1907
  agenticAnalyzer.init();
1864
1908
  }
1865
- if (features.ping && visitorId && sessionId) {
1866
- pingService = new PingService(sessionId, visitorId, VERSION, {
1867
- interval: DEFAULT_CONFIG.pingInterval,
1868
- endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping)
1869
- });
1870
- pingService.start();
1871
- }
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
+ });
1872
1939
  spaRouter = new SPARouter({
1873
1940
  onNavigate: handleSPANavigation
1874
1941
  });
@@ -1877,6 +1944,78 @@ function init(userConfig = {}) {
1877
1944
  reportHealth("initialized");
1878
1945
  log("Initialization complete");
1879
1946
  }
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
+ }
1961
+ });
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;
1984
+ }
1985
+ }
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
+ })
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 {
2013
+ }
2014
+ }
2015
+ const session = getSessionId();
2016
+ sessionId = session.sessionId;
2017
+ log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
2018
+ }
1880
2019
  function setupAdvancedBehavioralTracking(features) {
1881
2020
  if (features.scroll) {
1882
2021
  scrollTracker = new ScrollTracker({
@@ -1884,8 +2023,8 @@ function setupAdvancedBehavioralTracking(features) {
1884
2023
  onChunkReached: (event) => {
1885
2024
  log("Scroll chunk:", event.chunk);
1886
2025
  queueEvent("scroll_depth", {
1887
- depth: event.depth,
1888
- chunk: event.chunk,
2026
+ scroll_depth: Math.round(event.depth / 100 * 100) / 100,
2027
+ milestone: Math.round(event.chunk / 100 * 100) / 100,
1889
2028
  time_to_reach_ms: event.time_to_reach_ms
1890
2029
  });
1891
2030
  }
@@ -1899,10 +2038,8 @@ function setupAdvancedBehavioralTracking(features) {
1899
2038
  onUpdate: (event) => {
1900
2039
  if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
1901
2040
  queueEvent("time_spent", {
1902
- active_time_ms: event.active_time_ms,
1903
- total_time_ms: event.total_time_ms,
1904
- idle_time_ms: event.idle_time_ms,
1905
- is_engaged: event.is_engaged
2041
+ visible_time_ms: event.total_time_ms,
2042
+ page_start_time: pageStartTime || Date.now()
1906
2043
  });
1907
2044
  }
1908
2045
  }
@@ -1913,13 +2050,19 @@ function setupAdvancedBehavioralTracking(features) {
1913
2050
  formTracker = new FormTracker({
1914
2051
  onFormEvent: (event) => {
1915
2052
  log("Form event:", event.event_type, event.form_id);
1916
- queueEvent(event.event_type, {
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, {
1917
2058
  form_id: event.form_id,
1918
- form_type: event.form_type,
1919
- field_name: event.field_name,
1920
- field_type: event.field_type,
1921
- time_to_submit_ms: event.time_to_submit_ms,
1922
- is_conversion: event.is_conversion
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
1923
2066
  });
1924
2067
  }
1925
2068
  });
@@ -1940,7 +2083,7 @@ function setupAdvancedBehavioralTracking(features) {
1940
2083
  if (link && link.href) {
1941
2084
  const isExternal = link.hostname !== window.location.hostname;
1942
2085
  queueEvent("click", {
1943
- element: "link",
2086
+ element_type: "link",
1944
2087
  href: truncateText(link.href, 200),
1945
2088
  text: truncateText(link.textContent || "", 100),
1946
2089
  is_external: isExternal
@@ -1950,15 +2093,32 @@ function setupAdvancedBehavioralTracking(features) {
1950
2093
  }
1951
2094
  function queueEvent(eventType, data) {
1952
2095
  if (!eventQueue) return;
1953
- 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 = {
1954
2110
  visitor_id: visitorId,
1955
2111
  session_id: sessionId,
1956
2112
  event_type: eventType,
1957
- ...data,
1958
- url: window.location.href,
2113
+ event_data: data,
2114
+ page_url: window.location.href,
2115
+ page_path: window.location.pathname,
1959
2116
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1960
- tracker_version: VERSION
1961
- });
2117
+ tracker_version: VERSION,
2118
+ idempotency_key: idempotencyKey
2119
+ };
2120
+ payload.workspace_id = workspaceId;
2121
+ eventQueue.push(eventType, payload, buildHeaders(idempotencyKey));
1962
2122
  }
1963
2123
  function handleSPANavigation(event) {
1964
2124
  log("SPA navigation:", event.navigation_type, event.to_url);
@@ -1986,34 +2146,42 @@ function handleSPANavigation(event) {
1986
2146
  }
1987
2147
  function setupUnloadHandlers() {
1988
2148
  const handleUnload = () => {
2149
+ if (!workspaceId || !config.apiKey || !sessionId) return;
1989
2150
  const scrollEvent = scrollTracker?.getFinalEvent();
1990
2151
  if (scrollEvent) {
1991
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
2152
+ sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
2153
+ workspace_id: workspaceId,
1992
2154
  visitor_id: visitorId,
1993
2155
  session_id: sessionId,
1994
2156
  event_type: "scroll_depth_final",
1995
- data: scrollEvent,
1996
- 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")
1997
2167
  });
1998
2168
  }
1999
2169
  const timeEvent = timeTracker?.getFinalMetrics();
2000
2170
  if (timeEvent) {
2001
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
2171
+ sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
2172
+ workspace_id: workspaceId,
2002
2173
  visitor_id: visitorId,
2003
2174
  session_id: sessionId,
2004
- event_type: "time_spent_final",
2005
- data: timeEvent,
2006
- url: window.location.href
2007
- });
2008
- }
2009
- const agenticResult = agenticAnalyzer?.getResult();
2010
- if (agenticResult && agenticResult.agenticProbability > 0) {
2011
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
2012
- visitor_id: visitorId,
2013
- session_id: sessionId,
2014
- event_type: "agentic_detection",
2015
- data: agenticResult,
2016
- 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")
2017
2185
  });
2018
2186
  }
2019
2187
  eventQueue?.flushBeacon();
@@ -2036,30 +2204,55 @@ function pageview(customUrl) {
2036
2204
  log("Not initialized, call init() first");
2037
2205
  return;
2038
2206
  }
2207
+ if (!config.apiKey) {
2208
+ log("Missing apiKey, pageview skipped");
2209
+ return;
2210
+ }
2039
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
+ })();
2040
2223
  const payload = {
2041
2224
  visitor_id: visitorId,
2042
2225
  session_id: sessionId,
2043
- url,
2226
+ page_url: url,
2227
+ page_path: pagePath,
2044
2228
  referrer: document.referrer || null,
2045
2229
  title: document.title || null,
2046
- utm_source: extractUTMParams(url).utm_source || null,
2047
- utm_medium: extractUTMParams(url).utm_medium || null,
2048
- 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,
2049
2235
  user_agent: navigator.userAgent,
2050
2236
  screen_width: window.screen?.width,
2051
2237
  screen_height: window.screen?.height,
2052
2238
  language: navigator.language,
2053
2239
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
2054
2240
  tracker_version: VERSION,
2241
+ event_type: "pageview",
2242
+ event_data: null,
2243
+ timestamp,
2055
2244
  navigation_timing: navigationTiming,
2056
2245
  ai_platform: aiDetection?.platform || null,
2057
- is_ai_referrer: aiDetection?.isAI || false
2246
+ is_ai_referrer: aiDetection?.isAI || false,
2247
+ agentic_detection: agenticResult || null
2058
2248
  };
2249
+ if (workspaceId) {
2250
+ payload.workspace_id = workspaceId;
2251
+ }
2059
2252
  log("Pageview:", payload);
2060
2253
  safeFetch(endpoint(DEFAULT_CONFIG.endpoints.visit), {
2061
2254
  method: "POST",
2062
- headers: { "Content-Type": "application/json" },
2255
+ headers: buildHeaders(idempotencyKey),
2063
2256
  body: JSON.stringify(payload)
2064
2257
  });
2065
2258
  }
@@ -2068,6 +2261,11 @@ function track(eventName, options = {}) {
2068
2261
  log("Not initialized, call init() first");
2069
2262
  return;
2070
2263
  }
2264
+ if (!config.apiKey) {
2265
+ log("Missing apiKey, event skipped:", eventName);
2266
+ return;
2267
+ }
2268
+ const idempotencyKey = buildIdempotencyKey(`event:${eventName}`);
2071
2269
  const payload = {
2072
2270
  visitor_id: visitorId,
2073
2271
  session_id: sessionId,
@@ -2076,14 +2274,19 @@ function track(eventName, options = {}) {
2076
2274
  properties: options.properties || {},
2077
2275
  revenue: options.revenue,
2078
2276
  currency: options.currency || "USD",
2079
- url: window.location.href,
2277
+ page_url: window.location.href,
2278
+ referrer: document.referrer || null,
2080
2279
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2081
- tracker_version: VERSION
2280
+ tracker_version: VERSION,
2281
+ idempotency_key: idempotencyKey
2082
2282
  };
2283
+ if (workspaceId) {
2284
+ payload.workspace_id = workspaceId;
2285
+ }
2083
2286
  log("Event:", eventName, payload);
2084
2287
  safeFetch(endpoint("/api/ingest/event"), {
2085
2288
  method: "POST",
2086
- headers: { "Content-Type": "application/json" },
2289
+ headers: buildHeaders(idempotencyKey),
2087
2290
  body: JSON.stringify(payload)
2088
2291
  });
2089
2292
  }
@@ -2095,17 +2298,26 @@ function identify(userId, traits = {}) {
2095
2298
  log("Not initialized, call init() first");
2096
2299
  return;
2097
2300
  }
2301
+ if (!config.apiKey) {
2302
+ log("Missing apiKey, identify skipped");
2303
+ return;
2304
+ }
2098
2305
  log("Identify:", userId, traits);
2306
+ const idempotencyKey = buildIdempotencyKey("identify");
2099
2307
  const payload = {
2100
2308
  visitor_id: visitorId,
2101
2309
  session_id: sessionId,
2102
2310
  user_id: userId,
2103
2311
  traits,
2104
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2312
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2313
+ idempotency_key: idempotencyKey
2105
2314
  };
2315
+ if (workspaceId) {
2316
+ payload.workspace_id = workspaceId;
2317
+ }
2106
2318
  safeFetch(endpoint("/api/ingest/identify"), {
2107
2319
  method: "POST",
2108
- headers: { "Content-Type": "application/json" },
2320
+ headers: buildHeaders(idempotencyKey),
2109
2321
  body: JSON.stringify(payload)
2110
2322
  });
2111
2323
  }
@@ -2236,14 +2448,15 @@ function isTrackerInitialized() {
2236
2448
  return initialized;
2237
2449
  }
2238
2450
  function reportHealth(status, errorMessage) {
2239
- if (!config.apiKey) return;
2240
2451
  try {
2241
2452
  const healthData = {
2242
- workspace_id: config.apiKey,
2453
+ workspace_id: workspaceId,
2454
+ visitor_id: visitorId,
2455
+ session_id: sessionId,
2243
2456
  status,
2244
2457
  error_message: errorMessage || null,
2245
- version: VERSION,
2246
- url: typeof window !== "undefined" ? window.location.href : null,
2458
+ tracker_version: VERSION,
2459
+ page_url: typeof window !== "undefined" ? window.location.href : null,
2247
2460
  user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
2248
2461
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2249
2462
  features: {
@@ -2361,7 +2574,7 @@ var loamly = {
2361
2574
  * See what AI tells your customers — and track when they click.
2362
2575
  *
2363
2576
  * @module @loamly/tracker
2364
- * @version 1.8.0
2577
+ * @version 2.1.0
2365
2578
  * @license MIT
2366
2579
  * @see https://github.com/loamly/loamly
2367
2580
  * @see https://loamly.ai