@aranova/tracking-next 0.17.1 → 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 {
@@ -1040,6 +1040,27 @@ function getOrCreateTrackingClient(config) {
1040
1040
  globalClientKey = key;
1041
1041
  return globalClient;
1042
1042
  }
1043
+ var clientCaptureRegistry = /* @__PURE__ */ new WeakMap();
1044
+ function attachClientCapturesOnce(client, build) {
1045
+ let entry = clientCaptureRegistry.get(client);
1046
+ if (entry === void 0) {
1047
+ entry = { detach: build(), refCount: 0 };
1048
+ clientCaptureRegistry.set(client, entry);
1049
+ }
1050
+ entry.refCount += 1;
1051
+ let released = false;
1052
+ return () => {
1053
+ if (released) return;
1054
+ released = true;
1055
+ const current = clientCaptureRegistry.get(client);
1056
+ if (current === void 0) return;
1057
+ current.refCount -= 1;
1058
+ if (current.refCount <= 0) {
1059
+ current.detach();
1060
+ clientCaptureRegistry.delete(client);
1061
+ }
1062
+ };
1063
+ }
1043
1064
  function createTrackingClient(config) {
1044
1065
  const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
1045
1066
  const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);
@@ -1309,6 +1330,14 @@ var pageExitMetadataSchema = z7.object({
1309
1330
  // null = left without any scroll signal; floor is 0 so a valid 0% is never
1310
1331
  // rejected (a single bad field 422s the whole keepalive beacon batch).
1311
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(),
1312
1341
  page: z7.object({
1313
1342
  path: z7.string()
1314
1343
  }).strict()
@@ -1533,6 +1562,97 @@ function attachTimeOnSite(client, config) {
1533
1562
  };
1534
1563
  }
1535
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
+
1536
1656
  // ../tracking-core/src/triggers/scroll-depth.ts
1537
1657
  function attachScrollDepth(client, config) {
1538
1658
  if (typeof window === "undefined" || typeof document === "undefined") {
@@ -1543,17 +1663,14 @@ function attachScrollDepth(client, config) {
1543
1663
  let firedForPath = /* @__PURE__ */ new Set();
1544
1664
  let currentPath2 = window.location.pathname;
1545
1665
  let rafId = null;
1546
- function getScrollPercent() {
1547
- const doc = document.documentElement;
1548
- const scrollTop = window.scrollY || doc.scrollTop;
1549
- const scrollHeight = doc.scrollHeight;
1550
- const clientHeight = doc.clientHeight;
1551
- if (scrollHeight <= clientHeight) return 100;
1552
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1553
- }
1666
+ const gate = createBaselineGate();
1554
1667
  function checkThresholds() {
1555
- const percent = getScrollPercent();
1668
+ const percent = gate.sample();
1669
+ if (percent === null) return;
1670
+ const baseline = gate.baseline();
1671
+ if (baseline === null) return;
1556
1672
  for (const threshold of thresholds) {
1673
+ if (threshold <= baseline) continue;
1557
1674
  if (percent >= threshold && !firedForPath.has(threshold)) {
1558
1675
  firedForPath.add(threshold);
1559
1676
  client.trackEvent({
@@ -1580,28 +1697,15 @@ function attachScrollDepth(client, config) {
1580
1697
  if (newPath === currentPath2) return;
1581
1698
  currentPath2 = newPath;
1582
1699
  firedForPath = /* @__PURE__ */ new Set();
1583
- setTimeout(checkThresholds, 0);
1700
+ gate.rebaseline();
1584
1701
  }
1585
- const originalPushState = history.pushState.bind(history);
1586
- const originalReplaceState = history.replaceState.bind(history);
1587
- function patchedPushState(...args) {
1588
- originalPushState(...args);
1589
- setTimeout(resetIfPathChanged, 0);
1590
- }
1591
- function patchedReplaceState(...args) {
1592
- originalReplaceState(...args);
1593
- setTimeout(resetIfPathChanged, 0);
1594
- }
1595
- history.pushState = patchedPushState;
1596
- history.replaceState = patchedReplaceState;
1597
- window.addEventListener("popstate", resetIfPathChanged);
1702
+ const unsubscribeNav = onHistoryChange(resetIfPathChanged);
1598
1703
  window.addEventListener("scroll", onScroll, { passive: true });
1599
- setTimeout(checkThresholds, 0);
1704
+ gate.rebaseline();
1600
1705
  return () => {
1601
1706
  if (rafId !== null) cancelAnimationFrame(rafId);
1602
- history.pushState = originalPushState;
1603
- history.replaceState = originalReplaceState;
1604
- window.removeEventListener("popstate", resetIfPathChanged);
1707
+ gate.cancel();
1708
+ unsubscribeNav();
1605
1709
  window.removeEventListener("scroll", onScroll);
1606
1710
  };
1607
1711
  }
@@ -1683,21 +1787,14 @@ function attachPageExit(client) {
1683
1787
  let accumulatedMs = 0;
1684
1788
  let maxScrollPercent = null;
1685
1789
  let rafId = null;
1686
- function getScrollPercent() {
1687
- const doc = document.documentElement;
1688
- const scrollTop = window.scrollY || doc.scrollTop;
1689
- const scrollHeight = doc.scrollHeight;
1690
- const clientHeight = doc.clientHeight;
1691
- if (scrollHeight <= clientHeight) return 100;
1692
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1693
- }
1790
+ const gate = createBaselineGate();
1694
1791
  function onScroll() {
1695
1792
  if (rafId !== null) return;
1696
1793
  rafId = requestAnimationFrame(() => {
1697
1794
  rafId = null;
1698
- const percent = getScrollPercent();
1699
- if (percent >= 1 && (maxScrollPercent === null || percent > maxScrollPercent)) {
1700
- maxScrollPercent = Math.min(percent, 100);
1795
+ const percent = gate.sample();
1796
+ if (percent !== null && (maxScrollPercent === null || percent > maxScrollPercent)) {
1797
+ maxScrollPercent = percent;
1701
1798
  }
1702
1799
  });
1703
1800
  }
@@ -1716,6 +1813,12 @@ function attachPageExit(client) {
1716
1813
  metadata: {
1717
1814
  dwell_ms: dwell,
1718
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(),
1719
1822
  page: { path }
1720
1823
  },
1721
1824
  pageUrl: window.location.href,
@@ -1733,6 +1836,7 @@ function attachPageExit(client) {
1733
1836
  emitSegment(currentPath2, false);
1734
1837
  currentPath2 = newPath;
1735
1838
  maxScrollPercent = null;
1839
+ gate.rebaseline();
1736
1840
  accumulatedMs = 0;
1737
1841
  activeSince = document.visibilityState === "visible" ? Date.now() : null;
1738
1842
  }
@@ -1746,27 +1850,15 @@ function attachPageExit(client) {
1746
1850
  function onPageHide() {
1747
1851
  emitSegment(currentPath2, true);
1748
1852
  }
1749
- const originalPushState = history.pushState.bind(history);
1750
- const originalReplaceState = history.replaceState.bind(history);
1751
- function patchedPushState(...args) {
1752
- originalPushState(...args);
1753
- setTimeout(onNavigate, 0);
1754
- }
1755
- function patchedReplaceState(...args) {
1756
- originalReplaceState(...args);
1757
- setTimeout(onNavigate, 0);
1758
- }
1759
- history.pushState = patchedPushState;
1760
- history.replaceState = patchedReplaceState;
1761
- window.addEventListener("popstate", onNavigate);
1853
+ const unsubscribeNav = onHistoryChange(onNavigate);
1762
1854
  window.addEventListener("scroll", onScroll, { passive: true });
1763
1855
  document.addEventListener("visibilitychange", onVisibilityChange);
1764
1856
  window.addEventListener("pagehide", onPageHide);
1857
+ gate.rebaseline();
1765
1858
  return () => {
1766
1859
  if (rafId !== null) cancelAnimationFrame(rafId);
1767
- history.pushState = originalPushState;
1768
- history.replaceState = originalReplaceState;
1769
- window.removeEventListener("popstate", onNavigate);
1860
+ gate.cancel();
1861
+ unsubscribeNav();
1770
1862
  window.removeEventListener("scroll", onScroll);
1771
1863
  document.removeEventListener("visibilitychange", onVisibilityChange);
1772
1864
  window.removeEventListener("pagehide", onPageHide);
@@ -2585,7 +2677,7 @@ function GoogleAdsTracking(props) {
2585
2677
  import { usePathname, useSearchParams } from "next/navigation";
2586
2678
 
2587
2679
  // package.json
2588
- var version = "0.17.1";
2680
+ var version = "0.17.3";
2589
2681
 
2590
2682
  // src/factory.tsx
2591
2683
  import {
@@ -2799,33 +2891,6 @@ function createTracking(options) {
2799
2891
  });
2800
2892
  }
2801
2893
  }, [pathname]);
2802
- useEffect3(() => {
2803
- const rawClient = getOrCreateTrackingClient({
2804
- apiKey,
2805
- endpoint,
2806
- surface: "next",
2807
- packageName: "@aranova/tracking-next",
2808
- triggers,
2809
- environment,
2810
- activeGtagIds: gtagIds,
2811
- debug
2812
- });
2813
- return attachBfcacheRestore(wrapClient(rawClient));
2814
- }, []);
2815
- useEffect3(() => {
2816
- if (!timeOnSiteConfig) return;
2817
- const rawClient = getOrCreateTrackingClient({
2818
- apiKey,
2819
- endpoint,
2820
- surface: "next",
2821
- packageName: "@aranova/tracking-next",
2822
- triggers,
2823
- environment,
2824
- activeGtagIds: gtagIds,
2825
- debug
2826
- });
2827
- return attachTimeOnSite(wrapClient(rawClient), timeOnSiteConfig);
2828
- }, []);
2829
2894
  const multiPageSessionConfig = triggers.automatic.multi_page_session ?? null;
2830
2895
  const mpsLastPathRef = useRef(null);
2831
2896
  useEffect3(() => {
@@ -2876,7 +2941,6 @@ function createTracking(options) {
2876
2941
  }
2877
2942
  }, [pathname]);
2878
2943
  useEffect3(() => {
2879
- const detachers = [];
2880
2944
  const rawClient = getOrCreateTrackingClient({
2881
2945
  apiKey,
2882
2946
  endpoint,
@@ -2887,29 +2951,36 @@ function createTracking(options) {
2887
2951
  activeGtagIds: gtagIds,
2888
2952
  debug
2889
2953
  });
2890
- const detectorClient = wrapClient(rawClient);
2891
- detachers.push(attachPageExit(detectorClient));
2892
- const scrollDepth = triggers.automatic.scroll_depth;
2893
- if (scrollDepth) {
2894
- detachers.push(attachScrollDepth(detectorClient, scrollDepth));
2895
- }
2896
- const formStart = triggers.automatic.form_start;
2897
- if (formStart) {
2898
- detachers.push(attachFormStart(detectorClient, formStart));
2899
- }
2900
- const ctaClick = triggers.manual?.cta_click;
2901
- if (ctaClick) {
2902
- detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
2903
- }
2904
- const phoneClick = triggers.manual?.phone_click;
2905
- if (phoneClick) {
2906
- detachers.push(attachPhoneClickCapture(detectorClient, phoneClick));
2907
- }
2908
- return () => {
2909
- for (let i = detachers.length - 1; i >= 0; i--) {
2910
- detachers[i]();
2954
+ return attachClientCapturesOnce(rawClient, () => {
2955
+ const detachers = [];
2956
+ const detectorClient = wrapClient(rawClient);
2957
+ detachers.push(attachPageExit(detectorClient));
2958
+ detachers.push(attachBfcacheRestore(detectorClient));
2959
+ if (timeOnSiteConfig) {
2960
+ detachers.push(attachTimeOnSite(detectorClient, timeOnSiteConfig));
2961
+ }
2962
+ const scrollDepth = triggers.automatic.scroll_depth;
2963
+ if (scrollDepth) {
2964
+ detachers.push(attachScrollDepth(detectorClient, scrollDepth));
2911
2965
  }
2912
- };
2966
+ const formStart = triggers.automatic.form_start;
2967
+ if (formStart) {
2968
+ detachers.push(attachFormStart(detectorClient, formStart));
2969
+ }
2970
+ const ctaClick = triggers.manual?.cta_click;
2971
+ if (ctaClick) {
2972
+ detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
2973
+ }
2974
+ const phoneClick = triggers.manual?.phone_click;
2975
+ if (phoneClick) {
2976
+ detachers.push(attachPhoneClickCapture(detectorClient, phoneClick));
2977
+ }
2978
+ return () => {
2979
+ for (let i = detachers.length - 1; i >= 0; i--) {
2980
+ detachers[i]();
2981
+ }
2982
+ };
2983
+ });
2913
2984
  }, []);
2914
2985
  return /* @__PURE__ */ jsxs4(TrackingContext.Provider, { value: client, children: [
2915
2986
  /* @__PURE__ */ jsx5(Suspense, { fallback: null, children: /* @__PURE__ */ jsx5(PageViewTracker, { fire: firePageView }) }),