@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.
@@ -26,7 +26,7 @@ var Loamly = (() => {
26
26
  });
27
27
 
28
28
  // src/config.ts
29
- var VERSION = "2.0.2";
29
+ var VERSION = "2.1.1";
30
30
  var DEFAULT_CONFIG = {
31
31
  apiHost: "https://app.loamly.ai",
32
32
  endpoints: {
@@ -862,11 +862,12 @@ var Loamly = (() => {
862
862
  /**
863
863
  * Add event to queue
864
864
  */
865
- push(type, payload) {
865
+ push(type, payload, headers) {
866
866
  const event = {
867
867
  id: this.generateId(),
868
868
  type,
869
869
  payload,
870
+ headers,
870
871
  timestamp: Date.now(),
871
872
  retries: 0
872
873
  };
@@ -895,21 +896,25 @@ var Loamly = (() => {
895
896
  */
896
897
  flushBeacon() {
897
898
  if (this.queue.length === 0) return true;
898
- const events = this.queue.map((e) => ({
899
- type: e.type,
900
- ...e.payload,
901
- _queue_id: e.id,
902
- _queue_timestamp: e.timestamp
903
- }));
904
- const success = navigator.sendBeacon?.(
905
- this.endpoint,
906
- JSON.stringify({ events, beacon: true })
907
- ) ?? false;
908
- if (success) {
899
+ const baseUrl = this.config.apiKey ? `${this.endpoint}?api_key=${encodeURIComponent(this.config.apiKey)}` : this.endpoint;
900
+ let allSent = true;
901
+ for (const event of this.queue) {
902
+ const payload = {
903
+ ...event.payload,
904
+ _queue_id: event.id,
905
+ _queue_timestamp: event.timestamp
906
+ };
907
+ const success = navigator.sendBeacon?.(baseUrl, JSON.stringify(payload)) ?? false;
908
+ if (!success) {
909
+ allSent = false;
910
+ break;
911
+ }
912
+ }
913
+ if (allSent) {
909
914
  this.queue = [];
910
915
  this.clearStorage();
911
916
  }
912
- return success;
917
+ return allSent;
913
918
  }
914
919
  /**
915
920
  * Get current queue length
@@ -936,24 +941,38 @@ var Loamly = (() => {
936
941
  }
937
942
  async sendBatch(events) {
938
943
  if (events.length === 0) return;
939
- const payload = {
940
- events: events.map((e) => ({
941
- type: e.type,
942
- ...e.payload,
943
- _queue_id: e.id,
944
- _queue_timestamp: e.timestamp
945
- })),
946
- batch: true
947
- };
948
944
  try {
949
- const response = await fetch(this.endpoint, {
950
- method: "POST",
951
- headers: { "Content-Type": "application/json" },
952
- body: JSON.stringify(payload)
953
- });
954
- if (!response.ok) {
955
- throw new Error(`HTTP ${response.status}`);
945
+ const results = await Promise.allSettled(
946
+ events.map(async (event) => {
947
+ const response = await fetch(this.endpoint, {
948
+ method: "POST",
949
+ headers: {
950
+ "Content-Type": "application/json",
951
+ ...event.headers || {}
952
+ },
953
+ body: JSON.stringify({
954
+ ...event.payload,
955
+ _queue_id: event.id,
956
+ _queue_timestamp: event.timestamp
957
+ })
958
+ });
959
+ if (!response.ok) {
960
+ throw new Error(`HTTP ${response.status}`);
961
+ }
962
+ })
963
+ );
964
+ const failedEvents = events.filter((_, index) => results[index]?.status === "rejected");
965
+ if (failedEvents.length > 0) {
966
+ for (const event of failedEvents) {
967
+ if (event.retries < this.config.maxRetries) {
968
+ event.retries++;
969
+ this.queue.push(event);
970
+ }
971
+ }
972
+ const delay = this.config.retryDelayMs * Math.pow(2, failedEvents[0].retries - 1);
973
+ setTimeout(() => this.flush(), delay);
956
974
  }
975
+ return;
957
976
  } catch (error) {
958
977
  for (const event of events) {
959
978
  if (event.retries < this.config.maxRetries) {
@@ -1006,6 +1025,7 @@ var Loamly = (() => {
1006
1025
  constructor(sessionId2, visitorId2, version, config2 = {}) {
1007
1026
  this.intervalId = null;
1008
1027
  this.isVisible = true;
1028
+ this.isFocused = true;
1009
1029
  this.currentScrollDepth = 0;
1010
1030
  this.ping = async () => {
1011
1031
  const data = this.getData();
@@ -1024,6 +1044,12 @@ var Loamly = (() => {
1024
1044
  this.handleVisibilityChange = () => {
1025
1045
  this.isVisible = document.visibilityState === "visible";
1026
1046
  };
1047
+ this.handleFocusChange = () => {
1048
+ this.isFocused = typeof document.hasFocus === "function" ? document.hasFocus() : true;
1049
+ if (this.intervalId && this.isVisible && this.isFocused) {
1050
+ this.ping();
1051
+ }
1052
+ };
1027
1053
  this.handleScroll = () => {
1028
1054
  const scrollPercent = Math.round(
1029
1055
  (window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100
@@ -1036,12 +1062,16 @@ var Loamly = (() => {
1036
1062
  this.visitorId = visitorId2;
1037
1063
  this.version = version;
1038
1064
  this.pageLoadTime = Date.now();
1065
+ this.isVisible = document.visibilityState === "visible";
1066
+ this.isFocused = typeof document.hasFocus === "function" ? document.hasFocus() : true;
1039
1067
  this.config = {
1040
1068
  interval: DEFAULT_CONFIG.pingInterval,
1041
1069
  endpoint: "",
1042
1070
  ...config2
1043
1071
  };
1044
1072
  document.addEventListener("visibilitychange", this.handleVisibilityChange);
1073
+ window.addEventListener("focus", this.handleFocusChange);
1074
+ window.addEventListener("blur", this.handleFocusChange);
1045
1075
  window.addEventListener("scroll", this.handleScroll, { passive: true });
1046
1076
  }
1047
1077
  /**
@@ -1050,11 +1080,13 @@ var Loamly = (() => {
1050
1080
  start() {
1051
1081
  if (this.intervalId) return;
1052
1082
  this.intervalId = setInterval(() => {
1053
- if (this.isVisible) {
1083
+ if (this.isVisible && this.isFocused) {
1054
1084
  this.ping();
1055
1085
  }
1056
1086
  }, this.config.interval);
1057
- this.ping();
1087
+ if (this.isVisible && this.isFocused) {
1088
+ this.ping();
1089
+ }
1058
1090
  }
1059
1091
  /**
1060
1092
  * Stop the ping service
@@ -1065,6 +1097,8 @@ var Loamly = (() => {
1065
1097
  this.intervalId = null;
1066
1098
  }
1067
1099
  document.removeEventListener("visibilitychange", this.handleVisibilityChange);
1100
+ window.removeEventListener("focus", this.handleFocusChange);
1101
+ window.removeEventListener("blur", this.handleFocusChange);
1068
1102
  window.removeEventListener("scroll", this.handleScroll);
1069
1103
  }
1070
1104
  /**
@@ -1085,7 +1119,7 @@ var Loamly = (() => {
1085
1119
  url: window.location.href,
1086
1120
  time_on_page_ms: Date.now() - this.pageLoadTime,
1087
1121
  scroll_depth: this.currentScrollDepth,
1088
- is_active: this.isVisible,
1122
+ is_active: this.isVisible && this.isFocused,
1089
1123
  tracker_version: this.version
1090
1124
  };
1091
1125
  }
@@ -1392,7 +1426,8 @@ var Loamly = (() => {
1392
1426
  form_id: formId,
1393
1427
  form_type: this.detectFormType(form),
1394
1428
  time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
1395
- is_conversion: true
1429
+ is_conversion: true,
1430
+ submit_source: "submit"
1396
1431
  });
1397
1432
  };
1398
1433
  this.handleClick = (e) => {
@@ -1407,7 +1442,8 @@ var Loamly = (() => {
1407
1442
  form_id: formId,
1408
1443
  form_type: "hubspot",
1409
1444
  time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
1410
- is_conversion: true
1445
+ is_conversion: true,
1446
+ submit_source: "click"
1411
1447
  });
1412
1448
  }
1413
1449
  }
@@ -1416,7 +1452,8 @@ var Loamly = (() => {
1416
1452
  event_type: "form_submit",
1417
1453
  form_id: "typeform_embed",
1418
1454
  form_type: "typeform",
1419
- is_conversion: true
1455
+ is_conversion: true,
1456
+ submit_source: "click"
1420
1457
  });
1421
1458
  }
1422
1459
  };
@@ -1518,7 +1555,8 @@ var Loamly = (() => {
1518
1555
  event_type: "form_success",
1519
1556
  form_id: "page_conversion",
1520
1557
  form_type: "unknown",
1521
- is_conversion: true
1558
+ is_conversion: true,
1559
+ submit_source: "thank_you"
1522
1560
  });
1523
1561
  break;
1524
1562
  }
@@ -1742,8 +1780,10 @@ var Loamly = (() => {
1742
1780
  var debugMode = false;
1743
1781
  var visitorId = null;
1744
1782
  var sessionId = null;
1783
+ var workspaceId = null;
1745
1784
  var navigationTiming = null;
1746
1785
  var aiDetection = null;
1786
+ var pageStartTime = null;
1747
1787
  var behavioralClassifier = null;
1748
1788
  var behavioralMLResult = null;
1749
1789
  var focusBlurAnalyzer = null;
@@ -1763,6 +1803,28 @@ var Loamly = (() => {
1763
1803
  function endpoint(path) {
1764
1804
  return `${config.apiHost}${path}`;
1765
1805
  }
1806
+ function buildHeaders(idempotencyKey) {
1807
+ const headers = {
1808
+ "Content-Type": "application/json"
1809
+ };
1810
+ if (config.apiKey) {
1811
+ headers["X-Loamly-Api-Key"] = config.apiKey;
1812
+ }
1813
+ if (idempotencyKey) {
1814
+ headers["X-Idempotency-Key"] = idempotencyKey;
1815
+ }
1816
+ return headers;
1817
+ }
1818
+ function buildBeaconUrl(path) {
1819
+ if (!config.apiKey) return path;
1820
+ const url = new URL(path, config.apiHost);
1821
+ url.searchParams.set("api_key", config.apiKey);
1822
+ return url.toString();
1823
+ }
1824
+ function buildIdempotencyKey(prefix) {
1825
+ const base = sessionId || visitorId || "unknown";
1826
+ return `${prefix}:${base}:${Date.now()}`;
1827
+ }
1766
1828
  function init(userConfig = {}) {
1767
1829
  if (initialized) {
1768
1830
  log("Already initialized");
@@ -1773,17 +1835,35 @@ var Loamly = (() => {
1773
1835
  ...userConfig,
1774
1836
  apiHost: userConfig.apiHost || DEFAULT_CONFIG.apiHost
1775
1837
  };
1838
+ workspaceId = userConfig.workspaceId ?? null;
1776
1839
  debugMode = userConfig.debug ?? false;
1840
+ if (config.apiKey && !workspaceId) {
1841
+ log("Workspace ID missing. Behavioral events require workspaceId.");
1842
+ }
1843
+ const features = {
1844
+ scroll: true,
1845
+ time: true,
1846
+ forms: true,
1847
+ spa: true,
1848
+ behavioralML: true,
1849
+ focusBlur: true,
1850
+ agentic: true,
1851
+ eventQueue: true,
1852
+ ping: false,
1853
+ // Opt-in only
1854
+ ...userConfig.features
1855
+ };
1777
1856
  log("Initializing Loamly Tracker v" + VERSION);
1857
+ log("Features:", features);
1778
1858
  visitorId = getVisitorId();
1779
1859
  log("Visitor ID:", visitorId);
1780
- const session = getSessionId();
1781
- sessionId = session.sessionId;
1782
- log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
1783
- eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1784
- batchSize: DEFAULT_CONFIG.batchSize,
1785
- batchTimeout: DEFAULT_CONFIG.batchTimeout
1786
- });
1860
+ if (features.eventQueue) {
1861
+ eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1862
+ batchSize: DEFAULT_CONFIG.batchSize,
1863
+ batchTimeout: DEFAULT_CONFIG.batchTimeout,
1864
+ apiKey: config.apiKey
1865
+ });
1866
+ }
1787
1867
  navigationTiming = detectNavigationType();
1788
1868
  log("Navigation timing:", navigationTiming);
1789
1869
  aiDetection = detectAIFromReferrer(document.referrer) || detectAIFromUTM(window.location.href);
@@ -1791,31 +1871,40 @@ var Loamly = (() => {
1791
1871
  log("AI detected:", aiDetection);
1792
1872
  }
1793
1873
  initialized = true;
1794
- if (!userConfig.disableAutoPageview) {
1795
- pageview();
1796
- }
1797
- if (!userConfig.disableBehavioral) {
1798
- setupAdvancedBehavioralTracking();
1799
- }
1800
- behavioralClassifier = new BehavioralClassifier(1e4);
1801
- behavioralClassifier.setOnClassify(handleBehavioralClassification);
1802
- setupBehavioralMLTracking();
1803
- focusBlurAnalyzer = new FocusBlurAnalyzer();
1804
- focusBlurAnalyzer.initTracking();
1805
- setTimeout(() => {
1806
- if (focusBlurAnalyzer) {
1807
- handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
1808
- }
1809
- }, 5e3);
1810
- agenticAnalyzer = new AgenticBrowserAnalyzer();
1811
- agenticAnalyzer.init();
1812
- if (visitorId && sessionId) {
1813
- pingService = new PingService(sessionId, visitorId, VERSION, {
1814
- interval: DEFAULT_CONFIG.pingInterval,
1815
- endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping)
1816
- });
1817
- pingService.start();
1818
- }
1874
+ if (features.agentic) {
1875
+ agenticAnalyzer = new AgenticBrowserAnalyzer();
1876
+ agenticAnalyzer.init();
1877
+ }
1878
+ void initializeSession().finally(() => {
1879
+ void registerServiceWorker();
1880
+ if (!userConfig.disableAutoPageview) {
1881
+ pageview();
1882
+ }
1883
+ if (!userConfig.disableBehavioral) {
1884
+ setupAdvancedBehavioralTracking(features);
1885
+ }
1886
+ if (features.behavioralML) {
1887
+ behavioralClassifier = new BehavioralClassifier(1e4);
1888
+ behavioralClassifier.setOnClassify(handleBehavioralClassification);
1889
+ setupBehavioralMLTracking();
1890
+ }
1891
+ if (features.focusBlur) {
1892
+ focusBlurAnalyzer = new FocusBlurAnalyzer();
1893
+ focusBlurAnalyzer.initTracking();
1894
+ setTimeout(() => {
1895
+ if (focusBlurAnalyzer) {
1896
+ handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
1897
+ }
1898
+ }, 5e3);
1899
+ }
1900
+ if (features.ping && visitorId && sessionId) {
1901
+ pingService = new PingService(sessionId, visitorId, VERSION, {
1902
+ interval: DEFAULT_CONFIG.pingInterval,
1903
+ endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping)
1904
+ });
1905
+ pingService.start();
1906
+ }
1907
+ });
1819
1908
  spaRouter = new SPARouter({
1820
1909
  onNavigate: handleSPANavigation
1821
1910
  });
@@ -1824,55 +1913,146 @@ var Loamly = (() => {
1824
1913
  reportHealth("initialized");
1825
1914
  log("Initialization complete");
1826
1915
  }
1827
- function setupAdvancedBehavioralTracking() {
1828
- scrollTracker = new ScrollTracker({
1829
- chunks: [30, 60, 90, 100],
1830
- onChunkReached: (event) => {
1831
- log("Scroll chunk:", event.chunk);
1832
- queueEvent("scroll_depth", {
1833
- depth: event.depth,
1834
- chunk: event.chunk,
1835
- time_to_reach_ms: event.time_to_reach_ms
1916
+ async function registerServiceWorker() {
1917
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return;
1918
+ if (!config.apiKey || !workspaceId) return;
1919
+ try {
1920
+ const swUrl = new URL("/tracker/loamly-sw.js", window.location.origin);
1921
+ swUrl.searchParams.set("workspace_id", workspaceId);
1922
+ swUrl.searchParams.set("api_key", config.apiKey);
1923
+ const registration = await navigator.serviceWorker.register(swUrl.toString(), { scope: "/" });
1924
+ registration.addEventListener("updatefound", () => {
1925
+ const installing = registration.installing;
1926
+ installing?.addEventListener("statechange", () => {
1927
+ if (installing.state === "activated") {
1928
+ installing.postMessage({ type: "SKIP_WAITING" });
1929
+ }
1836
1930
  });
1837
- }
1838
- });
1839
- scrollTracker.start();
1840
- timeTracker = new TimeTracker({
1841
- updateIntervalMs: 1e4,
1842
- // Report every 10 seconds
1843
- onUpdate: (event) => {
1844
- if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
1845
- queueEvent("time_spent", {
1846
- active_time_ms: event.active_time_ms,
1847
- total_time_ms: event.total_time_ms,
1848
- idle_time_ms: event.idle_time_ms,
1849
- is_engaged: event.is_engaged
1850
- });
1931
+ });
1932
+ setInterval(() => {
1933
+ registration.update().catch(() => {
1934
+ });
1935
+ }, 24 * 60 * 60 * 1e3);
1936
+ } catch {
1937
+ }
1938
+ }
1939
+ async function initializeSession() {
1940
+ const now = Date.now();
1941
+ pageStartTime = now;
1942
+ try {
1943
+ const storedSession = sessionStorage.getItem("loamly_session");
1944
+ const storedStart = sessionStorage.getItem("loamly_start");
1945
+ const sessionTimeout = config.sessionTimeout ?? DEFAULT_CONFIG.sessionTimeout;
1946
+ if (storedSession && storedStart) {
1947
+ const startTime = parseInt(storedStart, 10);
1948
+ const elapsed = now - startTime;
1949
+ if (elapsed > 0 && elapsed < sessionTimeout) {
1950
+ sessionId = storedSession;
1951
+ log("Session ID:", sessionId, "(existing)");
1952
+ return;
1851
1953
  }
1852
1954
  }
1853
- });
1854
- timeTracker.start();
1855
- formTracker = new FormTracker({
1856
- onFormEvent: (event) => {
1857
- log("Form event:", event.event_type, event.form_id);
1858
- queueEvent(event.event_type, {
1859
- form_id: event.form_id,
1860
- form_type: event.form_type,
1861
- field_name: event.field_name,
1862
- field_type: event.field_type,
1863
- time_to_submit_ms: event.time_to_submit_ms,
1864
- is_conversion: event.is_conversion
1955
+ } catch {
1956
+ }
1957
+ if (config.apiKey && workspaceId && visitorId) {
1958
+ try {
1959
+ const response = await safeFetch(endpoint(DEFAULT_CONFIG.endpoints.session), {
1960
+ method: "POST",
1961
+ headers: buildHeaders(),
1962
+ body: JSON.stringify({
1963
+ workspace_id: workspaceId,
1964
+ visitor_id: visitorId
1965
+ })
1865
1966
  });
1967
+ if (response?.ok) {
1968
+ const data = await response.json();
1969
+ sessionId = data.session_id || sessionId;
1970
+ const startTime = data.start_time || now;
1971
+ if (sessionId) {
1972
+ try {
1973
+ sessionStorage.setItem("loamly_session", sessionId);
1974
+ sessionStorage.setItem("loamly_start", String(startTime));
1975
+ } catch {
1976
+ }
1977
+ log("Session ID:", sessionId, "(server)");
1978
+ return;
1979
+ }
1980
+ }
1981
+ } catch {
1866
1982
  }
1867
- });
1868
- formTracker.start();
1983
+ }
1984
+ const session = getSessionId();
1985
+ sessionId = session.sessionId;
1986
+ log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
1987
+ }
1988
+ function setupAdvancedBehavioralTracking(features) {
1989
+ if (features.scroll) {
1990
+ scrollTracker = new ScrollTracker({
1991
+ chunks: [30, 60, 90, 100],
1992
+ onChunkReached: (event) => {
1993
+ log("Scroll chunk:", event.chunk);
1994
+ queueEvent("scroll_depth", {
1995
+ scroll_depth: Math.round(event.depth / 100 * 100) / 100,
1996
+ milestone: Math.round(event.chunk / 100 * 100) / 100,
1997
+ time_to_reach_ms: event.time_to_reach_ms
1998
+ });
1999
+ }
2000
+ });
2001
+ scrollTracker.start();
2002
+ }
2003
+ if (features.time) {
2004
+ timeTracker = new TimeTracker({
2005
+ updateIntervalMs: 1e4,
2006
+ // Report every 10 seconds
2007
+ onUpdate: (event) => {
2008
+ if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
2009
+ queueEvent("time_spent", {
2010
+ visible_time_ms: event.total_time_ms,
2011
+ page_start_time: pageStartTime || Date.now()
2012
+ });
2013
+ }
2014
+ }
2015
+ });
2016
+ timeTracker.start();
2017
+ }
2018
+ if (features.forms) {
2019
+ formTracker = new FormTracker({
2020
+ onFormEvent: (event) => {
2021
+ log("Form event:", event.event_type, event.form_id);
2022
+ const isSubmitEvent = event.event_type === "form_submit";
2023
+ const isSuccessEvent = event.event_type === "form_success";
2024
+ const normalizedEventType = isSubmitEvent || isSuccessEvent ? "form_submit" : "form_focus";
2025
+ const submitSource = event.submit_source || (isSuccessEvent ? "thank_you" : isSubmitEvent ? "submit" : null);
2026
+ queueEvent(normalizedEventType, {
2027
+ form_id: event.form_id,
2028
+ form_provider: event.form_type || "unknown",
2029
+ form_field_type: event.field_type || null,
2030
+ form_field_name: event.field_name || null,
2031
+ form_event_type: event.event_type,
2032
+ submit_source: submitSource,
2033
+ is_inferred: isSuccessEvent,
2034
+ time_to_submit_seconds: event.time_to_submit_ms ? Math.round(event.time_to_submit_ms / 1e3) : null
2035
+ });
2036
+ }
2037
+ });
2038
+ formTracker.start();
2039
+ }
2040
+ if (features.spa) {
2041
+ spaRouter = new SPARouter({
2042
+ onNavigate: (event) => {
2043
+ log("SPA navigation:", event.navigation_type);
2044
+ pageview(event.to_url);
2045
+ }
2046
+ });
2047
+ spaRouter.start();
2048
+ }
1869
2049
  document.addEventListener("click", (e) => {
1870
2050
  const target = e.target;
1871
2051
  const link = target.closest("a");
1872
2052
  if (link && link.href) {
1873
2053
  const isExternal = link.hostname !== window.location.hostname;
1874
2054
  queueEvent("click", {
1875
- element: "link",
2055
+ element_type: "link",
1876
2056
  href: truncateText(link.href, 200),
1877
2057
  text: truncateText(link.textContent || "", 100),
1878
2058
  is_external: isExternal
@@ -1882,15 +2062,32 @@ var Loamly = (() => {
1882
2062
  }
1883
2063
  function queueEvent(eventType, data) {
1884
2064
  if (!eventQueue) return;
1885
- eventQueue.push(eventType, {
2065
+ if (!config.apiKey) {
2066
+ log("Missing apiKey, behavioral event skipped:", eventType);
2067
+ return;
2068
+ }
2069
+ if (!workspaceId) {
2070
+ log("Missing workspaceId, behavioral event skipped:", eventType);
2071
+ return;
2072
+ }
2073
+ if (!sessionId) {
2074
+ log("Missing sessionId, behavioral event skipped:", eventType);
2075
+ return;
2076
+ }
2077
+ const idempotencyKey = buildIdempotencyKey(eventType);
2078
+ const payload = {
1886
2079
  visitor_id: visitorId,
1887
2080
  session_id: sessionId,
1888
2081
  event_type: eventType,
1889
- ...data,
1890
- url: window.location.href,
2082
+ event_data: data,
2083
+ page_url: window.location.href,
2084
+ page_path: window.location.pathname,
1891
2085
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1892
- tracker_version: VERSION
1893
- });
2086
+ tracker_version: VERSION,
2087
+ idempotency_key: idempotencyKey
2088
+ };
2089
+ payload.workspace_id = workspaceId;
2090
+ eventQueue.push(eventType, payload, buildHeaders(idempotencyKey));
1894
2091
  }
1895
2092
  function handleSPANavigation(event) {
1896
2093
  log("SPA navigation:", event.navigation_type, event.to_url);
@@ -1918,34 +2115,42 @@ var Loamly = (() => {
1918
2115
  }
1919
2116
  function setupUnloadHandlers() {
1920
2117
  const handleUnload = () => {
2118
+ if (!workspaceId || !config.apiKey || !sessionId) return;
1921
2119
  const scrollEvent = scrollTracker?.getFinalEvent();
1922
2120
  if (scrollEvent) {
1923
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
2121
+ sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
2122
+ workspace_id: workspaceId,
1924
2123
  visitor_id: visitorId,
1925
2124
  session_id: sessionId,
1926
2125
  event_type: "scroll_depth_final",
1927
- data: scrollEvent,
1928
- url: window.location.href
2126
+ event_data: {
2127
+ scroll_depth: Math.round(scrollEvent.depth / 100 * 100) / 100,
2128
+ milestone: Math.round(scrollEvent.chunk / 100 * 100) / 100,
2129
+ time_to_reach_ms: scrollEvent.time_to_reach_ms
2130
+ },
2131
+ page_url: window.location.href,
2132
+ page_path: window.location.pathname,
2133
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2134
+ tracker_version: VERSION,
2135
+ idempotency_key: buildIdempotencyKey("scroll_depth_final")
1929
2136
  });
1930
2137
  }
1931
2138
  const timeEvent = timeTracker?.getFinalMetrics();
1932
2139
  if (timeEvent) {
1933
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
2140
+ sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
2141
+ workspace_id: workspaceId,
1934
2142
  visitor_id: visitorId,
1935
2143
  session_id: sessionId,
1936
- event_type: "time_spent_final",
1937
- data: timeEvent,
1938
- url: window.location.href
1939
- });
1940
- }
1941
- const agenticResult = agenticAnalyzer?.getResult();
1942
- if (agenticResult && agenticResult.agenticProbability > 0) {
1943
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1944
- visitor_id: visitorId,
1945
- session_id: sessionId,
1946
- event_type: "agentic_detection",
1947
- data: agenticResult,
1948
- url: window.location.href
2144
+ event_type: "time_spent",
2145
+ event_data: {
2146
+ visible_time_ms: timeEvent.total_time_ms,
2147
+ page_start_time: pageStartTime || Date.now()
2148
+ },
2149
+ page_url: window.location.href,
2150
+ page_path: window.location.pathname,
2151
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2152
+ tracker_version: VERSION,
2153
+ idempotency_key: buildIdempotencyKey("time_spent")
1949
2154
  });
1950
2155
  }
1951
2156
  eventQueue?.flushBeacon();
@@ -1968,30 +2173,55 @@ var Loamly = (() => {
1968
2173
  log("Not initialized, call init() first");
1969
2174
  return;
1970
2175
  }
2176
+ if (!config.apiKey) {
2177
+ log("Missing apiKey, pageview skipped");
2178
+ return;
2179
+ }
1971
2180
  const url = customUrl || window.location.href;
2181
+ const utmParams = extractUTMParams(url);
2182
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2183
+ const idempotencyKey = buildIdempotencyKey("visit");
2184
+ const agenticResult = agenticAnalyzer?.getResult();
2185
+ const pagePath = (() => {
2186
+ try {
2187
+ return new URL(url).pathname;
2188
+ } catch {
2189
+ return window.location.pathname;
2190
+ }
2191
+ })();
1972
2192
  const payload = {
1973
2193
  visitor_id: visitorId,
1974
2194
  session_id: sessionId,
1975
- url,
2195
+ page_url: url,
2196
+ page_path: pagePath,
1976
2197
  referrer: document.referrer || null,
1977
2198
  title: document.title || null,
1978
- utm_source: extractUTMParams(url).utm_source || null,
1979
- utm_medium: extractUTMParams(url).utm_medium || null,
1980
- utm_campaign: extractUTMParams(url).utm_campaign || null,
2199
+ utm_source: utmParams.utm_source || null,
2200
+ utm_medium: utmParams.utm_medium || null,
2201
+ utm_campaign: utmParams.utm_campaign || null,
2202
+ utm_term: utmParams.utm_term || null,
2203
+ utm_content: utmParams.utm_content || null,
1981
2204
  user_agent: navigator.userAgent,
1982
2205
  screen_width: window.screen?.width,
1983
2206
  screen_height: window.screen?.height,
1984
2207
  language: navigator.language,
1985
2208
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
1986
2209
  tracker_version: VERSION,
2210
+ event_type: "pageview",
2211
+ event_data: null,
2212
+ timestamp,
1987
2213
  navigation_timing: navigationTiming,
1988
2214
  ai_platform: aiDetection?.platform || null,
1989
- is_ai_referrer: aiDetection?.isAI || false
2215
+ is_ai_referrer: aiDetection?.isAI || false,
2216
+ agentic_detection: agenticResult || null
1990
2217
  };
2218
+ if (workspaceId) {
2219
+ payload.workspace_id = workspaceId;
2220
+ }
1991
2221
  log("Pageview:", payload);
1992
2222
  safeFetch(endpoint(DEFAULT_CONFIG.endpoints.visit), {
1993
2223
  method: "POST",
1994
- headers: { "Content-Type": "application/json" },
2224
+ headers: buildHeaders(idempotencyKey),
1995
2225
  body: JSON.stringify(payload)
1996
2226
  });
1997
2227
  }
@@ -2000,6 +2230,11 @@ var Loamly = (() => {
2000
2230
  log("Not initialized, call init() first");
2001
2231
  return;
2002
2232
  }
2233
+ if (!config.apiKey) {
2234
+ log("Missing apiKey, event skipped:", eventName);
2235
+ return;
2236
+ }
2237
+ const idempotencyKey = buildIdempotencyKey(`event:${eventName}`);
2003
2238
  const payload = {
2004
2239
  visitor_id: visitorId,
2005
2240
  session_id: sessionId,
@@ -2008,14 +2243,19 @@ var Loamly = (() => {
2008
2243
  properties: options.properties || {},
2009
2244
  revenue: options.revenue,
2010
2245
  currency: options.currency || "USD",
2011
- url: window.location.href,
2246
+ page_url: window.location.href,
2247
+ referrer: document.referrer || null,
2012
2248
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2013
- tracker_version: VERSION
2249
+ tracker_version: VERSION,
2250
+ idempotency_key: idempotencyKey
2014
2251
  };
2252
+ if (workspaceId) {
2253
+ payload.workspace_id = workspaceId;
2254
+ }
2015
2255
  log("Event:", eventName, payload);
2016
2256
  safeFetch(endpoint("/api/ingest/event"), {
2017
2257
  method: "POST",
2018
- headers: { "Content-Type": "application/json" },
2258
+ headers: buildHeaders(idempotencyKey),
2019
2259
  body: JSON.stringify(payload)
2020
2260
  });
2021
2261
  }
@@ -2027,17 +2267,26 @@ var Loamly = (() => {
2027
2267
  log("Not initialized, call init() first");
2028
2268
  return;
2029
2269
  }
2270
+ if (!config.apiKey) {
2271
+ log("Missing apiKey, identify skipped");
2272
+ return;
2273
+ }
2030
2274
  log("Identify:", userId, traits);
2275
+ const idempotencyKey = buildIdempotencyKey("identify");
2031
2276
  const payload = {
2032
2277
  visitor_id: visitorId,
2033
2278
  session_id: sessionId,
2034
2279
  user_id: userId,
2035
2280
  traits,
2036
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2281
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2282
+ idempotency_key: idempotencyKey
2037
2283
  };
2284
+ if (workspaceId) {
2285
+ payload.workspace_id = workspaceId;
2286
+ }
2038
2287
  safeFetch(endpoint("/api/ingest/identify"), {
2039
2288
  method: "POST",
2040
- headers: { "Content-Type": "application/json" },
2289
+ headers: buildHeaders(idempotencyKey),
2041
2290
  body: JSON.stringify(payload)
2042
2291
  });
2043
2292
  }
@@ -2168,14 +2417,15 @@ var Loamly = (() => {
2168
2417
  return initialized;
2169
2418
  }
2170
2419
  function reportHealth(status, errorMessage) {
2171
- if (!config.apiKey) return;
2172
2420
  try {
2173
2421
  const healthData = {
2174
- workspace_id: config.apiKey,
2422
+ workspace_id: workspaceId,
2423
+ visitor_id: visitorId,
2424
+ session_id: sessionId,
2175
2425
  status,
2176
2426
  error_message: errorMessage || null,
2177
- version: VERSION,
2178
- url: typeof window !== "undefined" ? window.location.href : null,
2427
+ tracker_version: VERSION,
2428
+ page_url: typeof window !== "undefined" ? window.location.href : null,
2179
2429
  user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
2180
2430
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2181
2431
  features: {
@@ -2271,15 +2521,16 @@ var Loamly = (() => {
2271
2521
  }
2272
2522
  async function resolveWorkspaceConfig(domain) {
2273
2523
  try {
2274
- const response = await fetch(`${DEFAULT_CONFIG.apiHost}${DEFAULT_CONFIG.endpoints.resolve}?domain=${encodeURIComponent(domain)}`);
2524
+ const response = await fetch(`${DEFAULT_CONFIG.apiHost}${DEFAULT_CONFIG.endpoints.resolve}?d=${encodeURIComponent(domain)}`);
2275
2525
  if (!response.ok) {
2276
2526
  console.warn("[Loamly] Failed to resolve workspace for domain:", domain);
2277
2527
  return null;
2278
2528
  }
2279
2529
  const data = await response.json();
2280
- if (data.workspace_id) {
2530
+ if (data.workspace_id && data.public_key) {
2281
2531
  return {
2282
- apiKey: data.workspace_api_key,
2532
+ apiKey: data.public_key,
2533
+ workspaceId: data.workspace_id,
2283
2534
  apiHost: DEFAULT_CONFIG.apiHost
2284
2535
  };
2285
2536
  }
@@ -2297,6 +2548,9 @@ var Loamly = (() => {
2297
2548
  if (script.dataset.apiKey) {
2298
2549
  config2.apiKey = script.dataset.apiKey;
2299
2550
  }
2551
+ if (script.dataset.workspaceId) {
2552
+ config2.workspaceId = script.dataset.workspaceId;
2553
+ }
2300
2554
  if (script.dataset.apiHost) {
2301
2555
  config2.apiHost = script.dataset.apiHost;
2302
2556
  }
@@ -2309,7 +2563,7 @@ var Loamly = (() => {
2309
2563
  if (script.dataset.disableBehavioral === "true") {
2310
2564
  config2.disableBehavioral = true;
2311
2565
  }
2312
- if (config2.apiKey) {
2566
+ if (config2.apiKey || config2.workspaceId) {
2313
2567
  return config2;
2314
2568
  }
2315
2569
  }
@@ -2330,6 +2584,16 @@ var Loamly = (() => {
2330
2584
  loamly.init(dataConfig);
2331
2585
  return;
2332
2586
  }
2587
+ const urlParams = new URLSearchParams(window.location.search);
2588
+ const apiKeyParam = urlParams.get("api_key");
2589
+ const workspaceIdParam = urlParams.get("workspace_id");
2590
+ if (apiKeyParam || workspaceIdParam) {
2591
+ loamly.init({
2592
+ apiKey: apiKeyParam || void 0,
2593
+ workspaceId: workspaceIdParam || void 0
2594
+ });
2595
+ return;
2596
+ }
2333
2597
  const currentDomain = window.location.hostname;
2334
2598
  if (currentDomain && currentDomain !== "localhost") {
2335
2599
  const resolvedConfig = await resolveWorkspaceConfig(currentDomain);