@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.d.mts CHANGED
@@ -349,6 +349,7 @@ type MultiPageSessionConfig = z.infer<typeof multiPageSessionConfigSchema>;
349
349
  declare const pageExitMetadataSchema: z.ZodObject<{
350
350
  dwell_ms: z.ZodNumber;
351
351
  max_scroll_percent: z.ZodNullable<z.ZodNumber>;
352
+ scroll_baseline_percent: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
352
353
  page: z.ZodObject<{
353
354
  path: z.ZodString;
354
355
  }, "strict", z.ZodTypeAny, {
@@ -362,12 +363,14 @@ declare const pageExitMetadataSchema: z.ZodObject<{
362
363
  };
363
364
  dwell_ms: number;
364
365
  max_scroll_percent: number | null;
366
+ scroll_baseline_percent?: number | null | undefined;
365
367
  }, {
366
368
  page: {
367
369
  path: string;
368
370
  };
369
371
  dwell_ms: number;
370
372
  max_scroll_percent: number | null;
373
+ scroll_baseline_percent?: number | null | undefined;
371
374
  }>;
372
375
  type PageExitMetadata = z.infer<typeof pageExitMetadataSchema>;
373
376
  /**
@@ -934,6 +937,7 @@ declare const EVENT_REGISTRY: {
934
937
  readonly metadataSchema: z.ZodObject<{
935
938
  dwell_ms: z.ZodNumber;
936
939
  max_scroll_percent: z.ZodNullable<z.ZodNumber>;
940
+ scroll_baseline_percent: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
937
941
  page: z.ZodObject<{
938
942
  path: z.ZodString;
939
943
  }, "strict", z.ZodTypeAny, {
@@ -947,12 +951,14 @@ declare const EVENT_REGISTRY: {
947
951
  };
948
952
  dwell_ms: number;
949
953
  max_scroll_percent: number | null;
954
+ scroll_baseline_percent?: number | null | undefined;
950
955
  }, {
951
956
  page: {
952
957
  path: string;
953
958
  };
954
959
  dwell_ms: number;
955
960
  max_scroll_percent: number | null;
961
+ scroll_baseline_percent?: number | null | undefined;
956
962
  }>;
957
963
  readonly configSchema: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
958
964
  };
package/dist/index.d.ts CHANGED
@@ -349,6 +349,7 @@ type MultiPageSessionConfig = z.infer<typeof multiPageSessionConfigSchema>;
349
349
  declare const pageExitMetadataSchema: z.ZodObject<{
350
350
  dwell_ms: z.ZodNumber;
351
351
  max_scroll_percent: z.ZodNullable<z.ZodNumber>;
352
+ scroll_baseline_percent: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
352
353
  page: z.ZodObject<{
353
354
  path: z.ZodString;
354
355
  }, "strict", z.ZodTypeAny, {
@@ -362,12 +363,14 @@ declare const pageExitMetadataSchema: z.ZodObject<{
362
363
  };
363
364
  dwell_ms: number;
364
365
  max_scroll_percent: number | null;
366
+ scroll_baseline_percent?: number | null | undefined;
365
367
  }, {
366
368
  page: {
367
369
  path: string;
368
370
  };
369
371
  dwell_ms: number;
370
372
  max_scroll_percent: number | null;
373
+ scroll_baseline_percent?: number | null | undefined;
371
374
  }>;
372
375
  type PageExitMetadata = z.infer<typeof pageExitMetadataSchema>;
373
376
  /**
@@ -934,6 +937,7 @@ declare const EVENT_REGISTRY: {
934
937
  readonly metadataSchema: z.ZodObject<{
935
938
  dwell_ms: z.ZodNumber;
936
939
  max_scroll_percent: z.ZodNullable<z.ZodNumber>;
940
+ scroll_baseline_percent: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
937
941
  page: z.ZodObject<{
938
942
  path: z.ZodString;
939
943
  }, "strict", z.ZodTypeAny, {
@@ -947,12 +951,14 @@ declare const EVENT_REGISTRY: {
947
951
  };
948
952
  dwell_ms: number;
949
953
  max_scroll_percent: number | null;
954
+ scroll_baseline_percent?: number | null | undefined;
950
955
  }, {
951
956
  page: {
952
957
  path: string;
953
958
  };
954
959
  dwell_ms: number;
955
960
  max_scroll_percent: number | null;
961
+ scroll_baseline_percent?: number | null | undefined;
956
962
  }>;
957
963
  readonly configSchema: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
958
964
  };
package/dist/index.js CHANGED
@@ -726,9 +726,9 @@ function resolveConversionConfig(options) {
726
726
  }
727
727
  }
728
728
  function notifyResolved() {
729
- const listeners = [...resolveListeners];
729
+ const listeners2 = [...resolveListeners];
730
730
  resolveListeners.clear();
731
- for (const listener of listeners) {
731
+ for (const listener of listeners2) {
732
732
  try {
733
733
  listener();
734
734
  } catch {
@@ -1206,6 +1206,27 @@ function getOrCreateTrackingClient(config) {
1206
1206
  globalClientKey = key;
1207
1207
  return globalClient;
1208
1208
  }
1209
+ var clientCaptureRegistry = /* @__PURE__ */ new WeakMap();
1210
+ function attachClientCapturesOnce(client, build) {
1211
+ let entry = clientCaptureRegistry.get(client);
1212
+ if (entry === void 0) {
1213
+ entry = { detach: build(), refCount: 0 };
1214
+ clientCaptureRegistry.set(client, entry);
1215
+ }
1216
+ entry.refCount += 1;
1217
+ let released = false;
1218
+ return () => {
1219
+ if (released) return;
1220
+ released = true;
1221
+ const current = clientCaptureRegistry.get(client);
1222
+ if (current === void 0) return;
1223
+ current.refCount -= 1;
1224
+ if (current.refCount <= 0) {
1225
+ current.detach();
1226
+ clientCaptureRegistry.delete(client);
1227
+ }
1228
+ };
1229
+ }
1209
1230
  function createTrackingClient(config) {
1210
1231
  const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
1211
1232
  const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);
@@ -1475,6 +1496,14 @@ var pageExitMetadataSchema = import_zod7.z.object({
1475
1496
  // null = left without any scroll signal; floor is 0 so a valid 0% is never
1476
1497
  // rejected (a single bad field 422s the whole keepalive beacon batch).
1477
1498
  max_scroll_percent: import_zod7.z.number().int().min(0).max(100).nullable(),
1499
+ // The gating baseline: the fraction of the page visible at load with
1500
+ // zero scrolling — or, for pages that grew after the post-paint snapshot
1501
+ // (skeleton/streaming renders), the first-scroll position that
1502
+ // established it. null = page had no scrollable range, or the segment
1503
+ // ended before the snapshot. `.optional()` is load-bearing: SDK builds
1504
+ // predating this field keep POSTing page_exit without the key —
1505
+ // requiring it would 422 whole keepalive beacon batches.
1506
+ scroll_baseline_percent: import_zod7.z.number().int().min(0).max(100).nullable().optional(),
1478
1507
  page: import_zod7.z.object({
1479
1508
  path: import_zod7.z.string()
1480
1509
  }).strict()
@@ -1744,6 +1773,97 @@ function attachSpecificPageVisit(client, config) {
1744
1773
  };
1745
1774
  }
1746
1775
 
1776
+ // ../tracking-core/src/triggers/navigation.ts
1777
+ var listeners = /* @__PURE__ */ new Set();
1778
+ var restorePatch = null;
1779
+ function notify() {
1780
+ for (const listener of listeners) listener();
1781
+ }
1782
+ function notifyDeferred() {
1783
+ setTimeout(notify, 0);
1784
+ }
1785
+ function installPatch() {
1786
+ const originalPushState = history.pushState;
1787
+ const originalReplaceState = history.replaceState;
1788
+ function patchedPushState(...args) {
1789
+ originalPushState.apply(this, args);
1790
+ notifyDeferred();
1791
+ }
1792
+ function patchedReplaceState(...args) {
1793
+ originalReplaceState.apply(this, args);
1794
+ notifyDeferred();
1795
+ }
1796
+ history.pushState = patchedPushState;
1797
+ history.replaceState = patchedReplaceState;
1798
+ window.addEventListener("popstate", notify);
1799
+ restorePatch = () => {
1800
+ history.pushState = originalPushState;
1801
+ history.replaceState = originalReplaceState;
1802
+ window.removeEventListener("popstate", notify);
1803
+ restorePatch = null;
1804
+ };
1805
+ }
1806
+ function onHistoryChange(listener) {
1807
+ if (listeners.size === 0) installPatch();
1808
+ listeners.add(listener);
1809
+ return () => {
1810
+ if (!listeners.delete(listener)) return;
1811
+ if (listeners.size === 0) restorePatch?.();
1812
+ };
1813
+ }
1814
+
1815
+ // ../tracking-core/src/triggers/scroll-measurement.ts
1816
+ var BOTTOM_EPSILON_PX = 2;
1817
+ function measureScrollPercent() {
1818
+ const root = document.scrollingElement ?? document.documentElement;
1819
+ const scrollHeight = root.scrollHeight;
1820
+ const clientHeight = root.clientHeight;
1821
+ if (scrollHeight <= 0 || clientHeight <= 0) return null;
1822
+ if (scrollHeight <= clientHeight + BOTTOM_EPSILON_PX) return null;
1823
+ const maxTop = scrollHeight - clientHeight;
1824
+ const scrollTop = Math.min(Math.max(root.scrollTop, 0), maxTop);
1825
+ if (scrollTop + clientHeight >= scrollHeight - BOTTOM_EPSILON_PX) return 100;
1826
+ return Math.max(1, Math.min(100, Math.round((scrollTop + clientHeight) / scrollHeight * 100)));
1827
+ }
1828
+ function scheduleBaselineSnapshot(onSnapshot) {
1829
+ let rafId = requestAnimationFrame(() => {
1830
+ rafId = requestAnimationFrame(() => {
1831
+ onSnapshot(measureScrollPercent());
1832
+ });
1833
+ });
1834
+ return () => cancelAnimationFrame(rafId);
1835
+ }
1836
+ function createBaselineGate() {
1837
+ let baselinePercent = null;
1838
+ let ready = false;
1839
+ let cancelSnapshot = null;
1840
+ return {
1841
+ rebaseline() {
1842
+ cancelSnapshot?.();
1843
+ ready = false;
1844
+ baselinePercent = null;
1845
+ cancelSnapshot = scheduleBaselineSnapshot((b) => {
1846
+ baselinePercent = b;
1847
+ ready = true;
1848
+ });
1849
+ },
1850
+ cancel() {
1851
+ cancelSnapshot?.();
1852
+ },
1853
+ baseline() {
1854
+ return ready ? baselinePercent : null;
1855
+ },
1856
+ sample() {
1857
+ if (!ready) return null;
1858
+ if (baselinePercent === null) {
1859
+ baselinePercent = measureScrollPercent();
1860
+ return null;
1861
+ }
1862
+ return measureScrollPercent();
1863
+ }
1864
+ };
1865
+ }
1866
+
1747
1867
  // ../tracking-core/src/triggers/scroll-depth.ts
1748
1868
  function attachScrollDepth(client, config) {
1749
1869
  if (typeof window === "undefined" || typeof document === "undefined") {
@@ -1754,17 +1874,14 @@ function attachScrollDepth(client, config) {
1754
1874
  let firedForPath = /* @__PURE__ */ new Set();
1755
1875
  let currentPath2 = window.location.pathname;
1756
1876
  let rafId = null;
1757
- function getScrollPercent() {
1758
- const doc = document.documentElement;
1759
- const scrollTop = window.scrollY || doc.scrollTop;
1760
- const scrollHeight = doc.scrollHeight;
1761
- const clientHeight = doc.clientHeight;
1762
- if (scrollHeight <= clientHeight) return 100;
1763
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1764
- }
1877
+ const gate = createBaselineGate();
1765
1878
  function checkThresholds() {
1766
- const percent = getScrollPercent();
1879
+ const percent = gate.sample();
1880
+ if (percent === null) return;
1881
+ const baseline = gate.baseline();
1882
+ if (baseline === null) return;
1767
1883
  for (const threshold of thresholds) {
1884
+ if (threshold <= baseline) continue;
1768
1885
  if (percent >= threshold && !firedForPath.has(threshold)) {
1769
1886
  firedForPath.add(threshold);
1770
1887
  client.trackEvent({
@@ -1791,28 +1908,15 @@ function attachScrollDepth(client, config) {
1791
1908
  if (newPath === currentPath2) return;
1792
1909
  currentPath2 = newPath;
1793
1910
  firedForPath = /* @__PURE__ */ new Set();
1794
- setTimeout(checkThresholds, 0);
1795
- }
1796
- const originalPushState = history.pushState.bind(history);
1797
- const originalReplaceState = history.replaceState.bind(history);
1798
- function patchedPushState(...args) {
1799
- originalPushState(...args);
1800
- setTimeout(resetIfPathChanged, 0);
1911
+ gate.rebaseline();
1801
1912
  }
1802
- function patchedReplaceState(...args) {
1803
- originalReplaceState(...args);
1804
- setTimeout(resetIfPathChanged, 0);
1805
- }
1806
- history.pushState = patchedPushState;
1807
- history.replaceState = patchedReplaceState;
1808
- window.addEventListener("popstate", resetIfPathChanged);
1913
+ const unsubscribeNav = onHistoryChange(resetIfPathChanged);
1809
1914
  window.addEventListener("scroll", onScroll, { passive: true });
1810
- setTimeout(checkThresholds, 0);
1915
+ gate.rebaseline();
1811
1916
  return () => {
1812
1917
  if (rafId !== null) cancelAnimationFrame(rafId);
1813
- history.pushState = originalPushState;
1814
- history.replaceState = originalReplaceState;
1815
- window.removeEventListener("popstate", resetIfPathChanged);
1918
+ gate.cancel();
1919
+ unsubscribeNav();
1816
1920
  window.removeEventListener("scroll", onScroll);
1817
1921
  };
1818
1922
  }
@@ -1984,21 +2088,14 @@ function attachPageExit(client) {
1984
2088
  let accumulatedMs = 0;
1985
2089
  let maxScrollPercent = null;
1986
2090
  let rafId = null;
1987
- function getScrollPercent() {
1988
- const doc = document.documentElement;
1989
- const scrollTop = window.scrollY || doc.scrollTop;
1990
- const scrollHeight = doc.scrollHeight;
1991
- const clientHeight = doc.clientHeight;
1992
- if (scrollHeight <= clientHeight) return 100;
1993
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1994
- }
2091
+ const gate = createBaselineGate();
1995
2092
  function onScroll() {
1996
2093
  if (rafId !== null) return;
1997
2094
  rafId = requestAnimationFrame(() => {
1998
2095
  rafId = null;
1999
- const percent = getScrollPercent();
2000
- if (percent >= 1 && (maxScrollPercent === null || percent > maxScrollPercent)) {
2001
- maxScrollPercent = Math.min(percent, 100);
2096
+ const percent = gate.sample();
2097
+ if (percent !== null && (maxScrollPercent === null || percent > maxScrollPercent)) {
2098
+ maxScrollPercent = percent;
2002
2099
  }
2003
2100
  });
2004
2101
  }
@@ -2017,6 +2114,12 @@ function attachPageExit(client) {
2017
2114
  metadata: {
2018
2115
  dwell_ms: dwell,
2019
2116
  max_scroll_percent: maxScrollPercent,
2117
+ // Lets the backend tell "scrolled to the bottom" apart from "the page
2118
+ // was barely scrollable". null = unscrollable page or the segment
2119
+ // ended before the post-paint snapshot landed. For pages that grew
2120
+ // after the snapshot this is the first-scroll position, not the
2121
+ // at-load fraction (see BaselineGate.baseline).
2122
+ scroll_baseline_percent: gate.baseline(),
2020
2123
  page: { path }
2021
2124
  },
2022
2125
  pageUrl: window.location.href,
@@ -2034,6 +2137,7 @@ function attachPageExit(client) {
2034
2137
  emitSegment(currentPath2, false);
2035
2138
  currentPath2 = newPath;
2036
2139
  maxScrollPercent = null;
2140
+ gate.rebaseline();
2037
2141
  accumulatedMs = 0;
2038
2142
  activeSince = document.visibilityState === "visible" ? Date.now() : null;
2039
2143
  }
@@ -2047,27 +2151,15 @@ function attachPageExit(client) {
2047
2151
  function onPageHide() {
2048
2152
  emitSegment(currentPath2, true);
2049
2153
  }
2050
- const originalPushState = history.pushState.bind(history);
2051
- const originalReplaceState = history.replaceState.bind(history);
2052
- function patchedPushState(...args) {
2053
- originalPushState(...args);
2054
- setTimeout(onNavigate, 0);
2055
- }
2056
- function patchedReplaceState(...args) {
2057
- originalReplaceState(...args);
2058
- setTimeout(onNavigate, 0);
2059
- }
2060
- history.pushState = patchedPushState;
2061
- history.replaceState = patchedReplaceState;
2062
- window.addEventListener("popstate", onNavigate);
2154
+ const unsubscribeNav = onHistoryChange(onNavigate);
2063
2155
  window.addEventListener("scroll", onScroll, { passive: true });
2064
2156
  document.addEventListener("visibilitychange", onVisibilityChange);
2065
2157
  window.addEventListener("pagehide", onPageHide);
2158
+ gate.rebaseline();
2066
2159
  return () => {
2067
2160
  if (rafId !== null) cancelAnimationFrame(rafId);
2068
- history.pushState = originalPushState;
2069
- history.replaceState = originalReplaceState;
2070
- window.removeEventListener("popstate", onNavigate);
2161
+ gate.cancel();
2162
+ unsubscribeNav();
2071
2163
  window.removeEventListener("scroll", onScroll);
2072
2164
  document.removeEventListener("visibilitychange", onVisibilityChange);
2073
2165
  window.removeEventListener("pagehide", onPageHide);
@@ -2872,7 +2964,7 @@ function GoogleAdsTracking(props) {
2872
2964
  var import_react6 = require("react");
2873
2965
 
2874
2966
  // package.json
2875
- var version = "0.17.1";
2967
+ var version = "0.17.3";
2876
2968
 
2877
2969
  // ../tracking-core/src/phone-react.tsx
2878
2970
  var import_react5 = require("react");
@@ -3035,7 +3127,6 @@ function createTracking(options) {
3035
3127
  }
3036
3128
  }, [metaPixelId, metaPixelIdsKey]);
3037
3129
  (0, import_react6.useEffect)(() => {
3038
- const detachers = [];
3039
3130
  const rawClient = getOrCreateTrackingClient({
3040
3131
  apiKey,
3041
3132
  endpoint,
@@ -3046,47 +3137,50 @@ function createTracking(options) {
3046
3137
  activeGtagIds: resolvedGtagIds,
3047
3138
  debug
3048
3139
  });
3049
- const conversionStore = conversionConfig ? resolveConversionConfig({
3050
- cdnUrl: conversionConfig.cdnUrl,
3051
- baked: conversionConfig.baked
3052
- }) : null;
3053
- const detectorClient = conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient;
3054
- detachers.push(attachAutoPageView(detectorClient));
3055
- detachers.push(attachBfcacheRestore(detectorClient));
3056
- detachers.push(attachPageExit(detectorClient));
3057
- const timeOnSite = triggers.automatic.time_on_site;
3058
- if (timeOnSite) {
3059
- detachers.push(attachTimeOnSite(detectorClient, timeOnSite));
3060
- }
3061
- const specificPageVisit = triggers.automatic.specific_page_visit;
3062
- if (specificPageVisit) {
3063
- detachers.push(attachSpecificPageVisit(detectorClient, specificPageVisit));
3064
- }
3065
- const scrollDepth = triggers.automatic.scroll_depth;
3066
- if (scrollDepth) {
3067
- detachers.push(attachScrollDepth(detectorClient, scrollDepth));
3068
- }
3069
- const multiPageSession = triggers.automatic.multi_page_session;
3070
- if (multiPageSession) {
3071
- detachers.push(attachMultiPageSession(detectorClient, multiPageSession));
3072
- }
3073
- const formStart = triggers.automatic.form_start;
3074
- if (formStart) {
3075
- detachers.push(attachFormStart(detectorClient, formStart));
3076
- }
3077
- const ctaClick = triggers.manual?.cta_click;
3078
- if (ctaClick) {
3079
- detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
3080
- }
3081
- const phoneClick = triggers.manual?.phone_click;
3082
- if (phoneClick) {
3083
- detachers.push(attachPhoneClickCapture(detectorClient, phoneClick));
3084
- }
3085
- return () => {
3086
- for (let i = detachers.length - 1; i >= 0; i--) {
3087
- detachers[i]();
3140
+ return attachClientCapturesOnce(rawClient, () => {
3141
+ const detachers = [];
3142
+ const conversionStore = conversionConfig ? resolveConversionConfig({
3143
+ cdnUrl: conversionConfig.cdnUrl,
3144
+ baked: conversionConfig.baked
3145
+ }) : null;
3146
+ const detectorClient = conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient;
3147
+ detachers.push(attachAutoPageView(detectorClient));
3148
+ detachers.push(attachBfcacheRestore(detectorClient));
3149
+ detachers.push(attachPageExit(detectorClient));
3150
+ const timeOnSite = triggers.automatic.time_on_site;
3151
+ if (timeOnSite) {
3152
+ detachers.push(attachTimeOnSite(detectorClient, timeOnSite));
3088
3153
  }
3089
- };
3154
+ const specificPageVisit = triggers.automatic.specific_page_visit;
3155
+ if (specificPageVisit) {
3156
+ detachers.push(attachSpecificPageVisit(detectorClient, specificPageVisit));
3157
+ }
3158
+ const scrollDepth = triggers.automatic.scroll_depth;
3159
+ if (scrollDepth) {
3160
+ detachers.push(attachScrollDepth(detectorClient, scrollDepth));
3161
+ }
3162
+ const multiPageSession = triggers.automatic.multi_page_session;
3163
+ if (multiPageSession) {
3164
+ detachers.push(attachMultiPageSession(detectorClient, multiPageSession));
3165
+ }
3166
+ const formStart = triggers.automatic.form_start;
3167
+ if (formStart) {
3168
+ detachers.push(attachFormStart(detectorClient, formStart));
3169
+ }
3170
+ const ctaClick = triggers.manual?.cta_click;
3171
+ if (ctaClick) {
3172
+ detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
3173
+ }
3174
+ const phoneClick = triggers.manual?.phone_click;
3175
+ if (phoneClick) {
3176
+ detachers.push(attachPhoneClickCapture(detectorClient, phoneClick));
3177
+ }
3178
+ return () => {
3179
+ for (let i = detachers.length - 1; i >= 0; i--) {
3180
+ detachers[i]();
3181
+ }
3182
+ };
3183
+ });
3090
3184
  }, []);
3091
3185
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TrackingContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PhoneConfigProvider, { value: phone ?? null, children }) });
3092
3186
  }