@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.
@@ -26,7 +26,7 @@ var Loamly = (() => {
26
26
  });
27
27
 
28
28
  // src/config.ts
29
- var VERSION = "2.1.0";
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,7 +1835,11 @@ 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
+ }
1777
1843
  const features = {
1778
1844
  scroll: true,
1779
1845
  time: true,
@@ -1791,13 +1857,11 @@ var Loamly = (() => {
1791
1857
  log("Features:", features);
1792
1858
  visitorId = getVisitorId();
1793
1859
  log("Visitor ID:", visitorId);
1794
- const session = getSessionId();
1795
- sessionId = session.sessionId;
1796
- log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
1797
1860
  if (features.eventQueue) {
1798
1861
  eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1799
1862
  batchSize: DEFAULT_CONFIG.batchSize,
1800
- batchTimeout: DEFAULT_CONFIG.batchTimeout
1863
+ batchTimeout: DEFAULT_CONFIG.batchTimeout,
1864
+ apiKey: config.apiKey
1801
1865
  });
1802
1866
  }
1803
1867
  navigationTiming = detectNavigationType();
@@ -1807,37 +1871,40 @@ var Loamly = (() => {
1807
1871
  log("AI detected:", aiDetection);
1808
1872
  }
1809
1873
  initialized = true;
1810
- if (!userConfig.disableAutoPageview) {
1811
- pageview();
1812
- }
1813
- if (!userConfig.disableBehavioral) {
1814
- setupAdvancedBehavioralTracking(features);
1815
- }
1816
- if (features.behavioralML) {
1817
- behavioralClassifier = new BehavioralClassifier(1e4);
1818
- behavioralClassifier.setOnClassify(handleBehavioralClassification);
1819
- setupBehavioralMLTracking();
1820
- }
1821
- if (features.focusBlur) {
1822
- focusBlurAnalyzer = new FocusBlurAnalyzer();
1823
- focusBlurAnalyzer.initTracking();
1824
- setTimeout(() => {
1825
- if (focusBlurAnalyzer) {
1826
- handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
1827
- }
1828
- }, 5e3);
1829
- }
1830
1874
  if (features.agentic) {
1831
1875
  agenticAnalyzer = new AgenticBrowserAnalyzer();
1832
1876
  agenticAnalyzer.init();
1833
1877
  }
1834
- if (features.ping && visitorId && sessionId) {
1835
- pingService = new PingService(sessionId, visitorId, VERSION, {
1836
- interval: DEFAULT_CONFIG.pingInterval,
1837
- endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping)
1838
- });
1839
- pingService.start();
1840
- }
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
+ });
1841
1908
  spaRouter = new SPARouter({
1842
1909
  onNavigate: handleSPANavigation
1843
1910
  });
@@ -1846,6 +1913,78 @@ var Loamly = (() => {
1846
1913
  reportHealth("initialized");
1847
1914
  log("Initialization complete");
1848
1915
  }
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
+ }
1930
+ });
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;
1953
+ }
1954
+ }
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
+ })
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 {
1982
+ }
1983
+ }
1984
+ const session = getSessionId();
1985
+ sessionId = session.sessionId;
1986
+ log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
1987
+ }
1849
1988
  function setupAdvancedBehavioralTracking(features) {
1850
1989
  if (features.scroll) {
1851
1990
  scrollTracker = new ScrollTracker({
@@ -1853,8 +1992,8 @@ var Loamly = (() => {
1853
1992
  onChunkReached: (event) => {
1854
1993
  log("Scroll chunk:", event.chunk);
1855
1994
  queueEvent("scroll_depth", {
1856
- depth: event.depth,
1857
- chunk: event.chunk,
1995
+ scroll_depth: Math.round(event.depth / 100 * 100) / 100,
1996
+ milestone: Math.round(event.chunk / 100 * 100) / 100,
1858
1997
  time_to_reach_ms: event.time_to_reach_ms
1859
1998
  });
1860
1999
  }
@@ -1868,10 +2007,8 @@ var Loamly = (() => {
1868
2007
  onUpdate: (event) => {
1869
2008
  if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
1870
2009
  queueEvent("time_spent", {
1871
- active_time_ms: event.active_time_ms,
1872
- total_time_ms: event.total_time_ms,
1873
- idle_time_ms: event.idle_time_ms,
1874
- is_engaged: event.is_engaged
2010
+ visible_time_ms: event.total_time_ms,
2011
+ page_start_time: pageStartTime || Date.now()
1875
2012
  });
1876
2013
  }
1877
2014
  }
@@ -1882,13 +2019,19 @@ var Loamly = (() => {
1882
2019
  formTracker = new FormTracker({
1883
2020
  onFormEvent: (event) => {
1884
2021
  log("Form event:", event.event_type, event.form_id);
1885
- queueEvent(event.event_type, {
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, {
1886
2027
  form_id: event.form_id,
1887
- form_type: event.form_type,
1888
- field_name: event.field_name,
1889
- field_type: event.field_type,
1890
- time_to_submit_ms: event.time_to_submit_ms,
1891
- is_conversion: event.is_conversion
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
1892
2035
  });
1893
2036
  }
1894
2037
  });
@@ -1909,7 +2052,7 @@ var Loamly = (() => {
1909
2052
  if (link && link.href) {
1910
2053
  const isExternal = link.hostname !== window.location.hostname;
1911
2054
  queueEvent("click", {
1912
- element: "link",
2055
+ element_type: "link",
1913
2056
  href: truncateText(link.href, 200),
1914
2057
  text: truncateText(link.textContent || "", 100),
1915
2058
  is_external: isExternal
@@ -1919,15 +2062,32 @@ var Loamly = (() => {
1919
2062
  }
1920
2063
  function queueEvent(eventType, data) {
1921
2064
  if (!eventQueue) return;
1922
- 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 = {
1923
2079
  visitor_id: visitorId,
1924
2080
  session_id: sessionId,
1925
2081
  event_type: eventType,
1926
- ...data,
1927
- url: window.location.href,
2082
+ event_data: data,
2083
+ page_url: window.location.href,
2084
+ page_path: window.location.pathname,
1928
2085
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1929
- tracker_version: VERSION
1930
- });
2086
+ tracker_version: VERSION,
2087
+ idempotency_key: idempotencyKey
2088
+ };
2089
+ payload.workspace_id = workspaceId;
2090
+ eventQueue.push(eventType, payload, buildHeaders(idempotencyKey));
1931
2091
  }
1932
2092
  function handleSPANavigation(event) {
1933
2093
  log("SPA navigation:", event.navigation_type, event.to_url);
@@ -1955,34 +2115,42 @@ var Loamly = (() => {
1955
2115
  }
1956
2116
  function setupUnloadHandlers() {
1957
2117
  const handleUnload = () => {
2118
+ if (!workspaceId || !config.apiKey || !sessionId) return;
1958
2119
  const scrollEvent = scrollTracker?.getFinalEvent();
1959
2120
  if (scrollEvent) {
1960
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
2121
+ sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
2122
+ workspace_id: workspaceId,
1961
2123
  visitor_id: visitorId,
1962
2124
  session_id: sessionId,
1963
2125
  event_type: "scroll_depth_final",
1964
- data: scrollEvent,
1965
- 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")
1966
2136
  });
1967
2137
  }
1968
2138
  const timeEvent = timeTracker?.getFinalMetrics();
1969
2139
  if (timeEvent) {
1970
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
1971
- visitor_id: visitorId,
1972
- session_id: sessionId,
1973
- event_type: "time_spent_final",
1974
- data: timeEvent,
1975
- url: window.location.href
1976
- });
1977
- }
1978
- const agenticResult = agenticAnalyzer?.getResult();
1979
- if (agenticResult && agenticResult.agenticProbability > 0) {
1980
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
2140
+ sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
2141
+ workspace_id: workspaceId,
1981
2142
  visitor_id: visitorId,
1982
2143
  session_id: sessionId,
1983
- event_type: "agentic_detection",
1984
- data: agenticResult,
1985
- 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")
1986
2154
  });
1987
2155
  }
1988
2156
  eventQueue?.flushBeacon();
@@ -2005,30 +2173,55 @@ var Loamly = (() => {
2005
2173
  log("Not initialized, call init() first");
2006
2174
  return;
2007
2175
  }
2176
+ if (!config.apiKey) {
2177
+ log("Missing apiKey, pageview skipped");
2178
+ return;
2179
+ }
2008
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
+ })();
2009
2192
  const payload = {
2010
2193
  visitor_id: visitorId,
2011
2194
  session_id: sessionId,
2012
- url,
2195
+ page_url: url,
2196
+ page_path: pagePath,
2013
2197
  referrer: document.referrer || null,
2014
2198
  title: document.title || null,
2015
- utm_source: extractUTMParams(url).utm_source || null,
2016
- utm_medium: extractUTMParams(url).utm_medium || null,
2017
- 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,
2018
2204
  user_agent: navigator.userAgent,
2019
2205
  screen_width: window.screen?.width,
2020
2206
  screen_height: window.screen?.height,
2021
2207
  language: navigator.language,
2022
2208
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
2023
2209
  tracker_version: VERSION,
2210
+ event_type: "pageview",
2211
+ event_data: null,
2212
+ timestamp,
2024
2213
  navigation_timing: navigationTiming,
2025
2214
  ai_platform: aiDetection?.platform || null,
2026
- is_ai_referrer: aiDetection?.isAI || false
2215
+ is_ai_referrer: aiDetection?.isAI || false,
2216
+ agentic_detection: agenticResult || null
2027
2217
  };
2218
+ if (workspaceId) {
2219
+ payload.workspace_id = workspaceId;
2220
+ }
2028
2221
  log("Pageview:", payload);
2029
2222
  safeFetch(endpoint(DEFAULT_CONFIG.endpoints.visit), {
2030
2223
  method: "POST",
2031
- headers: { "Content-Type": "application/json" },
2224
+ headers: buildHeaders(idempotencyKey),
2032
2225
  body: JSON.stringify(payload)
2033
2226
  });
2034
2227
  }
@@ -2037,6 +2230,11 @@ var Loamly = (() => {
2037
2230
  log("Not initialized, call init() first");
2038
2231
  return;
2039
2232
  }
2233
+ if (!config.apiKey) {
2234
+ log("Missing apiKey, event skipped:", eventName);
2235
+ return;
2236
+ }
2237
+ const idempotencyKey = buildIdempotencyKey(`event:${eventName}`);
2040
2238
  const payload = {
2041
2239
  visitor_id: visitorId,
2042
2240
  session_id: sessionId,
@@ -2045,14 +2243,19 @@ var Loamly = (() => {
2045
2243
  properties: options.properties || {},
2046
2244
  revenue: options.revenue,
2047
2245
  currency: options.currency || "USD",
2048
- url: window.location.href,
2246
+ page_url: window.location.href,
2247
+ referrer: document.referrer || null,
2049
2248
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2050
- tracker_version: VERSION
2249
+ tracker_version: VERSION,
2250
+ idempotency_key: idempotencyKey
2051
2251
  };
2252
+ if (workspaceId) {
2253
+ payload.workspace_id = workspaceId;
2254
+ }
2052
2255
  log("Event:", eventName, payload);
2053
2256
  safeFetch(endpoint("/api/ingest/event"), {
2054
2257
  method: "POST",
2055
- headers: { "Content-Type": "application/json" },
2258
+ headers: buildHeaders(idempotencyKey),
2056
2259
  body: JSON.stringify(payload)
2057
2260
  });
2058
2261
  }
@@ -2064,17 +2267,26 @@ var Loamly = (() => {
2064
2267
  log("Not initialized, call init() first");
2065
2268
  return;
2066
2269
  }
2270
+ if (!config.apiKey) {
2271
+ log("Missing apiKey, identify skipped");
2272
+ return;
2273
+ }
2067
2274
  log("Identify:", userId, traits);
2275
+ const idempotencyKey = buildIdempotencyKey("identify");
2068
2276
  const payload = {
2069
2277
  visitor_id: visitorId,
2070
2278
  session_id: sessionId,
2071
2279
  user_id: userId,
2072
2280
  traits,
2073
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2281
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2282
+ idempotency_key: idempotencyKey
2074
2283
  };
2284
+ if (workspaceId) {
2285
+ payload.workspace_id = workspaceId;
2286
+ }
2075
2287
  safeFetch(endpoint("/api/ingest/identify"), {
2076
2288
  method: "POST",
2077
- headers: { "Content-Type": "application/json" },
2289
+ headers: buildHeaders(idempotencyKey),
2078
2290
  body: JSON.stringify(payload)
2079
2291
  });
2080
2292
  }
@@ -2205,14 +2417,15 @@ var Loamly = (() => {
2205
2417
  return initialized;
2206
2418
  }
2207
2419
  function reportHealth(status, errorMessage) {
2208
- if (!config.apiKey) return;
2209
2420
  try {
2210
2421
  const healthData = {
2211
- workspace_id: config.apiKey,
2422
+ workspace_id: workspaceId,
2423
+ visitor_id: visitorId,
2424
+ session_id: sessionId,
2212
2425
  status,
2213
2426
  error_message: errorMessage || null,
2214
- version: VERSION,
2215
- url: typeof window !== "undefined" ? window.location.href : null,
2427
+ tracker_version: VERSION,
2428
+ page_url: typeof window !== "undefined" ? window.location.href : null,
2216
2429
  user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
2217
2430
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2218
2431
  features: {
@@ -2308,15 +2521,16 @@ var Loamly = (() => {
2308
2521
  }
2309
2522
  async function resolveWorkspaceConfig(domain) {
2310
2523
  try {
2311
- 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)}`);
2312
2525
  if (!response.ok) {
2313
2526
  console.warn("[Loamly] Failed to resolve workspace for domain:", domain);
2314
2527
  return null;
2315
2528
  }
2316
2529
  const data = await response.json();
2317
- if (data.workspace_id) {
2530
+ if (data.workspace_id && data.public_key) {
2318
2531
  return {
2319
- apiKey: data.workspace_api_key,
2532
+ apiKey: data.public_key,
2533
+ workspaceId: data.workspace_id,
2320
2534
  apiHost: DEFAULT_CONFIG.apiHost
2321
2535
  };
2322
2536
  }
@@ -2334,6 +2548,9 @@ var Loamly = (() => {
2334
2548
  if (script.dataset.apiKey) {
2335
2549
  config2.apiKey = script.dataset.apiKey;
2336
2550
  }
2551
+ if (script.dataset.workspaceId) {
2552
+ config2.workspaceId = script.dataset.workspaceId;
2553
+ }
2337
2554
  if (script.dataset.apiHost) {
2338
2555
  config2.apiHost = script.dataset.apiHost;
2339
2556
  }
@@ -2346,7 +2563,7 @@ var Loamly = (() => {
2346
2563
  if (script.dataset.disableBehavioral === "true") {
2347
2564
  config2.disableBehavioral = true;
2348
2565
  }
2349
- if (config2.apiKey) {
2566
+ if (config2.apiKey || config2.workspaceId) {
2350
2567
  return config2;
2351
2568
  }
2352
2569
  }
@@ -2367,6 +2584,16 @@ var Loamly = (() => {
2367
2584
  loamly.init(dataConfig);
2368
2585
  return;
2369
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
+ }
2370
2597
  const currentDomain = window.location.hostname;
2371
2598
  if (currentDomain && currentDomain !== "localhost") {
2372
2599
  const resolvedConfig = await resolveWorkspaceConfig(currentDomain);