@aranova/tracking-react 0.17.2 → 0.17.3

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
@@ -654,9 +654,9 @@ function resolveConversionConfig(options) {
654
654
  }
655
655
  }
656
656
  function notifyResolved() {
657
- const listeners = [...resolveListeners];
657
+ const listeners2 = [...resolveListeners];
658
658
  resolveListeners.clear();
659
- for (const listener of listeners) {
659
+ for (const listener of listeners2) {
660
660
  try {
661
661
  listener();
662
662
  } catch {
@@ -1424,6 +1424,14 @@ var pageExitMetadataSchema = z7.object({
1424
1424
  // null = left without any scroll signal; floor is 0 so a valid 0% is never
1425
1425
  // rejected (a single bad field 422s the whole keepalive beacon batch).
1426
1426
  max_scroll_percent: z7.number().int().min(0).max(100).nullable(),
1427
+ // The gating baseline: the fraction of the page visible at load with
1428
+ // zero scrolling — or, for pages that grew after the post-paint snapshot
1429
+ // (skeleton/streaming renders), the first-scroll position that
1430
+ // established it. null = page had no scrollable range, or the segment
1431
+ // ended before the snapshot. `.optional()` is load-bearing: SDK builds
1432
+ // predating this field keep POSTing page_exit without the key —
1433
+ // requiring it would 422 whole keepalive beacon batches.
1434
+ scroll_baseline_percent: z7.number().int().min(0).max(100).nullable().optional(),
1427
1435
  page: z7.object({
1428
1436
  path: z7.string()
1429
1437
  }).strict()
@@ -1693,6 +1701,97 @@ function attachSpecificPageVisit(client, config) {
1693
1701
  };
1694
1702
  }
1695
1703
 
1704
+ // ../tracking-core/src/triggers/navigation.ts
1705
+ var listeners = /* @__PURE__ */ new Set();
1706
+ var restorePatch = null;
1707
+ function notify() {
1708
+ for (const listener of listeners) listener();
1709
+ }
1710
+ function notifyDeferred() {
1711
+ setTimeout(notify, 0);
1712
+ }
1713
+ function installPatch() {
1714
+ const originalPushState = history.pushState;
1715
+ const originalReplaceState = history.replaceState;
1716
+ function patchedPushState(...args) {
1717
+ originalPushState.apply(this, args);
1718
+ notifyDeferred();
1719
+ }
1720
+ function patchedReplaceState(...args) {
1721
+ originalReplaceState.apply(this, args);
1722
+ notifyDeferred();
1723
+ }
1724
+ history.pushState = patchedPushState;
1725
+ history.replaceState = patchedReplaceState;
1726
+ window.addEventListener("popstate", notify);
1727
+ restorePatch = () => {
1728
+ history.pushState = originalPushState;
1729
+ history.replaceState = originalReplaceState;
1730
+ window.removeEventListener("popstate", notify);
1731
+ restorePatch = null;
1732
+ };
1733
+ }
1734
+ function onHistoryChange(listener) {
1735
+ if (listeners.size === 0) installPatch();
1736
+ listeners.add(listener);
1737
+ return () => {
1738
+ if (!listeners.delete(listener)) return;
1739
+ if (listeners.size === 0) restorePatch?.();
1740
+ };
1741
+ }
1742
+
1743
+ // ../tracking-core/src/triggers/scroll-measurement.ts
1744
+ var BOTTOM_EPSILON_PX = 2;
1745
+ function measureScrollPercent() {
1746
+ const root = document.scrollingElement ?? document.documentElement;
1747
+ const scrollHeight = root.scrollHeight;
1748
+ const clientHeight = root.clientHeight;
1749
+ if (scrollHeight <= 0 || clientHeight <= 0) return null;
1750
+ if (scrollHeight <= clientHeight + BOTTOM_EPSILON_PX) return null;
1751
+ const maxTop = scrollHeight - clientHeight;
1752
+ const scrollTop = Math.min(Math.max(root.scrollTop, 0), maxTop);
1753
+ if (scrollTop + clientHeight >= scrollHeight - BOTTOM_EPSILON_PX) return 100;
1754
+ return Math.max(1, Math.min(100, Math.round((scrollTop + clientHeight) / scrollHeight * 100)));
1755
+ }
1756
+ function scheduleBaselineSnapshot(onSnapshot) {
1757
+ let rafId = requestAnimationFrame(() => {
1758
+ rafId = requestAnimationFrame(() => {
1759
+ onSnapshot(measureScrollPercent());
1760
+ });
1761
+ });
1762
+ return () => cancelAnimationFrame(rafId);
1763
+ }
1764
+ function createBaselineGate() {
1765
+ let baselinePercent = null;
1766
+ let ready = false;
1767
+ let cancelSnapshot = null;
1768
+ return {
1769
+ rebaseline() {
1770
+ cancelSnapshot?.();
1771
+ ready = false;
1772
+ baselinePercent = null;
1773
+ cancelSnapshot = scheduleBaselineSnapshot((b) => {
1774
+ baselinePercent = b;
1775
+ ready = true;
1776
+ });
1777
+ },
1778
+ cancel() {
1779
+ cancelSnapshot?.();
1780
+ },
1781
+ baseline() {
1782
+ return ready ? baselinePercent : null;
1783
+ },
1784
+ sample() {
1785
+ if (!ready) return null;
1786
+ if (baselinePercent === null) {
1787
+ baselinePercent = measureScrollPercent();
1788
+ return null;
1789
+ }
1790
+ return measureScrollPercent();
1791
+ }
1792
+ };
1793
+ }
1794
+
1696
1795
  // ../tracking-core/src/triggers/scroll-depth.ts
1697
1796
  function attachScrollDepth(client, config) {
1698
1797
  if (typeof window === "undefined" || typeof document === "undefined") {
@@ -1703,17 +1802,14 @@ function attachScrollDepth(client, config) {
1703
1802
  let firedForPath = /* @__PURE__ */ new Set();
1704
1803
  let currentPath2 = window.location.pathname;
1705
1804
  let rafId = null;
1706
- function getScrollPercent() {
1707
- const doc = document.documentElement;
1708
- const scrollTop = window.scrollY || doc.scrollTop;
1709
- const scrollHeight = doc.scrollHeight;
1710
- const clientHeight = doc.clientHeight;
1711
- if (scrollHeight <= clientHeight) return 100;
1712
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1713
- }
1805
+ const gate = createBaselineGate();
1714
1806
  function checkThresholds() {
1715
- const percent = getScrollPercent();
1807
+ const percent = gate.sample();
1808
+ if (percent === null) return;
1809
+ const baseline = gate.baseline();
1810
+ if (baseline === null) return;
1716
1811
  for (const threshold of thresholds) {
1812
+ if (threshold <= baseline) continue;
1717
1813
  if (percent >= threshold && !firedForPath.has(threshold)) {
1718
1814
  firedForPath.add(threshold);
1719
1815
  client.trackEvent({
@@ -1740,28 +1836,15 @@ function attachScrollDepth(client, config) {
1740
1836
  if (newPath === currentPath2) return;
1741
1837
  currentPath2 = newPath;
1742
1838
  firedForPath = /* @__PURE__ */ new Set();
1743
- setTimeout(checkThresholds, 0);
1839
+ gate.rebaseline();
1744
1840
  }
1745
- const originalPushState = history.pushState.bind(history);
1746
- const originalReplaceState = history.replaceState.bind(history);
1747
- function patchedPushState(...args) {
1748
- originalPushState(...args);
1749
- setTimeout(resetIfPathChanged, 0);
1750
- }
1751
- function patchedReplaceState(...args) {
1752
- originalReplaceState(...args);
1753
- setTimeout(resetIfPathChanged, 0);
1754
- }
1755
- history.pushState = patchedPushState;
1756
- history.replaceState = patchedReplaceState;
1757
- window.addEventListener("popstate", resetIfPathChanged);
1841
+ const unsubscribeNav = onHistoryChange(resetIfPathChanged);
1758
1842
  window.addEventListener("scroll", onScroll, { passive: true });
1759
- setTimeout(checkThresholds, 0);
1843
+ gate.rebaseline();
1760
1844
  return () => {
1761
1845
  if (rafId !== null) cancelAnimationFrame(rafId);
1762
- history.pushState = originalPushState;
1763
- history.replaceState = originalReplaceState;
1764
- window.removeEventListener("popstate", resetIfPathChanged);
1846
+ gate.cancel();
1847
+ unsubscribeNav();
1765
1848
  window.removeEventListener("scroll", onScroll);
1766
1849
  };
1767
1850
  }
@@ -1933,21 +2016,14 @@ function attachPageExit(client) {
1933
2016
  let accumulatedMs = 0;
1934
2017
  let maxScrollPercent = null;
1935
2018
  let rafId = null;
1936
- function getScrollPercent() {
1937
- const doc = document.documentElement;
1938
- const scrollTop = window.scrollY || doc.scrollTop;
1939
- const scrollHeight = doc.scrollHeight;
1940
- const clientHeight = doc.clientHeight;
1941
- if (scrollHeight <= clientHeight) return 100;
1942
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1943
- }
2019
+ const gate = createBaselineGate();
1944
2020
  function onScroll() {
1945
2021
  if (rafId !== null) return;
1946
2022
  rafId = requestAnimationFrame(() => {
1947
2023
  rafId = null;
1948
- const percent = getScrollPercent();
1949
- if (percent >= 1 && (maxScrollPercent === null || percent > maxScrollPercent)) {
1950
- maxScrollPercent = Math.min(percent, 100);
2024
+ const percent = gate.sample();
2025
+ if (percent !== null && (maxScrollPercent === null || percent > maxScrollPercent)) {
2026
+ maxScrollPercent = percent;
1951
2027
  }
1952
2028
  });
1953
2029
  }
@@ -1966,6 +2042,12 @@ function attachPageExit(client) {
1966
2042
  metadata: {
1967
2043
  dwell_ms: dwell,
1968
2044
  max_scroll_percent: maxScrollPercent,
2045
+ // Lets the backend tell "scrolled to the bottom" apart from "the page
2046
+ // was barely scrollable". null = unscrollable page or the segment
2047
+ // ended before the post-paint snapshot landed. For pages that grew
2048
+ // after the snapshot this is the first-scroll position, not the
2049
+ // at-load fraction (see BaselineGate.baseline).
2050
+ scroll_baseline_percent: gate.baseline(),
1969
2051
  page: { path }
1970
2052
  },
1971
2053
  pageUrl: window.location.href,
@@ -1983,6 +2065,7 @@ function attachPageExit(client) {
1983
2065
  emitSegment(currentPath2, false);
1984
2066
  currentPath2 = newPath;
1985
2067
  maxScrollPercent = null;
2068
+ gate.rebaseline();
1986
2069
  accumulatedMs = 0;
1987
2070
  activeSince = document.visibilityState === "visible" ? Date.now() : null;
1988
2071
  }
@@ -1996,27 +2079,15 @@ function attachPageExit(client) {
1996
2079
  function onPageHide() {
1997
2080
  emitSegment(currentPath2, true);
1998
2081
  }
1999
- const originalPushState = history.pushState.bind(history);
2000
- const originalReplaceState = history.replaceState.bind(history);
2001
- function patchedPushState(...args) {
2002
- originalPushState(...args);
2003
- setTimeout(onNavigate, 0);
2004
- }
2005
- function patchedReplaceState(...args) {
2006
- originalReplaceState(...args);
2007
- setTimeout(onNavigate, 0);
2008
- }
2009
- history.pushState = patchedPushState;
2010
- history.replaceState = patchedReplaceState;
2011
- window.addEventListener("popstate", onNavigate);
2082
+ const unsubscribeNav = onHistoryChange(onNavigate);
2012
2083
  window.addEventListener("scroll", onScroll, { passive: true });
2013
2084
  document.addEventListener("visibilitychange", onVisibilityChange);
2014
2085
  window.addEventListener("pagehide", onPageHide);
2086
+ gate.rebaseline();
2015
2087
  return () => {
2016
2088
  if (rafId !== null) cancelAnimationFrame(rafId);
2017
- history.pushState = originalPushState;
2018
- history.replaceState = originalReplaceState;
2019
- window.removeEventListener("popstate", onNavigate);
2089
+ gate.cancel();
2090
+ unsubscribeNav();
2020
2091
  window.removeEventListener("scroll", onScroll);
2021
2092
  document.removeEventListener("visibilitychange", onVisibilityChange);
2022
2093
  window.removeEventListener("pagehide", onPageHide);
@@ -2821,7 +2892,7 @@ function GoogleAdsTracking(props) {
2821
2892
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo4 } from "react";
2822
2893
 
2823
2894
  // package.json
2824
- var version = "0.17.2";
2895
+ var version = "0.17.3";
2825
2896
 
2826
2897
  // ../tracking-core/src/phone-react.tsx
2827
2898
  import {