@aranova/tracking-react 1.0.0 → 1.1.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/client.js CHANGED
@@ -294,6 +294,7 @@ var TRACKING_CAPABILITIES = [
294
294
  "consent_controls",
295
295
  "conversion_goals_auto",
296
296
  "conversion_goals_manual",
297
+ "cross_site_linker",
297
298
  "cta_click_capture",
298
299
  "form_capture",
299
300
  "phone_click_capture",
@@ -320,6 +321,9 @@ function readDomMarkers() {
320
321
  return found;
321
322
  }
322
323
 
324
+ // ../tracking-core/src/cross-site-link.ts
325
+ var z = __toESM(require("zod/mini"));
326
+
323
327
  // ../tracking-core/src/tracking.ts
324
328
  var TRACKING_COOKIE_MAX_AGE_SECONDS = 7776e3;
325
329
  var TRACKING_PARAM_KEYS = [
@@ -687,6 +691,244 @@ function buildLandingPayloadFields(sessionId, override) {
687
691
  };
688
692
  }
689
693
 
694
+ // ../tracking-core/src/session.ts
695
+ var VISITOR_STORAGE_KEY = "aranova_tracking_visitor";
696
+ var SESSION_STORAGE_KEY = "aranova_tracking_session";
697
+ var SESSION_IDLE_MS = 30 * 60 * 1e3;
698
+ function safeUuid() {
699
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function")
700
+ return crypto.randomUUID();
701
+ return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;
702
+ }
703
+ function readLocalStorage(key) {
704
+ try {
705
+ return window.localStorage.getItem(key);
706
+ } catch {
707
+ return null;
708
+ }
709
+ }
710
+ function writeLocalStorage(key, value) {
711
+ try {
712
+ window.localStorage.setItem(key, value);
713
+ } catch {
714
+ }
715
+ }
716
+ function getVisitorId() {
717
+ if (typeof window === "undefined") return safeUuid();
718
+ const existing = readLocalStorage(VISITOR_STORAGE_KEY);
719
+ if (existing && existing.length > 0) return existing;
720
+ const fresh = safeUuid();
721
+ writeLocalStorage(VISITOR_STORAGE_KEY, fresh);
722
+ return fresh;
723
+ }
724
+ function adoptVisitorId(visitorId) {
725
+ if (typeof window === "undefined" || visitorId.length === 0) return false;
726
+ const existing = readLocalStorage(VISITOR_STORAGE_KEY);
727
+ if (existing && existing.length > 0) return false;
728
+ writeLocalStorage(VISITOR_STORAGE_KEY, visitorId);
729
+ return readLocalStorage(VISITOR_STORAGE_KEY) === visitorId;
730
+ }
731
+ function getOrRotateSessionId(now = Date.now()) {
732
+ if (typeof window === "undefined") return { id: safeUuid(), isNew: true };
733
+ const raw = readLocalStorage(SESSION_STORAGE_KEY);
734
+ if (raw) {
735
+ try {
736
+ const parsed = JSON.parse(raw);
737
+ if (typeof parsed.id === "string" && typeof parsed.last_event_at === "number") {
738
+ if (now - parsed.last_event_at <= SESSION_IDLE_MS) {
739
+ const refreshed = { id: parsed.id, last_event_at: now };
740
+ writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(refreshed));
741
+ return { id: parsed.id, isNew: false };
742
+ }
743
+ }
744
+ } catch {
745
+ }
746
+ }
747
+ const fresh = { id: safeUuid(), last_event_at: now };
748
+ writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(fresh));
749
+ return { id: fresh.id, isNew: true };
750
+ }
751
+
752
+ // ../tracking-core/src/cross-site-link.ts
753
+ var LINK_PARAM = "_arv";
754
+ var LINK_TOKEN_VERSION = "1";
755
+ var LINK_TOKEN_TTL_MS = 5 * 60 * 1e3;
756
+ var FUTURE_SKEW_MS = 60 * 1e3;
757
+ var CARRIED_KEYS = {
758
+ g: "gclid",
759
+ w: "wbraid",
760
+ gb: "gbraid",
761
+ f: "fbclid",
762
+ y: "ylpcid"
763
+ };
764
+ var CARRIED_ENTRIES = Object.entries(CARRIED_KEYS);
765
+ var identifier = z.string().check(z.minLength(1), z.maxLength(128));
766
+ var optionalIdentifier = z.optional(identifier);
767
+ var linkTokenSchema = z.object({
768
+ v: identifier,
769
+ s: identifier,
770
+ // Epoch ms in base36.
771
+ t: z.string().check(z.regex(/^[0-9a-z]{1,12}$/)),
772
+ g: optionalIdentifier,
773
+ w: optionalIdentifier,
774
+ gb: optionalIdentifier,
775
+ f: optionalIdentifier,
776
+ fc: optionalIdentifier,
777
+ y: optionalIdentifier
778
+ });
779
+ function toBase64Url(input) {
780
+ const bytes = new TextEncoder().encode(input);
781
+ let binary = "";
782
+ for (const byte of bytes) binary += String.fromCharCode(byte);
783
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
784
+ }
785
+ function fromBase64Url(input) {
786
+ try {
787
+ const padded = input.replace(/-/g, "+").replace(/_/g, "/");
788
+ const binary = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
789
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
790
+ return new TextDecoder().decode(bytes);
791
+ } catch {
792
+ return null;
793
+ }
794
+ }
795
+ function encodeLinkToken(payload) {
796
+ const params = new URLSearchParams();
797
+ for (const [key, value] of Object.entries(payload)) {
798
+ if (typeof value === "string" && value.length > 0) params.set(key, value);
799
+ }
800
+ return `${LINK_TOKEN_VERSION}.${toBase64Url(params.toString())}`;
801
+ }
802
+ function decodeLinkToken(raw) {
803
+ const dot = raw.indexOf(".");
804
+ if (dot <= 0 || raw.slice(0, dot) !== LINK_TOKEN_VERSION) return null;
805
+ const decoded = fromBase64Url(raw.slice(dot + 1));
806
+ if (decoded === null) return null;
807
+ const params = new URLSearchParams(decoded);
808
+ const candidate = {};
809
+ params.forEach((value, key) => {
810
+ candidate[key] = value;
811
+ });
812
+ const result = linkTokenSchema.safeParse(candidate);
813
+ return result.success ? result.data : null;
814
+ }
815
+ function payloadToLink(payload) {
816
+ const carried = {};
817
+ for (const [wire, param] of CARRIED_ENTRIES) {
818
+ const value = payload[wire];
819
+ if (value) carried[param] = value;
820
+ }
821
+ return {
822
+ visitorId: payload.v,
823
+ sessionId: payload.s,
824
+ linkedAt: Number.parseInt(payload.t, 36),
825
+ carried,
826
+ fbc: payload.fc ?? null
827
+ };
828
+ }
829
+ function stripWww(hostname) {
830
+ return hostname.toLowerCase().replace(/^www\./, "");
831
+ }
832
+ function isSiblingHost(hostname, sibling) {
833
+ const host = stripWww(hostname);
834
+ const target = stripWww(sibling);
835
+ return host === target || host.endsWith(`.${target}`);
836
+ }
837
+ function resolveCrossSiteLink(url, linkedHostnames, now = Date.now()) {
838
+ if (typeof window === "undefined" || linkedHostnames.length === 0) return null;
839
+ let target;
840
+ try {
841
+ target = new URL(url, window.location.href);
842
+ } catch {
843
+ return null;
844
+ }
845
+ if (target.protocol !== "http:" && target.protocol !== "https:") return null;
846
+ const here = window.location.hostname;
847
+ if (here && isSiblingHost(target.hostname, here)) return null;
848
+ const hostname = linkedHostnames.find((sibling) => isSiblingHost(target.hostname, sibling));
849
+ if (hostname === void 0) return null;
850
+ const consent = getConsentChoice();
851
+ if (consent.state === "denied" && consent.source === "explicit") {
852
+ return { hostname, decorated: url };
853
+ }
854
+ target.searchParams.set(LINK_PARAM, buildOutboundLinkToken(now));
855
+ return { hostname, decorated: target.href };
856
+ }
857
+ function stripLinkParam(url) {
858
+ try {
859
+ const parsed = new URL(url, typeof window === "undefined" ? void 0 : window.location.href);
860
+ if (!parsed.searchParams.has(LINK_PARAM)) return url;
861
+ parsed.searchParams.delete(LINK_PARAM);
862
+ return parsed.href;
863
+ } catch {
864
+ return url;
865
+ }
866
+ }
867
+ function stripLinkParamFromSearch(search) {
868
+ if (!search.includes(LINK_PARAM)) return search;
869
+ const params = new URLSearchParams(search);
870
+ if (!params.has(LINK_PARAM)) return search;
871
+ params.delete(LINK_PARAM);
872
+ const rest = params.toString();
873
+ return rest.length > 0 ? `?${rest}` : "";
874
+ }
875
+ function readCarried() {
876
+ const out = {};
877
+ for (const [wire, param] of CARRIED_ENTRIES) {
878
+ const value = readCookieValue(param);
879
+ if (value) out[wire] = value;
880
+ }
881
+ return out;
882
+ }
883
+ function buildOutboundLinkToken(now = Date.now()) {
884
+ const fbc = getFbcCookie();
885
+ return encodeLinkToken({
886
+ v: getVisitorId(),
887
+ s: getOrRotateSessionId(now).id,
888
+ t: now.toString(36),
889
+ ...readCarried(),
890
+ ...fbc ? { fc: fbc } : {}
891
+ });
892
+ }
893
+ function decorateCrossSiteUrl(url, linkedHostnames, now = Date.now()) {
894
+ return resolveCrossSiteLink(url, linkedHostnames, now)?.decorated ?? url;
895
+ }
896
+ function consumeInboundLink(href = typeof window === "undefined" ? "" : window.location.href, now = Date.now()) {
897
+ if (typeof window === "undefined" || !href) return null;
898
+ let raw;
899
+ try {
900
+ raw = new URL(href, window.location.href).searchParams.get(LINK_PARAM);
901
+ } catch {
902
+ return null;
903
+ }
904
+ if (!raw) return null;
905
+ const payload = decodeLinkToken(raw);
906
+ if (payload === null) return { outcome: "invalid", link: null, adoptedVisitor: false };
907
+ const link = payloadToLink(payload);
908
+ const age = now - link.linkedAt;
909
+ if (!Number.isFinite(age) || age < -FUTURE_SKEW_MS) {
910
+ return { outcome: "invalid", link: null, adoptedVisitor: false };
911
+ }
912
+ if (age > LINK_TOKEN_TTL_MS) {
913
+ return { outcome: "stale", link: null, adoptedVisitor: false };
914
+ }
915
+ const adoptedVisitor = adoptVisitorId(link.visitorId);
916
+ for (const [param, value] of Object.entries(link.carried)) {
917
+ if (!readCookieValue(param)) setTrackingCookie(param, value);
918
+ }
919
+ if (link.fbc && !getFbcCookie()) persistCookieValue(FBC_COOKIE, link.fbc);
920
+ return { outcome: "linked", link, adoptedVisitor };
921
+ }
922
+ function stripLinkParamFromLocation() {
923
+ if (typeof window === "undefined" || typeof history === "undefined") return;
924
+ const clean = stripLinkParam(window.location.href);
925
+ if (clean === window.location.href) return;
926
+ try {
927
+ history.replaceState(null, "", clean);
928
+ } catch {
929
+ }
930
+ }
931
+
690
932
  // ../tracking-core/src/resources/conversion-firing.ts
691
933
  var DEDUP_PREFIX = "_aranova_conv_";
692
934
  function dedupKey(input) {
@@ -793,7 +1035,8 @@ function parseConversionConfig(raw) {
793
1035
  gtag_ids: isStringMap(obj.gtag_ids) ? obj.gtag_ids : {},
794
1036
  meta_pixel_ids: isStringMap(obj.meta_pixel_ids) ? obj.meta_pixel_ids : {},
795
1037
  services,
796
- goals
1038
+ goals,
1039
+ linked_hostnames: Array.isArray(obj.linked_hostnames) ? obj.linked_hostnames.filter((h) => typeof h === "string" && h.length > 0) : []
797
1040
  };
798
1041
  }
799
1042
  var CACHE_PREFIX = "_aranova_cfg_";
@@ -877,6 +1120,7 @@ function resolveConversionConfig(options) {
877
1120
  getFiring: (key) => goalsByKey.get(key)?.firing ?? null,
878
1121
  getGoal: (key) => goalsByKey.get(key) ?? null,
879
1122
  listGoals: () => [...goalsByKey.values()],
1123
+ linkedHostnames: () => current?.linked_hostnames ?? [],
880
1124
  current: () => current,
881
1125
  isReady: () => current !== null,
882
1126
  onResolve: (listener) => {
@@ -1061,7 +1305,7 @@ function validateConfig(config, ref) {
1061
1305
  function pageViewSnapshot() {
1062
1306
  if (typeof window === "undefined" || typeof document === "undefined") return null;
1063
1307
  return {
1064
- href: window.location.href,
1308
+ href: stripLinkParam(window.location.href),
1065
1309
  title: document.title || null,
1066
1310
  referrer: document.referrer || null
1067
1311
  };
@@ -1212,6 +1456,10 @@ var TrackingConfigRuntime = class {
1212
1456
  listGoals() {
1213
1457
  return this.current?.goals ?? [];
1214
1458
  }
1459
+ /** Sibling hostnames for the linker — read from the cached object even while unconfirmed. */
1460
+ linkedHostnames() {
1461
+ return this.current?.linked_hostnames ?? [];
1462
+ }
1215
1463
  async revalidateNow() {
1216
1464
  try {
1217
1465
  const headers = {};
@@ -1449,83 +1697,32 @@ function withConversionAutoFire(client, autoFire) {
1449
1697
  };
1450
1698
  }
1451
1699
 
1452
- // ../tracking-core/src/session.ts
1453
- var VISITOR_STORAGE_KEY = "aranova_tracking_visitor";
1454
- var SESSION_STORAGE_KEY = "aranova_tracking_session";
1455
- var SESSION_IDLE_MS = 30 * 60 * 1e3;
1456
- function safeUuid() {
1457
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function")
1458
- return crypto.randomUUID();
1459
- return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;
1460
- }
1461
- function readLocalStorage(key) {
1462
- try {
1463
- return window.localStorage.getItem(key);
1464
- } catch {
1465
- return null;
1466
- }
1467
- }
1468
- function writeLocalStorage(key, value) {
1469
- try {
1470
- window.localStorage.setItem(key, value);
1471
- } catch {
1472
- }
1473
- }
1474
- function getVisitorId() {
1475
- if (typeof window === "undefined") return safeUuid();
1476
- const existing = readLocalStorage(VISITOR_STORAGE_KEY);
1477
- if (existing && existing.length > 0) return existing;
1478
- const fresh = safeUuid();
1479
- writeLocalStorage(VISITOR_STORAGE_KEY, fresh);
1480
- return fresh;
1481
- }
1482
- function getOrRotateSessionId(now = Date.now()) {
1483
- if (typeof window === "undefined") return { id: safeUuid(), isNew: true };
1484
- const raw = readLocalStorage(SESSION_STORAGE_KEY);
1485
- if (raw) {
1486
- try {
1487
- const parsed = JSON.parse(raw);
1488
- if (typeof parsed.id === "string" && typeof parsed.last_event_at === "number") {
1489
- if (now - parsed.last_event_at <= SESSION_IDLE_MS) {
1490
- const refreshed = { id: parsed.id, last_event_at: now };
1491
- writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(refreshed));
1492
- return { id: parsed.id, isNew: false };
1493
- }
1494
- }
1495
- } catch {
1496
- }
1497
- }
1498
- const fresh = { id: safeUuid(), last_event_at: now };
1499
- writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(fresh));
1500
- return { id: fresh.id, isNew: true };
1501
- }
1502
-
1503
1700
  // ../tracking-core/src/page-view.ts
1504
1701
  var import_mini = require("zod/mini");
1505
1702
 
1506
1703
  // ../tracking-core/src/events/page-view.ts
1507
- var z = __toESM(require("zod/mini"));
1508
- var pageViewMetadataSchema = z.strictObject({
1509
- page: z.strictObject({
1510
- title: z.nullable(z.string()),
1511
- path: z.string(),
1512
- search: z.string(),
1513
- hash: z.string()
1704
+ var z2 = __toESM(require("zod/mini"));
1705
+ var pageViewMetadataSchema = z2.strictObject({
1706
+ page: z2.strictObject({
1707
+ title: z2.nullable(z2.string()),
1708
+ path: z2.string(),
1709
+ search: z2.string(),
1710
+ hash: z2.string()
1514
1711
  }),
1515
- referrer: z.nullable(z.string()),
1712
+ referrer: z2.nullable(z2.string()),
1516
1713
  // Optional + nullable — absent (undefined) OR explicit null OR a
1517
1714
  // real viewport object. Mirrors Pydantic's `_Viewport | None = None`
1518
1715
  // on the backend side so the drift test stays clean.
1519
- viewport: z.optional(
1520
- z.nullable(
1521
- z.strictObject({
1522
- w: z.number(),
1523
- h: z.number()
1716
+ viewport: z2.optional(
1717
+ z2.nullable(
1718
+ z2.strictObject({
1719
+ w: z2.number(),
1720
+ h: z2.number()
1524
1721
  })
1525
1722
  )
1526
1723
  )
1527
1724
  });
1528
- var pageViewConfigSchema = z.strictObject({});
1725
+ var pageViewConfigSchema = z2.strictObject({});
1529
1726
 
1530
1727
  // ../tracking-core/src/page-view.ts
1531
1728
  var LAST_FIRED_URL_STORAGE_KEY = "aranova_tracking_last_fired_url";
@@ -1565,7 +1762,7 @@ function buildPageViewMetadata(referrerOverride) {
1565
1762
  page: {
1566
1763
  title: document.title || null,
1567
1764
  path: window.location.pathname,
1568
- search: window.location.search,
1765
+ search: stripLinkParamFromSearch(window.location.search),
1569
1766
  hash: window.location.hash
1570
1767
  },
1571
1768
  referrer: referrerOverride !== void 0 ? referrerOverride : document.referrer || null,
@@ -1864,11 +2061,19 @@ function createTrackingClient(config) {
1864
2061
  let firstPage = null;
1865
2062
  let initialParams = createEmptyTrackingParams();
1866
2063
  let destroyed = false;
2064
+ let inboundLink = null;
2065
+ if (typeof window !== "undefined") {
2066
+ try {
2067
+ inboundLink = consumeInboundLink();
2068
+ } catch {
2069
+ }
2070
+ }
2071
+ let originLink = inboundLink?.link ?? null;
1867
2072
  const visitorId = getVisitorId();
1868
2073
  const initialSession = getOrRotateSessionId();
1869
2074
  let sessionId = initialSession.id;
1870
2075
  if (typeof window !== "undefined") {
1871
- firstPage = window.location.href;
2076
+ firstPage = stripLinkParam(window.location.href);
1872
2077
  try {
1873
2078
  initialParams = captureTrackingParamsFromLocation();
1874
2079
  } catch {
@@ -1879,25 +2084,50 @@ function createTrackingClient(config) {
1879
2084
  } catch {
1880
2085
  }
1881
2086
  }
1882
- function enqueueHeartbeat() {
1883
- const metadata = buildHeartbeatMetadata(
1884
- config.surface,
1885
- sdkVersion,
1886
- packageName,
1887
- config.triggers ?? null,
1888
- activeGtagIds,
1889
- config.configSiteId ?? null
1890
- );
2087
+ function currentPageUrl() {
2088
+ return typeof window === "undefined" ? null : stripLinkParam(window.location.href);
2089
+ }
2090
+ function enqueueInternal(eventType, metadata, pageUrl) {
1891
2091
  queue.push({
1892
- event_type: "sdk_heartbeat",
1893
- page_url: typeof window === "undefined" ? null : window.location.href,
2092
+ event_type: eventType,
2093
+ page_url: pageUrl,
1894
2094
  metadata,
1895
2095
  occurred_at: (/* @__PURE__ */ new Date()).toISOString()
1896
2096
  });
1897
2097
  }
2098
+ function enqueueHeartbeat() {
2099
+ enqueueInternal(
2100
+ "sdk_heartbeat",
2101
+ buildHeartbeatMetadata(
2102
+ config.surface,
2103
+ sdkVersion,
2104
+ packageName,
2105
+ config.triggers ?? null,
2106
+ activeGtagIds,
2107
+ config.configSiteId ?? null
2108
+ ),
2109
+ currentPageUrl()
2110
+ );
2111
+ }
1898
2112
  if (initialSession.isNew) {
1899
2113
  enqueueHeartbeat();
1900
2114
  }
2115
+ if (inboundLink !== null && typeof window !== "undefined") {
2116
+ const link = inboundLink.link;
2117
+ enqueueInternal(
2118
+ "site_link_arrival",
2119
+ {
2120
+ outcome: inboundLink.outcome,
2121
+ origin_session_id: link?.sessionId ?? null,
2122
+ origin_visitor_id: link?.visitorId ?? null,
2123
+ transit_ms: link ? Math.max(0, Date.now() - link.linkedAt) : null,
2124
+ adopted_visitor: inboundLink.adoptedVisitor,
2125
+ page: { path: window.location.pathname }
2126
+ },
2127
+ firstPage
2128
+ );
2129
+ scheduleFlush();
2130
+ }
1901
2131
  if (pending.length > 0) {
1902
2132
  scheduleFlush();
1903
2133
  }
@@ -1905,6 +2135,7 @@ function createTrackingClient(config) {
1905
2135
  const rotated = getOrRotateSessionId();
1906
2136
  if (rotated.isNew && rotated.id !== sessionId) {
1907
2137
  enqueueHeartbeat();
2138
+ originLink = null;
1908
2139
  }
1909
2140
  sessionId = rotated.id;
1910
2141
  const params = mergeTrackingParams(readTrackingParams(), initialParams);
@@ -1935,6 +2166,11 @@ function createTrackingClient(config) {
1935
2166
  // landing), reused from the stored record otherwise. Keys are omitted
1936
2167
  // entirely when the landing isn't observable (SSR).
1937
2168
  ...buildLandingPayloadFields(sessionId),
2169
+ ...originLink ? {
2170
+ origin_session_id: originLink.sessionId,
2171
+ origin_visitor_id: originLink.visitorId,
2172
+ origin_linked_at: new Date(originLink.linkedAt).toISOString()
2173
+ } : {},
1938
2174
  first_page: firstPage,
1939
2175
  consent_state: consentSnapshot(),
1940
2176
  context
@@ -2011,7 +2247,8 @@ function createTrackingClient(config) {
2011
2247
  const occurredAt = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : (/* @__PURE__ */ new Date()).toISOString();
2012
2248
  queue.push({
2013
2249
  event_type: input.eventType,
2014
- page_url: input.pageUrl ?? (typeof window === "undefined" ? null : window.location.href),
2250
+ // A caller's href still carries `_arv` until the surface strips it after hydration.
2251
+ page_url: input.pageUrl != null ? stripLinkParam(input.pageUrl) : currentPageUrl(),
2015
2252
  metadata: input.metadata ?? null,
2016
2253
  occurred_at: occurredAt
2017
2254
  });
@@ -2061,112 +2298,112 @@ function createTrackingClient(config) {
2061
2298
  var import_mini2 = require("zod/mini");
2062
2299
 
2063
2300
  // ../tracking-core/src/events/cta-click.ts
2064
- var z2 = __toESM(require("zod/mini"));
2065
- var ctaClickMetadataSchema = z2.strictObject({
2066
- cta_name: z2.string(),
2067
- page: z2.strictObject({
2068
- path: z2.string()
2301
+ var z3 = __toESM(require("zod/mini"));
2302
+ var ctaClickMetadataSchema = z3.strictObject({
2303
+ cta_name: z3.string(),
2304
+ page: z3.strictObject({
2305
+ path: z3.string()
2069
2306
  }),
2070
- section: z2.optional(z2.nullable(z2.string())),
2071
- destination_url: z2.optional(z2.nullable(z2.string())),
2307
+ section: z3.optional(z3.nullable(z3.string())),
2308
+ destination_url: z3.optional(z3.nullable(z3.string())),
2072
2309
  // Set by auto-capture (and available to manual callers): the link target
2073
2310
  // and a short element descriptor (tag#id) for tying clicks to specific UI.
2074
- href: z2.optional(z2.nullable(z2.string())),
2075
- element: z2.optional(z2.nullable(z2.string()))
2311
+ href: z3.optional(z3.nullable(z3.string())),
2312
+ element: z3.optional(z3.nullable(z3.string()))
2076
2313
  });
2077
- var ctaClickConfigSchema = z2.strictObject({
2078
- autoCapture: z2.optional(
2079
- z2.strictObject({
2080
- selector: z2.optional(z2.string())
2314
+ var ctaClickConfigSchema = z3.strictObject({
2315
+ autoCapture: z3.optional(
2316
+ z3.strictObject({
2317
+ selector: z3.optional(z3.string())
2081
2318
  })
2082
2319
  )
2083
2320
  });
2084
2321
 
2085
2322
  // ../tracking-core/src/events/sdk-heartbeat.ts
2086
- var z3 = __toESM(require("zod/mini"));
2087
- var sdkHeartbeatTriggersSchema = z3.strictObject({
2088
- automatic: z3.array(z3.string()),
2089
- manual: z3.array(z3.string())
2323
+ var z4 = __toESM(require("zod/mini"));
2324
+ var sdkHeartbeatTriggersSchema = z4.strictObject({
2325
+ automatic: z4.array(z4.string()),
2326
+ manual: z4.array(z4.string())
2090
2327
  });
2091
- var sdkHeartbeatMetadataSchema = z3.strictObject({
2092
- sdk_version: z3.string(),
2093
- package_name: z3.nullable(z3.string()),
2094
- surface: z3.enum(["next", "react", "script"]),
2328
+ var sdkHeartbeatMetadataSchema = z4.strictObject({
2329
+ sdk_version: z4.string(),
2330
+ package_name: z4.nullable(z4.string()),
2331
+ surface: z4.enum(["next", "react", "script"]),
2095
2332
  triggers: sdkHeartbeatTriggersSchema,
2096
- trigger_config: z3.optional(z3.nullable(z3.record(z3.string(), z3.record(z3.string(), z3.unknown())))),
2097
- configured_gtag_ids: z3.optional(z3.nullable(z3.record(z3.string(), z3.string()))),
2333
+ trigger_config: z4.optional(z4.nullable(z4.record(z4.string(), z4.record(z4.string(), z4.unknown())))),
2334
+ configured_gtag_ids: z4.optional(z4.nullable(z4.record(z4.string(), z4.string()))),
2098
2335
  // Site whose config object the SDK resolves; null on a legacy business ref.
2099
- config_site_id: z3.optional(z3.nullable(z3.uuid()))
2336
+ config_site_id: z4.optional(z4.nullable(z4.uuid()))
2100
2337
  });
2101
- var sdkHeartbeatConfigSchema = z3.strictObject({});
2338
+ var sdkHeartbeatConfigSchema = z4.strictObject({});
2102
2339
 
2103
2340
  // ../tracking-core/src/events/form-start.ts
2104
- var z4 = __toESM(require("zod/mini"));
2105
- var formStartMetadataSchema = z4.strictObject({
2106
- form: z4.strictObject({
2107
- id: z4.string(),
2108
- action: z4.nullable(z4.string())
2341
+ var z5 = __toESM(require("zod/mini"));
2342
+ var formStartMetadataSchema = z5.strictObject({
2343
+ form: z5.strictObject({
2344
+ id: z5.string(),
2345
+ action: z5.nullable(z5.string())
2109
2346
  }),
2110
- page: z4.strictObject({
2111
- path: z4.string()
2347
+ page: z5.strictObject({
2348
+ path: z5.string()
2112
2349
  })
2113
2350
  });
2114
- var formStartConfigSchema = z4.strictObject({
2115
- selector: z4.optional(z4.string())
2351
+ var formStartConfigSchema = z5.strictObject({
2352
+ selector: z5.optional(z5.string())
2116
2353
  });
2117
2354
 
2118
2355
  // ../tracking-core/src/events/form-submit.ts
2119
- var z5 = __toESM(require("zod/mini"));
2120
- var jsonValueSchema = z5.lazy(
2121
- () => z5.union([
2122
- z5.string(),
2123
- z5.number(),
2124
- z5.boolean(),
2125
- z5.null(),
2126
- z5.array(jsonValueSchema),
2127
- z5.record(z5.string(), jsonValueSchema)
2356
+ var z6 = __toESM(require("zod/mini"));
2357
+ var jsonValueSchema = z6.lazy(
2358
+ () => z6.union([
2359
+ z6.string(),
2360
+ z6.number(),
2361
+ z6.boolean(),
2362
+ z6.null(),
2363
+ z6.array(jsonValueSchema),
2364
+ z6.record(z6.string(), jsonValueSchema)
2128
2365
  ])
2129
- ).register(z5.globalRegistry, { id: "JsonSerializableValue" });
2130
- var formSubmitMetadataSchema = z5.strictObject({
2131
- form: z5.strictObject({
2132
- id: z5.string(),
2133
- action: z5.nullable(z5.string()),
2134
- fields: z5.optional(
2135
- z5.array(
2136
- z5.strictObject({
2137
- name: z5.string(),
2138
- type: z5.string(),
2139
- label: z5.nullable(z5.string()),
2366
+ ).register(z6.globalRegistry, { id: "JsonSerializableValue" });
2367
+ var formSubmitMetadataSchema = z6.strictObject({
2368
+ form: z6.strictObject({
2369
+ id: z6.string(),
2370
+ action: z6.nullable(z6.string()),
2371
+ fields: z6.optional(
2372
+ z6.array(
2373
+ z6.strictObject({
2374
+ name: z6.string(),
2375
+ type: z6.string(),
2376
+ label: z6.nullable(z6.string()),
2140
2377
  value: jsonValueSchema
2141
2378
  })
2142
2379
  )
2143
2380
  )
2144
2381
  }),
2145
- page: z5.strictObject({
2146
- path: z5.string()
2382
+ page: z6.strictObject({
2383
+ path: z6.string()
2147
2384
  })
2148
2385
  });
2149
- var formSubmitConfigSchema = z5.strictObject({});
2386
+ var formSubmitConfigSchema = z6.strictObject({});
2150
2387
 
2151
2388
  // ../tracking-core/src/events/multi-page-session.ts
2152
- var z6 = __toESM(require("zod/mini"));
2153
- var multiPageSessionMetadataSchema = z6.strictObject({
2154
- page_count: z6.int().check(z6.gte(2)),
2155
- page: z6.strictObject({
2156
- path: z6.string()
2389
+ var z7 = __toESM(require("zod/mini"));
2390
+ var multiPageSessionMetadataSchema = z7.strictObject({
2391
+ page_count: z7.int().check(z7.gte(2)),
2392
+ page: z7.strictObject({
2393
+ path: z7.string()
2157
2394
  })
2158
2395
  });
2159
- var multiPageSessionConfigSchema = z6.strictObject({
2160
- pageThreshold: z6.int().check(z6.gte(2))
2396
+ var multiPageSessionConfigSchema = z7.strictObject({
2397
+ pageThreshold: z7.int().check(z7.gte(2))
2161
2398
  });
2162
2399
 
2163
2400
  // ../tracking-core/src/events/page-exit.ts
2164
- var z7 = __toESM(require("zod/mini"));
2165
- var pageExitMetadataSchema = z7.strictObject({
2166
- dwell_ms: z7.int().check(z7.gte(0)),
2401
+ var z8 = __toESM(require("zod/mini"));
2402
+ var pageExitMetadataSchema = z8.strictObject({
2403
+ dwell_ms: z8.int().check(z8.gte(0)),
2167
2404
  // null = left without any scroll signal; floor is 0 so a valid 0% is never
2168
2405
  // rejected (a single bad field 422s the whole keepalive beacon batch).
2169
- max_scroll_percent: z7.nullable(z7.int().check(z7.gte(0), z7.lte(100))),
2406
+ max_scroll_percent: z8.nullable(z8.int().check(z8.gte(0), z8.lte(100))),
2170
2407
  // The gating baseline: the fraction of the page visible at load with
2171
2408
  // zero scrolling — or, for pages that grew after the post-paint snapshot
2172
2409
  // (skeleton/streaming renders), the first-scroll position that
@@ -2174,80 +2411,104 @@ var pageExitMetadataSchema = z7.strictObject({
2174
2411
  // ended before the snapshot. Optionality is load-bearing: SDK builds
2175
2412
  // predating this field keep POSTing page_exit without the key —
2176
2413
  // requiring it would 422 whole keepalive beacon batches.
2177
- scroll_baseline_percent: z7.optional(z7.nullable(z7.int().check(z7.gte(0), z7.lte(100)))),
2178
- page: z7.strictObject({
2179
- path: z7.string()
2414
+ scroll_baseline_percent: z8.optional(z8.nullable(z8.int().check(z8.gte(0), z8.lte(100)))),
2415
+ page: z8.strictObject({
2416
+ path: z8.string()
2180
2417
  })
2181
2418
  });
2182
- var pageExitConfigSchema = z7.strictObject({});
2419
+ var pageExitConfigSchema = z8.strictObject({});
2183
2420
 
2184
2421
  // ../tracking-core/src/events/phone-click.ts
2185
- var z8 = __toESM(require("zod/mini"));
2186
- var phoneClickMetadataSchema = z8.strictObject({
2187
- phone_number: z8.string(),
2188
- page: z8.strictObject({
2189
- path: z8.string()
2422
+ var z9 = __toESM(require("zod/mini"));
2423
+ var phoneClickMetadataSchema = z9.strictObject({
2424
+ phone_number: z9.string(),
2425
+ page: z9.strictObject({
2426
+ path: z9.string()
2190
2427
  }),
2191
- section: z8.optional(z8.nullable(z8.string()))
2428
+ section: z9.optional(z9.nullable(z9.string()))
2192
2429
  });
2193
- var phoneClickConfigSchema = z8.strictObject({
2194
- autoCapture: z8.optional(
2195
- z8.strictObject({
2196
- selector: z8.optional(z8.string())
2430
+ var phoneClickConfigSchema = z9.strictObject({
2431
+ autoCapture: z9.optional(
2432
+ z9.strictObject({
2433
+ selector: z9.optional(z9.string())
2197
2434
  })
2198
2435
  )
2199
2436
  });
2200
2437
 
2201
2438
  // ../tracking-core/src/events/scroll-depth.ts
2202
- var z9 = __toESM(require("zod/mini"));
2203
- var scrollDepthMetadataSchema = z9.strictObject({
2204
- depth_percent: z9.int().check(z9.gte(1), z9.lte(100)),
2205
- page: z9.strictObject({
2206
- path: z9.string()
2439
+ var z10 = __toESM(require("zod/mini"));
2440
+ var scrollDepthMetadataSchema = z10.strictObject({
2441
+ depth_percent: z10.int().check(z10.gte(1), z10.lte(100)),
2442
+ page: z10.strictObject({
2443
+ path: z10.string()
2207
2444
  })
2208
2445
  });
2209
- var scrollDepthConfigSchema = z9.strictObject({
2210
- thresholds: z9.array(z9.int().check(z9.gte(1), z9.lte(100))).check(z9.minLength(1))
2446
+ var scrollDepthConfigSchema = z10.strictObject({
2447
+ thresholds: z10.array(z10.int().check(z10.gte(1), z10.lte(100))).check(z10.minLength(1))
2211
2448
  });
2212
2449
 
2213
2450
  // ../tracking-core/src/events/specific-page-visit.ts
2214
- var z10 = __toESM(require("zod/mini"));
2451
+ var z11 = __toESM(require("zod/mini"));
2215
2452
  var PAGE_IDENTITY_MAX_LENGTH = 64;
2216
2453
  var PAGE_IDENTITY_PATTERN = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
2217
- var pageIdentitySchema = z10.string().check(
2218
- z10.maxLength(PAGE_IDENTITY_MAX_LENGTH),
2219
- z10.regex(PAGE_IDENTITY_PATTERN, {
2454
+ var pageIdentitySchema = z11.string().check(
2455
+ z11.maxLength(PAGE_IDENTITY_MAX_LENGTH),
2456
+ z11.regex(PAGE_IDENTITY_PATTERN, {
2220
2457
  message: "page identity must be lowercase snake_case, start with a letter, and contain at most 64 characters"
2221
2458
  })
2222
2459
  );
2223
2460
  var specificPageNameSchema = pageIdentitySchema;
2224
- var specificPageVisitMetadataSchema = z10.strictObject({
2461
+ var specificPageVisitMetadataSchema = z11.strictObject({
2225
2462
  page_name: specificPageNameSchema,
2226
- page: z10.strictObject({
2227
- path: z10.string()
2463
+ page: z11.strictObject({
2464
+ path: z11.string()
2228
2465
  })
2229
2466
  });
2230
- var specificPageVisitConfigSchema = z10.strictObject({
2231
- pages: z10.array(
2232
- z10.strictObject({
2467
+ var specificPageVisitConfigSchema = z11.strictObject({
2468
+ pages: z11.array(
2469
+ z11.strictObject({
2233
2470
  name: specificPageNameSchema,
2234
- pathPattern: z10.custom((value) => value instanceof RegExp, {
2471
+ pathPattern: z11.custom((value) => value instanceof RegExp, {
2235
2472
  message: "pathPattern must be a RegExp"
2236
2473
  })
2237
2474
  })
2238
- ).check(z10.minLength(1))
2475
+ ).check(z11.minLength(1))
2239
2476
  });
2240
2477
 
2478
+ // ../tracking-core/src/events/site-link.ts
2479
+ var z12 = __toESM(require("zod/mini"));
2480
+ var siteLinkClickMetadataSchema = z12.strictObject({
2481
+ destination_host: z12.string(),
2482
+ destination_url: z12.string(),
2483
+ page: z12.strictObject({
2484
+ path: z12.string()
2485
+ })
2486
+ });
2487
+ var siteLinkClickConfigSchema = z12.strictObject({});
2488
+ var siteLinkArrivalMetadataSchema = z12.strictObject({
2489
+ outcome: z12.enum(["linked", "stale", "invalid"]),
2490
+ origin_session_id: z12.optional(z12.nullable(z12.string())),
2491
+ origin_visitor_id: z12.optional(z12.nullable(z12.string())),
2492
+ // Milliseconds between the origin click and this page load.
2493
+ transit_ms: z12.optional(z12.nullable(z12.number())),
2494
+ // True when this browser had no visitor id and took the origin's.
2495
+ adopted_visitor: z12.boolean(),
2496
+ page: z12.strictObject({
2497
+ path: z12.string()
2498
+ })
2499
+ });
2500
+ var siteLinkArrivalConfigSchema = z12.strictObject({});
2501
+
2241
2502
  // ../tracking-core/src/events/time-on-site.ts
2242
- var z11 = __toESM(require("zod/mini"));
2243
- var timeOnSiteMetadataSchema = z11.strictObject({
2244
- duration_ms: z11.int().check(z11.nonnegative()),
2245
- page: z11.strictObject({
2246
- path: z11.string()
2503
+ var z13 = __toESM(require("zod/mini"));
2504
+ var timeOnSiteMetadataSchema = z13.strictObject({
2505
+ duration_ms: z13.int().check(z13.nonnegative()),
2506
+ page: z13.strictObject({
2507
+ path: z13.string()
2247
2508
  })
2248
2509
  });
2249
- var timeOnSiteConfigSchema = z11.strictObject({
2250
- thresholdSeconds: z11.int().check(z11.positive())
2510
+ var timeOnSiteConfigSchema = z13.strictObject({
2511
+ thresholdSeconds: z13.int().check(z13.positive())
2251
2512
  });
2252
2513
 
2253
2514
  // ../tracking-core/src/events/semantics.ts
@@ -2317,6 +2578,18 @@ var EVENT_SEMANTICS = {
2317
2578
  category: "engagement",
2318
2579
  outcomeRole: "engagement",
2319
2580
  clientVisibility: "simple"
2581
+ },
2582
+ site_link_click: {
2583
+ label: "Left for sibling site",
2584
+ category: "page",
2585
+ outcomeRole: "navigation",
2586
+ clientVisibility: "detailed"
2587
+ },
2588
+ site_link_arrival: {
2589
+ label: "Arrived from sibling site",
2590
+ category: "page",
2591
+ outcomeRole: "navigation",
2592
+ clientVisibility: "simple"
2320
2593
  }
2321
2594
  };
2322
2595
 
@@ -2372,6 +2645,20 @@ var EVENT_REGISTRY = {
2372
2645
  metadataSchema: pageExitMetadataSchema,
2373
2646
  configSchema: pageExitConfigSchema
2374
2647
  },
2648
+ // Cross-site linkage: emitted by the linker (roster comes from the site
2649
+ // config, not the trigger registry), so neither is consumer-configurable.
2650
+ site_link_click: {
2651
+ kind: "automatic",
2652
+ semantics: EVENT_SEMANTICS.site_link_click,
2653
+ metadataSchema: siteLinkClickMetadataSchema,
2654
+ configSchema: siteLinkClickConfigSchema
2655
+ },
2656
+ site_link_arrival: {
2657
+ kind: "automatic",
2658
+ semantics: EVENT_SEMANTICS.site_link_arrival,
2659
+ metadataSchema: siteLinkArrivalMetadataSchema,
2660
+ configSchema: siteLinkArrivalConfigSchema
2661
+ },
2375
2662
  // --- manual triggers ---
2376
2663
  form_submit: {
2377
2664
  kind: "manual",
@@ -2977,6 +3264,57 @@ function attachCtaClickCapture(client, config) {
2977
3264
  };
2978
3265
  }
2979
3266
 
3267
+ // ../tracking-core/src/triggers/site-link-capture.ts
3268
+ function anchorFrom(target) {
3269
+ if (!(target instanceof Element)) return null;
3270
+ const anchor = target.closest("a[href]");
3271
+ return anchor instanceof HTMLAnchorElement ? anchor : null;
3272
+ }
3273
+ function opensTheLink(event) {
3274
+ return event.button === 0 || event.button === 1;
3275
+ }
3276
+ function attachSiteLinkCapture(client, options) {
3277
+ if (typeof window === "undefined" || typeof document === "undefined") {
3278
+ return () => {
3279
+ };
3280
+ }
3281
+ registerCapability("cross_site_linker");
3282
+ function hopFrom(event) {
3283
+ const anchor = anchorFrom(event.target);
3284
+ if (anchor === null) return null;
3285
+ const link = resolveCrossSiteLink(anchor.href, options.linkedHostnames());
3286
+ return link === null ? null : { anchor, link };
3287
+ }
3288
+ function onPointerDown(event) {
3289
+ if (!opensTheLink(event)) return;
3290
+ const hop = hopFrom(event);
3291
+ if (hop !== null) hop.anchor.href = hop.link.decorated;
3292
+ }
3293
+ function onClick(event) {
3294
+ if (!opensTheLink(event)) return;
3295
+ const hop = hopFrom(event);
3296
+ if (hop === null) return;
3297
+ hop.anchor.href = hop.link.decorated;
3298
+ client.trackEvent({
3299
+ eventType: "site_link_click",
3300
+ metadata: {
3301
+ destination_host: hop.anchor.hostname,
3302
+ destination_url: stripLinkParam(hop.anchor.href),
3303
+ page: { path: window.location.pathname }
3304
+ },
3305
+ pageUrl: window.location.href,
3306
+ occurredAt: null
3307
+ });
3308
+ client.flushBeacon();
3309
+ }
3310
+ document.addEventListener("pointerdown", onPointerDown, true);
3311
+ document.addEventListener("click", onClick, true);
3312
+ return () => {
3313
+ document.removeEventListener("pointerdown", onPointerDown, true);
3314
+ document.removeEventListener("click", onClick, true);
3315
+ };
3316
+ }
3317
+
2980
3318
  // ../tracking-core/src/triggers/phone-click-capture.ts
2981
3319
  var DEFAULT_TEL_SELECTOR = 'a[href^="tel:"]';
2982
3320
  function safeDecodeURIComponent(value) {
@@ -3362,7 +3700,7 @@ function GoogleAdsTracking(props) {
3362
3700
  var import_react6 = require("react");
3363
3701
 
3364
3702
  // package.json
3365
- var version = "1.0.0";
3703
+ var version = "1.1.0";
3366
3704
 
3367
3705
  // ../tracking-core/src/phone-react.tsx
3368
3706
  var import_react5 = require("react");
@@ -3485,7 +3823,8 @@ function createTracking(options) {
3485
3823
  // Still publish phone config so usePhoneField/<PhoneField> work even when
3486
3824
  // tracking is disabled (missing apiKey/endpoint).
3487
3825
  TrackingProvider: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PhoneConfigProvider, { value: phone ?? null, children }),
3488
- useTracking: () => noopTyped
3826
+ useTracking: () => noopTyped,
3827
+ decorateUrl: (url) => url
3489
3828
  };
3490
3829
  }
3491
3830
  const TrackingContext = (0, import_react6.createContext)(null);
@@ -3504,8 +3843,8 @@ function createTracking(options) {
3504
3843
  // eslint-disable-next-line react-hooks/exhaustive-deps -- gtagIdsKey is stable proxy
3505
3844
  [gtagId, gtagIdsKey]
3506
3845
  );
3507
- const rawClient = (0, import_react6.useMemo)(
3508
- () => getOrCreateTrackingClient({
3846
+ const rawClient = (0, import_react6.useMemo)(() => {
3847
+ const created = getOrCreateTrackingClient({
3509
3848
  apiKey,
3510
3849
  endpoint,
3511
3850
  surface: "react",
@@ -3516,9 +3855,10 @@ function createTracking(options) {
3516
3855
  activeGtagIds: resolvedGtagIds,
3517
3856
  configSiteId: trackingConfig?.siteId ?? null,
3518
3857
  debug
3519
- }),
3520
- [resolvedGtagIds]
3521
- );
3858
+ });
3859
+ stripLinkParamFromLocation();
3860
+ return created;
3861
+ }, [resolvedGtagIds]);
3522
3862
  const conversionStore = (0, import_react6.useMemo)(
3523
3863
  () => trackingConfig ? getTrackingConfigRuntime(trackingConfig) : conversionConfig ? resolveConversionConfig({
3524
3864
  cdnUrl: conversionConfig.cdnUrl,
@@ -3572,6 +3912,11 @@ function createTracking(options) {
3572
3912
  if (timeOnSite) {
3573
3913
  detachers.push(attachTimeOnSite(detectorClient, timeOnSite));
3574
3914
  }
3915
+ detachers.push(
3916
+ attachSiteLinkCapture(detectorClient, {
3917
+ linkedHostnames: () => conversionStore?.linkedHostnames() ?? []
3918
+ })
3919
+ );
3575
3920
  const specificPageVisit = triggers.automatic.specific_page_visit;
3576
3921
  if (specificPageVisit) {
3577
3922
  detachers.push(attachSpecificPageVisit(detectorClient, specificPageVisit));
@@ -3614,7 +3959,14 @@ function createTracking(options) {
3614
3959
  }
3615
3960
  return client;
3616
3961
  }
3617
- return { TrackingProvider, useTracking };
3962
+ return {
3963
+ TrackingProvider,
3964
+ useTracking,
3965
+ decorateUrl: (url) => decorateCrossSiteUrl(
3966
+ url,
3967
+ trackingConfig ? getTrackingConfigRuntime(trackingConfig).linkedHostnames() : []
3968
+ )
3969
+ };
3618
3970
  }
3619
3971
  // Annotate the CommonJS export names for ESM import in node:
3620
3972
  0 && (module.exports = {