@aranova/tracking-next 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
@@ -220,6 +220,11 @@ function captureTrackingParamsFromLocation(url = typeof window === "undefined" ?
220
220
  }
221
221
 
222
222
  // ../tracking-core/src/gtag.ts
223
+ var GTAG_SCRIPT_HOST = "https://www.googletagmanager.com/gtag/js";
224
+ var TRACKING_SCRIPT_ATTRIBUTE = "data-aranova-tracking";
225
+ function getScriptMarker(id) {
226
+ return `aranova-${id}`;
227
+ }
223
228
  var GTAG_ID_PATTERN = /^[A-Z]{1,3}-[A-Za-z0-9_-]+$/;
224
229
  function isValidGtagId(id) {
225
230
  return GTAG_ID_PATTERN.test(id);
@@ -248,6 +253,15 @@ gtag('js', new Date());
248
253
  gtag('config', '${gtagId}');
249
254
  `.trim();
250
255
  }
256
+ function ensureGtagFunction() {
257
+ window.dataLayer = window.dataLayer || [];
258
+ if (typeof window.gtag === "function") return window.gtag;
259
+ function gtag() {
260
+ window.dataLayer?.push(arguments);
261
+ }
262
+ window.gtag = gtag;
263
+ return window.gtag;
264
+ }
251
265
  var SEND_TO_RE = /^AW-[A-Za-z0-9]+\/[A-Za-z0-9_-]+$/;
252
266
  function isValidSendTo(sendTo) {
253
267
  return SEND_TO_RE.test(sendTo);
@@ -266,6 +280,27 @@ function fireGtagConversion(input) {
266
280
  return false;
267
281
  }
268
282
  }
283
+ function applyDefaultConsentState() {
284
+ const gtag = ensureGtagFunction();
285
+ gtag(
286
+ "consent",
287
+ "default",
288
+ buildConsentPayload(getConsentState() === "denied" ? "denied" : "granted")
289
+ );
290
+ }
291
+ function loadGtagScript(gtagId) {
292
+ if (typeof document === "undefined") return;
293
+ const marker = getScriptMarker("gtag-loader");
294
+ const existingScript = document.querySelector(
295
+ `script[${TRACKING_SCRIPT_ATTRIBUTE}="${marker}"]`
296
+ );
297
+ if (existingScript) return;
298
+ const script = document.createElement("script");
299
+ script.async = true;
300
+ script.src = `${GTAG_SCRIPT_HOST}?id=${encodeURIComponent(gtagId)}`;
301
+ script.setAttribute(TRACKING_SCRIPT_ATTRIBUTE, marker);
302
+ document.head.append(script);
303
+ }
269
304
  function createGtagInitScriptMulti(gtagIds) {
270
305
  const safeIds = gtagIds.filter(isValidGtagId);
271
306
  const configs = safeIds.map((id) => `gtag('config', '${id}');`).join("\n");
@@ -552,6 +587,7 @@ function parseConversionConfig(raw) {
552
587
  business_id: typeof obj.business_id === "string" ? obj.business_id : void 0,
553
588
  customer_id: typeof obj.customer_id === "string" ? obj.customer_id : null,
554
589
  environment: typeof obj.environment === "string" ? obj.environment : void 0,
590
+ google_tracking_state: obj.google_tracking_state === "active" ? "active" : "disabled",
555
591
  gtag_ids: isStringMap(obj.gtag_ids) ? obj.gtag_ids : {},
556
592
  meta_pixel_ids: isStringMap(obj.meta_pixel_ids) ? obj.meta_pixel_ids : {},
557
593
  services,
@@ -598,9 +634,9 @@ function resolveConversionConfig(options) {
598
634
  }
599
635
  }
600
636
  function notifyResolved() {
601
- const listeners = [...resolveListeners];
637
+ const listeners2 = [...resolveListeners];
602
638
  resolveListeners.clear();
603
- for (const listener of listeners) {
639
+ for (const listener of listeners2) {
604
640
  try {
605
641
  listener();
606
642
  } catch {
@@ -687,6 +723,283 @@ function formatDateInTz(iso, timeZone, opts, locale) {
687
723
  }).format(date);
688
724
  }
689
725
 
726
+ // ../tracking-core/src/resources/tracking-config-runtime.ts
727
+ var CACHE_PREFIX2 = "_aranova_cfg_runtime_";
728
+ var AUTHORITY_TTL_MS = 6e4;
729
+ var runtimes = /* @__PURE__ */ new Map();
730
+ var configuredIds = /* @__PURE__ */ new Set();
731
+ var scriptLoad = null;
732
+ var jsInitialized = false;
733
+ function cacheKey2(url) {
734
+ return `${CACHE_PREFIX2}${url}`;
735
+ }
736
+ function readCache2(url) {
737
+ if (typeof window === "undefined") return null;
738
+ try {
739
+ const raw = window.sessionStorage.getItem(cacheKey2(url));
740
+ if (!raw) return null;
741
+ const parsed = JSON.parse(raw);
742
+ const config = parseConversionConfig(parsed.config);
743
+ if (!config) return null;
744
+ return {
745
+ etag: typeof parsed.etag === "string" ? parsed.etag : null,
746
+ config
747
+ };
748
+ } catch {
749
+ return null;
750
+ }
751
+ }
752
+ function writeCache2(url, entry) {
753
+ if (typeof window === "undefined") return;
754
+ try {
755
+ window.sessionStorage.setItem(cacheKey2(url), JSON.stringify(entry));
756
+ } catch {
757
+ }
758
+ }
759
+ function isTombstone(config) {
760
+ return config.google_tracking_state === "disabled" || Object.keys(config.gtag_ids).length === 0;
761
+ }
762
+ function validateConfig(config, ref) {
763
+ return config.business_id === ref.businessId && config.environment === ref.environment && (config.google_tracking_state === "active" || config.google_tracking_state === "disabled");
764
+ }
765
+ function pageViewSnapshot() {
766
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
767
+ return {
768
+ href: window.location.href,
769
+ title: document.title || null,
770
+ referrer: document.referrer || null
771
+ };
772
+ }
773
+ function ensureScript(gtagId) {
774
+ if (typeof window === "undefined" || typeof document === "undefined") return Promise.resolve();
775
+ applyDefaultConsentState();
776
+ loadGtagScript(gtagId);
777
+ if (scriptLoad) return scriptLoad;
778
+ scriptLoad = new Promise((resolve) => {
779
+ const script = document.querySelector(
780
+ 'script[data-aranova-tracking="aranova-gtag-loader"]'
781
+ );
782
+ if (!script) {
783
+ resolve();
784
+ return;
785
+ }
786
+ if (script.dataset.loaded === "true") {
787
+ resolve();
788
+ return;
789
+ }
790
+ window.setTimeout(resolve, 0);
791
+ script.addEventListener(
792
+ "load",
793
+ () => {
794
+ script.dataset.loaded = "true";
795
+ resolve();
796
+ },
797
+ { once: true }
798
+ );
799
+ script.addEventListener("error", () => resolve(), { once: true });
800
+ });
801
+ return scriptLoad;
802
+ }
803
+ var TrackingConfigRuntime = class {
804
+ constructor(ref, fetchImpl = globalThis.fetch) {
805
+ this.ref = ref;
806
+ this.fetchImpl = fetchImpl;
807
+ this.current = null;
808
+ this.etag = null;
809
+ this.stateValue = "unconfirmed";
810
+ this.confirmedAt = 0;
811
+ this.inFlight = null;
812
+ this.conversionQueue = [];
813
+ this.automaticQueue = [];
814
+ this.pageQueue = [];
815
+ this.listeners = /* @__PURE__ */ new Set();
816
+ const cached = readCache2(ref.cdnUrl);
817
+ this.current = cached?.config ?? null;
818
+ this.etag = cached?.etag ?? null;
819
+ void this.revalidate();
820
+ if (typeof window !== "undefined") {
821
+ window.addEventListener("visibilitychange", () => {
822
+ if (document.visibilityState === "visible") void this.revalidate();
823
+ });
824
+ window.addEventListener("focus", () => void this.revalidate());
825
+ }
826
+ }
827
+ state() {
828
+ return this.stateValue;
829
+ }
830
+ config() {
831
+ return this.stateValue === "active" || this.stateValue === "tombstone" ? this.current : null;
832
+ }
833
+ __unsafeExpireAuthorityForTests() {
834
+ this.confirmedAt = 0;
835
+ }
836
+ subscribe(listener) {
837
+ this.listeners.add(listener);
838
+ return () => this.listeners.delete(listener);
839
+ }
840
+ async ensureAuthority() {
841
+ if (this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS) {
842
+ return true;
843
+ }
844
+ await this.revalidate();
845
+ return this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS;
846
+ }
847
+ async revalidate() {
848
+ if (typeof window === "undefined" || !this.fetchImpl) return;
849
+ if (this.inFlight) return this.inFlight;
850
+ this.inFlight = this.revalidateNow().finally(() => {
851
+ this.inFlight = null;
852
+ });
853
+ return this.inFlight;
854
+ }
855
+ queuePageView(snapshot = pageViewSnapshot()) {
856
+ if (!snapshot) return;
857
+ this.pageQueue.push(snapshot);
858
+ void this.flush();
859
+ }
860
+ fireConversion(key, options) {
861
+ this.conversionQueue.push({ key, ...options });
862
+ void this.flush();
863
+ }
864
+ queueAutomaticEvent(eventType, metadata, transactionPath) {
865
+ this.automaticQueue.push({ eventType, metadata, transactionPath });
866
+ void this.flush();
867
+ }
868
+ listGoals() {
869
+ return this.current?.goals ?? [];
870
+ }
871
+ async revalidateNow() {
872
+ try {
873
+ const headers = {};
874
+ if (this.etag) headers["If-None-Match"] = this.etag;
875
+ const response = await this.fetchImpl(this.ref.cdnUrl, {
876
+ method: "GET",
877
+ headers,
878
+ cache: "no-cache"
879
+ });
880
+ if (response.status === 304 && this.current && validateConfig(this.current, this.ref)) {
881
+ this.confirm(this.current, this.etag);
882
+ await this.flush();
883
+ return;
884
+ }
885
+ if (!response.ok) {
886
+ this.expireAuthority();
887
+ return;
888
+ }
889
+ const next = parseConversionConfig(await response.json());
890
+ if (!next || !validateConfig(next, this.ref)) {
891
+ this.expireAuthority();
892
+ return;
893
+ }
894
+ if (this.current && next.config_version < this.current.config_version) {
895
+ this.expireAuthority();
896
+ return;
897
+ }
898
+ this.confirm(next, response.headers.get("ETag"));
899
+ await this.flush();
900
+ } catch {
901
+ this.expireAuthority();
902
+ }
903
+ }
904
+ expireAuthority() {
905
+ if (this.stateValue !== "unconfirmed") this.stateValue = "unconfirmed";
906
+ }
907
+ confirm(config, etag) {
908
+ this.current = config;
909
+ this.etag = etag;
910
+ this.confirmedAt = Date.now();
911
+ this.stateValue = isTombstone(config) ? "tombstone" : "active";
912
+ writeCache2(this.ref.cdnUrl, { etag, config });
913
+ if (this.stateValue === "tombstone") {
914
+ this.conversionQueue.length = 0;
915
+ this.automaticQueue.length = 0;
916
+ this.pageQueue.length = 0;
917
+ }
918
+ for (const listener of this.listeners) listener();
919
+ }
920
+ async flush() {
921
+ if (!await this.ensureAuthority()) return;
922
+ if (this.stateValue !== "active" || !this.current) return;
923
+ const ids = Object.values(this.current.gtag_ids).filter(
924
+ (id) => typeof id === "string" && isValidGtagId(id)
925
+ );
926
+ if (ids.length === 0) return;
927
+ await ensureScript(ids[0]);
928
+ const gtag = window.gtag;
929
+ if (typeof gtag !== "function") return;
930
+ if (!jsInitialized) {
931
+ gtag("js", /* @__PURE__ */ new Date());
932
+ jsInitialized = true;
933
+ }
934
+ for (const id of ids) {
935
+ if (configuredIds.has(id)) continue;
936
+ gtag("config", id, { send_page_view: false });
937
+ configuredIds.add(id);
938
+ }
939
+ while (this.pageQueue.length) {
940
+ const page = this.pageQueue.shift();
941
+ gtag("event", "page_view", {
942
+ page_location: page.href,
943
+ page_title: page.title ?? void 0,
944
+ page_referrer: page.referrer ?? void 0
945
+ });
946
+ }
947
+ while (this.automaticQueue.length) {
948
+ const event = this.automaticQueue.shift();
949
+ for (const goal of this.current.goals) {
950
+ if (goal.kind !== "event" || !goal.firing) continue;
951
+ if (!automaticThresholdMet(goal, event.eventType, event.metadata)) continue;
952
+ this.conversionQueue.push({
953
+ key: goal.key,
954
+ transactionId: `auto:${goal.key}:${event.transactionPath}`
955
+ });
956
+ }
957
+ }
958
+ while (this.conversionQueue.length) {
959
+ const item = this.conversionQueue.shift();
960
+ const goal = this.current.goals.find((g) => g.key === item.key);
961
+ const firing = goal?.firing;
962
+ if (!firing || getConsentState() === "denied") continue;
963
+ const currency = item.currency ?? firing.currency ?? null;
964
+ const value = item.value ?? (firing.value_cents != null && currency ? fromMinor(firing.value_cents, currency) : null);
965
+ fireGtagConversion({
966
+ sendTo: firing.send_to,
967
+ value,
968
+ currency,
969
+ transactionId: item.transactionId ?? null
970
+ });
971
+ }
972
+ }
973
+ };
974
+ function automaticThresholdMet(goal, eventType, metadata) {
975
+ const t = goal.trigger;
976
+ if (!t || t.event_type !== eventType) return false;
977
+ switch (eventType) {
978
+ case "scroll_depth":
979
+ return typeof metadata.depth_percent === "number" && t.threshold_percent != null && metadata.depth_percent >= t.threshold_percent;
980
+ case "time_on_site":
981
+ return typeof metadata.duration_ms === "number" && t.threshold_seconds != null && metadata.duration_ms >= t.threshold_seconds * 1e3;
982
+ case "multi_page_session":
983
+ return typeof metadata.page_count === "number" && t.page_threshold != null && metadata.page_count >= t.page_threshold;
984
+ case "specific_page_visit":
985
+ return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
986
+ case "page_view":
987
+ case "form_start":
988
+ case "phone_click":
989
+ return true;
990
+ default:
991
+ return false;
992
+ }
993
+ }
994
+ function getTrackingConfigRuntime(ref, fetchImpl) {
995
+ const key = `${ref.cdnUrl}|${ref.businessId}|${ref.environment}`;
996
+ const existing = runtimes.get(key);
997
+ if (existing) return existing;
998
+ const runtime = new TrackingConfigRuntime(ref, fetchImpl ?? globalThis.fetch);
999
+ runtimes.set(key, runtime);
1000
+ return runtime;
1001
+ }
1002
+
690
1003
  // ../tracking-core/src/resources/conversion-autofire.ts
691
1004
  function thresholdMet(goal, eventType, metadata) {
692
1005
  const t = goal.trigger;
@@ -719,6 +1032,12 @@ function createConversionAutoFire(store) {
719
1032
  for (const goal of store.listGoals()) {
720
1033
  if (goal.kind !== "event" || !goal.firing) continue;
721
1034
  if (!thresholdMet(goal, eventType, metadata)) continue;
1035
+ if ("queueAutomaticEvent" in store) {
1036
+ store.fireConversion(goal.key, {
1037
+ transactionId: `auto:${goal.key}:${currentPath()}`
1038
+ });
1039
+ continue;
1040
+ }
722
1041
  const firing = goal.firing;
723
1042
  const cents = firing.value_cents ?? null;
724
1043
  const currency = firing.currency ?? null;
@@ -734,6 +1053,10 @@ function createConversionAutoFire(store) {
734
1053
  }
735
1054
  return {
736
1055
  onAutomaticEvent(eventType, metadata) {
1056
+ if ("queueAutomaticEvent" in store) {
1057
+ store.queueAutomaticEvent(eventType, metadata, currentPath());
1058
+ return;
1059
+ }
737
1060
  if (store.isReady()) {
738
1061
  fireMatching(eventType, metadata);
739
1062
  return;
@@ -1330,6 +1653,14 @@ var pageExitMetadataSchema = z7.object({
1330
1653
  // null = left without any scroll signal; floor is 0 so a valid 0% is never
1331
1654
  // rejected (a single bad field 422s the whole keepalive beacon batch).
1332
1655
  max_scroll_percent: z7.number().int().min(0).max(100).nullable(),
1656
+ // The gating baseline: the fraction of the page visible at load with
1657
+ // zero scrolling — or, for pages that grew after the post-paint snapshot
1658
+ // (skeleton/streaming renders), the first-scroll position that
1659
+ // established it. null = page had no scrollable range, or the segment
1660
+ // ended before the snapshot. `.optional()` is load-bearing: SDK builds
1661
+ // predating this field keep POSTing page_exit without the key —
1662
+ // requiring it would 422 whole keepalive beacon batches.
1663
+ scroll_baseline_percent: z7.number().int().min(0).max(100).nullable().optional(),
1333
1664
  page: z7.object({
1334
1665
  path: z7.string()
1335
1666
  }).strict()
@@ -1405,69 +1736,154 @@ var timeOnSiteConfigSchema = z11.object({
1405
1736
  thresholdSeconds: z11.number().int().positive()
1406
1737
  }).strict();
1407
1738
 
1739
+ // ../tracking-core/src/events/semantics.ts
1740
+ var EVENT_SEMANTICS = {
1741
+ page_view: {
1742
+ label: "Page view",
1743
+ category: "page",
1744
+ outcomeRole: "navigation",
1745
+ clientVisibility: "simple"
1746
+ },
1747
+ time_on_site: {
1748
+ label: "Time on site",
1749
+ category: "engagement",
1750
+ outcomeRole: "engagement",
1751
+ clientVisibility: "detailed"
1752
+ },
1753
+ specific_page_visit: {
1754
+ label: "Key page visit",
1755
+ category: "engagement",
1756
+ outcomeRole: "engagement",
1757
+ clientVisibility: "detailed"
1758
+ },
1759
+ scroll_depth: {
1760
+ label: "Scroll depth",
1761
+ category: "engagement",
1762
+ outcomeRole: "engagement",
1763
+ clientVisibility: "detailed"
1764
+ },
1765
+ multi_page_session: {
1766
+ label: "Multi-page session",
1767
+ category: "engagement",
1768
+ outcomeRole: "engagement",
1769
+ clientVisibility: "detailed"
1770
+ },
1771
+ form_start: {
1772
+ label: "Form started",
1773
+ category: "engagement",
1774
+ outcomeRole: "engagement",
1775
+ clientVisibility: "simple"
1776
+ },
1777
+ sdk_heartbeat: {
1778
+ label: "SDK heartbeat",
1779
+ category: "system",
1780
+ outcomeRole: "diagnostic",
1781
+ clientVisibility: "hidden"
1782
+ },
1783
+ page_exit: {
1784
+ label: "Page exit",
1785
+ category: "engagement",
1786
+ outcomeRole: "diagnostic",
1787
+ clientVisibility: "detailed"
1788
+ },
1789
+ form_submit: {
1790
+ label: "Form submitted",
1791
+ category: "lead",
1792
+ outcomeRole: "lead",
1793
+ clientVisibility: "simple"
1794
+ },
1795
+ phone_click: {
1796
+ label: "Phone click",
1797
+ category: "lead",
1798
+ outcomeRole: "lead",
1799
+ clientVisibility: "simple"
1800
+ },
1801
+ cta_click: {
1802
+ label: "CTA click",
1803
+ category: "engagement",
1804
+ outcomeRole: "engagement",
1805
+ clientVisibility: "simple"
1806
+ }
1807
+ };
1808
+ function getEventSemantics(eventName) {
1809
+ return eventName in EVENT_SEMANTICS ? EVENT_SEMANTICS[eventName] : null;
1810
+ }
1811
+
1408
1812
  // ../tracking-core/src/events/registry.ts
1409
1813
  var EVENT_REGISTRY = {
1410
1814
  // --- automatic triggers ---
1411
1815
  page_view: {
1412
1816
  kind: "automatic",
1817
+ semantics: EVENT_SEMANTICS.page_view,
1413
1818
  metadataSchema: pageViewMetadataSchema,
1414
1819
  configSchema: pageViewConfigSchema
1415
1820
  },
1416
1821
  time_on_site: {
1417
1822
  kind: "automatic",
1823
+ semantics: EVENT_SEMANTICS.time_on_site,
1418
1824
  metadataSchema: timeOnSiteMetadataSchema,
1419
1825
  configSchema: timeOnSiteConfigSchema
1420
1826
  },
1421
1827
  specific_page_visit: {
1422
1828
  kind: "automatic",
1829
+ semantics: EVENT_SEMANTICS.specific_page_visit,
1423
1830
  metadataSchema: specificPageVisitMetadataSchema,
1424
1831
  configSchema: specificPageVisitConfigSchema
1425
1832
  },
1426
1833
  scroll_depth: {
1427
1834
  kind: "automatic",
1835
+ semantics: EVENT_SEMANTICS.scroll_depth,
1428
1836
  metadataSchema: scrollDepthMetadataSchema,
1429
1837
  configSchema: scrollDepthConfigSchema
1430
1838
  },
1431
1839
  multi_page_session: {
1432
1840
  kind: "automatic",
1841
+ semantics: EVENT_SEMANTICS.multi_page_session,
1433
1842
  metadataSchema: multiPageSessionMetadataSchema,
1434
1843
  configSchema: multiPageSessionConfigSchema
1435
1844
  },
1436
1845
  form_start: {
1437
1846
  kind: "automatic",
1847
+ semantics: EVENT_SEMANTICS.form_start,
1438
1848
  metadataSchema: formStartMetadataSchema,
1439
1849
  configSchema: formStartConfigSchema
1440
1850
  },
1441
1851
  // --- SDK-internal automatic (not consumer-configurable) ---
1442
1852
  sdk_heartbeat: {
1443
1853
  kind: "automatic",
1854
+ semantics: EVENT_SEMANTICS.sdk_heartbeat,
1444
1855
  metadataSchema: sdkHeartbeatMetadataSchema,
1445
1856
  configSchema: sdkHeartbeatConfigSchema
1446
1857
  },
1447
1858
  page_exit: {
1448
1859
  kind: "automatic",
1860
+ semantics: EVENT_SEMANTICS.page_exit,
1449
1861
  metadataSchema: pageExitMetadataSchema,
1450
1862
  configSchema: pageExitConfigSchema
1451
1863
  },
1452
1864
  // --- manual triggers ---
1453
1865
  form_submit: {
1454
1866
  kind: "manual",
1867
+ semantics: EVENT_SEMANTICS.form_submit,
1455
1868
  metadataSchema: formSubmitMetadataSchema,
1456
1869
  configSchema: formSubmitConfigSchema
1457
1870
  },
1458
1871
  phone_click: {
1459
1872
  kind: "manual",
1873
+ semantics: EVENT_SEMANTICS.phone_click,
1460
1874
  metadataSchema: phoneClickMetadataSchema,
1461
1875
  configSchema: phoneClickConfigSchema
1462
1876
  },
1463
1877
  cta_click: {
1464
1878
  kind: "manual",
1879
+ semantics: EVENT_SEMANTICS.cta_click,
1465
1880
  metadataSchema: ctaClickMetadataSchema,
1466
1881
  configSchema: ctaClickConfigSchema
1467
1882
  }
1468
1883
  };
1469
1884
  var ALL_AUTOMATIC_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "automatic").map(([name]) => name);
1470
1885
  var ALL_MANUAL_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "manual").map(([name]) => name);
1886
+ var ALL_LEAD_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.semantics.outcomeRole === "lead").map(([name]) => name);
1471
1887
  function getEventDefinition(name) {
1472
1888
  return EVENT_REGISTRY[name];
1473
1889
  }
@@ -1554,6 +1970,97 @@ function attachTimeOnSite(client, config) {
1554
1970
  };
1555
1971
  }
1556
1972
 
1973
+ // ../tracking-core/src/triggers/navigation.ts
1974
+ var listeners = /* @__PURE__ */ new Set();
1975
+ var restorePatch = null;
1976
+ function notify() {
1977
+ for (const listener of listeners) listener();
1978
+ }
1979
+ function notifyDeferred() {
1980
+ setTimeout(notify, 0);
1981
+ }
1982
+ function installPatch() {
1983
+ const originalPushState = history.pushState;
1984
+ const originalReplaceState = history.replaceState;
1985
+ function patchedPushState(...args) {
1986
+ originalPushState.apply(this, args);
1987
+ notifyDeferred();
1988
+ }
1989
+ function patchedReplaceState(...args) {
1990
+ originalReplaceState.apply(this, args);
1991
+ notifyDeferred();
1992
+ }
1993
+ history.pushState = patchedPushState;
1994
+ history.replaceState = patchedReplaceState;
1995
+ window.addEventListener("popstate", notify);
1996
+ restorePatch = () => {
1997
+ history.pushState = originalPushState;
1998
+ history.replaceState = originalReplaceState;
1999
+ window.removeEventListener("popstate", notify);
2000
+ restorePatch = null;
2001
+ };
2002
+ }
2003
+ function onHistoryChange(listener) {
2004
+ if (listeners.size === 0) installPatch();
2005
+ listeners.add(listener);
2006
+ return () => {
2007
+ if (!listeners.delete(listener)) return;
2008
+ if (listeners.size === 0) restorePatch?.();
2009
+ };
2010
+ }
2011
+
2012
+ // ../tracking-core/src/triggers/scroll-measurement.ts
2013
+ var BOTTOM_EPSILON_PX = 2;
2014
+ function measureScrollPercent() {
2015
+ const root = document.scrollingElement ?? document.documentElement;
2016
+ const scrollHeight = root.scrollHeight;
2017
+ const clientHeight = root.clientHeight;
2018
+ if (scrollHeight <= 0 || clientHeight <= 0) return null;
2019
+ if (scrollHeight <= clientHeight + BOTTOM_EPSILON_PX) return null;
2020
+ const maxTop = scrollHeight - clientHeight;
2021
+ const scrollTop = Math.min(Math.max(root.scrollTop, 0), maxTop);
2022
+ if (scrollTop + clientHeight >= scrollHeight - BOTTOM_EPSILON_PX) return 100;
2023
+ return Math.max(1, Math.min(100, Math.round((scrollTop + clientHeight) / scrollHeight * 100)));
2024
+ }
2025
+ function scheduleBaselineSnapshot(onSnapshot) {
2026
+ let rafId = requestAnimationFrame(() => {
2027
+ rafId = requestAnimationFrame(() => {
2028
+ onSnapshot(measureScrollPercent());
2029
+ });
2030
+ });
2031
+ return () => cancelAnimationFrame(rafId);
2032
+ }
2033
+ function createBaselineGate() {
2034
+ let baselinePercent = null;
2035
+ let ready = false;
2036
+ let cancelSnapshot = null;
2037
+ return {
2038
+ rebaseline() {
2039
+ cancelSnapshot?.();
2040
+ ready = false;
2041
+ baselinePercent = null;
2042
+ cancelSnapshot = scheduleBaselineSnapshot((b) => {
2043
+ baselinePercent = b;
2044
+ ready = true;
2045
+ });
2046
+ },
2047
+ cancel() {
2048
+ cancelSnapshot?.();
2049
+ },
2050
+ baseline() {
2051
+ return ready ? baselinePercent : null;
2052
+ },
2053
+ sample() {
2054
+ if (!ready) return null;
2055
+ if (baselinePercent === null) {
2056
+ baselinePercent = measureScrollPercent();
2057
+ return null;
2058
+ }
2059
+ return measureScrollPercent();
2060
+ }
2061
+ };
2062
+ }
2063
+
1557
2064
  // ../tracking-core/src/triggers/scroll-depth.ts
1558
2065
  function attachScrollDepth(client, config) {
1559
2066
  if (typeof window === "undefined" || typeof document === "undefined") {
@@ -1564,17 +2071,14 @@ function attachScrollDepth(client, config) {
1564
2071
  let firedForPath = /* @__PURE__ */ new Set();
1565
2072
  let currentPath2 = window.location.pathname;
1566
2073
  let rafId = null;
1567
- function getScrollPercent() {
1568
- const doc = document.documentElement;
1569
- const scrollTop = window.scrollY || doc.scrollTop;
1570
- const scrollHeight = doc.scrollHeight;
1571
- const clientHeight = doc.clientHeight;
1572
- if (scrollHeight <= clientHeight) return 100;
1573
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1574
- }
2074
+ const gate = createBaselineGate();
1575
2075
  function checkThresholds() {
1576
- const percent = getScrollPercent();
2076
+ const percent = gate.sample();
2077
+ if (percent === null) return;
2078
+ const baseline = gate.baseline();
2079
+ if (baseline === null) return;
1577
2080
  for (const threshold of thresholds) {
2081
+ if (threshold <= baseline) continue;
1578
2082
  if (percent >= threshold && !firedForPath.has(threshold)) {
1579
2083
  firedForPath.add(threshold);
1580
2084
  client.trackEvent({
@@ -1601,28 +2105,15 @@ function attachScrollDepth(client, config) {
1601
2105
  if (newPath === currentPath2) return;
1602
2106
  currentPath2 = newPath;
1603
2107
  firedForPath = /* @__PURE__ */ new Set();
1604
- setTimeout(checkThresholds, 0);
1605
- }
1606
- const originalPushState = history.pushState.bind(history);
1607
- const originalReplaceState = history.replaceState.bind(history);
1608
- function patchedPushState(...args) {
1609
- originalPushState(...args);
1610
- setTimeout(resetIfPathChanged, 0);
2108
+ gate.rebaseline();
1611
2109
  }
1612
- function patchedReplaceState(...args) {
1613
- originalReplaceState(...args);
1614
- setTimeout(resetIfPathChanged, 0);
1615
- }
1616
- history.pushState = patchedPushState;
1617
- history.replaceState = patchedReplaceState;
1618
- window.addEventListener("popstate", resetIfPathChanged);
2110
+ const unsubscribeNav = onHistoryChange(resetIfPathChanged);
1619
2111
  window.addEventListener("scroll", onScroll, { passive: true });
1620
- setTimeout(checkThresholds, 0);
2112
+ gate.rebaseline();
1621
2113
  return () => {
1622
2114
  if (rafId !== null) cancelAnimationFrame(rafId);
1623
- history.pushState = originalPushState;
1624
- history.replaceState = originalReplaceState;
1625
- window.removeEventListener("popstate", resetIfPathChanged);
2115
+ gate.cancel();
2116
+ unsubscribeNav();
1626
2117
  window.removeEventListener("scroll", onScroll);
1627
2118
  };
1628
2119
  }
@@ -1704,21 +2195,14 @@ function attachPageExit(client) {
1704
2195
  let accumulatedMs = 0;
1705
2196
  let maxScrollPercent = null;
1706
2197
  let rafId = null;
1707
- function getScrollPercent() {
1708
- const doc = document.documentElement;
1709
- const scrollTop = window.scrollY || doc.scrollTop;
1710
- const scrollHeight = doc.scrollHeight;
1711
- const clientHeight = doc.clientHeight;
1712
- if (scrollHeight <= clientHeight) return 100;
1713
- return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1714
- }
2198
+ const gate = createBaselineGate();
1715
2199
  function onScroll() {
1716
2200
  if (rafId !== null) return;
1717
2201
  rafId = requestAnimationFrame(() => {
1718
2202
  rafId = null;
1719
- const percent = getScrollPercent();
1720
- if (percent >= 1 && (maxScrollPercent === null || percent > maxScrollPercent)) {
1721
- maxScrollPercent = Math.min(percent, 100);
2203
+ const percent = gate.sample();
2204
+ if (percent !== null && (maxScrollPercent === null || percent > maxScrollPercent)) {
2205
+ maxScrollPercent = percent;
1722
2206
  }
1723
2207
  });
1724
2208
  }
@@ -1737,6 +2221,12 @@ function attachPageExit(client) {
1737
2221
  metadata: {
1738
2222
  dwell_ms: dwell,
1739
2223
  max_scroll_percent: maxScrollPercent,
2224
+ // Lets the backend tell "scrolled to the bottom" apart from "the page
2225
+ // was barely scrollable". null = unscrollable page or the segment
2226
+ // ended before the post-paint snapshot landed. For pages that grew
2227
+ // after the snapshot this is the first-scroll position, not the
2228
+ // at-load fraction (see BaselineGate.baseline).
2229
+ scroll_baseline_percent: gate.baseline(),
1740
2230
  page: { path }
1741
2231
  },
1742
2232
  pageUrl: window.location.href,
@@ -1754,6 +2244,7 @@ function attachPageExit(client) {
1754
2244
  emitSegment(currentPath2, false);
1755
2245
  currentPath2 = newPath;
1756
2246
  maxScrollPercent = null;
2247
+ gate.rebaseline();
1757
2248
  accumulatedMs = 0;
1758
2249
  activeSince = document.visibilityState === "visible" ? Date.now() : null;
1759
2250
  }
@@ -1767,27 +2258,15 @@ function attachPageExit(client) {
1767
2258
  function onPageHide() {
1768
2259
  emitSegment(currentPath2, true);
1769
2260
  }
1770
- const originalPushState = history.pushState.bind(history);
1771
- const originalReplaceState = history.replaceState.bind(history);
1772
- function patchedPushState(...args) {
1773
- originalPushState(...args);
1774
- setTimeout(onNavigate, 0);
1775
- }
1776
- function patchedReplaceState(...args) {
1777
- originalReplaceState(...args);
1778
- setTimeout(onNavigate, 0);
1779
- }
1780
- history.pushState = patchedPushState;
1781
- history.replaceState = patchedReplaceState;
1782
- window.addEventListener("popstate", onNavigate);
2261
+ const unsubscribeNav = onHistoryChange(onNavigate);
1783
2262
  window.addEventListener("scroll", onScroll, { passive: true });
1784
2263
  document.addEventListener("visibilitychange", onVisibilityChange);
1785
2264
  window.addEventListener("pagehide", onPageHide);
2265
+ gate.rebaseline();
1786
2266
  return () => {
1787
2267
  if (rafId !== null) cancelAnimationFrame(rafId);
1788
- history.pushState = originalPushState;
1789
- history.replaceState = originalReplaceState;
1790
- window.removeEventListener("popstate", onNavigate);
2268
+ gate.cancel();
2269
+ unsubscribeNav();
1791
2270
  window.removeEventListener("scroll", onScroll);
1792
2271
  document.removeEventListener("visibilitychange", onVisibilityChange);
1793
2272
  window.removeEventListener("pagehide", onPageHide);
@@ -2002,6 +2481,14 @@ function fireRecordedConversions(firing, input, recorded, sale, currency) {
2002
2481
  const txnBase = input.external_id ?? sale.id;
2003
2482
  for (const item of recorded) {
2004
2483
  if (!item.service) continue;
2484
+ if ("fireConversion" in firing) {
2485
+ firing.fireConversion(item.service, {
2486
+ value: item.amount_cents != null ? fromMinor(item.amount_cents, currency) : void 0,
2487
+ currency,
2488
+ transactionId: `${txnBase}:${item.service}`
2489
+ });
2490
+ continue;
2491
+ }
2005
2492
  const config = firing.getFiring(item.service);
2006
2493
  if (!config) continue;
2007
2494
  const cents = item.amount_cents ?? config.value_cents ?? null;
@@ -2044,6 +2531,14 @@ function createSalesClient(config) {
2044
2531
  // recordSale is the intent-revealing alias — same behavior, clearer call site.
2045
2532
  recordSale: record,
2046
2533
  trackConversion(key, options) {
2534
+ if (config.firing && "fireConversion" in config.firing) {
2535
+ config.firing.fireConversion(key, {
2536
+ value: options?.value ?? void 0,
2537
+ currency: options?.currency ?? config.defaultCurrency ?? void 0,
2538
+ transactionId: options?.transactionId ?? null
2539
+ });
2540
+ return;
2541
+ }
2047
2542
  const firing = config.firing?.getFiring(key);
2048
2543
  if (!firing) return;
2049
2544
  const currency = firing.currency ?? options?.currency ?? config.defaultCurrency ?? null;
@@ -2549,14 +3044,24 @@ var ANIMATION_KEYFRAMES = `
2549
3044
 
2550
3045
  // src/AdPlatformTracking.tsx
2551
3046
  import Script from "next/script";
3047
+ import { useEffect as useEffect3 } from "react";
2552
3048
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2553
3049
  function AdPlatformTracking({
2554
3050
  gtagId,
2555
3051
  gtagIds,
3052
+ trackingConfig,
3053
+ standalonePageView = false,
2556
3054
  metaPixelId,
2557
3055
  metaPixelIds
2558
3056
  }) {
2559
- const gtagAll = gtagIds ? Object.values(gtagIds).filter((id) => id != null) : gtagId ? [gtagId] : [];
3057
+ const trackingConfigKey = trackingConfig ? `${trackingConfig.cdnUrl}:${trackingConfig.businessId}:${trackingConfig.environment}` : "";
3058
+ useEffect3(() => {
3059
+ if (!trackingConfig) return;
3060
+ const runtime = getTrackingConfigRuntime(trackingConfig);
3061
+ if (standalonePageView) runtime.queuePageView();
3062
+ else void runtime.revalidate();
3063
+ }, [trackingConfigKey, standalonePageView]);
3064
+ const gtagAll = trackingConfig ? [] : gtagIds ? Object.values(gtagIds).filter((id) => id != null) : gtagId ? [gtagId] : [];
2560
3065
  const metaAll = metaPixelIds ? Object.values(metaPixelIds).filter((id) => id != null) : metaPixelId ? [metaPixelId] : [];
2561
3066
  const gtagLoaderId = gtagAll[0];
2562
3067
  const gtagInitScript = gtagAll.length === 1 ? createGtagInitScript(gtagAll[0]) : createGtagInitScriptMulti(gtagAll);
@@ -2606,7 +3111,7 @@ function GoogleAdsTracking(props) {
2606
3111
  import { usePathname, useSearchParams } from "next/navigation";
2607
3112
 
2608
3113
  // package.json
2609
- var version = "0.17.2";
3114
+ var version = "0.18.0";
2610
3115
 
2611
3116
  // src/factory.tsx
2612
3117
  import {
@@ -2614,7 +3119,7 @@ import {
2614
3119
  Suspense,
2615
3120
  useCallback as useCallback3,
2616
3121
  useContext as useContext2,
2617
- useEffect as useEffect3,
3122
+ useEffect as useEffect4,
2618
3123
  useMemo as useMemo2,
2619
3124
  useRef
2620
3125
  } from "react";
@@ -2717,13 +3222,23 @@ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2717
3222
  function PageViewTracker({ fire }) {
2718
3223
  const pathname = usePathname();
2719
3224
  const searchParams = useSearchParams();
2720
- useEffect3(() => {
3225
+ useEffect4(() => {
2721
3226
  fire();
2722
3227
  }, [pathname, searchParams, fire]);
2723
3228
  return null;
2724
3229
  }
2725
3230
  function createTracking(options) {
2726
- const { apiKey, endpoint, triggers, environment, gtagIds, debug, phone, conversionConfig } = options;
3231
+ const {
3232
+ apiKey,
3233
+ endpoint,
3234
+ triggers,
3235
+ environment,
3236
+ gtagIds,
3237
+ debug,
3238
+ phone,
3239
+ conversionConfig,
3240
+ trackingConfig
3241
+ } = options;
2727
3242
  if (!apiKey || !endpoint) {
2728
3243
  if (apiKey || endpoint) {
2729
3244
  console.warn(
@@ -2767,7 +3282,7 @@ function createTracking(options) {
2767
3282
  []
2768
3283
  );
2769
3284
  const conversionStore = useMemo2(
2770
- () => conversionConfig ? resolveConversionConfig({
3285
+ () => trackingConfig ? getTrackingConfigRuntime(trackingConfig) : conversionConfig ? resolveConversionConfig({
2771
3286
  cdnUrl: conversionConfig.cdnUrl,
2772
3287
  baked: conversionConfig.baked
2773
3288
  }) : null,
@@ -2789,9 +3304,10 @@ function createTracking(options) {
2789
3304
  debug
2790
3305
  });
2791
3306
  fireManualPageView(wrapClient(rawClient));
3307
+ if (trackingConfig) getTrackingConfigRuntime(trackingConfig).queuePageView();
2792
3308
  }, [wrapClient]);
2793
3309
  const firedPagesRef = useRef(/* @__PURE__ */ new Set());
2794
- useEffect3(() => {
3310
+ useEffect4(() => {
2795
3311
  if (!specificPageVisitConfig || pathname === null) return;
2796
3312
  const rawClient = getOrCreateTrackingClient({
2797
3313
  apiKey,
@@ -2822,7 +3338,7 @@ function createTracking(options) {
2822
3338
  }, [pathname]);
2823
3339
  const multiPageSessionConfig = triggers.automatic.multi_page_session ?? null;
2824
3340
  const mpsLastPathRef = useRef(null);
2825
- useEffect3(() => {
3341
+ useEffect4(() => {
2826
3342
  if (!multiPageSessionConfig || pathname === null) return;
2827
3343
  if (pathname === mpsLastPathRef.current) return;
2828
3344
  mpsLastPathRef.current = pathname;
@@ -2869,7 +3385,7 @@ function createTracking(options) {
2869
3385
  } catch {
2870
3386
  }
2871
3387
  }, [pathname]);
2872
- useEffect3(() => {
3388
+ useEffect4(() => {
2873
3389
  const rawClient = getOrCreateTrackingClient({
2874
3390
  apiKey,
2875
3391
  endpoint,
@@ -2933,6 +3449,7 @@ export {
2933
3449
  ConsentBanner,
2934
3450
  DEFAULT_DECLINE_TTL_DAYS,
2935
3451
  DEFAULT_PHONE_COUNTRY,
3452
+ EVENT_SEMANTICS,
2936
3453
  GoogleAdsTracking,
2937
3454
  NAMED_RANGES,
2938
3455
  PhoneField,
@@ -2953,6 +3470,8 @@ export {
2953
3470
  fromMinor,
2954
3471
  getConsentChoice,
2955
3472
  getConsentState,
3473
+ getEventSemantics,
3474
+ getTrackingConfigRuntime,
2956
3475
  onConsentChange,
2957
3476
  optIn,
2958
3477
  optOut,