@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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/config.ts
2
- var VERSION = "2.1.0";
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 events = this.queue.map((e) => ({
895
- type: e.type,
896
- ...e.payload,
897
- _queue_id: e.id,
898
- _queue_timestamp: e.timestamp
899
- }));
900
- const success = navigator.sendBeacon?.(
901
- this.endpoint,
902
- JSON.stringify({ events, beacon: true })
903
- ) ?? false;
904
- if (success) {
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 success;
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 response = await fetch(this.endpoint, {
946
- method: "POST",
947
- headers: { "Content-Type": "application/json" },
948
- body: JSON.stringify(payload)
949
- });
950
- if (!response.ok) {
951
- throw new Error(`HTTP ${response.status}`);
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.ping();
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,7 +1831,11 @@ 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
+ }
1773
1839
  const features = {
1774
1840
  scroll: true,
1775
1841
  time: true,
@@ -1787,13 +1853,11 @@ function init(userConfig = {}) {
1787
1853
  log("Features:", features);
1788
1854
  visitorId = getVisitorId();
1789
1855
  log("Visitor ID:", visitorId);
1790
- const session = getSessionId();
1791
- sessionId = session.sessionId;
1792
- log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
1793
1856
  if (features.eventQueue) {
1794
1857
  eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1795
1858
  batchSize: DEFAULT_CONFIG.batchSize,
1796
- batchTimeout: DEFAULT_CONFIG.batchTimeout
1859
+ batchTimeout: DEFAULT_CONFIG.batchTimeout,
1860
+ apiKey: config.apiKey
1797
1861
  });
1798
1862
  }
1799
1863
  navigationTiming = detectNavigationType();
@@ -1803,37 +1867,40 @@ function init(userConfig = {}) {
1803
1867
  log("AI detected:", aiDetection);
1804
1868
  }
1805
1869
  initialized = true;
1806
- if (!userConfig.disableAutoPageview) {
1807
- pageview();
1808
- }
1809
- if (!userConfig.disableBehavioral) {
1810
- setupAdvancedBehavioralTracking(features);
1811
- }
1812
- if (features.behavioralML) {
1813
- behavioralClassifier = new BehavioralClassifier(1e4);
1814
- behavioralClassifier.setOnClassify(handleBehavioralClassification);
1815
- setupBehavioralMLTracking();
1816
- }
1817
- if (features.focusBlur) {
1818
- focusBlurAnalyzer = new FocusBlurAnalyzer();
1819
- focusBlurAnalyzer.initTracking();
1820
- setTimeout(() => {
1821
- if (focusBlurAnalyzer) {
1822
- handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
1823
- }
1824
- }, 5e3);
1825
- }
1826
1870
  if (features.agentic) {
1827
1871
  agenticAnalyzer = new AgenticBrowserAnalyzer();
1828
1872
  agenticAnalyzer.init();
1829
1873
  }
1830
- if (features.ping && visitorId && sessionId) {
1831
- pingService = new PingService(sessionId, visitorId, VERSION, {
1832
- interval: DEFAULT_CONFIG.pingInterval,
1833
- endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping)
1834
- });
1835
- pingService.start();
1836
- }
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
+ });
1837
1904
  spaRouter = new SPARouter({
1838
1905
  onNavigate: handleSPANavigation
1839
1906
  });
@@ -1842,6 +1909,78 @@ function init(userConfig = {}) {
1842
1909
  reportHealth("initialized");
1843
1910
  log("Initialization complete");
1844
1911
  }
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
+ }
1926
+ });
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;
1949
+ }
1950
+ }
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
+ })
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 {
1978
+ }
1979
+ }
1980
+ const session = getSessionId();
1981
+ sessionId = session.sessionId;
1982
+ log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
1983
+ }
1845
1984
  function setupAdvancedBehavioralTracking(features) {
1846
1985
  if (features.scroll) {
1847
1986
  scrollTracker = new ScrollTracker({
@@ -1849,8 +1988,8 @@ function setupAdvancedBehavioralTracking(features) {
1849
1988
  onChunkReached: (event) => {
1850
1989
  log("Scroll chunk:", event.chunk);
1851
1990
  queueEvent("scroll_depth", {
1852
- depth: event.depth,
1853
- chunk: event.chunk,
1991
+ scroll_depth: Math.round(event.depth / 100 * 100) / 100,
1992
+ milestone: Math.round(event.chunk / 100 * 100) / 100,
1854
1993
  time_to_reach_ms: event.time_to_reach_ms
1855
1994
  });
1856
1995
  }
@@ -1864,10 +2003,8 @@ function setupAdvancedBehavioralTracking(features) {
1864
2003
  onUpdate: (event) => {
1865
2004
  if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
1866
2005
  queueEvent("time_spent", {
1867
- active_time_ms: event.active_time_ms,
1868
- total_time_ms: event.total_time_ms,
1869
- idle_time_ms: event.idle_time_ms,
1870
- is_engaged: event.is_engaged
2006
+ visible_time_ms: event.total_time_ms,
2007
+ page_start_time: pageStartTime || Date.now()
1871
2008
  });
1872
2009
  }
1873
2010
  }
@@ -1878,13 +2015,19 @@ function setupAdvancedBehavioralTracking(features) {
1878
2015
  formTracker = new FormTracker({
1879
2016
  onFormEvent: (event) => {
1880
2017
  log("Form event:", event.event_type, event.form_id);
1881
- queueEvent(event.event_type, {
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, {
1882
2023
  form_id: event.form_id,
1883
- form_type: event.form_type,
1884
- field_name: event.field_name,
1885
- field_type: event.field_type,
1886
- time_to_submit_ms: event.time_to_submit_ms,
1887
- is_conversion: event.is_conversion
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
1888
2031
  });
1889
2032
  }
1890
2033
  });
@@ -1905,7 +2048,7 @@ function setupAdvancedBehavioralTracking(features) {
1905
2048
  if (link && link.href) {
1906
2049
  const isExternal = link.hostname !== window.location.hostname;
1907
2050
  queueEvent("click", {
1908
- element: "link",
2051
+ element_type: "link",
1909
2052
  href: truncateText(link.href, 200),
1910
2053
  text: truncateText(link.textContent || "", 100),
1911
2054
  is_external: isExternal
@@ -1915,15 +2058,32 @@ function setupAdvancedBehavioralTracking(features) {
1915
2058
  }
1916
2059
  function queueEvent(eventType, data) {
1917
2060
  if (!eventQueue) return;
1918
- eventQueue.push(eventType, {
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 = {
1919
2075
  visitor_id: visitorId,
1920
2076
  session_id: sessionId,
1921
2077
  event_type: eventType,
1922
- ...data,
1923
- url: window.location.href,
2078
+ event_data: data,
2079
+ page_url: window.location.href,
2080
+ page_path: window.location.pathname,
1924
2081
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1925
- tracker_version: VERSION
1926
- });
2082
+ tracker_version: VERSION,
2083
+ idempotency_key: idempotencyKey
2084
+ };
2085
+ payload.workspace_id = workspaceId;
2086
+ eventQueue.push(eventType, payload, buildHeaders(idempotencyKey));
1927
2087
  }
1928
2088
  function handleSPANavigation(event) {
1929
2089
  log("SPA navigation:", event.navigation_type, event.to_url);
@@ -1951,34 +2111,42 @@ function handleSPANavigation(event) {
1951
2111
  }
1952
2112
  function setupUnloadHandlers() {
1953
2113
  const handleUnload = () => {
2114
+ if (!workspaceId || !config.apiKey || !sessionId) return;
1954
2115
  const scrollEvent = scrollTracker?.getFinalEvent();
1955
2116
  if (scrollEvent) {
1956
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
2117
+ sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
2118
+ workspace_id: workspaceId,
1957
2119
  visitor_id: visitorId,
1958
2120
  session_id: sessionId,
1959
2121
  event_type: "scroll_depth_final",
1960
- data: scrollEvent,
1961
- url: window.location.href
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")
1962
2132
  });
1963
2133
  }
1964
2134
  const timeEvent = timeTracker?.getFinalMetrics();
1965
2135
  if (timeEvent) {
1966
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
2136
+ sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
2137
+ workspace_id: workspaceId,
1967
2138
  visitor_id: visitorId,
1968
2139
  session_id: sessionId,
1969
- event_type: "time_spent_final",
1970
- data: timeEvent,
1971
- url: window.location.href
1972
- });
1973
- }
1974
- const agenticResult = agenticAnalyzer?.getResult();
1975
- if (agenticResult && agenticResult.agenticProbability > 0) {
1976
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1977
- visitor_id: visitorId,
1978
- session_id: sessionId,
1979
- event_type: "agentic_detection",
1980
- data: agenticResult,
1981
- url: window.location.href
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")
1982
2150
  });
1983
2151
  }
1984
2152
  eventQueue?.flushBeacon();
@@ -2001,30 +2169,55 @@ function pageview(customUrl) {
2001
2169
  log("Not initialized, call init() first");
2002
2170
  return;
2003
2171
  }
2172
+ if (!config.apiKey) {
2173
+ log("Missing apiKey, pageview skipped");
2174
+ return;
2175
+ }
2004
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
+ })();
2005
2188
  const payload = {
2006
2189
  visitor_id: visitorId,
2007
2190
  session_id: sessionId,
2008
- url,
2191
+ page_url: url,
2192
+ page_path: pagePath,
2009
2193
  referrer: document.referrer || null,
2010
2194
  title: document.title || null,
2011
- utm_source: extractUTMParams(url).utm_source || null,
2012
- utm_medium: extractUTMParams(url).utm_medium || null,
2013
- utm_campaign: extractUTMParams(url).utm_campaign || null,
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,
2014
2200
  user_agent: navigator.userAgent,
2015
2201
  screen_width: window.screen?.width,
2016
2202
  screen_height: window.screen?.height,
2017
2203
  language: navigator.language,
2018
2204
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
2019
2205
  tracker_version: VERSION,
2206
+ event_type: "pageview",
2207
+ event_data: null,
2208
+ timestamp,
2020
2209
  navigation_timing: navigationTiming,
2021
2210
  ai_platform: aiDetection?.platform || null,
2022
- is_ai_referrer: aiDetection?.isAI || false
2211
+ is_ai_referrer: aiDetection?.isAI || false,
2212
+ agentic_detection: agenticResult || null
2023
2213
  };
2214
+ if (workspaceId) {
2215
+ payload.workspace_id = workspaceId;
2216
+ }
2024
2217
  log("Pageview:", payload);
2025
2218
  safeFetch(endpoint(DEFAULT_CONFIG.endpoints.visit), {
2026
2219
  method: "POST",
2027
- headers: { "Content-Type": "application/json" },
2220
+ headers: buildHeaders(idempotencyKey),
2028
2221
  body: JSON.stringify(payload)
2029
2222
  });
2030
2223
  }
@@ -2033,6 +2226,11 @@ function track(eventName, options = {}) {
2033
2226
  log("Not initialized, call init() first");
2034
2227
  return;
2035
2228
  }
2229
+ if (!config.apiKey) {
2230
+ log("Missing apiKey, event skipped:", eventName);
2231
+ return;
2232
+ }
2233
+ const idempotencyKey = buildIdempotencyKey(`event:${eventName}`);
2036
2234
  const payload = {
2037
2235
  visitor_id: visitorId,
2038
2236
  session_id: sessionId,
@@ -2041,14 +2239,19 @@ function track(eventName, options = {}) {
2041
2239
  properties: options.properties || {},
2042
2240
  revenue: options.revenue,
2043
2241
  currency: options.currency || "USD",
2044
- url: window.location.href,
2242
+ page_url: window.location.href,
2243
+ referrer: document.referrer || null,
2045
2244
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2046
- tracker_version: VERSION
2245
+ tracker_version: VERSION,
2246
+ idempotency_key: idempotencyKey
2047
2247
  };
2248
+ if (workspaceId) {
2249
+ payload.workspace_id = workspaceId;
2250
+ }
2048
2251
  log("Event:", eventName, payload);
2049
2252
  safeFetch(endpoint("/api/ingest/event"), {
2050
2253
  method: "POST",
2051
- headers: { "Content-Type": "application/json" },
2254
+ headers: buildHeaders(idempotencyKey),
2052
2255
  body: JSON.stringify(payload)
2053
2256
  });
2054
2257
  }
@@ -2060,17 +2263,26 @@ function identify(userId, traits = {}) {
2060
2263
  log("Not initialized, call init() first");
2061
2264
  return;
2062
2265
  }
2266
+ if (!config.apiKey) {
2267
+ log("Missing apiKey, identify skipped");
2268
+ return;
2269
+ }
2063
2270
  log("Identify:", userId, traits);
2271
+ const idempotencyKey = buildIdempotencyKey("identify");
2064
2272
  const payload = {
2065
2273
  visitor_id: visitorId,
2066
2274
  session_id: sessionId,
2067
2275
  user_id: userId,
2068
2276
  traits,
2069
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2277
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2278
+ idempotency_key: idempotencyKey
2070
2279
  };
2280
+ if (workspaceId) {
2281
+ payload.workspace_id = workspaceId;
2282
+ }
2071
2283
  safeFetch(endpoint("/api/ingest/identify"), {
2072
2284
  method: "POST",
2073
- headers: { "Content-Type": "application/json" },
2285
+ headers: buildHeaders(idempotencyKey),
2074
2286
  body: JSON.stringify(payload)
2075
2287
  });
2076
2288
  }
@@ -2201,14 +2413,15 @@ function isTrackerInitialized() {
2201
2413
  return initialized;
2202
2414
  }
2203
2415
  function reportHealth(status, errorMessage) {
2204
- if (!config.apiKey) return;
2205
2416
  try {
2206
2417
  const healthData = {
2207
- workspace_id: config.apiKey,
2418
+ workspace_id: workspaceId,
2419
+ visitor_id: visitorId,
2420
+ session_id: sessionId,
2208
2421
  status,
2209
2422
  error_message: errorMessage || null,
2210
- version: VERSION,
2211
- url: typeof window !== "undefined" ? window.location.href : null,
2423
+ tracker_version: VERSION,
2424
+ page_url: typeof window !== "undefined" ? window.location.href : null,
2212
2425
  user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
2213
2426
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2214
2427
  features: {
@@ -2326,7 +2539,7 @@ export {
2326
2539
  * See what AI tells your customers — and track when they click.
2327
2540
  *
2328
2541
  * @module @loamly/tracker
2329
- * @version 1.8.0
2542
+ * @version 2.1.0
2330
2543
  * @license MIT
2331
2544
  * @see https://github.com/loamly/loamly
2332
2545
  * @see https://loamly.ai