@aranova/tracking-react 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
@@ -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 {
@@ -1134,6 +1134,27 @@ function getOrCreateTrackingClient(config) {
1134
1134
  globalClientKey = key;
1135
1135
  return globalClient;
1136
1136
  }
1137
+ var clientCaptureRegistry = /* @__PURE__ */ new WeakMap();
1138
+ function attachClientCapturesOnce(client, build) {
1139
+ let entry = clientCaptureRegistry.get(client);
1140
+ if (entry === void 0) {
1141
+ entry = { detach: build(), refCount: 0 };
1142
+ clientCaptureRegistry.set(client, entry);
1143
+ }
1144
+ entry.refCount += 1;
1145
+ let released = false;
1146
+ return () => {
1147
+ if (released) return;
1148
+ released = true;
1149
+ const current = clientCaptureRegistry.get(client);
1150
+ if (current === void 0) return;
1151
+ current.refCount -= 1;
1152
+ if (current.refCount <= 0) {
1153
+ current.detach();
1154
+ clientCaptureRegistry.delete(client);
1155
+ }
1156
+ };
1157
+ }
1137
1158
  function createTrackingClient(config) {
1138
1159
  const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
1139
1160
  const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);
@@ -1403,6 +1424,14 @@ var pageExitMetadataSchema = z7.object({
1403
1424
  // null = left without any scroll signal; floor is 0 so a valid 0% is never
1404
1425
  // rejected (a single bad field 422s the whole keepalive beacon batch).
1405
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(),
1406
1435
  page: z7.object({
1407
1436
  path: z7.string()
1408
1437
  }).strict()
@@ -1672,6 +1701,97 @@ function attachSpecificPageVisit(client, config) {
1672
1701
  };
1673
1702
  }
1674
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
+
1675
1795
  // ../tracking-core/src/triggers/scroll-depth.ts
1676
1796
  function attachScrollDepth(client, config) {
1677
1797
  if (typeof window === "undefined" || typeof document === "undefined") {
@@ -1682,17 +1802,14 @@ function attachScrollDepth(client, config) {
1682
1802
  let firedForPath = /* @__PURE__ */ new Set();
1683
1803
  let currentPath2 = window.location.pathname;
1684
1804
  let rafId = null;
1685
- function getScrollPercent() {
1686
- const doc = document.documentElement;
1687
- const scrollTop = window.scrollY || doc.scrollTop;
1688
- const scrollHeight = doc.scrollHeight;
1689
- const clientHeight = doc.clientHeight;
1690
- if (scrollHeight <= clientHeight) return 100;
1691
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1692
- }
1805
+ const gate = createBaselineGate();
1693
1806
  function checkThresholds() {
1694
- const percent = getScrollPercent();
1807
+ const percent = gate.sample();
1808
+ if (percent === null) return;
1809
+ const baseline = gate.baseline();
1810
+ if (baseline === null) return;
1695
1811
  for (const threshold of thresholds) {
1812
+ if (threshold <= baseline) continue;
1696
1813
  if (percent >= threshold && !firedForPath.has(threshold)) {
1697
1814
  firedForPath.add(threshold);
1698
1815
  client.trackEvent({
@@ -1719,28 +1836,15 @@ function attachScrollDepth(client, config) {
1719
1836
  if (newPath === currentPath2) return;
1720
1837
  currentPath2 = newPath;
1721
1838
  firedForPath = /* @__PURE__ */ new Set();
1722
- setTimeout(checkThresholds, 0);
1723
- }
1724
- const originalPushState = history.pushState.bind(history);
1725
- const originalReplaceState = history.replaceState.bind(history);
1726
- function patchedPushState(...args) {
1727
- originalPushState(...args);
1728
- setTimeout(resetIfPathChanged, 0);
1839
+ gate.rebaseline();
1729
1840
  }
1730
- function patchedReplaceState(...args) {
1731
- originalReplaceState(...args);
1732
- setTimeout(resetIfPathChanged, 0);
1733
- }
1734
- history.pushState = patchedPushState;
1735
- history.replaceState = patchedReplaceState;
1736
- window.addEventListener("popstate", resetIfPathChanged);
1841
+ const unsubscribeNav = onHistoryChange(resetIfPathChanged);
1737
1842
  window.addEventListener("scroll", onScroll, { passive: true });
1738
- setTimeout(checkThresholds, 0);
1843
+ gate.rebaseline();
1739
1844
  return () => {
1740
1845
  if (rafId !== null) cancelAnimationFrame(rafId);
1741
- history.pushState = originalPushState;
1742
- history.replaceState = originalReplaceState;
1743
- window.removeEventListener("popstate", resetIfPathChanged);
1846
+ gate.cancel();
1847
+ unsubscribeNav();
1744
1848
  window.removeEventListener("scroll", onScroll);
1745
1849
  };
1746
1850
  }
@@ -1912,21 +2016,14 @@ function attachPageExit(client) {
1912
2016
  let accumulatedMs = 0;
1913
2017
  let maxScrollPercent = null;
1914
2018
  let rafId = null;
1915
- function getScrollPercent() {
1916
- const doc = document.documentElement;
1917
- const scrollTop = window.scrollY || doc.scrollTop;
1918
- const scrollHeight = doc.scrollHeight;
1919
- const clientHeight = doc.clientHeight;
1920
- if (scrollHeight <= clientHeight) return 100;
1921
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1922
- }
2019
+ const gate = createBaselineGate();
1923
2020
  function onScroll() {
1924
2021
  if (rafId !== null) return;
1925
2022
  rafId = requestAnimationFrame(() => {
1926
2023
  rafId = null;
1927
- const percent = getScrollPercent();
1928
- if (percent >= 1 && (maxScrollPercent === null || percent > maxScrollPercent)) {
1929
- maxScrollPercent = Math.min(percent, 100);
2024
+ const percent = gate.sample();
2025
+ if (percent !== null && (maxScrollPercent === null || percent > maxScrollPercent)) {
2026
+ maxScrollPercent = percent;
1930
2027
  }
1931
2028
  });
1932
2029
  }
@@ -1945,6 +2042,12 @@ function attachPageExit(client) {
1945
2042
  metadata: {
1946
2043
  dwell_ms: dwell,
1947
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(),
1948
2051
  page: { path }
1949
2052
  },
1950
2053
  pageUrl: window.location.href,
@@ -1962,6 +2065,7 @@ function attachPageExit(client) {
1962
2065
  emitSegment(currentPath2, false);
1963
2066
  currentPath2 = newPath;
1964
2067
  maxScrollPercent = null;
2068
+ gate.rebaseline();
1965
2069
  accumulatedMs = 0;
1966
2070
  activeSince = document.visibilityState === "visible" ? Date.now() : null;
1967
2071
  }
@@ -1975,27 +2079,15 @@ function attachPageExit(client) {
1975
2079
  function onPageHide() {
1976
2080
  emitSegment(currentPath2, true);
1977
2081
  }
1978
- const originalPushState = history.pushState.bind(history);
1979
- const originalReplaceState = history.replaceState.bind(history);
1980
- function patchedPushState(...args) {
1981
- originalPushState(...args);
1982
- setTimeout(onNavigate, 0);
1983
- }
1984
- function patchedReplaceState(...args) {
1985
- originalReplaceState(...args);
1986
- setTimeout(onNavigate, 0);
1987
- }
1988
- history.pushState = patchedPushState;
1989
- history.replaceState = patchedReplaceState;
1990
- window.addEventListener("popstate", onNavigate);
2082
+ const unsubscribeNav = onHistoryChange(onNavigate);
1991
2083
  window.addEventListener("scroll", onScroll, { passive: true });
1992
2084
  document.addEventListener("visibilitychange", onVisibilityChange);
1993
2085
  window.addEventListener("pagehide", onPageHide);
2086
+ gate.rebaseline();
1994
2087
  return () => {
1995
2088
  if (rafId !== null) cancelAnimationFrame(rafId);
1996
- history.pushState = originalPushState;
1997
- history.replaceState = originalReplaceState;
1998
- window.removeEventListener("popstate", onNavigate);
2089
+ gate.cancel();
2090
+ unsubscribeNav();
1999
2091
  window.removeEventListener("scroll", onScroll);
2000
2092
  document.removeEventListener("visibilitychange", onVisibilityChange);
2001
2093
  window.removeEventListener("pagehide", onPageHide);
@@ -2800,7 +2892,7 @@ function GoogleAdsTracking(props) {
2800
2892
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo4 } from "react";
2801
2893
 
2802
2894
  // package.json
2803
- var version = "0.17.1";
2895
+ var version = "0.17.3";
2804
2896
 
2805
2897
  // ../tracking-core/src/phone-react.tsx
2806
2898
  import {
@@ -2970,7 +3062,6 @@ function createTracking(options) {
2970
3062
  }
2971
3063
  }, [metaPixelId, metaPixelIdsKey]);
2972
3064
  useEffect5(() => {
2973
- const detachers = [];
2974
3065
  const rawClient = getOrCreateTrackingClient({
2975
3066
  apiKey,
2976
3067
  endpoint,
@@ -2981,47 +3072,50 @@ function createTracking(options) {
2981
3072
  activeGtagIds: resolvedGtagIds,
2982
3073
  debug
2983
3074
  });
2984
- const conversionStore = conversionConfig ? resolveConversionConfig({
2985
- cdnUrl: conversionConfig.cdnUrl,
2986
- baked: conversionConfig.baked
2987
- }) : null;
2988
- const detectorClient = conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient;
2989
- detachers.push(attachAutoPageView(detectorClient));
2990
- detachers.push(attachBfcacheRestore(detectorClient));
2991
- detachers.push(attachPageExit(detectorClient));
2992
- const timeOnSite = triggers.automatic.time_on_site;
2993
- if (timeOnSite) {
2994
- detachers.push(attachTimeOnSite(detectorClient, timeOnSite));
2995
- }
2996
- const specificPageVisit = triggers.automatic.specific_page_visit;
2997
- if (specificPageVisit) {
2998
- detachers.push(attachSpecificPageVisit(detectorClient, specificPageVisit));
2999
- }
3000
- const scrollDepth = triggers.automatic.scroll_depth;
3001
- if (scrollDepth) {
3002
- detachers.push(attachScrollDepth(detectorClient, scrollDepth));
3003
- }
3004
- const multiPageSession = triggers.automatic.multi_page_session;
3005
- if (multiPageSession) {
3006
- detachers.push(attachMultiPageSession(detectorClient, multiPageSession));
3007
- }
3008
- const formStart = triggers.automatic.form_start;
3009
- if (formStart) {
3010
- detachers.push(attachFormStart(detectorClient, formStart));
3011
- }
3012
- const ctaClick = triggers.manual?.cta_click;
3013
- if (ctaClick) {
3014
- detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
3015
- }
3016
- const phoneClick = triggers.manual?.phone_click;
3017
- if (phoneClick) {
3018
- detachers.push(attachPhoneClickCapture(detectorClient, phoneClick));
3019
- }
3020
- return () => {
3021
- for (let i = detachers.length - 1; i >= 0; i--) {
3022
- detachers[i]();
3075
+ return attachClientCapturesOnce(rawClient, () => {
3076
+ const detachers = [];
3077
+ const conversionStore = conversionConfig ? resolveConversionConfig({
3078
+ cdnUrl: conversionConfig.cdnUrl,
3079
+ baked: conversionConfig.baked
3080
+ }) : null;
3081
+ const detectorClient = conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient;
3082
+ detachers.push(attachAutoPageView(detectorClient));
3083
+ detachers.push(attachBfcacheRestore(detectorClient));
3084
+ detachers.push(attachPageExit(detectorClient));
3085
+ const timeOnSite = triggers.automatic.time_on_site;
3086
+ if (timeOnSite) {
3087
+ detachers.push(attachTimeOnSite(detectorClient, timeOnSite));
3023
3088
  }
3024
- };
3089
+ const specificPageVisit = triggers.automatic.specific_page_visit;
3090
+ if (specificPageVisit) {
3091
+ detachers.push(attachSpecificPageVisit(detectorClient, specificPageVisit));
3092
+ }
3093
+ const scrollDepth = triggers.automatic.scroll_depth;
3094
+ if (scrollDepth) {
3095
+ detachers.push(attachScrollDepth(detectorClient, scrollDepth));
3096
+ }
3097
+ const multiPageSession = triggers.automatic.multi_page_session;
3098
+ if (multiPageSession) {
3099
+ detachers.push(attachMultiPageSession(detectorClient, multiPageSession));
3100
+ }
3101
+ const formStart = triggers.automatic.form_start;
3102
+ if (formStart) {
3103
+ detachers.push(attachFormStart(detectorClient, formStart));
3104
+ }
3105
+ const ctaClick = triggers.manual?.cta_click;
3106
+ if (ctaClick) {
3107
+ detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
3108
+ }
3109
+ const phoneClick = triggers.manual?.phone_click;
3110
+ if (phoneClick) {
3111
+ detachers.push(attachPhoneClickCapture(detectorClient, phoneClick));
3112
+ }
3113
+ return () => {
3114
+ for (let i = detachers.length - 1; i >= 0; i--) {
3115
+ detachers[i]();
3116
+ }
3117
+ };
3118
+ });
3025
3119
  }, []);
3026
3120
  return /* @__PURE__ */ jsx3(TrackingContext.Provider, { value: client, children: /* @__PURE__ */ jsx3(PhoneConfigProvider, { value: phone ?? null, children }) });
3027
3121
  }