@aranova/tracking-react 0.18.1 → 0.19.1

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
@@ -4,6 +4,140 @@ import { useEffect as useEffect2, useState as useState2 } from "react";
4
4
  // src/hooks.ts
5
5
  import { useCallback, useEffect, useState } from "react";
6
6
 
7
+ // ../tracking-core/src/phone.ts
8
+ import { AsYouType, parsePhoneNumberFromString } from "libphonenumber-js";
9
+ var DEFAULT_PHONE_COUNTRY = "CA";
10
+ function parsePhone(raw, country) {
11
+ const region = country ?? DEFAULT_PHONE_COUNTRY;
12
+ const parsed = parsePhoneNumberFromString(raw ?? "", region);
13
+ if (!parsed) {
14
+ return { e164: null, national: "", international: "", country: region, isValid: false };
15
+ }
16
+ const isValid = parsed.isValid();
17
+ return {
18
+ // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
19
+ // (e.g. too few digits) still parses but must not be transmitted.
20
+ e164: isValid ? parsed.number : null,
21
+ national: parsed.formatNational(),
22
+ international: parsed.formatInternational(),
23
+ country: parsed.country ?? region,
24
+ isValid
25
+ };
26
+ }
27
+ function toE164(raw, country) {
28
+ return parsePhone(raw, country).e164;
29
+ }
30
+ function formatPhone(value, format = "national", country) {
31
+ const parsed = parsePhone(value, country);
32
+ if (typeof format === "function") return format(parsed);
33
+ switch (format) {
34
+ case "international":
35
+ return parsed.international || value;
36
+ case "e164":
37
+ return parsed.e164 ?? value;
38
+ case "national":
39
+ default:
40
+ return parsed.national || value;
41
+ }
42
+ }
43
+ function formatPhoneAsTyped(raw, country) {
44
+ return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
45
+ }
46
+
47
+ // ../tracking-core/src/user-data.ts
48
+ var EMAIL_SHAPE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
49
+ var EMAIL_NAME_HINT = /e[-_]?mail/i;
50
+ var PHONE_NAME_HINT = /(^|[^a-z])(phone|tel|mobile|cell)/i;
51
+ var stash = { email: null, phoneNumber: null };
52
+ function normalizeEmail(raw) {
53
+ if (typeof raw !== "string") return null;
54
+ const cleaned = raw.trim().toLowerCase();
55
+ return EMAIL_SHAPE.test(cleaned) ? cleaned : null;
56
+ }
57
+ function fieldText(field, key) {
58
+ const value = field[key];
59
+ return typeof value === "string" ? value : "";
60
+ }
61
+ function extractUserDataFromFormFields(fields, country) {
62
+ const result = { email: null, phoneNumber: null };
63
+ if (!Array.isArray(fields)) return result;
64
+ const passes = [
65
+ (field, _hint, type) => fieldText(field, "type").toLowerCase() === type,
66
+ (field, hint) => hint.test(fieldText(field, "name")) || hint.test(fieldText(field, "label"))
67
+ ];
68
+ for (const matches of passes) {
69
+ for (const raw of fields) {
70
+ if (raw === null || typeof raw !== "object") continue;
71
+ const field = raw;
72
+ if (typeof field.value !== "string" || field.value.length === 0) continue;
73
+ if (result.email === null && matches(field, EMAIL_NAME_HINT, "email")) {
74
+ result.email = normalizeEmail(field.value);
75
+ }
76
+ if (result.phoneNumber === null && matches(field, PHONE_NAME_HINT, "tel")) {
77
+ try {
78
+ result.phoneNumber = toE164(field.value, country);
79
+ } catch {
80
+ }
81
+ }
82
+ }
83
+ if (result.email !== null && result.phoneNumber !== null) break;
84
+ }
85
+ return result;
86
+ }
87
+ function stashUserData(data, country) {
88
+ const email = normalizeEmail(data.email);
89
+ let phoneNumber = null;
90
+ if (typeof data.phone === "string" && data.phone.length > 0) {
91
+ try {
92
+ phoneNumber = toE164(data.phone, country);
93
+ } catch {
94
+ phoneNumber = null;
95
+ }
96
+ }
97
+ stash = {
98
+ email: email ?? stash.email,
99
+ phoneNumber: phoneNumber ?? stash.phoneNumber
100
+ };
101
+ }
102
+ function stashUserDataFromFormFields(fields, country) {
103
+ try {
104
+ const extracted = extractUserDataFromFormFields(fields, country);
105
+ stash = {
106
+ email: extracted.email ?? stash.email,
107
+ phoneNumber: extracted.phoneNumber ?? stash.phoneNumber
108
+ };
109
+ } catch {
110
+ }
111
+ }
112
+ function clearStashedUserData() {
113
+ stash = { email: null, phoneNumber: null };
114
+ }
115
+ function applyUserDataForConversion(explicit, country) {
116
+ if (typeof window === "undefined" || typeof window.gtag !== "function") return false;
117
+ let email = stash.email;
118
+ let phoneNumber = stash.phoneNumber;
119
+ if (explicit) {
120
+ const normalizedEmail = normalizeEmail(explicit.email);
121
+ if (normalizedEmail) email = normalizedEmail;
122
+ if (typeof explicit.phone === "string" && explicit.phone.length > 0) {
123
+ try {
124
+ phoneNumber = toE164(explicit.phone, country) ?? phoneNumber;
125
+ } catch {
126
+ }
127
+ }
128
+ }
129
+ if (email === null && phoneNumber === null) return false;
130
+ try {
131
+ window.gtag("set", "user_data", {
132
+ ...email !== null ? { email } : {},
133
+ ...phoneNumber !== null ? { phone_number: phoneNumber } : {}
134
+ });
135
+ return true;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
7
141
  // ../tracking-core/src/consent.ts
8
142
  var CONSENT_STATE_KEY = "consent_state";
9
143
  var CONSENT_TIMESTAMP_KEY = "consent_timestamp";
@@ -89,6 +223,13 @@ function setConsentState(state, options) {
89
223
  } catch {
90
224
  }
91
225
  pushConsentToPlatforms(state);
226
+ if (state === "denied") {
227
+ clearStashedUserData();
228
+ try {
229
+ if (typeof window.gtag === "function") window.gtag("set", "user_data", null);
230
+ } catch {
231
+ }
232
+ }
92
233
  notifyConsentChanged({ state, source: "explicit", updatedAt, expiresAt });
93
234
  }
94
235
  function optIn() {
@@ -113,6 +254,11 @@ function resetConsent() {
113
254
  var TRACKING_COOKIE_MAX_AGE_SECONDS = 7776e3;
114
255
  var TRACKING_PARAM_KEYS = [
115
256
  "gclid",
257
+ // Google's iOS/Safari replacement click IDs — issued when privacy features
258
+ // withhold gclid (wbraid: web-to-web, gbraid: app-to-web). First-class
259
+ // citizens: captured, persisted, and attributed exactly like gclid.
260
+ "wbraid",
261
+ "gbraid",
116
262
  "fbclid",
117
263
  "utm_source",
118
264
  "utm_medium",
@@ -123,6 +269,8 @@ var TRACKING_PARAM_KEYS = [
123
269
  function createEmptyTrackingParams() {
124
270
  return {
125
271
  gclid: null,
272
+ wbraid: null,
273
+ gbraid: null,
126
274
  fbclid: null,
127
275
  utm_source: null,
128
276
  utm_medium: null,
@@ -454,6 +602,8 @@ function buildLandingPayloadFields(sessionId, override) {
454
602
  if (params === null) return {};
455
603
  return {
456
604
  landing_gclid: params.gclid ?? null,
605
+ landing_wbraid: params.wbraid ?? null,
606
+ landing_gbraid: params.gbraid ?? null,
457
607
  landing_fbclid: params.fbclid ?? null,
458
608
  landing_utm_source: params.utm_source ?? null,
459
609
  landing_utm_medium: params.utm_medium ?? null,
@@ -481,6 +631,8 @@ function createTrackingSessionUpsertPayload(trackingParams, input, context) {
481
631
  session_id: input.sessionId,
482
632
  visitor_id: input.visitorId ?? null,
483
633
  gclid: trackingParams.gclid,
634
+ wbraid: trackingParams.wbraid,
635
+ gbraid: trackingParams.gbraid,
484
636
  fbclid: trackingParams.fbclid,
485
637
  fbc: getFbcCookie(),
486
638
  fbp: getFbpCookie(),
@@ -502,6 +654,8 @@ function createTrackingEventCreatePayload(trackingParams, input, context) {
502
654
  session_id: input.sessionId,
503
655
  event_type: input.eventType,
504
656
  gclid: trackingParams.gclid,
657
+ wbraid: trackingParams.wbraid,
658
+ gbraid: trackingParams.gbraid,
505
659
  fbclid: trackingParams.fbclid,
506
660
  fbc: getFbcCookie(),
507
661
  fbp: getFbpCookie(),
@@ -531,13 +685,18 @@ function markFired(input) {
531
685
  } catch {
532
686
  }
533
687
  }
534
- function fireOnce(input) {
535
- if (alreadyFired(input)) return;
536
- if (fireGtagConversion(input)) markFired(input);
537
- }
538
- function fireConversionWithConsent(input) {
539
- if (getConsentState() === "denied") return;
540
- fireOnce(input);
688
+ function fireConversionWithConsent(input, options) {
689
+ if (getConsentState() === "denied") return "denied";
690
+ if (alreadyFired(input)) return "duplicate";
691
+ if (!isValidSendTo(input.sendTo)) return "invalid";
692
+ try {
693
+ applyUserDataForConversion(options?.userData);
694
+ if (!fireGtagConversion(input)) return "retryable";
695
+ markFired(input);
696
+ return "fired";
697
+ } catch {
698
+ return "retryable";
699
+ }
541
700
  }
542
701
 
543
702
  // ../tracking-core/src/resources/conversion-config.ts
@@ -711,6 +870,103 @@ function resolveConversionConfig(options) {
711
870
  };
712
871
  }
713
872
 
873
+ // ../tracking-core/src/resources/automatic-transaction.ts
874
+ var STORAGE_KEY = "_aranova_auto_txn_map";
875
+ var transactionIds = /* @__PURE__ */ new Map();
876
+ var legacyCounter = 0;
877
+ function scopeKey(sessionId, goalKey, path) {
878
+ return JSON.stringify([sessionId, goalKey, path]);
879
+ }
880
+ function randomId() {
881
+ const cryptoApi = globalThis.crypto;
882
+ if (typeof cryptoApi?.randomUUID === "function") return cryptoApi.randomUUID();
883
+ if (typeof cryptoApi?.getRandomValues === "function") {
884
+ const bytes = cryptoApi.getRandomValues(new Uint8Array(16));
885
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
886
+ }
887
+ legacyCounter = (legacyCounter + 1) % 4294967296;
888
+ const timestamp = Date.now().toString(16).padStart(12, "0");
889
+ const counter = legacyCounter.toString(16).padStart(8, "0");
890
+ const random = Math.floor(Math.random() * 281474976710655).toString(16).padStart(12, "0");
891
+ return `${timestamp}${counter}${random}`.slice(0, 32);
892
+ }
893
+ function isValidTransactionId(value) {
894
+ return typeof value === "string" && /^auto:(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i.test(
895
+ value
896
+ ) && value.length <= 64;
897
+ }
898
+ function readStoredMap(sessionId) {
899
+ if (typeof window === "undefined") return { sessionId, entries: {} };
900
+ try {
901
+ const raw = window.localStorage.getItem(STORAGE_KEY);
902
+ if (!raw) return { sessionId, entries: {} };
903
+ const parsed = JSON.parse(raw);
904
+ if (parsed.sessionId !== sessionId || !parsed.entries || typeof parsed.entries !== "object" || Array.isArray(parsed.entries)) {
905
+ return { sessionId, entries: {} };
906
+ }
907
+ return { sessionId, entries: parsed.entries };
908
+ } catch {
909
+ return { sessionId, entries: {} };
910
+ }
911
+ }
912
+ function writeStoredMap(stored) {
913
+ if (typeof window === "undefined") return;
914
+ try {
915
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
916
+ } catch {
917
+ }
918
+ }
919
+ function getAutomaticTransactionId(sessionId, goalKey, path) {
920
+ const key = scopeKey(sessionId, goalKey, path);
921
+ const existing = transactionIds.get(key);
922
+ if (existing) return existing;
923
+ const stored = readStoredMap(sessionId);
924
+ const storedId = stored.entries[key];
925
+ if (isValidTransactionId(storedId)) {
926
+ transactionIds.set(key, storedId);
927
+ return storedId;
928
+ }
929
+ const transactionId = `auto:${randomId()}`;
930
+ transactionIds.set(key, transactionId);
931
+ const latest = readStoredMap(sessionId);
932
+ const concurrentId = latest.entries[key];
933
+ if (isValidTransactionId(concurrentId)) {
934
+ transactionIds.set(key, concurrentId);
935
+ return concurrentId;
936
+ }
937
+ latest.entries[key] = transactionId;
938
+ writeStoredMap(latest);
939
+ return transactionId;
940
+ }
941
+
942
+ // ../tracking-core/src/resources/automatic-trigger.ts
943
+ function hasNumberField(metadata, key) {
944
+ return typeof metadata[key] === "number";
945
+ }
946
+ function hasStringField(metadata, key) {
947
+ return typeof metadata[key] === "string";
948
+ }
949
+ function automaticThresholdMet(goal, eventType, metadata) {
950
+ const trigger = goal.trigger;
951
+ if (!trigger || trigger.event_type !== eventType) return false;
952
+ switch (eventType) {
953
+ case "scroll_depth":
954
+ return hasNumberField(metadata, "depth_percent") && trigger.threshold_percent != null && metadata.depth_percent >= trigger.threshold_percent;
955
+ case "time_on_site":
956
+ return hasNumberField(metadata, "duration_ms") && trigger.threshold_seconds != null && metadata.duration_ms >= trigger.threshold_seconds * 1e3;
957
+ case "multi_page_session":
958
+ return hasNumberField(metadata, "page_count") && trigger.page_threshold != null && metadata.page_count >= trigger.page_threshold;
959
+ case "specific_page_visit":
960
+ return hasStringField(metadata, "page_name") && metadata.page_name === trigger.page_name;
961
+ case "page_view":
962
+ case "form_start":
963
+ case "phone_click":
964
+ return true;
965
+ default:
966
+ return false;
967
+ }
968
+ }
969
+
714
970
  // ../tracking-core/src/resources/sales/money.ts
715
971
  var MINOR_UNIT_EXPONENT = {
716
972
  USD: 2,
@@ -829,7 +1085,12 @@ var TrackingConfigRuntime = class {
829
1085
  this.etag = null;
830
1086
  this.stateValue = "unconfirmed";
831
1087
  this.confirmedAt = 0;
1088
+ this.authorityGeneration = 0;
832
1089
  this.inFlight = null;
1090
+ this.flushInFlight = null;
1091
+ this.flushRequested = false;
1092
+ this.retryTimer = null;
1093
+ this.started = false;
833
1094
  this.conversionQueue = [];
834
1095
  this.automaticQueue = [];
835
1096
  this.pageQueue = [];
@@ -837,6 +1098,20 @@ var TrackingConfigRuntime = class {
837
1098
  const cached = readCache2(ref.cdnUrl);
838
1099
  this.current = cached?.config ?? null;
839
1100
  this.etag = cached?.etag ?? null;
1101
+ this.start();
1102
+ }
1103
+ /**
1104
+ * Explicitly start authority resolution and Google-tag bootstrap.
1105
+ *
1106
+ * Idempotent so framework effects can call it after hydration without
1107
+ * depending on constructor timing.
1108
+ */
1109
+ start() {
1110
+ if (this.started) {
1111
+ void this.flush();
1112
+ return;
1113
+ }
1114
+ this.started = true;
840
1115
  void this.revalidate();
841
1116
  if (typeof window !== "undefined") {
842
1117
  window.addEventListener("visibilitychange", () => {
@@ -852,7 +1127,7 @@ var TrackingConfigRuntime = class {
852
1127
  return this.stateValue === "active" || this.stateValue === "tombstone" ? this.current : null;
853
1128
  }
854
1129
  __unsafeExpireAuthorityForTests() {
855
- this.confirmedAt = 0;
1130
+ this.confirmedAt = Number.NEGATIVE_INFINITY;
856
1131
  }
857
1132
  subscribe(listener) {
858
1133
  this.listeners.add(listener);
@@ -862,10 +1137,14 @@ var TrackingConfigRuntime = class {
862
1137
  if (this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS) {
863
1138
  return true;
864
1139
  }
865
- await this.revalidate();
1140
+ await this.revalidateAuthority();
866
1141
  return this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS;
867
1142
  }
868
1143
  async revalidate() {
1144
+ await this.revalidateAuthority();
1145
+ await this.flush();
1146
+ }
1147
+ async revalidateAuthority() {
869
1148
  if (typeof window === "undefined" || !this.fetchImpl) return;
870
1149
  if (this.inFlight) return this.inFlight;
871
1150
  this.inFlight = this.revalidateNow().finally(() => {
@@ -882,8 +1161,8 @@ var TrackingConfigRuntime = class {
882
1161
  this.conversionQueue.push({ key, ...options });
883
1162
  void this.flush();
884
1163
  }
885
- queueAutomaticEvent(eventType, metadata, transactionPath) {
886
- this.automaticQueue.push({ eventType, metadata, transactionPath });
1164
+ queueAutomaticEvent(eventType, metadata, transactionPath, transactionScope) {
1165
+ this.automaticQueue.push({ eventType, metadata, transactionPath, transactionScope });
887
1166
  void this.flush();
888
1167
  }
889
1168
  listGoals() {
@@ -900,7 +1179,6 @@ var TrackingConfigRuntime = class {
900
1179
  });
901
1180
  if (response.status === 304 && this.current && validateConfig(this.current, this.ref)) {
902
1181
  this.confirm(this.current, this.etag);
903
- await this.flush();
904
1182
  return;
905
1183
  }
906
1184
  if (!response.ok) {
@@ -917,21 +1195,26 @@ var TrackingConfigRuntime = class {
917
1195
  return;
918
1196
  }
919
1197
  this.confirm(next, response.headers.get("ETag"));
920
- await this.flush();
921
1198
  } catch {
922
1199
  this.expireAuthority();
923
1200
  }
924
1201
  }
925
1202
  expireAuthority() {
1203
+ this.authorityGeneration += 1;
926
1204
  if (this.stateValue !== "unconfirmed") this.stateValue = "unconfirmed";
927
1205
  }
928
1206
  confirm(config, etag) {
1207
+ this.authorityGeneration += 1;
929
1208
  this.current = config;
930
1209
  this.etag = etag;
931
1210
  this.confirmedAt = Date.now();
932
1211
  this.stateValue = isTombstone(config) ? "tombstone" : "active";
933
1212
  writeCache2(this.ref.cdnUrl, { etag, config });
934
1213
  if (this.stateValue === "tombstone") {
1214
+ if (this.retryTimer !== null) {
1215
+ window.clearTimeout(this.retryTimer);
1216
+ this.retryTimer = null;
1217
+ }
935
1218
  this.conversionQueue.length = 0;
936
1219
  this.automaticQueue.length = 0;
937
1220
  this.pageQueue.length = 0;
@@ -939,79 +1222,107 @@ var TrackingConfigRuntime = class {
939
1222
  for (const listener of this.listeners) listener();
940
1223
  }
941
1224
  async flush() {
1225
+ if (this.flushInFlight) {
1226
+ this.flushRequested = true;
1227
+ return this.flushInFlight;
1228
+ }
1229
+ this.flushRequested = false;
1230
+ this.flushInFlight = this.flushNow().finally(() => {
1231
+ this.flushInFlight = null;
1232
+ if (this.flushRequested && this.stateValue === "active") void this.flush();
1233
+ });
1234
+ return this.flushInFlight;
1235
+ }
1236
+ scheduleRetry() {
1237
+ if (this.retryTimer !== null || typeof window === "undefined") return;
1238
+ this.retryTimer = window.setTimeout(() => {
1239
+ this.retryTimer = null;
1240
+ void this.flush();
1241
+ }, 1e3);
1242
+ }
1243
+ async flushNow() {
942
1244
  if (!await this.ensureAuthority()) return;
943
1245
  if (this.stateValue !== "active" || !this.current) return;
944
- const ids = Object.values(this.current.gtag_ids).filter(
1246
+ const config = this.current;
1247
+ const generation = this.authorityGeneration;
1248
+ const ids = Object.values(config.gtag_ids).filter(
945
1249
  (id) => typeof id === "string" && isValidGtagId(id)
946
1250
  );
947
1251
  if (ids.length === 0) return;
948
1252
  await ensureScript(ids[0]);
1253
+ if (this.authorityGeneration !== generation || this.stateValue !== "active" || this.current !== config) {
1254
+ return;
1255
+ }
949
1256
  const gtag = window.gtag;
950
1257
  if (typeof gtag !== "function") return;
951
- if (!jsInitialized) {
952
- gtag("js", /* @__PURE__ */ new Date());
953
- jsInitialized = true;
954
- }
955
- for (const id of ids) {
956
- if (configuredIds.has(id)) continue;
957
- gtag("config", id, { send_page_view: false });
958
- configuredIds.add(id);
959
- }
960
- while (this.pageQueue.length) {
961
- const page = this.pageQueue.shift();
962
- gtag("event", "page_view", {
963
- page_location: page.href,
964
- page_title: page.title ?? void 0,
965
- page_referrer: page.referrer ?? void 0
966
- });
1258
+ try {
1259
+ if (!jsInitialized) {
1260
+ gtag("js", /* @__PURE__ */ new Date());
1261
+ jsInitialized = true;
1262
+ }
1263
+ for (const id of ids) {
1264
+ if (configuredIds.has(id)) continue;
1265
+ gtag("config", id, { send_page_view: false });
1266
+ configuredIds.add(id);
1267
+ }
1268
+ } catch {
1269
+ this.scheduleRetry();
1270
+ return;
1271
+ }
1272
+ while (this.pageQueue.length > 0) {
1273
+ const page = this.pageQueue[0];
1274
+ try {
1275
+ gtag("event", "page_view", {
1276
+ page_location: page.href,
1277
+ page_title: page.title ?? void 0,
1278
+ page_referrer: page.referrer ?? void 0
1279
+ });
1280
+ this.pageQueue.shift();
1281
+ } catch {
1282
+ this.scheduleRetry();
1283
+ return;
1284
+ }
967
1285
  }
968
- while (this.automaticQueue.length) {
969
- const event = this.automaticQueue.shift();
970
- for (const goal of this.current.goals) {
1286
+ while (this.automaticQueue.length > 0) {
1287
+ const event = this.automaticQueue[0];
1288
+ for (const goal of config.goals) {
971
1289
  if (goal.kind !== "event" || !goal.firing) continue;
972
1290
  if (!automaticThresholdMet(goal, event.eventType, event.metadata)) continue;
973
1291
  this.conversionQueue.push({
974
1292
  key: goal.key,
975
- transactionId: `auto:${goal.key}:${event.transactionPath}`
1293
+ transactionId: getAutomaticTransactionId(
1294
+ event.transactionScope,
1295
+ goal.key,
1296
+ event.transactionPath
1297
+ )
976
1298
  });
977
1299
  }
1300
+ this.automaticQueue.shift();
978
1301
  }
979
- while (this.conversionQueue.length) {
980
- const item = this.conversionQueue.shift();
981
- const goal = this.current.goals.find((g) => g.key === item.key);
1302
+ while (this.conversionQueue.length > 0) {
1303
+ const item = this.conversionQueue[0];
1304
+ const goal = config.goals.find((g) => g.key === item.key);
982
1305
  const firing = goal?.firing;
983
- if (!firing || getConsentState() === "denied") continue;
1306
+ if (!firing) {
1307
+ this.conversionQueue.shift();
1308
+ continue;
1309
+ }
984
1310
  const currency = item.currency ?? firing.currency ?? null;
985
1311
  const value = item.value ?? (firing.value_cents != null && currency ? fromMinor(firing.value_cents, currency) : null);
986
- fireGtagConversion({
1312
+ const outcome = fireConversionWithConsent({
987
1313
  sendTo: firing.send_to,
988
1314
  value,
989
1315
  currency,
990
1316
  transactionId: item.transactionId ?? null
991
1317
  });
1318
+ if (outcome === "retryable") {
1319
+ this.scheduleRetry();
1320
+ return;
1321
+ }
1322
+ this.conversionQueue.shift();
992
1323
  }
993
1324
  }
994
1325
  };
995
- function automaticThresholdMet(goal, eventType, metadata) {
996
- const t = goal.trigger;
997
- if (!t || t.event_type !== eventType) return false;
998
- switch (eventType) {
999
- case "scroll_depth":
1000
- return typeof metadata.depth_percent === "number" && t.threshold_percent != null && metadata.depth_percent >= t.threshold_percent;
1001
- case "time_on_site":
1002
- return typeof metadata.duration_ms === "number" && t.threshold_seconds != null && metadata.duration_ms >= t.threshold_seconds * 1e3;
1003
- case "multi_page_session":
1004
- return typeof metadata.page_count === "number" && t.page_threshold != null && metadata.page_count >= t.page_threshold;
1005
- case "specific_page_visit":
1006
- return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
1007
- case "page_view":
1008
- case "form_start":
1009
- case "phone_click":
1010
- return true;
1011
- default:
1012
- return false;
1013
- }
1014
- }
1015
1326
  function getTrackingConfigRuntime(ref, fetchImpl) {
1016
1327
  const key = `${ref.cdnUrl}|${ref.businessId}|${ref.environment}`;
1017
1328
  const existing = runtimes.get(key);
@@ -1022,26 +1333,6 @@ function getTrackingConfigRuntime(ref, fetchImpl) {
1022
1333
  }
1023
1334
 
1024
1335
  // ../tracking-core/src/resources/conversion-autofire.ts
1025
- function thresholdMet(goal, eventType, metadata) {
1026
- const t = goal.trigger;
1027
- if (!t || t.event_type !== eventType) return false;
1028
- switch (eventType) {
1029
- case "scroll_depth":
1030
- return typeof metadata.depth_percent === "number" && t.threshold_percent != null && metadata.depth_percent >= t.threshold_percent;
1031
- case "time_on_site":
1032
- return typeof metadata.duration_ms === "number" && t.threshold_seconds != null && metadata.duration_ms >= t.threshold_seconds * 1e3;
1033
- case "multi_page_session":
1034
- return typeof metadata.page_count === "number" && t.page_threshold != null && metadata.page_count >= t.page_threshold;
1035
- case "specific_page_visit":
1036
- return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
1037
- case "page_view":
1038
- case "form_start":
1039
- case "phone_click":
1040
- return true;
1041
- default:
1042
- return false;
1043
- }
1044
- }
1045
1336
  function currentPath() {
1046
1337
  return typeof window === "undefined" ? "" : window.location.pathname;
1047
1338
  }
@@ -1049,13 +1340,14 @@ var MAX_BUFFERED_EVENTS = 50;
1049
1340
  function createConversionAutoFire(store) {
1050
1341
  const pending = [];
1051
1342
  let subscribed = false;
1052
- function fireMatching(eventType, metadata) {
1343
+ function fireMatching(eventType, metadata, transactionScope) {
1053
1344
  for (const goal of store.listGoals()) {
1054
1345
  if (goal.kind !== "event" || !goal.firing) continue;
1055
- if (!thresholdMet(goal, eventType, metadata)) continue;
1346
+ if (!automaticThresholdMet(goal, eventType, metadata)) continue;
1347
+ const transactionId = getAutomaticTransactionId(transactionScope, goal.key, currentPath());
1056
1348
  if ("queueAutomaticEvent" in store) {
1057
1349
  store.fireConversion(goal.key, {
1058
- transactionId: `auto:${goal.key}:${currentPath()}`
1350
+ transactionId
1059
1351
  });
1060
1352
  continue;
1061
1353
  }
@@ -1066,28 +1358,30 @@ function createConversionAutoFire(store) {
1066
1358
  sendTo: firing.send_to,
1067
1359
  value: cents != null && currency ? fromMinor(cents, currency) : null,
1068
1360
  currency,
1069
- // Page-scoped txn id → fire once per (goal, path) per session; engagement conversions
1070
- // shouldn't re-fire as the visitor scrolls back and forth or re-enters a page.
1071
- transactionId: `auto:${goal.key}:${currentPath()}`
1361
+ transactionId
1072
1362
  });
1073
1363
  }
1074
1364
  }
1075
1365
  return {
1076
- onAutomaticEvent(eventType, metadata) {
1366
+ onAutomaticEvent(eventType, metadata, transactionScope) {
1077
1367
  if ("queueAutomaticEvent" in store) {
1078
- store.queueAutomaticEvent(eventType, metadata, currentPath());
1368
+ store.queueAutomaticEvent(eventType, metadata, currentPath(), transactionScope);
1079
1369
  return;
1080
1370
  }
1081
1371
  if (store.isReady()) {
1082
- fireMatching(eventType, metadata);
1372
+ fireMatching(eventType, metadata, transactionScope);
1083
1373
  return;
1084
1374
  }
1085
- if (pending.length < MAX_BUFFERED_EVENTS) pending.push({ eventType, metadata });
1375
+ if (pending.length < MAX_BUFFERED_EVENTS) {
1376
+ pending.push({ eventType, metadata, transactionScope });
1377
+ }
1086
1378
  if (!subscribed) {
1087
1379
  subscribed = true;
1088
1380
  store.onResolve(() => {
1089
1381
  const buffered = pending.splice(0);
1090
- for (const event of buffered) fireMatching(event.eventType, event.metadata);
1382
+ for (const event of buffered) {
1383
+ fireMatching(event.eventType, event.metadata, event.transactionScope);
1384
+ }
1091
1385
  });
1092
1386
  }
1093
1387
  }
@@ -1099,7 +1393,7 @@ function withConversionAutoFire(client, autoFire) {
1099
1393
  trackEvent: (input) => {
1100
1394
  client.trackEvent(input);
1101
1395
  try {
1102
- autoFire.onAutomaticEvent(input.eventType, input.metadata ?? {});
1396
+ autoFire.onAutomaticEvent(input.eventType, input.metadata ?? {}, client.getSessionId());
1103
1397
  } catch {
1104
1398
  }
1105
1399
  }
@@ -1514,6 +1808,8 @@ function createTrackingClient(config) {
1514
1808
  session_id: sessionId,
1515
1809
  visitor_id: visitorId,
1516
1810
  gclid: params.gclid,
1811
+ wbraid: params.wbraid,
1812
+ gbraid: params.gbraid,
1517
1813
  fbclid: params.fbclid,
1518
1814
  fbc: getFbcCookie(),
1519
1815
  fbp: getFbpCookie(),
@@ -1560,6 +1856,13 @@ function createTrackingClient(config) {
1560
1856
  function trackEvent(input) {
1561
1857
  if (destroyed) return;
1562
1858
  if (!input || typeof input.eventType !== "string" || input.eventType.length === 0) return;
1859
+ if (input.eventType === "form_submit" && getConsentState() !== "denied") {
1860
+ try {
1861
+ const fields = input.metadata?.form?.fields;
1862
+ if (fields) stashUserDataFromFormFields(fields, config.phone?.defaultCountry);
1863
+ } catch {
1864
+ }
1865
+ }
1563
1866
  const occurredAt = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : (/* @__PURE__ */ new Date()).toISOString();
1564
1867
  queue.push({
1565
1868
  event_type: input.eventType,
@@ -2223,7 +2526,7 @@ function attachScrollDepth(client, config) {
2223
2526
  }
2224
2527
 
2225
2528
  // ../tracking-core/src/triggers/multi-page-session.ts
2226
- var STORAGE_KEY = "aranova_tracking_mps_paths";
2529
+ var STORAGE_KEY2 = "aranova_tracking_mps_paths";
2227
2530
  var SESSION_KEY = "aranova_tracking_mps_session";
2228
2531
  var FIRED_KEY = "aranova_tracking_mps_fired";
2229
2532
  function getSessionStorage() {
@@ -2245,7 +2548,7 @@ function attachMultiPageSession(client, config) {
2245
2548
  let lastCheckedPath = "";
2246
2549
  function getDistinctPaths() {
2247
2550
  try {
2248
- const raw = storage.getItem(STORAGE_KEY);
2551
+ const raw = storage.getItem(STORAGE_KEY2);
2249
2552
  return raw ? new Set(JSON.parse(raw)) : /* @__PURE__ */ new Set();
2250
2553
  } catch {
2251
2554
  return /* @__PURE__ */ new Set();
@@ -2253,7 +2556,7 @@ function attachMultiPageSession(client, config) {
2253
2556
  }
2254
2557
  function saveDistinctPaths(paths) {
2255
2558
  try {
2256
- storage.setItem(STORAGE_KEY, JSON.stringify([...paths]));
2559
+ storage.setItem(STORAGE_KEY2, JSON.stringify([...paths]));
2257
2560
  } catch {
2258
2561
  }
2259
2562
  }
@@ -2262,7 +2565,7 @@ function attachMultiPageSession(client, config) {
2262
2565
  const storedSession = storage.getItem(SESSION_KEY);
2263
2566
  if (storedSession !== currentSession) {
2264
2567
  storage.setItem(SESSION_KEY, currentSession);
2265
- storage.removeItem(STORAGE_KEY);
2568
+ storage.removeItem(STORAGE_KEY2);
2266
2569
  storage.removeItem(FIRED_KEY);
2267
2570
  }
2268
2571
  }
@@ -2523,46 +2826,6 @@ function attachCtaClickCapture(client, config) {
2523
2826
  };
2524
2827
  }
2525
2828
 
2526
- // ../tracking-core/src/phone.ts
2527
- import { AsYouType, parsePhoneNumberFromString } from "libphonenumber-js";
2528
- var DEFAULT_PHONE_COUNTRY = "CA";
2529
- function parsePhone(raw, country) {
2530
- const region = country ?? DEFAULT_PHONE_COUNTRY;
2531
- const parsed = parsePhoneNumberFromString(raw ?? "", region);
2532
- if (!parsed) {
2533
- return { e164: null, national: "", international: "", country: region, isValid: false };
2534
- }
2535
- const isValid = parsed.isValid();
2536
- return {
2537
- // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
2538
- // (e.g. too few digits) still parses but must not be transmitted.
2539
- e164: isValid ? parsed.number : null,
2540
- national: parsed.formatNational(),
2541
- international: parsed.formatInternational(),
2542
- country: parsed.country ?? region,
2543
- isValid
2544
- };
2545
- }
2546
- function toE164(raw, country) {
2547
- return parsePhone(raw, country).e164;
2548
- }
2549
- function formatPhone(value, format = "national", country) {
2550
- const parsed = parsePhone(value, country);
2551
- if (typeof format === "function") return format(parsed);
2552
- switch (format) {
2553
- case "international":
2554
- return parsed.international || value;
2555
- case "e164":
2556
- return parsed.e164 ?? value;
2557
- case "national":
2558
- default:
2559
- return parsed.national || value;
2560
- }
2561
- }
2562
- function formatPhoneAsTyped(raw, country) {
2563
- return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
2564
- }
2565
-
2566
2829
  // ../tracking-core/src/triggers/phone-click-capture.ts
2567
2830
  var DEFAULT_TEL_SELECTOR = 'a[href^="tel:"]';
2568
2831
  function safeDecodeURIComponent(value) {
@@ -2672,6 +2935,8 @@ async function salesRequest(config, method, path, body) {
2672
2935
  // ../tracking-core/src/resources/sales/client.ts
2673
2936
  function fireRecordedConversions(firing, input, recorded, sale, currency) {
2674
2937
  if (!firing) return;
2938
+ const userData = input.customer_email || input.customer_phone ? { email: input.customer_email ?? null, phone: input.customer_phone ?? null } : null;
2939
+ if (userData && getConsentState() !== "denied") stashUserData(userData);
2675
2940
  const txnBase = input.external_id ?? sale.id;
2676
2941
  for (const item of recorded) {
2677
2942
  if (!item.service) continue;
@@ -2686,12 +2951,15 @@ function fireRecordedConversions(firing, input, recorded, sale, currency) {
2686
2951
  const config = firing.getFiring(item.service);
2687
2952
  if (!config) continue;
2688
2953
  const cents = item.amount_cents ?? config.value_cents ?? null;
2689
- fireConversionWithConsent({
2690
- sendTo: config.send_to,
2691
- value: cents != null ? fromMinor(cents, currency) : null,
2692
- currency: config.currency ?? currency,
2693
- transactionId: `${txnBase}:${item.service}`
2694
- });
2954
+ fireConversionWithConsent(
2955
+ {
2956
+ sendTo: config.send_to,
2957
+ value: cents != null ? fromMinor(cents, currency) : null,
2958
+ currency: config.currency ?? currency,
2959
+ transactionId: `${txnBase}:${item.service}`
2960
+ },
2961
+ { userData }
2962
+ );
2695
2963
  }
2696
2964
  }
2697
2965
  function createSalesClient(config) {
@@ -2726,6 +2994,7 @@ function createSalesClient(config) {
2726
2994
  recordSale: record,
2727
2995
  trackConversion(key, options) {
2728
2996
  if (config.firing && "fireConversion" in config.firing) {
2997
+ if (options?.userData && getConsentState() !== "denied") stashUserData(options.userData);
2729
2998
  config.firing.fireConversion(key, {
2730
2999
  value: options?.value ?? void 0,
2731
3000
  currency: options?.currency ?? config.defaultCurrency ?? void 0,
@@ -2738,12 +3007,15 @@ function createSalesClient(config) {
2738
3007
  const currency = firing.currency ?? options?.currency ?? config.defaultCurrency ?? null;
2739
3008
  const cents = firing.value_cents ?? null;
2740
3009
  const value = options?.value ?? (cents != null && currency ? fromMinor(cents, currency) : null);
2741
- fireConversionWithConsent({
2742
- sendTo: firing.send_to,
2743
- value,
2744
- currency,
2745
- transactionId: options?.transactionId ?? null
2746
- });
3010
+ fireConversionWithConsent(
3011
+ {
3012
+ sendTo: firing.send_to,
3013
+ value,
3014
+ currency,
3015
+ transactionId: options?.transactionId ?? null
3016
+ },
3017
+ { userData: options?.userData }
3018
+ );
2747
3019
  },
2748
3020
  async list(query) {
2749
3021
  const { cursor, limit, sort, order, want_total, ...filters } = query ?? {};
@@ -3288,7 +3560,7 @@ function GoogleAdsTracking(props) {
3288
3560
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo4 } from "react";
3289
3561
 
3290
3562
  // package.json
3291
- var version = "0.18.1";
3563
+ var version = "0.19.1";
3292
3564
 
3293
3565
  // ../tracking-core/src/phone-react.tsx
3294
3566
  import {
@@ -3426,31 +3698,46 @@ function createTracking(options) {
3426
3698
  metaPixelIds,
3427
3699
  children
3428
3700
  }) {
3429
- const resolvedGtagIds = gtagIds ? Object.fromEntries(
3430
- Object.entries(gtagIds).filter((e) => e[1] != null)
3431
- ) : gtagId ? { default: gtagId } : void 0;
3432
- const client = useMemo4(
3433
- () => createTypedClient(
3434
- getOrCreateTrackingClient({
3435
- apiKey,
3436
- endpoint,
3437
- surface: "react",
3438
- packageName: "@aranova/tracking-react",
3439
- sdkVersion: version,
3440
- triggers,
3441
- environment,
3442
- activeGtagIds: resolvedGtagIds,
3443
- debug
3444
- }),
3701
+ const gtagIdsKey = useMemo4(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
3702
+ const resolvedGtagIds = useMemo4(
3703
+ () => gtagIds ? Object.fromEntries(
3704
+ Object.entries(gtagIds).filter((e) => e[1] != null)
3705
+ ) : gtagId ? { default: gtagId } : void 0,
3706
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- gtagIdsKey is stable proxy
3707
+ [gtagId, gtagIdsKey]
3708
+ );
3709
+ const rawClient = useMemo4(
3710
+ () => getOrCreateTrackingClient({
3711
+ apiKey,
3712
+ endpoint,
3713
+ surface: "react",
3714
+ packageName: "@aranova/tracking-react",
3715
+ sdkVersion: version,
3445
3716
  triggers,
3446
- { debug }
3447
- ),
3717
+ environment,
3718
+ activeGtagIds: resolvedGtagIds,
3719
+ debug
3720
+ }),
3721
+ [resolvedGtagIds]
3722
+ );
3723
+ const conversionStore = useMemo4(
3724
+ () => trackingConfig ? getTrackingConfigRuntime(trackingConfig) : conversionConfig ? resolveConversionConfig({
3725
+ cdnUrl: conversionConfig.cdnUrl,
3726
+ baked: conversionConfig.baked
3727
+ }) : null,
3448
3728
  []
3449
3729
  );
3450
- const gtagIdsKey = useMemo4(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
3730
+ const conversionClient = useMemo4(
3731
+ () => conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient,
3732
+ [rawClient, conversionStore]
3733
+ );
3734
+ const client = useMemo4(
3735
+ () => createTypedClient(conversionClient, triggers, { debug }),
3736
+ [conversionClient]
3737
+ );
3451
3738
  useEffect5(() => {
3452
3739
  if (trackingConfig) {
3453
- void getTrackingConfigRuntime(trackingConfig).revalidate();
3740
+ getTrackingConfigRuntime(trackingConfig).start();
3454
3741
  } else if (gtagIds && Object.keys(gtagIds).length > 0) {
3455
3742
  bootstrapMultipleGtags(gtagIds);
3456
3743
  } else if (gtagId) {
@@ -3469,23 +3756,9 @@ function createTracking(options) {
3469
3756
  }
3470
3757
  }, [metaPixelId, metaPixelIdsKey]);
3471
3758
  useEffect5(() => {
3472
- const rawClient = getOrCreateTrackingClient({
3473
- apiKey,
3474
- endpoint,
3475
- surface: "react",
3476
- packageName: "@aranova/tracking-react",
3477
- triggers,
3478
- environment,
3479
- activeGtagIds: resolvedGtagIds,
3480
- debug
3481
- });
3482
3759
  return attachClientCapturesOnce(rawClient, () => {
3483
3760
  const detachers = [];
3484
- const conversionStore = trackingConfig ? getTrackingConfigRuntime(trackingConfig) : conversionConfig ? resolveConversionConfig({
3485
- cdnUrl: conversionConfig.cdnUrl,
3486
- baked: conversionConfig.baked
3487
- }) : null;
3488
- const detectorClient = conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient;
3761
+ const detectorClient = conversionStore ? conversionClient : rawClient;
3489
3762
  const pageClient = trackingConfig && conversionStore && "queuePageView" in conversionStore ? {
3490
3763
  ...detectorClient,
3491
3764
  trackEvent: (input) => {
@@ -3530,7 +3803,7 @@ function createTracking(options) {
3530
3803
  }
3531
3804
  };
3532
3805
  });
3533
- }, []);
3806
+ }, [conversionClient, conversionStore, rawClient]);
3534
3807
  return /* @__PURE__ */ jsx3(TrackingContext.Provider, { value: client, children: /* @__PURE__ */ jsx3(PhoneConfigProvider, { value: phone ?? null, children }) });
3535
3808
  }
3536
3809
  function useTracking() {
@@ -3558,6 +3831,7 @@ export {
3558
3831
  TRACKING_PARAM_KEYS,
3559
3832
  TRACKING_RANGES,
3560
3833
  captureTrackingParamsFromLocation,
3834
+ clearStashedUserData,
3561
3835
  createSalesClient,
3562
3836
  createTracking,
3563
3837
  createTrackingClientContext,
@@ -3586,6 +3860,7 @@ export {
3586
3860
  saleUpdateSchema,
3587
3861
  salesRequest,
3588
3862
  setConsentState,
3863
+ stashUserData,
3589
3864
  toE164,
3590
3865
  toMinor,
3591
3866
  useConsent,