@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.js CHANGED
@@ -25,6 +25,7 @@ __export(src_exports, {
25
25
  ConsentBanner: () => ConsentBanner,
26
26
  DEFAULT_DECLINE_TTL_DAYS: () => DEFAULT_DECLINE_TTL_DAYS,
27
27
  DEFAULT_PHONE_COUNTRY: () => DEFAULT_PHONE_COUNTRY,
28
+ EVENT_SEMANTICS: () => EVENT_SEMANTICS,
28
29
  GoogleAdsTracking: () => GoogleAdsTracking,
29
30
  NAMED_RANGES: () => NAMED_RANGES,
30
31
  PhoneField: () => PhoneField,
@@ -45,6 +46,8 @@ __export(src_exports, {
45
46
  fromMinor: () => fromMinor,
46
47
  getConsentChoice: () => getConsentChoice,
47
48
  getConsentState: () => getConsentState,
49
+ getEventSemantics: () => getEventSemantics,
50
+ getTrackingConfigRuntime: () => getTrackingConfigRuntime,
48
51
  onConsentChange: () => onConsentChange,
49
52
  optIn: () => optIn,
50
53
  optOut: () => optOut,
@@ -680,6 +683,7 @@ function parseConversionConfig(raw) {
680
683
  business_id: typeof obj.business_id === "string" ? obj.business_id : void 0,
681
684
  customer_id: typeof obj.customer_id === "string" ? obj.customer_id : null,
682
685
  environment: typeof obj.environment === "string" ? obj.environment : void 0,
686
+ google_tracking_state: obj.google_tracking_state === "active" ? "active" : "disabled",
683
687
  gtag_ids: isStringMap(obj.gtag_ids) ? obj.gtag_ids : {},
684
688
  meta_pixel_ids: isStringMap(obj.meta_pixel_ids) ? obj.meta_pixel_ids : {},
685
689
  services,
@@ -726,9 +730,9 @@ function resolveConversionConfig(options) {
726
730
  }
727
731
  }
728
732
  function notifyResolved() {
729
- const listeners = [...resolveListeners];
733
+ const listeners2 = [...resolveListeners];
730
734
  resolveListeners.clear();
731
- for (const listener of listeners) {
735
+ for (const listener of listeners2) {
732
736
  try {
733
737
  listener();
734
738
  } catch {
@@ -815,6 +819,283 @@ function formatDateInTz(iso, timeZone, opts, locale) {
815
819
  }).format(date);
816
820
  }
817
821
 
822
+ // ../tracking-core/src/resources/tracking-config-runtime.ts
823
+ var CACHE_PREFIX2 = "_aranova_cfg_runtime_";
824
+ var AUTHORITY_TTL_MS = 6e4;
825
+ var runtimes = /* @__PURE__ */ new Map();
826
+ var configuredIds = /* @__PURE__ */ new Set();
827
+ var scriptLoad = null;
828
+ var jsInitialized = false;
829
+ function cacheKey2(url) {
830
+ return `${CACHE_PREFIX2}${url}`;
831
+ }
832
+ function readCache2(url) {
833
+ if (typeof window === "undefined") return null;
834
+ try {
835
+ const raw = window.sessionStorage.getItem(cacheKey2(url));
836
+ if (!raw) return null;
837
+ const parsed = JSON.parse(raw);
838
+ const config = parseConversionConfig(parsed.config);
839
+ if (!config) return null;
840
+ return {
841
+ etag: typeof parsed.etag === "string" ? parsed.etag : null,
842
+ config
843
+ };
844
+ } catch {
845
+ return null;
846
+ }
847
+ }
848
+ function writeCache2(url, entry) {
849
+ if (typeof window === "undefined") return;
850
+ try {
851
+ window.sessionStorage.setItem(cacheKey2(url), JSON.stringify(entry));
852
+ } catch {
853
+ }
854
+ }
855
+ function isTombstone(config) {
856
+ return config.google_tracking_state === "disabled" || Object.keys(config.gtag_ids).length === 0;
857
+ }
858
+ function validateConfig(config, ref) {
859
+ return config.business_id === ref.businessId && config.environment === ref.environment && (config.google_tracking_state === "active" || config.google_tracking_state === "disabled");
860
+ }
861
+ function pageViewSnapshot() {
862
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
863
+ return {
864
+ href: window.location.href,
865
+ title: document.title || null,
866
+ referrer: document.referrer || null
867
+ };
868
+ }
869
+ function ensureScript(gtagId) {
870
+ if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
871
+ applyDefaultConsentState();
872
+ loadGtagScript(gtagId);
873
+ if (scriptLoad) return scriptLoad;
874
+ scriptLoad = new Promise((resolve) => {
875
+ const script = document.querySelector(
876
+ 'script[data-aranova-tracking="aranova-gtag-loader"]'
877
+ );
878
+ if (!script) {
879
+ resolve();
880
+ return;
881
+ }
882
+ if (script.dataset.loaded === "true") {
883
+ resolve();
884
+ return;
885
+ }
886
+ window.setTimeout(resolve, 0);
887
+ script.addEventListener(
888
+ "load",
889
+ () => {
890
+ script.dataset.loaded = "true";
891
+ resolve();
892
+ },
893
+ { once: true }
894
+ );
895
+ script.addEventListener("error", () => resolve(), { once: true });
896
+ });
897
+ return scriptLoad;
898
+ }
899
+ var TrackingConfigRuntime = class {
900
+ constructor(ref, fetchImpl = globalThis.fetch) {
901
+ this.ref = ref;
902
+ this.fetchImpl = fetchImpl;
903
+ this.current = null;
904
+ this.etag = null;
905
+ this.stateValue = "unconfirmed";
906
+ this.confirmedAt = 0;
907
+ this.inFlight = null;
908
+ this.conversionQueue = [];
909
+ this.automaticQueue = [];
910
+ this.pageQueue = [];
911
+ this.listeners = /* @__PURE__ */ new Set();
912
+ const cached = readCache2(ref.cdnUrl);
913
+ this.current = cached?.config ?? null;
914
+ this.etag = cached?.etag ?? null;
915
+ void this.revalidate();
916
+ if (typeof window !== "undefined") {
917
+ window.addEventListener("visibilitychange", () => {
918
+ if (document.visibilityState === "visible") void this.revalidate();
919
+ });
920
+ window.addEventListener("focus", () => void this.revalidate());
921
+ }
922
+ }
923
+ state() {
924
+ return this.stateValue;
925
+ }
926
+ config() {
927
+ return this.stateValue === "active" || this.stateValue === "tombstone" ? this.current : null;
928
+ }
929
+ __unsafeExpireAuthorityForTests() {
930
+ this.confirmedAt = 0;
931
+ }
932
+ subscribe(listener) {
933
+ this.listeners.add(listener);
934
+ return () => this.listeners.delete(listener);
935
+ }
936
+ async ensureAuthority() {
937
+ if (this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS) {
938
+ return true;
939
+ }
940
+ await this.revalidate();
941
+ return this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS;
942
+ }
943
+ async revalidate() {
944
+ if (typeof window === "undefined" || !this.fetchImpl) return;
945
+ if (this.inFlight) return this.inFlight;
946
+ this.inFlight = this.revalidateNow().finally(() => {
947
+ this.inFlight = null;
948
+ });
949
+ return this.inFlight;
950
+ }
951
+ queuePageView(snapshot = pageViewSnapshot()) {
952
+ if (!snapshot) return;
953
+ this.pageQueue.push(snapshot);
954
+ void this.flush();
955
+ }
956
+ fireConversion(key, options) {
957
+ this.conversionQueue.push({ key, ...options });
958
+ void this.flush();
959
+ }
960
+ queueAutomaticEvent(eventType, metadata, transactionPath) {
961
+ this.automaticQueue.push({ eventType, metadata, transactionPath });
962
+ void this.flush();
963
+ }
964
+ listGoals() {
965
+ return this.current?.goals ?? [];
966
+ }
967
+ async revalidateNow() {
968
+ try {
969
+ const headers = {};
970
+ if (this.etag) headers["If-None-Match"] = this.etag;
971
+ const response = await this.fetchImpl(this.ref.cdnUrl, {
972
+ method: "GET",
973
+ headers,
974
+ cache: "no-cache"
975
+ });
976
+ if (response.status === 304 && this.current && validateConfig(this.current, this.ref)) {
977
+ this.confirm(this.current, this.etag);
978
+ await this.flush();
979
+ return;
980
+ }
981
+ if (!response.ok) {
982
+ this.expireAuthority();
983
+ return;
984
+ }
985
+ const next = parseConversionConfig(await response.json());
986
+ if (!next || !validateConfig(next, this.ref)) {
987
+ this.expireAuthority();
988
+ return;
989
+ }
990
+ if (this.current && next.config_version < this.current.config_version) {
991
+ this.expireAuthority();
992
+ return;
993
+ }
994
+ this.confirm(next, response.headers.get("ETag"));
995
+ await this.flush();
996
+ } catch {
997
+ this.expireAuthority();
998
+ }
999
+ }
1000
+ expireAuthority() {
1001
+ if (this.stateValue !== "unconfirmed") this.stateValue = "unconfirmed";
1002
+ }
1003
+ confirm(config, etag) {
1004
+ this.current = config;
1005
+ this.etag = etag;
1006
+ this.confirmedAt = Date.now();
1007
+ this.stateValue = isTombstone(config) ? "tombstone" : "active";
1008
+ writeCache2(this.ref.cdnUrl, { etag, config });
1009
+ if (this.stateValue === "tombstone") {
1010
+ this.conversionQueue.length = 0;
1011
+ this.automaticQueue.length = 0;
1012
+ this.pageQueue.length = 0;
1013
+ }
1014
+ for (const listener of this.listeners) listener();
1015
+ }
1016
+ async flush() {
1017
+ if (!await this.ensureAuthority()) return;
1018
+ if (this.stateValue !== "active" || !this.current) return;
1019
+ const ids = Object.values(this.current.gtag_ids).filter(
1020
+ (id) => typeof id === "string" && isValidGtagId(id)
1021
+ );
1022
+ if (ids.length === 0) return;
1023
+ await ensureScript(ids[0]);
1024
+ const gtag = window.gtag;
1025
+ if (typeof gtag !== "function") return;
1026
+ if (!jsInitialized) {
1027
+ gtag("js", /* @__PURE__ */ new Date());
1028
+ jsInitialized = true;
1029
+ }
1030
+ for (const id of ids) {
1031
+ if (configuredIds.has(id)) continue;
1032
+ gtag("config", id, { send_page_view: false });
1033
+ configuredIds.add(id);
1034
+ }
1035
+ while (this.pageQueue.length) {
1036
+ const page = this.pageQueue.shift();
1037
+ gtag("event", "page_view", {
1038
+ page_location: page.href,
1039
+ page_title: page.title ?? void 0,
1040
+ page_referrer: page.referrer ?? void 0
1041
+ });
1042
+ }
1043
+ while (this.automaticQueue.length) {
1044
+ const event = this.automaticQueue.shift();
1045
+ for (const goal of this.current.goals) {
1046
+ if (goal.kind !== "event" || !goal.firing) continue;
1047
+ if (!automaticThresholdMet(goal, event.eventType, event.metadata)) continue;
1048
+ this.conversionQueue.push({
1049
+ key: goal.key,
1050
+ transactionId: `auto:${goal.key}:${event.transactionPath}`
1051
+ });
1052
+ }
1053
+ }
1054
+ while (this.conversionQueue.length) {
1055
+ const item = this.conversionQueue.shift();
1056
+ const goal = this.current.goals.find((g) => g.key === item.key);
1057
+ const firing = goal?.firing;
1058
+ if (!firing || getConsentState() === "denied") continue;
1059
+ const currency = item.currency ?? firing.currency ?? null;
1060
+ const value = item.value ?? (firing.value_cents != null && currency ? fromMinor(firing.value_cents, currency) : null);
1061
+ fireGtagConversion({
1062
+ sendTo: firing.send_to,
1063
+ value,
1064
+ currency,
1065
+ transactionId: item.transactionId ?? null
1066
+ });
1067
+ }
1068
+ }
1069
+ };
1070
+ function automaticThresholdMet(goal, eventType, metadata) {
1071
+ const t = goal.trigger;
1072
+ if (!t || t.event_type !== eventType) return false;
1073
+ switch (eventType) {
1074
+ case "scroll_depth":
1075
+ return typeof metadata.depth_percent === "number" && t.threshold_percent != null && metadata.depth_percent >= t.threshold_percent;
1076
+ case "time_on_site":
1077
+ return typeof metadata.duration_ms === "number" && t.threshold_seconds != null && metadata.duration_ms >= t.threshold_seconds * 1e3;
1078
+ case "multi_page_session":
1079
+ return typeof metadata.page_count === "number" && t.page_threshold != null && metadata.page_count >= t.page_threshold;
1080
+ case "specific_page_visit":
1081
+ return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
1082
+ case "page_view":
1083
+ case "form_start":
1084
+ case "phone_click":
1085
+ return true;
1086
+ default:
1087
+ return false;
1088
+ }
1089
+ }
1090
+ function getTrackingConfigRuntime(ref, fetchImpl) {
1091
+ const key = `${ref.cdnUrl}|${ref.businessId}|${ref.environment}`;
1092
+ const existing = runtimes.get(key);
1093
+ if (existing) return existing;
1094
+ const runtime = new TrackingConfigRuntime(ref, fetchImpl ?? globalThis.fetch);
1095
+ runtimes.set(key, runtime);
1096
+ return runtime;
1097
+ }
1098
+
818
1099
  // ../tracking-core/src/resources/conversion-autofire.ts
819
1100
  function thresholdMet(goal, eventType, metadata) {
820
1101
  const t = goal.trigger;
@@ -847,6 +1128,12 @@ function createConversionAutoFire(store) {
847
1128
  for (const goal of store.listGoals()) {
848
1129
  if (goal.kind !== "event" || !goal.firing) continue;
849
1130
  if (!thresholdMet(goal, eventType, metadata)) continue;
1131
+ if ("queueAutomaticEvent" in store) {
1132
+ store.fireConversion(goal.key, {
1133
+ transactionId: `auto:${goal.key}:${currentPath()}`
1134
+ });
1135
+ continue;
1136
+ }
850
1137
  const firing = goal.firing;
851
1138
  const cents = firing.value_cents ?? null;
852
1139
  const currency = firing.currency ?? null;
@@ -862,6 +1149,10 @@ function createConversionAutoFire(store) {
862
1149
  }
863
1150
  return {
864
1151
  onAutomaticEvent(eventType, metadata) {
1152
+ if ("queueAutomaticEvent" in store) {
1153
+ store.queueAutomaticEvent(eventType, metadata, currentPath());
1154
+ return;
1155
+ }
865
1156
  if (store.isReady()) {
866
1157
  fireMatching(eventType, metadata);
867
1158
  return;
@@ -1496,6 +1787,14 @@ var pageExitMetadataSchema = import_zod7.z.object({
1496
1787
  // null = left without any scroll signal; floor is 0 so a valid 0% is never
1497
1788
  // rejected (a single bad field 422s the whole keepalive beacon batch).
1498
1789
  max_scroll_percent: import_zod7.z.number().int().min(0).max(100).nullable(),
1790
+ // The gating baseline: the fraction of the page visible at load with
1791
+ // zero scrolling — or, for pages that grew after the post-paint snapshot
1792
+ // (skeleton/streaming renders), the first-scroll position that
1793
+ // established it. null = page had no scrollable range, or the segment
1794
+ // ended before the snapshot. `.optional()` is load-bearing: SDK builds
1795
+ // predating this field keep POSTing page_exit without the key —
1796
+ // requiring it would 422 whole keepalive beacon batches.
1797
+ scroll_baseline_percent: import_zod7.z.number().int().min(0).max(100).nullable().optional(),
1499
1798
  page: import_zod7.z.object({
1500
1799
  path: import_zod7.z.string()
1501
1800
  }).strict()
@@ -1571,69 +1870,154 @@ var timeOnSiteConfigSchema = import_zod11.z.object({
1571
1870
  thresholdSeconds: import_zod11.z.number().int().positive()
1572
1871
  }).strict();
1573
1872
 
1873
+ // ../tracking-core/src/events/semantics.ts
1874
+ var EVENT_SEMANTICS = {
1875
+ page_view: {
1876
+ label: "Page view",
1877
+ category: "page",
1878
+ outcomeRole: "navigation",
1879
+ clientVisibility: "simple"
1880
+ },
1881
+ time_on_site: {
1882
+ label: "Time on site",
1883
+ category: "engagement",
1884
+ outcomeRole: "engagement",
1885
+ clientVisibility: "detailed"
1886
+ },
1887
+ specific_page_visit: {
1888
+ label: "Key page visit",
1889
+ category: "engagement",
1890
+ outcomeRole: "engagement",
1891
+ clientVisibility: "detailed"
1892
+ },
1893
+ scroll_depth: {
1894
+ label: "Scroll depth",
1895
+ category: "engagement",
1896
+ outcomeRole: "engagement",
1897
+ clientVisibility: "detailed"
1898
+ },
1899
+ multi_page_session: {
1900
+ label: "Multi-page session",
1901
+ category: "engagement",
1902
+ outcomeRole: "engagement",
1903
+ clientVisibility: "detailed"
1904
+ },
1905
+ form_start: {
1906
+ label: "Form started",
1907
+ category: "engagement",
1908
+ outcomeRole: "engagement",
1909
+ clientVisibility: "simple"
1910
+ },
1911
+ sdk_heartbeat: {
1912
+ label: "SDK heartbeat",
1913
+ category: "system",
1914
+ outcomeRole: "diagnostic",
1915
+ clientVisibility: "hidden"
1916
+ },
1917
+ page_exit: {
1918
+ label: "Page exit",
1919
+ category: "engagement",
1920
+ outcomeRole: "diagnostic",
1921
+ clientVisibility: "detailed"
1922
+ },
1923
+ form_submit: {
1924
+ label: "Form submitted",
1925
+ category: "lead",
1926
+ outcomeRole: "lead",
1927
+ clientVisibility: "simple"
1928
+ },
1929
+ phone_click: {
1930
+ label: "Phone click",
1931
+ category: "lead",
1932
+ outcomeRole: "lead",
1933
+ clientVisibility: "simple"
1934
+ },
1935
+ cta_click: {
1936
+ label: "CTA click",
1937
+ category: "engagement",
1938
+ outcomeRole: "engagement",
1939
+ clientVisibility: "simple"
1940
+ }
1941
+ };
1942
+ function getEventSemantics(eventName) {
1943
+ return eventName in EVENT_SEMANTICS ? EVENT_SEMANTICS[eventName] : null;
1944
+ }
1945
+
1574
1946
  // ../tracking-core/src/events/registry.ts
1575
1947
  var EVENT_REGISTRY = {
1576
1948
  // --- automatic triggers ---
1577
1949
  page_view: {
1578
1950
  kind: "automatic",
1951
+ semantics: EVENT_SEMANTICS.page_view,
1579
1952
  metadataSchema: pageViewMetadataSchema,
1580
1953
  configSchema: pageViewConfigSchema
1581
1954
  },
1582
1955
  time_on_site: {
1583
1956
  kind: "automatic",
1957
+ semantics: EVENT_SEMANTICS.time_on_site,
1584
1958
  metadataSchema: timeOnSiteMetadataSchema,
1585
1959
  configSchema: timeOnSiteConfigSchema
1586
1960
  },
1587
1961
  specific_page_visit: {
1588
1962
  kind: "automatic",
1963
+ semantics: EVENT_SEMANTICS.specific_page_visit,
1589
1964
  metadataSchema: specificPageVisitMetadataSchema,
1590
1965
  configSchema: specificPageVisitConfigSchema
1591
1966
  },
1592
1967
  scroll_depth: {
1593
1968
  kind: "automatic",
1969
+ semantics: EVENT_SEMANTICS.scroll_depth,
1594
1970
  metadataSchema: scrollDepthMetadataSchema,
1595
1971
  configSchema: scrollDepthConfigSchema
1596
1972
  },
1597
1973
  multi_page_session: {
1598
1974
  kind: "automatic",
1975
+ semantics: EVENT_SEMANTICS.multi_page_session,
1599
1976
  metadataSchema: multiPageSessionMetadataSchema,
1600
1977
  configSchema: multiPageSessionConfigSchema
1601
1978
  },
1602
1979
  form_start: {
1603
1980
  kind: "automatic",
1981
+ semantics: EVENT_SEMANTICS.form_start,
1604
1982
  metadataSchema: formStartMetadataSchema,
1605
1983
  configSchema: formStartConfigSchema
1606
1984
  },
1607
1985
  // --- SDK-internal automatic (not consumer-configurable) ---
1608
1986
  sdk_heartbeat: {
1609
1987
  kind: "automatic",
1988
+ semantics: EVENT_SEMANTICS.sdk_heartbeat,
1610
1989
  metadataSchema: sdkHeartbeatMetadataSchema,
1611
1990
  configSchema: sdkHeartbeatConfigSchema
1612
1991
  },
1613
1992
  page_exit: {
1614
1993
  kind: "automatic",
1994
+ semantics: EVENT_SEMANTICS.page_exit,
1615
1995
  metadataSchema: pageExitMetadataSchema,
1616
1996
  configSchema: pageExitConfigSchema
1617
1997
  },
1618
1998
  // --- manual triggers ---
1619
1999
  form_submit: {
1620
2000
  kind: "manual",
2001
+ semantics: EVENT_SEMANTICS.form_submit,
1621
2002
  metadataSchema: formSubmitMetadataSchema,
1622
2003
  configSchema: formSubmitConfigSchema
1623
2004
  },
1624
2005
  phone_click: {
1625
2006
  kind: "manual",
2007
+ semantics: EVENT_SEMANTICS.phone_click,
1626
2008
  metadataSchema: phoneClickMetadataSchema,
1627
2009
  configSchema: phoneClickConfigSchema
1628
2010
  },
1629
2011
  cta_click: {
1630
2012
  kind: "manual",
2013
+ semantics: EVENT_SEMANTICS.cta_click,
1631
2014
  metadataSchema: ctaClickMetadataSchema,
1632
2015
  configSchema: ctaClickConfigSchema
1633
2016
  }
1634
2017
  };
1635
2018
  var ALL_AUTOMATIC_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "automatic").map(([name]) => name);
1636
2019
  var ALL_MANUAL_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "manual").map(([name]) => name);
2020
+ var ALL_LEAD_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.semantics.outcomeRole === "lead").map(([name]) => name);
1637
2021
  function getEventDefinition(name) {
1638
2022
  return EVENT_REGISTRY[name];
1639
2023
  }
@@ -1765,6 +2149,97 @@ function attachSpecificPageVisit(client, config) {
1765
2149
  };
1766
2150
  }
1767
2151
 
2152
+ // ../tracking-core/src/triggers/navigation.ts
2153
+ var listeners = /* @__PURE__ */ new Set();
2154
+ var restorePatch = null;
2155
+ function notify() {
2156
+ for (const listener of listeners) listener();
2157
+ }
2158
+ function notifyDeferred() {
2159
+ setTimeout(notify, 0);
2160
+ }
2161
+ function installPatch() {
2162
+ const originalPushState = history.pushState;
2163
+ const originalReplaceState = history.replaceState;
2164
+ function patchedPushState(...args) {
2165
+ originalPushState.apply(this, args);
2166
+ notifyDeferred();
2167
+ }
2168
+ function patchedReplaceState(...args) {
2169
+ originalReplaceState.apply(this, args);
2170
+ notifyDeferred();
2171
+ }
2172
+ history.pushState = patchedPushState;
2173
+ history.replaceState = patchedReplaceState;
2174
+ window.addEventListener("popstate", notify);
2175
+ restorePatch = () => {
2176
+ history.pushState = originalPushState;
2177
+ history.replaceState = originalReplaceState;
2178
+ window.removeEventListener("popstate", notify);
2179
+ restorePatch = null;
2180
+ };
2181
+ }
2182
+ function onHistoryChange(listener) {
2183
+ if (listeners.size === 0) installPatch();
2184
+ listeners.add(listener);
2185
+ return () => {
2186
+ if (!listeners.delete(listener)) return;
2187
+ if (listeners.size === 0) restorePatch?.();
2188
+ };
2189
+ }
2190
+
2191
+ // ../tracking-core/src/triggers/scroll-measurement.ts
2192
+ var BOTTOM_EPSILON_PX = 2;
2193
+ function measureScrollPercent() {
2194
+ const root = document.scrollingElement ?? document.documentElement;
2195
+ const scrollHeight = root.scrollHeight;
2196
+ const clientHeight = root.clientHeight;
2197
+ if (scrollHeight <= 0 || clientHeight <= 0) return null;
2198
+ if (scrollHeight <= clientHeight + BOTTOM_EPSILON_PX) return null;
2199
+ const maxTop = scrollHeight - clientHeight;
2200
+ const scrollTop = Math.min(Math.max(root.scrollTop, 0), maxTop);
2201
+ if (scrollTop + clientHeight >= scrollHeight - BOTTOM_EPSILON_PX) return 100;
2202
+ return Math.max(1, Math.min(100, Math.round((scrollTop + clientHeight) / scrollHeight * 100)));
2203
+ }
2204
+ function scheduleBaselineSnapshot(onSnapshot) {
2205
+ let rafId = requestAnimationFrame(() => {
2206
+ rafId = requestAnimationFrame(() => {
2207
+ onSnapshot(measureScrollPercent());
2208
+ });
2209
+ });
2210
+ return () => cancelAnimationFrame(rafId);
2211
+ }
2212
+ function createBaselineGate() {
2213
+ let baselinePercent = null;
2214
+ let ready = false;
2215
+ let cancelSnapshot = null;
2216
+ return {
2217
+ rebaseline() {
2218
+ cancelSnapshot?.();
2219
+ ready = false;
2220
+ baselinePercent = null;
2221
+ cancelSnapshot = scheduleBaselineSnapshot((b) => {
2222
+ baselinePercent = b;
2223
+ ready = true;
2224
+ });
2225
+ },
2226
+ cancel() {
2227
+ cancelSnapshot?.();
2228
+ },
2229
+ baseline() {
2230
+ return ready ? baselinePercent : null;
2231
+ },
2232
+ sample() {
2233
+ if (!ready) return null;
2234
+ if (baselinePercent === null) {
2235
+ baselinePercent = measureScrollPercent();
2236
+ return null;
2237
+ }
2238
+ return measureScrollPercent();
2239
+ }
2240
+ };
2241
+ }
2242
+
1768
2243
  // ../tracking-core/src/triggers/scroll-depth.ts
1769
2244
  function attachScrollDepth(client, config) {
1770
2245
  if (typeof window === "undefined" || typeof document === "undefined") {
@@ -1775,17 +2250,14 @@ function attachScrollDepth(client, config) {
1775
2250
  let firedForPath = /* @__PURE__ */ new Set();
1776
2251
  let currentPath2 = window.location.pathname;
1777
2252
  let rafId = null;
1778
- function getScrollPercent() {
1779
- const doc = document.documentElement;
1780
- const scrollTop = window.scrollY || doc.scrollTop;
1781
- const scrollHeight = doc.scrollHeight;
1782
- const clientHeight = doc.clientHeight;
1783
- if (scrollHeight <= clientHeight) return 100;
1784
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1785
- }
2253
+ const gate = createBaselineGate();
1786
2254
  function checkThresholds() {
1787
- const percent = getScrollPercent();
2255
+ const percent = gate.sample();
2256
+ if (percent === null) return;
2257
+ const baseline = gate.baseline();
2258
+ if (baseline === null) return;
1788
2259
  for (const threshold of thresholds) {
2260
+ if (threshold <= baseline) continue;
1789
2261
  if (percent >= threshold && !firedForPath.has(threshold)) {
1790
2262
  firedForPath.add(threshold);
1791
2263
  client.trackEvent({
@@ -1812,28 +2284,15 @@ function attachScrollDepth(client, config) {
1812
2284
  if (newPath === currentPath2) return;
1813
2285
  currentPath2 = newPath;
1814
2286
  firedForPath = /* @__PURE__ */ new Set();
1815
- setTimeout(checkThresholds, 0);
2287
+ gate.rebaseline();
1816
2288
  }
1817
- const originalPushState = history.pushState.bind(history);
1818
- const originalReplaceState = history.replaceState.bind(history);
1819
- function patchedPushState(...args) {
1820
- originalPushState(...args);
1821
- setTimeout(resetIfPathChanged, 0);
1822
- }
1823
- function patchedReplaceState(...args) {
1824
- originalReplaceState(...args);
1825
- setTimeout(resetIfPathChanged, 0);
1826
- }
1827
- history.pushState = patchedPushState;
1828
- history.replaceState = patchedReplaceState;
1829
- window.addEventListener("popstate", resetIfPathChanged);
2289
+ const unsubscribeNav = onHistoryChange(resetIfPathChanged);
1830
2290
  window.addEventListener("scroll", onScroll, { passive: true });
1831
- setTimeout(checkThresholds, 0);
2291
+ gate.rebaseline();
1832
2292
  return () => {
1833
2293
  if (rafId !== null) cancelAnimationFrame(rafId);
1834
- history.pushState = originalPushState;
1835
- history.replaceState = originalReplaceState;
1836
- window.removeEventListener("popstate", resetIfPathChanged);
2294
+ gate.cancel();
2295
+ unsubscribeNav();
1837
2296
  window.removeEventListener("scroll", onScroll);
1838
2297
  };
1839
2298
  }
@@ -2005,21 +2464,14 @@ function attachPageExit(client) {
2005
2464
  let accumulatedMs = 0;
2006
2465
  let maxScrollPercent = null;
2007
2466
  let rafId = null;
2008
- function getScrollPercent() {
2009
- const doc = document.documentElement;
2010
- const scrollTop = window.scrollY || doc.scrollTop;
2011
- const scrollHeight = doc.scrollHeight;
2012
- const clientHeight = doc.clientHeight;
2013
- if (scrollHeight <= clientHeight) return 100;
2014
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
2015
- }
2467
+ const gate = createBaselineGate();
2016
2468
  function onScroll() {
2017
2469
  if (rafId !== null) return;
2018
2470
  rafId = requestAnimationFrame(() => {
2019
2471
  rafId = null;
2020
- const percent = getScrollPercent();
2021
- if (percent >= 1 && (maxScrollPercent === null || percent > maxScrollPercent)) {
2022
- maxScrollPercent = Math.min(percent, 100);
2472
+ const percent = gate.sample();
2473
+ if (percent !== null && (maxScrollPercent === null || percent > maxScrollPercent)) {
2474
+ maxScrollPercent = percent;
2023
2475
  }
2024
2476
  });
2025
2477
  }
@@ -2038,6 +2490,12 @@ function attachPageExit(client) {
2038
2490
  metadata: {
2039
2491
  dwell_ms: dwell,
2040
2492
  max_scroll_percent: maxScrollPercent,
2493
+ // Lets the backend tell "scrolled to the bottom" apart from "the page
2494
+ // was barely scrollable". null = unscrollable page or the segment
2495
+ // ended before the post-paint snapshot landed. For pages that grew
2496
+ // after the snapshot this is the first-scroll position, not the
2497
+ // at-load fraction (see BaselineGate.baseline).
2498
+ scroll_baseline_percent: gate.baseline(),
2041
2499
  page: { path }
2042
2500
  },
2043
2501
  pageUrl: window.location.href,
@@ -2055,6 +2513,7 @@ function attachPageExit(client) {
2055
2513
  emitSegment(currentPath2, false);
2056
2514
  currentPath2 = newPath;
2057
2515
  maxScrollPercent = null;
2516
+ gate.rebaseline();
2058
2517
  accumulatedMs = 0;
2059
2518
  activeSince = document.visibilityState === "visible" ? Date.now() : null;
2060
2519
  }
@@ -2068,27 +2527,15 @@ function attachPageExit(client) {
2068
2527
  function onPageHide() {
2069
2528
  emitSegment(currentPath2, true);
2070
2529
  }
2071
- const originalPushState = history.pushState.bind(history);
2072
- const originalReplaceState = history.replaceState.bind(history);
2073
- function patchedPushState(...args) {
2074
- originalPushState(...args);
2075
- setTimeout(onNavigate, 0);
2076
- }
2077
- function patchedReplaceState(...args) {
2078
- originalReplaceState(...args);
2079
- setTimeout(onNavigate, 0);
2080
- }
2081
- history.pushState = patchedPushState;
2082
- history.replaceState = patchedReplaceState;
2083
- window.addEventListener("popstate", onNavigate);
2530
+ const unsubscribeNav = onHistoryChange(onNavigate);
2084
2531
  window.addEventListener("scroll", onScroll, { passive: true });
2085
2532
  document.addEventListener("visibilitychange", onVisibilityChange);
2086
2533
  window.addEventListener("pagehide", onPageHide);
2534
+ gate.rebaseline();
2087
2535
  return () => {
2088
2536
  if (rafId !== null) cancelAnimationFrame(rafId);
2089
- history.pushState = originalPushState;
2090
- history.replaceState = originalReplaceState;
2091
- window.removeEventListener("popstate", onNavigate);
2537
+ gate.cancel();
2538
+ unsubscribeNav();
2092
2539
  window.removeEventListener("scroll", onScroll);
2093
2540
  document.removeEventListener("visibilitychange", onVisibilityChange);
2094
2541
  window.removeEventListener("pagehide", onPageHide);
@@ -2303,6 +2750,14 @@ function fireRecordedConversions(firing, input, recorded, sale, currency) {
2303
2750
  const txnBase = input.external_id ?? sale.id;
2304
2751
  for (const item of recorded) {
2305
2752
  if (!item.service) continue;
2753
+ if ("fireConversion" in firing) {
2754
+ firing.fireConversion(item.service, {
2755
+ value: item.amount_cents != null ? fromMinor(item.amount_cents, currency) : void 0,
2756
+ currency,
2757
+ transactionId: `${txnBase}:${item.service}`
2758
+ });
2759
+ continue;
2760
+ }
2306
2761
  const config = firing.getFiring(item.service);
2307
2762
  if (!config) continue;
2308
2763
  const cents = item.amount_cents ?? config.value_cents ?? null;
@@ -2345,6 +2800,14 @@ function createSalesClient(config) {
2345
2800
  // recordSale is the intent-revealing alias — same behavior, clearer call site.
2346
2801
  recordSale: record,
2347
2802
  trackConversion(key, options) {
2803
+ if (config.firing && "fireConversion" in config.firing) {
2804
+ config.firing.fireConversion(key, {
2805
+ value: options?.value ?? void 0,
2806
+ currency: options?.currency ?? config.defaultCurrency ?? void 0,
2807
+ transactionId: options?.transactionId ?? null
2808
+ });
2809
+ return;
2810
+ }
2348
2811
  const firing = config.firing?.getFiring(key);
2349
2812
  if (!firing) return;
2350
2813
  const currency = firing.currency ?? options?.currency ?? config.defaultCurrency ?? null;
@@ -2849,21 +3312,28 @@ var import_react3 = require("react");
2849
3312
  function AdPlatformTracking({
2850
3313
  gtagId,
2851
3314
  gtagIds,
3315
+ trackingConfig,
3316
+ standalonePageView = false,
2852
3317
  metaPixelId,
2853
3318
  metaPixelIds
2854
3319
  }) {
3320
+ const trackingConfigKey = trackingConfig ? `${trackingConfig.cdnUrl}:${trackingConfig.businessId}:${trackingConfig.environment}` : "";
2855
3321
  const gtagIdsKey = (0, import_react3.useMemo)(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
2856
3322
  const metaPixelIdsKey = (0, import_react3.useMemo)(
2857
3323
  () => metaPixelIds ? JSON.stringify(metaPixelIds) : "",
2858
3324
  [metaPixelIds]
2859
3325
  );
2860
3326
  (0, import_react3.useEffect)(() => {
2861
- if (gtagIds && Object.keys(gtagIds).length > 0) {
3327
+ if (trackingConfig) {
3328
+ const runtime = getTrackingConfigRuntime(trackingConfig);
3329
+ if (standalonePageView) runtime.queuePageView();
3330
+ else void runtime.revalidate();
3331
+ } else if (gtagIds && Object.keys(gtagIds).length > 0) {
2862
3332
  bootstrapMultipleGtags(gtagIds);
2863
3333
  } else if (gtagId) {
2864
3334
  bootstrapGoogleAdsTracking(gtagId);
2865
3335
  }
2866
- }, [gtagId, gtagIdsKey]);
3336
+ }, [gtagId, gtagIdsKey, trackingConfigKey, standalonePageView]);
2867
3337
  (0, import_react3.useEffect)(() => {
2868
3338
  if (metaPixelIds && Object.keys(metaPixelIds).length > 0) {
2869
3339
  bootstrapMultiplePixels(metaPixelIds);
@@ -2893,7 +3363,7 @@ function GoogleAdsTracking(props) {
2893
3363
  var import_react6 = require("react");
2894
3364
 
2895
3365
  // package.json
2896
- var version = "0.17.2";
3366
+ var version = "0.18.0";
2897
3367
 
2898
3368
  // ../tracking-core/src/phone-react.tsx
2899
3369
  var import_react5 = require("react");
@@ -2992,7 +3462,16 @@ var NOOP_CLIENT = {
2992
3462
  getVisitorId: () => ""
2993
3463
  };
2994
3464
  function createTracking(options) {
2995
- const { apiKey, endpoint, triggers, environment, debug, phone, conversionConfig } = options;
3465
+ const {
3466
+ apiKey,
3467
+ endpoint,
3468
+ triggers,
3469
+ environment,
3470
+ debug,
3471
+ phone,
3472
+ conversionConfig,
3473
+ trackingConfig
3474
+ } = options;
2996
3475
  if (!apiKey || !endpoint) {
2997
3476
  if (apiKey || endpoint) {
2998
3477
  console.warn(
@@ -3038,7 +3517,9 @@ function createTracking(options) {
3038
3517
  );
3039
3518
  const gtagIdsKey = (0, import_react6.useMemo)(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
3040
3519
  (0, import_react6.useEffect)(() => {
3041
- if (gtagIds && Object.keys(gtagIds).length > 0) {
3520
+ if (trackingConfig) {
3521
+ void getTrackingConfigRuntime(trackingConfig).revalidate();
3522
+ } else if (gtagIds && Object.keys(gtagIds).length > 0) {
3042
3523
  bootstrapMultipleGtags(gtagIds);
3043
3524
  } else if (gtagId) {
3044
3525
  bootstrapGoogleAdsTracking(gtagId);
@@ -3068,13 +3549,20 @@ function createTracking(options) {
3068
3549
  });
3069
3550
  return attachClientCapturesOnce(rawClient, () => {
3070
3551
  const detachers = [];
3071
- const conversionStore = conversionConfig ? resolveConversionConfig({
3552
+ const conversionStore = trackingConfig ? getTrackingConfigRuntime(trackingConfig) : conversionConfig ? resolveConversionConfig({
3072
3553
  cdnUrl: conversionConfig.cdnUrl,
3073
3554
  baked: conversionConfig.baked
3074
3555
  }) : null;
3075
3556
  const detectorClient = conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient;
3076
- detachers.push(attachAutoPageView(detectorClient));
3077
- detachers.push(attachBfcacheRestore(detectorClient));
3557
+ const pageClient = trackingConfig && conversionStore && "queuePageView" in conversionStore ? {
3558
+ ...detectorClient,
3559
+ trackEvent: (input) => {
3560
+ detectorClient.trackEvent(input);
3561
+ if (input.eventType === "page_view") conversionStore.queuePageView();
3562
+ }
3563
+ } : detectorClient;
3564
+ detachers.push(attachAutoPageView(pageClient));
3565
+ detachers.push(attachBfcacheRestore(pageClient));
3078
3566
  detachers.push(attachPageExit(detectorClient));
3079
3567
  const timeOnSite = triggers.automatic.time_on_site;
3080
3568
  if (timeOnSite) {
@@ -3131,6 +3619,7 @@ function createTracking(options) {
3131
3619
  ConsentBanner,
3132
3620
  DEFAULT_DECLINE_TTL_DAYS,
3133
3621
  DEFAULT_PHONE_COUNTRY,
3622
+ EVENT_SEMANTICS,
3134
3623
  GoogleAdsTracking,
3135
3624
  NAMED_RANGES,
3136
3625
  PhoneField,
@@ -3151,6 +3640,8 @@ function createTracking(options) {
3151
3640
  fromMinor,
3152
3641
  getConsentChoice,
3153
3642
  getConsentState,
3643
+ getEventSemantics,
3644
+ getTrackingConfigRuntime,
3154
3645
  onConsentChange,
3155
3646
  optIn,
3156
3647
  optOut,