@aranova/tracking-react 0.17.2 → 0.18.0

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
@@ -608,6 +608,7 @@ function parseConversionConfig(raw) {
608
608
  business_id: typeof obj.business_id === "string" ? obj.business_id : void 0,
609
609
  customer_id: typeof obj.customer_id === "string" ? obj.customer_id : null,
610
610
  environment: typeof obj.environment === "string" ? obj.environment : void 0,
611
+ google_tracking_state: obj.google_tracking_state === "active" ? "active" : "disabled",
611
612
  gtag_ids: isStringMap(obj.gtag_ids) ? obj.gtag_ids : {},
612
613
  meta_pixel_ids: isStringMap(obj.meta_pixel_ids) ? obj.meta_pixel_ids : {},
613
614
  services,
@@ -654,9 +655,9 @@ function resolveConversionConfig(options) {
654
655
  }
655
656
  }
656
657
  function notifyResolved() {
657
- const listeners = [...resolveListeners];
658
+ const listeners2 = [...resolveListeners];
658
659
  resolveListeners.clear();
659
- for (const listener of listeners) {
660
+ for (const listener of listeners2) {
660
661
  try {
661
662
  listener();
662
663
  } catch {
@@ -743,6 +744,283 @@ function formatDateInTz(iso, timeZone, opts, locale) {
743
744
  }).format(date);
744
745
  }
745
746
 
747
+ // ../tracking-core/src/resources/tracking-config-runtime.ts
748
+ var CACHE_PREFIX2 = "_aranova_cfg_runtime_";
749
+ var AUTHORITY_TTL_MS = 6e4;
750
+ var runtimes = /* @__PURE__ */ new Map();
751
+ var configuredIds = /* @__PURE__ */ new Set();
752
+ var scriptLoad = null;
753
+ var jsInitialized = false;
754
+ function cacheKey2(url) {
755
+ return `${CACHE_PREFIX2}${url}`;
756
+ }
757
+ function readCache2(url) {
758
+ if (typeof window === "undefined") return null;
759
+ try {
760
+ const raw = window.sessionStorage.getItem(cacheKey2(url));
761
+ if (!raw) return null;
762
+ const parsed = JSON.parse(raw);
763
+ const config = parseConversionConfig(parsed.config);
764
+ if (!config) return null;
765
+ return {
766
+ etag: typeof parsed.etag === "string" ? parsed.etag : null,
767
+ config
768
+ };
769
+ } catch {
770
+ return null;
771
+ }
772
+ }
773
+ function writeCache2(url, entry) {
774
+ if (typeof window === "undefined") return;
775
+ try {
776
+ window.sessionStorage.setItem(cacheKey2(url), JSON.stringify(entry));
777
+ } catch {
778
+ }
779
+ }
780
+ function isTombstone(config) {
781
+ return config.google_tracking_state === "disabled" || Object.keys(config.gtag_ids).length === 0;
782
+ }
783
+ function validateConfig(config, ref) {
784
+ return config.business_id === ref.businessId && config.environment === ref.environment && (config.google_tracking_state === "active" || config.google_tracking_state === "disabled");
785
+ }
786
+ function pageViewSnapshot() {
787
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
788
+ return {
789
+ href: window.location.href,
790
+ title: document.title || null,
791
+ referrer: document.referrer || null
792
+ };
793
+ }
794
+ function ensureScript(gtagId) {
795
+ if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
796
+ applyDefaultConsentState();
797
+ loadGtagScript(gtagId);
798
+ if (scriptLoad) return scriptLoad;
799
+ scriptLoad = new Promise((resolve) => {
800
+ const script = document.querySelector(
801
+ 'script[data-aranova-tracking="aranova-gtag-loader"]'
802
+ );
803
+ if (!script) {
804
+ resolve();
805
+ return;
806
+ }
807
+ if (script.dataset.loaded === "true") {
808
+ resolve();
809
+ return;
810
+ }
811
+ window.setTimeout(resolve, 0);
812
+ script.addEventListener(
813
+ "load",
814
+ () => {
815
+ script.dataset.loaded = "true";
816
+ resolve();
817
+ },
818
+ { once: true }
819
+ );
820
+ script.addEventListener("error", () => resolve(), { once: true });
821
+ });
822
+ return scriptLoad;
823
+ }
824
+ var TrackingConfigRuntime = class {
825
+ constructor(ref, fetchImpl = globalThis.fetch) {
826
+ this.ref = ref;
827
+ this.fetchImpl = fetchImpl;
828
+ this.current = null;
829
+ this.etag = null;
830
+ this.stateValue = "unconfirmed";
831
+ this.confirmedAt = 0;
832
+ this.inFlight = null;
833
+ this.conversionQueue = [];
834
+ this.automaticQueue = [];
835
+ this.pageQueue = [];
836
+ this.listeners = /* @__PURE__ */ new Set();
837
+ const cached = readCache2(ref.cdnUrl);
838
+ this.current = cached?.config ?? null;
839
+ this.etag = cached?.etag ?? null;
840
+ void this.revalidate();
841
+ if (typeof window !== "undefined") {
842
+ window.addEventListener("visibilitychange", () => {
843
+ if (document.visibilityState === "visible") void this.revalidate();
844
+ });
845
+ window.addEventListener("focus", () => void this.revalidate());
846
+ }
847
+ }
848
+ state() {
849
+ return this.stateValue;
850
+ }
851
+ config() {
852
+ return this.stateValue === "active" || this.stateValue === "tombstone" ? this.current : null;
853
+ }
854
+ __unsafeExpireAuthorityForTests() {
855
+ this.confirmedAt = 0;
856
+ }
857
+ subscribe(listener) {
858
+ this.listeners.add(listener);
859
+ return () => this.listeners.delete(listener);
860
+ }
861
+ async ensureAuthority() {
862
+ if (this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS) {
863
+ return true;
864
+ }
865
+ await this.revalidate();
866
+ return this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS;
867
+ }
868
+ async revalidate() {
869
+ if (typeof window === "undefined" || !this.fetchImpl) return;
870
+ if (this.inFlight) return this.inFlight;
871
+ this.inFlight = this.revalidateNow().finally(() => {
872
+ this.inFlight = null;
873
+ });
874
+ return this.inFlight;
875
+ }
876
+ queuePageView(snapshot = pageViewSnapshot()) {
877
+ if (!snapshot) return;
878
+ this.pageQueue.push(snapshot);
879
+ void this.flush();
880
+ }
881
+ fireConversion(key, options) {
882
+ this.conversionQueue.push({ key, ...options });
883
+ void this.flush();
884
+ }
885
+ queueAutomaticEvent(eventType, metadata, transactionPath) {
886
+ this.automaticQueue.push({ eventType, metadata, transactionPath });
887
+ void this.flush();
888
+ }
889
+ listGoals() {
890
+ return this.current?.goals ?? [];
891
+ }
892
+ async revalidateNow() {
893
+ try {
894
+ const headers = {};
895
+ if (this.etag) headers["If-None-Match"] = this.etag;
896
+ const response = await this.fetchImpl(this.ref.cdnUrl, {
897
+ method: "GET",
898
+ headers,
899
+ cache: "no-cache"
900
+ });
901
+ if (response.status === 304 && this.current && validateConfig(this.current, this.ref)) {
902
+ this.confirm(this.current, this.etag);
903
+ await this.flush();
904
+ return;
905
+ }
906
+ if (!response.ok) {
907
+ this.expireAuthority();
908
+ return;
909
+ }
910
+ const next = parseConversionConfig(await response.json());
911
+ if (!next || !validateConfig(next, this.ref)) {
912
+ this.expireAuthority();
913
+ return;
914
+ }
915
+ if (this.current && next.config_version < this.current.config_version) {
916
+ this.expireAuthority();
917
+ return;
918
+ }
919
+ this.confirm(next, response.headers.get("ETag"));
920
+ await this.flush();
921
+ } catch {
922
+ this.expireAuthority();
923
+ }
924
+ }
925
+ expireAuthority() {
926
+ if (this.stateValue !== "unconfirmed") this.stateValue = "unconfirmed";
927
+ }
928
+ confirm(config, etag) {
929
+ this.current = config;
930
+ this.etag = etag;
931
+ this.confirmedAt = Date.now();
932
+ this.stateValue = isTombstone(config) ? "tombstone" : "active";
933
+ writeCache2(this.ref.cdnUrl, { etag, config });
934
+ if (this.stateValue === "tombstone") {
935
+ this.conversionQueue.length = 0;
936
+ this.automaticQueue.length = 0;
937
+ this.pageQueue.length = 0;
938
+ }
939
+ for (const listener of this.listeners) listener();
940
+ }
941
+ async flush() {
942
+ if (!await this.ensureAuthority()) return;
943
+ if (this.stateValue !== "active" || !this.current) return;
944
+ const ids = Object.values(this.current.gtag_ids).filter(
945
+ (id) => typeof id === "string" && isValidGtagId(id)
946
+ );
947
+ if (ids.length === 0) return;
948
+ await ensureScript(ids[0]);
949
+ const gtag = window.gtag;
950
+ if (typeof gtag !== "function") return;
951
+ if (!jsInitialized) {
952
+ gtag("js", /* @__PURE__ */ new Date());
953
+ jsInitialized = true;
954
+ }
955
+ for (const id of ids) {
956
+ if (configuredIds.has(id)) continue;
957
+ gtag("config", id, { send_page_view: false });
958
+ configuredIds.add(id);
959
+ }
960
+ while (this.pageQueue.length) {
961
+ const page = this.pageQueue.shift();
962
+ gtag("event", "page_view", {
963
+ page_location: page.href,
964
+ page_title: page.title ?? void 0,
965
+ page_referrer: page.referrer ?? void 0
966
+ });
967
+ }
968
+ while (this.automaticQueue.length) {
969
+ const event = this.automaticQueue.shift();
970
+ for (const goal of this.current.goals) {
971
+ if (goal.kind !== "event" || !goal.firing) continue;
972
+ if (!automaticThresholdMet(goal, event.eventType, event.metadata)) continue;
973
+ this.conversionQueue.push({
974
+ key: goal.key,
975
+ transactionId: `auto:${goal.key}:${event.transactionPath}`
976
+ });
977
+ }
978
+ }
979
+ while (this.conversionQueue.length) {
980
+ const item = this.conversionQueue.shift();
981
+ const goal = this.current.goals.find((g) => g.key === item.key);
982
+ const firing = goal?.firing;
983
+ if (!firing || getConsentState() === "denied") continue;
984
+ const currency = item.currency ?? firing.currency ?? null;
985
+ const value = item.value ?? (firing.value_cents != null && currency ? fromMinor(firing.value_cents, currency) : null);
986
+ fireGtagConversion({
987
+ sendTo: firing.send_to,
988
+ value,
989
+ currency,
990
+ transactionId: item.transactionId ?? null
991
+ });
992
+ }
993
+ }
994
+ };
995
+ function automaticThresholdMet(goal, eventType, metadata) {
996
+ const t = goal.trigger;
997
+ if (!t || t.event_type !== eventType) return false;
998
+ switch (eventType) {
999
+ case "scroll_depth":
1000
+ return typeof metadata.depth_percent === "number" && t.threshold_percent != null && metadata.depth_percent >= t.threshold_percent;
1001
+ case "time_on_site":
1002
+ return typeof metadata.duration_ms === "number" && t.threshold_seconds != null && metadata.duration_ms >= t.threshold_seconds * 1e3;
1003
+ case "multi_page_session":
1004
+ return typeof metadata.page_count === "number" && t.page_threshold != null && metadata.page_count >= t.page_threshold;
1005
+ case "specific_page_visit":
1006
+ return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
1007
+ case "page_view":
1008
+ case "form_start":
1009
+ case "phone_click":
1010
+ return true;
1011
+ default:
1012
+ return false;
1013
+ }
1014
+ }
1015
+ function getTrackingConfigRuntime(ref, fetchImpl) {
1016
+ const key = `${ref.cdnUrl}|${ref.businessId}|${ref.environment}`;
1017
+ const existing = runtimes.get(key);
1018
+ if (existing) return existing;
1019
+ const runtime = new TrackingConfigRuntime(ref, fetchImpl ?? globalThis.fetch);
1020
+ runtimes.set(key, runtime);
1021
+ return runtime;
1022
+ }
1023
+
746
1024
  // ../tracking-core/src/resources/conversion-autofire.ts
747
1025
  function thresholdMet(goal, eventType, metadata) {
748
1026
  const t = goal.trigger;
@@ -775,6 +1053,12 @@ function createConversionAutoFire(store) {
775
1053
  for (const goal of store.listGoals()) {
776
1054
  if (goal.kind !== "event" || !goal.firing) continue;
777
1055
  if (!thresholdMet(goal, eventType, metadata)) continue;
1056
+ if ("queueAutomaticEvent" in store) {
1057
+ store.fireConversion(goal.key, {
1058
+ transactionId: `auto:${goal.key}:${currentPath()}`
1059
+ });
1060
+ continue;
1061
+ }
778
1062
  const firing = goal.firing;
779
1063
  const cents = firing.value_cents ?? null;
780
1064
  const currency = firing.currency ?? null;
@@ -790,6 +1074,10 @@ function createConversionAutoFire(store) {
790
1074
  }
791
1075
  return {
792
1076
  onAutomaticEvent(eventType, metadata) {
1077
+ if ("queueAutomaticEvent" in store) {
1078
+ store.queueAutomaticEvent(eventType, metadata, currentPath());
1079
+ return;
1080
+ }
793
1081
  if (store.isReady()) {
794
1082
  fireMatching(eventType, metadata);
795
1083
  return;
@@ -1424,6 +1712,14 @@ var pageExitMetadataSchema = z7.object({
1424
1712
  // null = left without any scroll signal; floor is 0 so a valid 0% is never
1425
1713
  // rejected (a single bad field 422s the whole keepalive beacon batch).
1426
1714
  max_scroll_percent: z7.number().int().min(0).max(100).nullable(),
1715
+ // The gating baseline: the fraction of the page visible at load with
1716
+ // zero scrolling — or, for pages that grew after the post-paint snapshot
1717
+ // (skeleton/streaming renders), the first-scroll position that
1718
+ // established it. null = page had no scrollable range, or the segment
1719
+ // ended before the snapshot. `.optional()` is load-bearing: SDK builds
1720
+ // predating this field keep POSTing page_exit without the key —
1721
+ // requiring it would 422 whole keepalive beacon batches.
1722
+ scroll_baseline_percent: z7.number().int().min(0).max(100).nullable().optional(),
1427
1723
  page: z7.object({
1428
1724
  path: z7.string()
1429
1725
  }).strict()
@@ -1499,69 +1795,154 @@ var timeOnSiteConfigSchema = z11.object({
1499
1795
  thresholdSeconds: z11.number().int().positive()
1500
1796
  }).strict();
1501
1797
 
1798
+ // ../tracking-core/src/events/semantics.ts
1799
+ var EVENT_SEMANTICS = {
1800
+ page_view: {
1801
+ label: "Page view",
1802
+ category: "page",
1803
+ outcomeRole: "navigation",
1804
+ clientVisibility: "simple"
1805
+ },
1806
+ time_on_site: {
1807
+ label: "Time on site",
1808
+ category: "engagement",
1809
+ outcomeRole: "engagement",
1810
+ clientVisibility: "detailed"
1811
+ },
1812
+ specific_page_visit: {
1813
+ label: "Key page visit",
1814
+ category: "engagement",
1815
+ outcomeRole: "engagement",
1816
+ clientVisibility: "detailed"
1817
+ },
1818
+ scroll_depth: {
1819
+ label: "Scroll depth",
1820
+ category: "engagement",
1821
+ outcomeRole: "engagement",
1822
+ clientVisibility: "detailed"
1823
+ },
1824
+ multi_page_session: {
1825
+ label: "Multi-page session",
1826
+ category: "engagement",
1827
+ outcomeRole: "engagement",
1828
+ clientVisibility: "detailed"
1829
+ },
1830
+ form_start: {
1831
+ label: "Form started",
1832
+ category: "engagement",
1833
+ outcomeRole: "engagement",
1834
+ clientVisibility: "simple"
1835
+ },
1836
+ sdk_heartbeat: {
1837
+ label: "SDK heartbeat",
1838
+ category: "system",
1839
+ outcomeRole: "diagnostic",
1840
+ clientVisibility: "hidden"
1841
+ },
1842
+ page_exit: {
1843
+ label: "Page exit",
1844
+ category: "engagement",
1845
+ outcomeRole: "diagnostic",
1846
+ clientVisibility: "detailed"
1847
+ },
1848
+ form_submit: {
1849
+ label: "Form submitted",
1850
+ category: "lead",
1851
+ outcomeRole: "lead",
1852
+ clientVisibility: "simple"
1853
+ },
1854
+ phone_click: {
1855
+ label: "Phone click",
1856
+ category: "lead",
1857
+ outcomeRole: "lead",
1858
+ clientVisibility: "simple"
1859
+ },
1860
+ cta_click: {
1861
+ label: "CTA click",
1862
+ category: "engagement",
1863
+ outcomeRole: "engagement",
1864
+ clientVisibility: "simple"
1865
+ }
1866
+ };
1867
+ function getEventSemantics(eventName) {
1868
+ return eventName in EVENT_SEMANTICS ? EVENT_SEMANTICS[eventName] : null;
1869
+ }
1870
+
1502
1871
  // ../tracking-core/src/events/registry.ts
1503
1872
  var EVENT_REGISTRY = {
1504
1873
  // --- automatic triggers ---
1505
1874
  page_view: {
1506
1875
  kind: "automatic",
1876
+ semantics: EVENT_SEMANTICS.page_view,
1507
1877
  metadataSchema: pageViewMetadataSchema,
1508
1878
  configSchema: pageViewConfigSchema
1509
1879
  },
1510
1880
  time_on_site: {
1511
1881
  kind: "automatic",
1882
+ semantics: EVENT_SEMANTICS.time_on_site,
1512
1883
  metadataSchema: timeOnSiteMetadataSchema,
1513
1884
  configSchema: timeOnSiteConfigSchema
1514
1885
  },
1515
1886
  specific_page_visit: {
1516
1887
  kind: "automatic",
1888
+ semantics: EVENT_SEMANTICS.specific_page_visit,
1517
1889
  metadataSchema: specificPageVisitMetadataSchema,
1518
1890
  configSchema: specificPageVisitConfigSchema
1519
1891
  },
1520
1892
  scroll_depth: {
1521
1893
  kind: "automatic",
1894
+ semantics: EVENT_SEMANTICS.scroll_depth,
1522
1895
  metadataSchema: scrollDepthMetadataSchema,
1523
1896
  configSchema: scrollDepthConfigSchema
1524
1897
  },
1525
1898
  multi_page_session: {
1526
1899
  kind: "automatic",
1900
+ semantics: EVENT_SEMANTICS.multi_page_session,
1527
1901
  metadataSchema: multiPageSessionMetadataSchema,
1528
1902
  configSchema: multiPageSessionConfigSchema
1529
1903
  },
1530
1904
  form_start: {
1531
1905
  kind: "automatic",
1906
+ semantics: EVENT_SEMANTICS.form_start,
1532
1907
  metadataSchema: formStartMetadataSchema,
1533
1908
  configSchema: formStartConfigSchema
1534
1909
  },
1535
1910
  // --- SDK-internal automatic (not consumer-configurable) ---
1536
1911
  sdk_heartbeat: {
1537
1912
  kind: "automatic",
1913
+ semantics: EVENT_SEMANTICS.sdk_heartbeat,
1538
1914
  metadataSchema: sdkHeartbeatMetadataSchema,
1539
1915
  configSchema: sdkHeartbeatConfigSchema
1540
1916
  },
1541
1917
  page_exit: {
1542
1918
  kind: "automatic",
1919
+ semantics: EVENT_SEMANTICS.page_exit,
1543
1920
  metadataSchema: pageExitMetadataSchema,
1544
1921
  configSchema: pageExitConfigSchema
1545
1922
  },
1546
1923
  // --- manual triggers ---
1547
1924
  form_submit: {
1548
1925
  kind: "manual",
1926
+ semantics: EVENT_SEMANTICS.form_submit,
1549
1927
  metadataSchema: formSubmitMetadataSchema,
1550
1928
  configSchema: formSubmitConfigSchema
1551
1929
  },
1552
1930
  phone_click: {
1553
1931
  kind: "manual",
1932
+ semantics: EVENT_SEMANTICS.phone_click,
1554
1933
  metadataSchema: phoneClickMetadataSchema,
1555
1934
  configSchema: phoneClickConfigSchema
1556
1935
  },
1557
1936
  cta_click: {
1558
1937
  kind: "manual",
1938
+ semantics: EVENT_SEMANTICS.cta_click,
1559
1939
  metadataSchema: ctaClickMetadataSchema,
1560
1940
  configSchema: ctaClickConfigSchema
1561
1941
  }
1562
1942
  };
1563
1943
  var ALL_AUTOMATIC_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "automatic").map(([name]) => name);
1564
1944
  var ALL_MANUAL_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "manual").map(([name]) => name);
1945
+ var ALL_LEAD_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.semantics.outcomeRole === "lead").map(([name]) => name);
1565
1946
  function getEventDefinition(name) {
1566
1947
  return EVENT_REGISTRY[name];
1567
1948
  }
@@ -1693,6 +2074,97 @@ function attachSpecificPageVisit(client, config) {
1693
2074
  };
1694
2075
  }
1695
2076
 
2077
+ // ../tracking-core/src/triggers/navigation.ts
2078
+ var listeners = /* @__PURE__ */ new Set();
2079
+ var restorePatch = null;
2080
+ function notify() {
2081
+ for (const listener of listeners) listener();
2082
+ }
2083
+ function notifyDeferred() {
2084
+ setTimeout(notify, 0);
2085
+ }
2086
+ function installPatch() {
2087
+ const originalPushState = history.pushState;
2088
+ const originalReplaceState = history.replaceState;
2089
+ function patchedPushState(...args) {
2090
+ originalPushState.apply(this, args);
2091
+ notifyDeferred();
2092
+ }
2093
+ function patchedReplaceState(...args) {
2094
+ originalReplaceState.apply(this, args);
2095
+ notifyDeferred();
2096
+ }
2097
+ history.pushState = patchedPushState;
2098
+ history.replaceState = patchedReplaceState;
2099
+ window.addEventListener("popstate", notify);
2100
+ restorePatch = () => {
2101
+ history.pushState = originalPushState;
2102
+ history.replaceState = originalReplaceState;
2103
+ window.removeEventListener("popstate", notify);
2104
+ restorePatch = null;
2105
+ };
2106
+ }
2107
+ function onHistoryChange(listener) {
2108
+ if (listeners.size === 0) installPatch();
2109
+ listeners.add(listener);
2110
+ return () => {
2111
+ if (!listeners.delete(listener)) return;
2112
+ if (listeners.size === 0) restorePatch?.();
2113
+ };
2114
+ }
2115
+
2116
+ // ../tracking-core/src/triggers/scroll-measurement.ts
2117
+ var BOTTOM_EPSILON_PX = 2;
2118
+ function measureScrollPercent() {
2119
+ const root = document.scrollingElement ?? document.documentElement;
2120
+ const scrollHeight = root.scrollHeight;
2121
+ const clientHeight = root.clientHeight;
2122
+ if (scrollHeight <= 0 || clientHeight <= 0) return null;
2123
+ if (scrollHeight <= clientHeight + BOTTOM_EPSILON_PX) return null;
2124
+ const maxTop = scrollHeight - clientHeight;
2125
+ const scrollTop = Math.min(Math.max(root.scrollTop, 0), maxTop);
2126
+ if (scrollTop + clientHeight >= scrollHeight - BOTTOM_EPSILON_PX) return 100;
2127
+ return Math.max(1, Math.min(100, Math.round((scrollTop + clientHeight) / scrollHeight * 100)));
2128
+ }
2129
+ function scheduleBaselineSnapshot(onSnapshot) {
2130
+ let rafId = requestAnimationFrame(() => {
2131
+ rafId = requestAnimationFrame(() => {
2132
+ onSnapshot(measureScrollPercent());
2133
+ });
2134
+ });
2135
+ return () => cancelAnimationFrame(rafId);
2136
+ }
2137
+ function createBaselineGate() {
2138
+ let baselinePercent = null;
2139
+ let ready = false;
2140
+ let cancelSnapshot = null;
2141
+ return {
2142
+ rebaseline() {
2143
+ cancelSnapshot?.();
2144
+ ready = false;
2145
+ baselinePercent = null;
2146
+ cancelSnapshot = scheduleBaselineSnapshot((b) => {
2147
+ baselinePercent = b;
2148
+ ready = true;
2149
+ });
2150
+ },
2151
+ cancel() {
2152
+ cancelSnapshot?.();
2153
+ },
2154
+ baseline() {
2155
+ return ready ? baselinePercent : null;
2156
+ },
2157
+ sample() {
2158
+ if (!ready) return null;
2159
+ if (baselinePercent === null) {
2160
+ baselinePercent = measureScrollPercent();
2161
+ return null;
2162
+ }
2163
+ return measureScrollPercent();
2164
+ }
2165
+ };
2166
+ }
2167
+
1696
2168
  // ../tracking-core/src/triggers/scroll-depth.ts
1697
2169
  function attachScrollDepth(client, config) {
1698
2170
  if (typeof window === "undefined" || typeof document === "undefined") {
@@ -1703,17 +2175,14 @@ function attachScrollDepth(client, config) {
1703
2175
  let firedForPath = /* @__PURE__ */ new Set();
1704
2176
  let currentPath2 = window.location.pathname;
1705
2177
  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
- }
2178
+ const gate = createBaselineGate();
1714
2179
  function checkThresholds() {
1715
- const percent = getScrollPercent();
2180
+ const percent = gate.sample();
2181
+ if (percent === null) return;
2182
+ const baseline = gate.baseline();
2183
+ if (baseline === null) return;
1716
2184
  for (const threshold of thresholds) {
2185
+ if (threshold <= baseline) continue;
1717
2186
  if (percent >= threshold && !firedForPath.has(threshold)) {
1718
2187
  firedForPath.add(threshold);
1719
2188
  client.trackEvent({
@@ -1740,28 +2209,15 @@ function attachScrollDepth(client, config) {
1740
2209
  if (newPath === currentPath2) return;
1741
2210
  currentPath2 = newPath;
1742
2211
  firedForPath = /* @__PURE__ */ new Set();
1743
- setTimeout(checkThresholds, 0);
2212
+ gate.rebaseline();
1744
2213
  }
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);
2214
+ const unsubscribeNav = onHistoryChange(resetIfPathChanged);
1758
2215
  window.addEventListener("scroll", onScroll, { passive: true });
1759
- setTimeout(checkThresholds, 0);
2216
+ gate.rebaseline();
1760
2217
  return () => {
1761
2218
  if (rafId !== null) cancelAnimationFrame(rafId);
1762
- history.pushState = originalPushState;
1763
- history.replaceState = originalReplaceState;
1764
- window.removeEventListener("popstate", resetIfPathChanged);
2219
+ gate.cancel();
2220
+ unsubscribeNav();
1765
2221
  window.removeEventListener("scroll", onScroll);
1766
2222
  };
1767
2223
  }
@@ -1933,21 +2389,14 @@ function attachPageExit(client) {
1933
2389
  let accumulatedMs = 0;
1934
2390
  let maxScrollPercent = null;
1935
2391
  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
- }
2392
+ const gate = createBaselineGate();
1944
2393
  function onScroll() {
1945
2394
  if (rafId !== null) return;
1946
2395
  rafId = requestAnimationFrame(() => {
1947
2396
  rafId = null;
1948
- const percent = getScrollPercent();
1949
- if (percent >= 1 && (maxScrollPercent === null || percent > maxScrollPercent)) {
1950
- maxScrollPercent = Math.min(percent, 100);
2397
+ const percent = gate.sample();
2398
+ if (percent !== null && (maxScrollPercent === null || percent > maxScrollPercent)) {
2399
+ maxScrollPercent = percent;
1951
2400
  }
1952
2401
  });
1953
2402
  }
@@ -1966,6 +2415,12 @@ function attachPageExit(client) {
1966
2415
  metadata: {
1967
2416
  dwell_ms: dwell,
1968
2417
  max_scroll_percent: maxScrollPercent,
2418
+ // Lets the backend tell "scrolled to the bottom" apart from "the page
2419
+ // was barely scrollable". null = unscrollable page or the segment
2420
+ // ended before the post-paint snapshot landed. For pages that grew
2421
+ // after the snapshot this is the first-scroll position, not the
2422
+ // at-load fraction (see BaselineGate.baseline).
2423
+ scroll_baseline_percent: gate.baseline(),
1969
2424
  page: { path }
1970
2425
  },
1971
2426
  pageUrl: window.location.href,
@@ -1983,6 +2438,7 @@ function attachPageExit(client) {
1983
2438
  emitSegment(currentPath2, false);
1984
2439
  currentPath2 = newPath;
1985
2440
  maxScrollPercent = null;
2441
+ gate.rebaseline();
1986
2442
  accumulatedMs = 0;
1987
2443
  activeSince = document.visibilityState === "visible" ? Date.now() : null;
1988
2444
  }
@@ -1996,27 +2452,15 @@ function attachPageExit(client) {
1996
2452
  function onPageHide() {
1997
2453
  emitSegment(currentPath2, true);
1998
2454
  }
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);
2455
+ const unsubscribeNav = onHistoryChange(onNavigate);
2012
2456
  window.addEventListener("scroll", onScroll, { passive: true });
2013
2457
  document.addEventListener("visibilitychange", onVisibilityChange);
2014
2458
  window.addEventListener("pagehide", onPageHide);
2459
+ gate.rebaseline();
2015
2460
  return () => {
2016
2461
  if (rafId !== null) cancelAnimationFrame(rafId);
2017
- history.pushState = originalPushState;
2018
- history.replaceState = originalReplaceState;
2019
- window.removeEventListener("popstate", onNavigate);
2462
+ gate.cancel();
2463
+ unsubscribeNav();
2020
2464
  window.removeEventListener("scroll", onScroll);
2021
2465
  document.removeEventListener("visibilitychange", onVisibilityChange);
2022
2466
  window.removeEventListener("pagehide", onPageHide);
@@ -2231,6 +2675,14 @@ function fireRecordedConversions(firing, input, recorded, sale, currency) {
2231
2675
  const txnBase = input.external_id ?? sale.id;
2232
2676
  for (const item of recorded) {
2233
2677
  if (!item.service) continue;
2678
+ if ("fireConversion" in firing) {
2679
+ firing.fireConversion(item.service, {
2680
+ value: item.amount_cents != null ? fromMinor(item.amount_cents, currency) : void 0,
2681
+ currency,
2682
+ transactionId: `${txnBase}:${item.service}`
2683
+ });
2684
+ continue;
2685
+ }
2234
2686
  const config = firing.getFiring(item.service);
2235
2687
  if (!config) continue;
2236
2688
  const cents = item.amount_cents ?? config.value_cents ?? null;
@@ -2273,6 +2725,14 @@ function createSalesClient(config) {
2273
2725
  // recordSale is the intent-revealing alias — same behavior, clearer call site.
2274
2726
  recordSale: record,
2275
2727
  trackConversion(key, options) {
2728
+ if (config.firing && "fireConversion" in config.firing) {
2729
+ config.firing.fireConversion(key, {
2730
+ value: options?.value ?? void 0,
2731
+ currency: options?.currency ?? config.defaultCurrency ?? void 0,
2732
+ transactionId: options?.transactionId ?? null
2733
+ });
2734
+ return;
2735
+ }
2276
2736
  const firing = config.firing?.getFiring(key);
2277
2737
  if (!firing) return;
2278
2738
  const currency = firing.currency ?? options?.currency ?? config.defaultCurrency ?? null;
@@ -2777,21 +3237,28 @@ import { useEffect as useEffect3, useMemo } from "react";
2777
3237
  function AdPlatformTracking({
2778
3238
  gtagId,
2779
3239
  gtagIds,
3240
+ trackingConfig,
3241
+ standalonePageView = false,
2780
3242
  metaPixelId,
2781
3243
  metaPixelIds
2782
3244
  }) {
3245
+ const trackingConfigKey = trackingConfig ? `${trackingConfig.cdnUrl}:${trackingConfig.businessId}:${trackingConfig.environment}` : "";
2783
3246
  const gtagIdsKey = useMemo(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
2784
3247
  const metaPixelIdsKey = useMemo(
2785
3248
  () => metaPixelIds ? JSON.stringify(metaPixelIds) : "",
2786
3249
  [metaPixelIds]
2787
3250
  );
2788
3251
  useEffect3(() => {
2789
- if (gtagIds && Object.keys(gtagIds).length > 0) {
3252
+ if (trackingConfig) {
3253
+ const runtime = getTrackingConfigRuntime(trackingConfig);
3254
+ if (standalonePageView) runtime.queuePageView();
3255
+ else void runtime.revalidate();
3256
+ } else if (gtagIds && Object.keys(gtagIds).length > 0) {
2790
3257
  bootstrapMultipleGtags(gtagIds);
2791
3258
  } else if (gtagId) {
2792
3259
  bootstrapGoogleAdsTracking(gtagId);
2793
3260
  }
2794
- }, [gtagId, gtagIdsKey]);
3261
+ }, [gtagId, gtagIdsKey, trackingConfigKey, standalonePageView]);
2795
3262
  useEffect3(() => {
2796
3263
  if (metaPixelIds && Object.keys(metaPixelIds).length > 0) {
2797
3264
  bootstrapMultiplePixels(metaPixelIds);
@@ -2821,7 +3288,7 @@ function GoogleAdsTracking(props) {
2821
3288
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo4 } from "react";
2822
3289
 
2823
3290
  // package.json
2824
- var version = "0.17.2";
3291
+ var version = "0.18.0";
2825
3292
 
2826
3293
  // ../tracking-core/src/phone-react.tsx
2827
3294
  import {
@@ -2927,7 +3394,16 @@ var NOOP_CLIENT = {
2927
3394
  getVisitorId: () => ""
2928
3395
  };
2929
3396
  function createTracking(options) {
2930
- const { apiKey, endpoint, triggers, environment, debug, phone, conversionConfig } = options;
3397
+ const {
3398
+ apiKey,
3399
+ endpoint,
3400
+ triggers,
3401
+ environment,
3402
+ debug,
3403
+ phone,
3404
+ conversionConfig,
3405
+ trackingConfig
3406
+ } = options;
2931
3407
  if (!apiKey || !endpoint) {
2932
3408
  if (apiKey || endpoint) {
2933
3409
  console.warn(
@@ -2973,7 +3449,9 @@ function createTracking(options) {
2973
3449
  );
2974
3450
  const gtagIdsKey = useMemo4(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
2975
3451
  useEffect5(() => {
2976
- if (gtagIds && Object.keys(gtagIds).length > 0) {
3452
+ if (trackingConfig) {
3453
+ void getTrackingConfigRuntime(trackingConfig).revalidate();
3454
+ } else if (gtagIds && Object.keys(gtagIds).length > 0) {
2977
3455
  bootstrapMultipleGtags(gtagIds);
2978
3456
  } else if (gtagId) {
2979
3457
  bootstrapGoogleAdsTracking(gtagId);
@@ -3003,13 +3481,20 @@ function createTracking(options) {
3003
3481
  });
3004
3482
  return attachClientCapturesOnce(rawClient, () => {
3005
3483
  const detachers = [];
3006
- const conversionStore = conversionConfig ? resolveConversionConfig({
3484
+ const conversionStore = trackingConfig ? getTrackingConfigRuntime(trackingConfig) : conversionConfig ? resolveConversionConfig({
3007
3485
  cdnUrl: conversionConfig.cdnUrl,
3008
3486
  baked: conversionConfig.baked
3009
3487
  }) : null;
3010
3488
  const detectorClient = conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient;
3011
- detachers.push(attachAutoPageView(detectorClient));
3012
- detachers.push(attachBfcacheRestore(detectorClient));
3489
+ const pageClient = trackingConfig && conversionStore && "queuePageView" in conversionStore ? {
3490
+ ...detectorClient,
3491
+ trackEvent: (input) => {
3492
+ detectorClient.trackEvent(input);
3493
+ if (input.eventType === "page_view") conversionStore.queuePageView();
3494
+ }
3495
+ } : detectorClient;
3496
+ detachers.push(attachAutoPageView(pageClient));
3497
+ detachers.push(attachBfcacheRestore(pageClient));
3013
3498
  detachers.push(attachPageExit(detectorClient));
3014
3499
  const timeOnSite = triggers.automatic.time_on_site;
3015
3500
  if (timeOnSite) {
@@ -3065,6 +3550,7 @@ export {
3065
3550
  ConsentBanner,
3066
3551
  DEFAULT_DECLINE_TTL_DAYS,
3067
3552
  DEFAULT_PHONE_COUNTRY,
3553
+ EVENT_SEMANTICS,
3068
3554
  GoogleAdsTracking,
3069
3555
  NAMED_RANGES,
3070
3556
  PhoneField,
@@ -3085,6 +3571,8 @@ export {
3085
3571
  fromMinor,
3086
3572
  getConsentChoice,
3087
3573
  getConsentState,
3574
+ getEventSemantics,
3575
+ getTrackingConfigRuntime,
3088
3576
  onConsentChange,
3089
3577
  optIn,
3090
3578
  optOut,