@aranova/tracking-next 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
@@ -598,9 +598,9 @@ function resolveConversionConfig(options) {
598
598
  }
599
599
  }
600
600
  function notifyResolved() {
601
- const listeners = [...resolveListeners];
601
+ const listeners2 = [...resolveListeners];
602
602
  resolveListeners.clear();
603
- for (const listener of listeners) {
603
+ for (const listener of listeners2) {
604
604
  try {
605
605
  listener();
606
606
  } catch {
@@ -1330,6 +1330,14 @@ var pageExitMetadataSchema = z7.object({
1330
1330
  // null = left without any scroll signal; floor is 0 so a valid 0% is never
1331
1331
  // rejected (a single bad field 422s the whole keepalive beacon batch).
1332
1332
  max_scroll_percent: z7.number().int().min(0).max(100).nullable(),
1333
+ // The gating baseline: the fraction of the page visible at load with
1334
+ // zero scrolling — or, for pages that grew after the post-paint snapshot
1335
+ // (skeleton/streaming renders), the first-scroll position that
1336
+ // established it. null = page had no scrollable range, or the segment
1337
+ // ended before the snapshot. `.optional()` is load-bearing: SDK builds
1338
+ // predating this field keep POSTing page_exit without the key —
1339
+ // requiring it would 422 whole keepalive beacon batches.
1340
+ scroll_baseline_percent: z7.number().int().min(0).max(100).nullable().optional(),
1333
1341
  page: z7.object({
1334
1342
  path: z7.string()
1335
1343
  }).strict()
@@ -1554,6 +1562,97 @@ function attachTimeOnSite(client, config) {
1554
1562
  };
1555
1563
  }
1556
1564
 
1565
+ // ../tracking-core/src/triggers/navigation.ts
1566
+ var listeners = /* @__PURE__ */ new Set();
1567
+ var restorePatch = null;
1568
+ function notify() {
1569
+ for (const listener of listeners) listener();
1570
+ }
1571
+ function notifyDeferred() {
1572
+ setTimeout(notify, 0);
1573
+ }
1574
+ function installPatch() {
1575
+ const originalPushState = history.pushState;
1576
+ const originalReplaceState = history.replaceState;
1577
+ function patchedPushState(...args) {
1578
+ originalPushState.apply(this, args);
1579
+ notifyDeferred();
1580
+ }
1581
+ function patchedReplaceState(...args) {
1582
+ originalReplaceState.apply(this, args);
1583
+ notifyDeferred();
1584
+ }
1585
+ history.pushState = patchedPushState;
1586
+ history.replaceState = patchedReplaceState;
1587
+ window.addEventListener("popstate", notify);
1588
+ restorePatch = () => {
1589
+ history.pushState = originalPushState;
1590
+ history.replaceState = originalReplaceState;
1591
+ window.removeEventListener("popstate", notify);
1592
+ restorePatch = null;
1593
+ };
1594
+ }
1595
+ function onHistoryChange(listener) {
1596
+ if (listeners.size === 0) installPatch();
1597
+ listeners.add(listener);
1598
+ return () => {
1599
+ if (!listeners.delete(listener)) return;
1600
+ if (listeners.size === 0) restorePatch?.();
1601
+ };
1602
+ }
1603
+
1604
+ // ../tracking-core/src/triggers/scroll-measurement.ts
1605
+ var BOTTOM_EPSILON_PX = 2;
1606
+ function measureScrollPercent() {
1607
+ const root = document.scrollingElement ?? document.documentElement;
1608
+ const scrollHeight = root.scrollHeight;
1609
+ const clientHeight = root.clientHeight;
1610
+ if (scrollHeight <= 0 || clientHeight <= 0) return null;
1611
+ if (scrollHeight <= clientHeight + BOTTOM_EPSILON_PX) return null;
1612
+ const maxTop = scrollHeight - clientHeight;
1613
+ const scrollTop = Math.min(Math.max(root.scrollTop, 0), maxTop);
1614
+ if (scrollTop + clientHeight >= scrollHeight - BOTTOM_EPSILON_PX) return 100;
1615
+ return Math.max(1, Math.min(100, Math.round((scrollTop + clientHeight) / scrollHeight * 100)));
1616
+ }
1617
+ function scheduleBaselineSnapshot(onSnapshot) {
1618
+ let rafId = requestAnimationFrame(() => {
1619
+ rafId = requestAnimationFrame(() => {
1620
+ onSnapshot(measureScrollPercent());
1621
+ });
1622
+ });
1623
+ return () => cancelAnimationFrame(rafId);
1624
+ }
1625
+ function createBaselineGate() {
1626
+ let baselinePercent = null;
1627
+ let ready = false;
1628
+ let cancelSnapshot = null;
1629
+ return {
1630
+ rebaseline() {
1631
+ cancelSnapshot?.();
1632
+ ready = false;
1633
+ baselinePercent = null;
1634
+ cancelSnapshot = scheduleBaselineSnapshot((b) => {
1635
+ baselinePercent = b;
1636
+ ready = true;
1637
+ });
1638
+ },
1639
+ cancel() {
1640
+ cancelSnapshot?.();
1641
+ },
1642
+ baseline() {
1643
+ return ready ? baselinePercent : null;
1644
+ },
1645
+ sample() {
1646
+ if (!ready) return null;
1647
+ if (baselinePercent === null) {
1648
+ baselinePercent = measureScrollPercent();
1649
+ return null;
1650
+ }
1651
+ return measureScrollPercent();
1652
+ }
1653
+ };
1654
+ }
1655
+
1557
1656
  // ../tracking-core/src/triggers/scroll-depth.ts
1558
1657
  function attachScrollDepth(client, config) {
1559
1658
  if (typeof window === "undefined" || typeof document === "undefined") {
@@ -1564,17 +1663,14 @@ function attachScrollDepth(client, config) {
1564
1663
  let firedForPath = /* @__PURE__ */ new Set();
1565
1664
  let currentPath2 = window.location.pathname;
1566
1665
  let rafId = null;
1567
- function getScrollPercent() {
1568
- const doc = document.documentElement;
1569
- const scrollTop = window.scrollY || doc.scrollTop;
1570
- const scrollHeight = doc.scrollHeight;
1571
- const clientHeight = doc.clientHeight;
1572
- if (scrollHeight <= clientHeight) return 100;
1573
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1574
- }
1666
+ const gate = createBaselineGate();
1575
1667
  function checkThresholds() {
1576
- const percent = getScrollPercent();
1668
+ const percent = gate.sample();
1669
+ if (percent === null) return;
1670
+ const baseline = gate.baseline();
1671
+ if (baseline === null) return;
1577
1672
  for (const threshold of thresholds) {
1673
+ if (threshold <= baseline) continue;
1578
1674
  if (percent >= threshold && !firedForPath.has(threshold)) {
1579
1675
  firedForPath.add(threshold);
1580
1676
  client.trackEvent({
@@ -1601,28 +1697,15 @@ function attachScrollDepth(client, config) {
1601
1697
  if (newPath === currentPath2) return;
1602
1698
  currentPath2 = newPath;
1603
1699
  firedForPath = /* @__PURE__ */ new Set();
1604
- setTimeout(checkThresholds, 0);
1700
+ gate.rebaseline();
1605
1701
  }
1606
- const originalPushState = history.pushState.bind(history);
1607
- const originalReplaceState = history.replaceState.bind(history);
1608
- function patchedPushState(...args) {
1609
- originalPushState(...args);
1610
- setTimeout(resetIfPathChanged, 0);
1611
- }
1612
- function patchedReplaceState(...args) {
1613
- originalReplaceState(...args);
1614
- setTimeout(resetIfPathChanged, 0);
1615
- }
1616
- history.pushState = patchedPushState;
1617
- history.replaceState = patchedReplaceState;
1618
- window.addEventListener("popstate", resetIfPathChanged);
1702
+ const unsubscribeNav = onHistoryChange(resetIfPathChanged);
1619
1703
  window.addEventListener("scroll", onScroll, { passive: true });
1620
- setTimeout(checkThresholds, 0);
1704
+ gate.rebaseline();
1621
1705
  return () => {
1622
1706
  if (rafId !== null) cancelAnimationFrame(rafId);
1623
- history.pushState = originalPushState;
1624
- history.replaceState = originalReplaceState;
1625
- window.removeEventListener("popstate", resetIfPathChanged);
1707
+ gate.cancel();
1708
+ unsubscribeNav();
1626
1709
  window.removeEventListener("scroll", onScroll);
1627
1710
  };
1628
1711
  }
@@ -1704,21 +1787,14 @@ function attachPageExit(client) {
1704
1787
  let accumulatedMs = 0;
1705
1788
  let maxScrollPercent = null;
1706
1789
  let rafId = null;
1707
- function getScrollPercent() {
1708
- const doc = document.documentElement;
1709
- const scrollTop = window.scrollY || doc.scrollTop;
1710
- const scrollHeight = doc.scrollHeight;
1711
- const clientHeight = doc.clientHeight;
1712
- if (scrollHeight <= clientHeight) return 100;
1713
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1714
- }
1790
+ const gate = createBaselineGate();
1715
1791
  function onScroll() {
1716
1792
  if (rafId !== null) return;
1717
1793
  rafId = requestAnimationFrame(() => {
1718
1794
  rafId = null;
1719
- const percent = getScrollPercent();
1720
- if (percent >= 1 && (maxScrollPercent === null || percent > maxScrollPercent)) {
1721
- maxScrollPercent = Math.min(percent, 100);
1795
+ const percent = gate.sample();
1796
+ if (percent !== null && (maxScrollPercent === null || percent > maxScrollPercent)) {
1797
+ maxScrollPercent = percent;
1722
1798
  }
1723
1799
  });
1724
1800
  }
@@ -1737,6 +1813,12 @@ function attachPageExit(client) {
1737
1813
  metadata: {
1738
1814
  dwell_ms: dwell,
1739
1815
  max_scroll_percent: maxScrollPercent,
1816
+ // Lets the backend tell "scrolled to the bottom" apart from "the page
1817
+ // was barely scrollable". null = unscrollable page or the segment
1818
+ // ended before the post-paint snapshot landed. For pages that grew
1819
+ // after the snapshot this is the first-scroll position, not the
1820
+ // at-load fraction (see BaselineGate.baseline).
1821
+ scroll_baseline_percent: gate.baseline(),
1740
1822
  page: { path }
1741
1823
  },
1742
1824
  pageUrl: window.location.href,
@@ -1754,6 +1836,7 @@ function attachPageExit(client) {
1754
1836
  emitSegment(currentPath2, false);
1755
1837
  currentPath2 = newPath;
1756
1838
  maxScrollPercent = null;
1839
+ gate.rebaseline();
1757
1840
  accumulatedMs = 0;
1758
1841
  activeSince = document.visibilityState === "visible" ? Date.now() : null;
1759
1842
  }
@@ -1767,27 +1850,15 @@ function attachPageExit(client) {
1767
1850
  function onPageHide() {
1768
1851
  emitSegment(currentPath2, true);
1769
1852
  }
1770
- const originalPushState = history.pushState.bind(history);
1771
- const originalReplaceState = history.replaceState.bind(history);
1772
- function patchedPushState(...args) {
1773
- originalPushState(...args);
1774
- setTimeout(onNavigate, 0);
1775
- }
1776
- function patchedReplaceState(...args) {
1777
- originalReplaceState(...args);
1778
- setTimeout(onNavigate, 0);
1779
- }
1780
- history.pushState = patchedPushState;
1781
- history.replaceState = patchedReplaceState;
1782
- window.addEventListener("popstate", onNavigate);
1853
+ const unsubscribeNav = onHistoryChange(onNavigate);
1783
1854
  window.addEventListener("scroll", onScroll, { passive: true });
1784
1855
  document.addEventListener("visibilitychange", onVisibilityChange);
1785
1856
  window.addEventListener("pagehide", onPageHide);
1857
+ gate.rebaseline();
1786
1858
  return () => {
1787
1859
  if (rafId !== null) cancelAnimationFrame(rafId);
1788
- history.pushState = originalPushState;
1789
- history.replaceState = originalReplaceState;
1790
- window.removeEventListener("popstate", onNavigate);
1860
+ gate.cancel();
1861
+ unsubscribeNav();
1791
1862
  window.removeEventListener("scroll", onScroll);
1792
1863
  document.removeEventListener("visibilitychange", onVisibilityChange);
1793
1864
  window.removeEventListener("pagehide", onPageHide);
@@ -2606,7 +2677,7 @@ function GoogleAdsTracking(props) {
2606
2677
  import { usePathname, useSearchParams } from "next/navigation";
2607
2678
 
2608
2679
  // package.json
2609
- var version = "0.17.2";
2680
+ var version = "0.17.3";
2610
2681
 
2611
2682
  // src/factory.tsx
2612
2683
  import {