@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.js CHANGED
@@ -33,6 +33,7 @@ __export(src_exports, {
33
33
  TRACKING_PARAM_KEYS: () => TRACKING_PARAM_KEYS,
34
34
  TRACKING_RANGES: () => TRACKING_RANGES,
35
35
  captureTrackingParamsFromLocation: () => captureTrackingParamsFromLocation,
36
+ clearStashedUserData: () => clearStashedUserData,
36
37
  createSalesClient: () => createSalesClient,
37
38
  createTracking: () => createTracking,
38
39
  createTrackingClientContext: () => createTrackingClientContext,
@@ -61,6 +62,7 @@ __export(src_exports, {
61
62
  saleUpdateSchema: () => saleUpdateSchema,
62
63
  salesRequest: () => salesRequest,
63
64
  setConsentState: () => setConsentState,
65
+ stashUserData: () => stashUserData,
64
66
  toE164: () => toE164,
65
67
  toMinor: () => toMinor,
66
68
  useConsent: () => useConsent,
@@ -79,6 +81,140 @@ var import_react2 = require("react");
79
81
  // src/hooks.ts
80
82
  var import_react = require("react");
81
83
 
84
+ // ../tracking-core/src/phone.ts
85
+ var import_libphonenumber_js = require("libphonenumber-js");
86
+ var DEFAULT_PHONE_COUNTRY = "CA";
87
+ function parsePhone(raw, country) {
88
+ const region = country ?? DEFAULT_PHONE_COUNTRY;
89
+ const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(raw ?? "", region);
90
+ if (!parsed) {
91
+ return { e164: null, national: "", international: "", country: region, isValid: false };
92
+ }
93
+ const isValid = parsed.isValid();
94
+ return {
95
+ // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
96
+ // (e.g. too few digits) still parses but must not be transmitted.
97
+ e164: isValid ? parsed.number : null,
98
+ national: parsed.formatNational(),
99
+ international: parsed.formatInternational(),
100
+ country: parsed.country ?? region,
101
+ isValid
102
+ };
103
+ }
104
+ function toE164(raw, country) {
105
+ return parsePhone(raw, country).e164;
106
+ }
107
+ function formatPhone(value, format = "national", country) {
108
+ const parsed = parsePhone(value, country);
109
+ if (typeof format === "function") return format(parsed);
110
+ switch (format) {
111
+ case "international":
112
+ return parsed.international || value;
113
+ case "e164":
114
+ return parsed.e164 ?? value;
115
+ case "national":
116
+ default:
117
+ return parsed.national || value;
118
+ }
119
+ }
120
+ function formatPhoneAsTyped(raw, country) {
121
+ return new import_libphonenumber_js.AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
122
+ }
123
+
124
+ // ../tracking-core/src/user-data.ts
125
+ var EMAIL_SHAPE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
126
+ var EMAIL_NAME_HINT = /e[-_]?mail/i;
127
+ var PHONE_NAME_HINT = /(^|[^a-z])(phone|tel|mobile|cell)/i;
128
+ var stash = { email: null, phoneNumber: null };
129
+ function normalizeEmail(raw) {
130
+ if (typeof raw !== "string") return null;
131
+ const cleaned = raw.trim().toLowerCase();
132
+ return EMAIL_SHAPE.test(cleaned) ? cleaned : null;
133
+ }
134
+ function fieldText(field, key) {
135
+ const value = field[key];
136
+ return typeof value === "string" ? value : "";
137
+ }
138
+ function extractUserDataFromFormFields(fields, country) {
139
+ const result = { email: null, phoneNumber: null };
140
+ if (!Array.isArray(fields)) return result;
141
+ const passes = [
142
+ (field, _hint, type) => fieldText(field, "type").toLowerCase() === type,
143
+ (field, hint) => hint.test(fieldText(field, "name")) || hint.test(fieldText(field, "label"))
144
+ ];
145
+ for (const matches of passes) {
146
+ for (const raw of fields) {
147
+ if (raw === null || typeof raw !== "object") continue;
148
+ const field = raw;
149
+ if (typeof field.value !== "string" || field.value.length === 0) continue;
150
+ if (result.email === null && matches(field, EMAIL_NAME_HINT, "email")) {
151
+ result.email = normalizeEmail(field.value);
152
+ }
153
+ if (result.phoneNumber === null && matches(field, PHONE_NAME_HINT, "tel")) {
154
+ try {
155
+ result.phoneNumber = toE164(field.value, country);
156
+ } catch {
157
+ }
158
+ }
159
+ }
160
+ if (result.email !== null && result.phoneNumber !== null) break;
161
+ }
162
+ return result;
163
+ }
164
+ function stashUserData(data, country) {
165
+ const email = normalizeEmail(data.email);
166
+ let phoneNumber = null;
167
+ if (typeof data.phone === "string" && data.phone.length > 0) {
168
+ try {
169
+ phoneNumber = toE164(data.phone, country);
170
+ } catch {
171
+ phoneNumber = null;
172
+ }
173
+ }
174
+ stash = {
175
+ email: email ?? stash.email,
176
+ phoneNumber: phoneNumber ?? stash.phoneNumber
177
+ };
178
+ }
179
+ function stashUserDataFromFormFields(fields, country) {
180
+ try {
181
+ const extracted = extractUserDataFromFormFields(fields, country);
182
+ stash = {
183
+ email: extracted.email ?? stash.email,
184
+ phoneNumber: extracted.phoneNumber ?? stash.phoneNumber
185
+ };
186
+ } catch {
187
+ }
188
+ }
189
+ function clearStashedUserData() {
190
+ stash = { email: null, phoneNumber: null };
191
+ }
192
+ function applyUserDataForConversion(explicit, country) {
193
+ if (typeof window === "undefined" || typeof window.gtag !== "function") return false;
194
+ let email = stash.email;
195
+ let phoneNumber = stash.phoneNumber;
196
+ if (explicit) {
197
+ const normalizedEmail = normalizeEmail(explicit.email);
198
+ if (normalizedEmail) email = normalizedEmail;
199
+ if (typeof explicit.phone === "string" && explicit.phone.length > 0) {
200
+ try {
201
+ phoneNumber = toE164(explicit.phone, country) ?? phoneNumber;
202
+ } catch {
203
+ }
204
+ }
205
+ }
206
+ if (email === null && phoneNumber === null) return false;
207
+ try {
208
+ window.gtag("set", "user_data", {
209
+ ...email !== null ? { email } : {},
210
+ ...phoneNumber !== null ? { phone_number: phoneNumber } : {}
211
+ });
212
+ return true;
213
+ } catch {
214
+ return false;
215
+ }
216
+ }
217
+
82
218
  // ../tracking-core/src/consent.ts
83
219
  var CONSENT_STATE_KEY = "consent_state";
84
220
  var CONSENT_TIMESTAMP_KEY = "consent_timestamp";
@@ -164,6 +300,13 @@ function setConsentState(state, options) {
164
300
  } catch {
165
301
  }
166
302
  pushConsentToPlatforms(state);
303
+ if (state === "denied") {
304
+ clearStashedUserData();
305
+ try {
306
+ if (typeof window.gtag === "function") window.gtag("set", "user_data", null);
307
+ } catch {
308
+ }
309
+ }
167
310
  notifyConsentChanged({ state, source: "explicit", updatedAt, expiresAt });
168
311
  }
169
312
  function optIn() {
@@ -188,6 +331,11 @@ function resetConsent() {
188
331
  var TRACKING_COOKIE_MAX_AGE_SECONDS = 7776e3;
189
332
  var TRACKING_PARAM_KEYS = [
190
333
  "gclid",
334
+ // Google's iOS/Safari replacement click IDs — issued when privacy features
335
+ // withhold gclid (wbraid: web-to-web, gbraid: app-to-web). First-class
336
+ // citizens: captured, persisted, and attributed exactly like gclid.
337
+ "wbraid",
338
+ "gbraid",
191
339
  "fbclid",
192
340
  "utm_source",
193
341
  "utm_medium",
@@ -198,6 +346,8 @@ var TRACKING_PARAM_KEYS = [
198
346
  function createEmptyTrackingParams() {
199
347
  return {
200
348
  gclid: null,
349
+ wbraid: null,
350
+ gbraid: null,
201
351
  fbclid: null,
202
352
  utm_source: null,
203
353
  utm_medium: null,
@@ -529,6 +679,8 @@ function buildLandingPayloadFields(sessionId, override) {
529
679
  if (params === null) return {};
530
680
  return {
531
681
  landing_gclid: params.gclid ?? null,
682
+ landing_wbraid: params.wbraid ?? null,
683
+ landing_gbraid: params.gbraid ?? null,
532
684
  landing_fbclid: params.fbclid ?? null,
533
685
  landing_utm_source: params.utm_source ?? null,
534
686
  landing_utm_medium: params.utm_medium ?? null,
@@ -556,6 +708,8 @@ function createTrackingSessionUpsertPayload(trackingParams, input, context) {
556
708
  session_id: input.sessionId,
557
709
  visitor_id: input.visitorId ?? null,
558
710
  gclid: trackingParams.gclid,
711
+ wbraid: trackingParams.wbraid,
712
+ gbraid: trackingParams.gbraid,
559
713
  fbclid: trackingParams.fbclid,
560
714
  fbc: getFbcCookie(),
561
715
  fbp: getFbpCookie(),
@@ -577,6 +731,8 @@ function createTrackingEventCreatePayload(trackingParams, input, context) {
577
731
  session_id: input.sessionId,
578
732
  event_type: input.eventType,
579
733
  gclid: trackingParams.gclid,
734
+ wbraid: trackingParams.wbraid,
735
+ gbraid: trackingParams.gbraid,
580
736
  fbclid: trackingParams.fbclid,
581
737
  fbc: getFbcCookie(),
582
738
  fbp: getFbpCookie(),
@@ -606,13 +762,18 @@ function markFired(input) {
606
762
  } catch {
607
763
  }
608
764
  }
609
- function fireOnce(input) {
610
- if (alreadyFired(input)) return;
611
- if (fireGtagConversion(input)) markFired(input);
612
- }
613
- function fireConversionWithConsent(input) {
614
- if (getConsentState() === "denied") return;
615
- fireOnce(input);
765
+ function fireConversionWithConsent(input, options) {
766
+ if (getConsentState() === "denied") return "denied";
767
+ if (alreadyFired(input)) return "duplicate";
768
+ if (!isValidSendTo(input.sendTo)) return "invalid";
769
+ try {
770
+ applyUserDataForConversion(options?.userData);
771
+ if (!fireGtagConversion(input)) return "retryable";
772
+ markFired(input);
773
+ return "fired";
774
+ } catch {
775
+ return "retryable";
776
+ }
616
777
  }
617
778
 
618
779
  // ../tracking-core/src/resources/conversion-config.ts
@@ -786,6 +947,103 @@ function resolveConversionConfig(options) {
786
947
  };
787
948
  }
788
949
 
950
+ // ../tracking-core/src/resources/automatic-transaction.ts
951
+ var STORAGE_KEY = "_aranova_auto_txn_map";
952
+ var transactionIds = /* @__PURE__ */ new Map();
953
+ var legacyCounter = 0;
954
+ function scopeKey(sessionId, goalKey, path) {
955
+ return JSON.stringify([sessionId, goalKey, path]);
956
+ }
957
+ function randomId() {
958
+ const cryptoApi = globalThis.crypto;
959
+ if (typeof cryptoApi?.randomUUID === "function") return cryptoApi.randomUUID();
960
+ if (typeof cryptoApi?.getRandomValues === "function") {
961
+ const bytes = cryptoApi.getRandomValues(new Uint8Array(16));
962
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
963
+ }
964
+ legacyCounter = (legacyCounter + 1) % 4294967296;
965
+ const timestamp = Date.now().toString(16).padStart(12, "0");
966
+ const counter = legacyCounter.toString(16).padStart(8, "0");
967
+ const random = Math.floor(Math.random() * 281474976710655).toString(16).padStart(12, "0");
968
+ return `${timestamp}${counter}${random}`.slice(0, 32);
969
+ }
970
+ function isValidTransactionId(value) {
971
+ 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(
972
+ value
973
+ ) && value.length <= 64;
974
+ }
975
+ function readStoredMap(sessionId) {
976
+ if (typeof window === "undefined") return { sessionId, entries: {} };
977
+ try {
978
+ const raw = window.localStorage.getItem(STORAGE_KEY);
979
+ if (!raw) return { sessionId, entries: {} };
980
+ const parsed = JSON.parse(raw);
981
+ if (parsed.sessionId !== sessionId || !parsed.entries || typeof parsed.entries !== "object" || Array.isArray(parsed.entries)) {
982
+ return { sessionId, entries: {} };
983
+ }
984
+ return { sessionId, entries: parsed.entries };
985
+ } catch {
986
+ return { sessionId, entries: {} };
987
+ }
988
+ }
989
+ function writeStoredMap(stored) {
990
+ if (typeof window === "undefined") return;
991
+ try {
992
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
993
+ } catch {
994
+ }
995
+ }
996
+ function getAutomaticTransactionId(sessionId, goalKey, path) {
997
+ const key = scopeKey(sessionId, goalKey, path);
998
+ const existing = transactionIds.get(key);
999
+ if (existing) return existing;
1000
+ const stored = readStoredMap(sessionId);
1001
+ const storedId = stored.entries[key];
1002
+ if (isValidTransactionId(storedId)) {
1003
+ transactionIds.set(key, storedId);
1004
+ return storedId;
1005
+ }
1006
+ const transactionId = `auto:${randomId()}`;
1007
+ transactionIds.set(key, transactionId);
1008
+ const latest = readStoredMap(sessionId);
1009
+ const concurrentId = latest.entries[key];
1010
+ if (isValidTransactionId(concurrentId)) {
1011
+ transactionIds.set(key, concurrentId);
1012
+ return concurrentId;
1013
+ }
1014
+ latest.entries[key] = transactionId;
1015
+ writeStoredMap(latest);
1016
+ return transactionId;
1017
+ }
1018
+
1019
+ // ../tracking-core/src/resources/automatic-trigger.ts
1020
+ function hasNumberField(metadata, key) {
1021
+ return typeof metadata[key] === "number";
1022
+ }
1023
+ function hasStringField(metadata, key) {
1024
+ return typeof metadata[key] === "string";
1025
+ }
1026
+ function automaticThresholdMet(goal, eventType, metadata) {
1027
+ const trigger = goal.trigger;
1028
+ if (!trigger || trigger.event_type !== eventType) return false;
1029
+ switch (eventType) {
1030
+ case "scroll_depth":
1031
+ return hasNumberField(metadata, "depth_percent") && trigger.threshold_percent != null && metadata.depth_percent >= trigger.threshold_percent;
1032
+ case "time_on_site":
1033
+ return hasNumberField(metadata, "duration_ms") && trigger.threshold_seconds != null && metadata.duration_ms >= trigger.threshold_seconds * 1e3;
1034
+ case "multi_page_session":
1035
+ return hasNumberField(metadata, "page_count") && trigger.page_threshold != null && metadata.page_count >= trigger.page_threshold;
1036
+ case "specific_page_visit":
1037
+ return hasStringField(metadata, "page_name") && metadata.page_name === trigger.page_name;
1038
+ case "page_view":
1039
+ case "form_start":
1040
+ case "phone_click":
1041
+ return true;
1042
+ default:
1043
+ return false;
1044
+ }
1045
+ }
1046
+
789
1047
  // ../tracking-core/src/resources/sales/money.ts
790
1048
  var MINOR_UNIT_EXPONENT = {
791
1049
  USD: 2,
@@ -904,7 +1162,12 @@ var TrackingConfigRuntime = class {
904
1162
  this.etag = null;
905
1163
  this.stateValue = "unconfirmed";
906
1164
  this.confirmedAt = 0;
1165
+ this.authorityGeneration = 0;
907
1166
  this.inFlight = null;
1167
+ this.flushInFlight = null;
1168
+ this.flushRequested = false;
1169
+ this.retryTimer = null;
1170
+ this.started = false;
908
1171
  this.conversionQueue = [];
909
1172
  this.automaticQueue = [];
910
1173
  this.pageQueue = [];
@@ -912,6 +1175,20 @@ var TrackingConfigRuntime = class {
912
1175
  const cached = readCache2(ref.cdnUrl);
913
1176
  this.current = cached?.config ?? null;
914
1177
  this.etag = cached?.etag ?? null;
1178
+ this.start();
1179
+ }
1180
+ /**
1181
+ * Explicitly start authority resolution and Google-tag bootstrap.
1182
+ *
1183
+ * Idempotent so framework effects can call it after hydration without
1184
+ * depending on constructor timing.
1185
+ */
1186
+ start() {
1187
+ if (this.started) {
1188
+ void this.flush();
1189
+ return;
1190
+ }
1191
+ this.started = true;
915
1192
  void this.revalidate();
916
1193
  if (typeof window !== "undefined") {
917
1194
  window.addEventListener("visibilitychange", () => {
@@ -927,7 +1204,7 @@ var TrackingConfigRuntime = class {
927
1204
  return this.stateValue === "active" || this.stateValue === "tombstone" ? this.current : null;
928
1205
  }
929
1206
  __unsafeExpireAuthorityForTests() {
930
- this.confirmedAt = 0;
1207
+ this.confirmedAt = Number.NEGATIVE_INFINITY;
931
1208
  }
932
1209
  subscribe(listener) {
933
1210
  this.listeners.add(listener);
@@ -937,10 +1214,14 @@ var TrackingConfigRuntime = class {
937
1214
  if (this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS) {
938
1215
  return true;
939
1216
  }
940
- await this.revalidate();
1217
+ await this.revalidateAuthority();
941
1218
  return this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS;
942
1219
  }
943
1220
  async revalidate() {
1221
+ await this.revalidateAuthority();
1222
+ await this.flush();
1223
+ }
1224
+ async revalidateAuthority() {
944
1225
  if (typeof window === "undefined" || !this.fetchImpl) return;
945
1226
  if (this.inFlight) return this.inFlight;
946
1227
  this.inFlight = this.revalidateNow().finally(() => {
@@ -957,8 +1238,8 @@ var TrackingConfigRuntime = class {
957
1238
  this.conversionQueue.push({ key, ...options });
958
1239
  void this.flush();
959
1240
  }
960
- queueAutomaticEvent(eventType, metadata, transactionPath) {
961
- this.automaticQueue.push({ eventType, metadata, transactionPath });
1241
+ queueAutomaticEvent(eventType, metadata, transactionPath, transactionScope) {
1242
+ this.automaticQueue.push({ eventType, metadata, transactionPath, transactionScope });
962
1243
  void this.flush();
963
1244
  }
964
1245
  listGoals() {
@@ -975,7 +1256,6 @@ var TrackingConfigRuntime = class {
975
1256
  });
976
1257
  if (response.status === 304 && this.current && validateConfig(this.current, this.ref)) {
977
1258
  this.confirm(this.current, this.etag);
978
- await this.flush();
979
1259
  return;
980
1260
  }
981
1261
  if (!response.ok) {
@@ -992,21 +1272,26 @@ var TrackingConfigRuntime = class {
992
1272
  return;
993
1273
  }
994
1274
  this.confirm(next, response.headers.get("ETag"));
995
- await this.flush();
996
1275
  } catch {
997
1276
  this.expireAuthority();
998
1277
  }
999
1278
  }
1000
1279
  expireAuthority() {
1280
+ this.authorityGeneration += 1;
1001
1281
  if (this.stateValue !== "unconfirmed") this.stateValue = "unconfirmed";
1002
1282
  }
1003
1283
  confirm(config, etag) {
1284
+ this.authorityGeneration += 1;
1004
1285
  this.current = config;
1005
1286
  this.etag = etag;
1006
1287
  this.confirmedAt = Date.now();
1007
1288
  this.stateValue = isTombstone(config) ? "tombstone" : "active";
1008
1289
  writeCache2(this.ref.cdnUrl, { etag, config });
1009
1290
  if (this.stateValue === "tombstone") {
1291
+ if (this.retryTimer !== null) {
1292
+ window.clearTimeout(this.retryTimer);
1293
+ this.retryTimer = null;
1294
+ }
1010
1295
  this.conversionQueue.length = 0;
1011
1296
  this.automaticQueue.length = 0;
1012
1297
  this.pageQueue.length = 0;
@@ -1014,79 +1299,107 @@ var TrackingConfigRuntime = class {
1014
1299
  for (const listener of this.listeners) listener();
1015
1300
  }
1016
1301
  async flush() {
1302
+ if (this.flushInFlight) {
1303
+ this.flushRequested = true;
1304
+ return this.flushInFlight;
1305
+ }
1306
+ this.flushRequested = false;
1307
+ this.flushInFlight = this.flushNow().finally(() => {
1308
+ this.flushInFlight = null;
1309
+ if (this.flushRequested && this.stateValue === "active") void this.flush();
1310
+ });
1311
+ return this.flushInFlight;
1312
+ }
1313
+ scheduleRetry() {
1314
+ if (this.retryTimer !== null || typeof window === "undefined") return;
1315
+ this.retryTimer = window.setTimeout(() => {
1316
+ this.retryTimer = null;
1317
+ void this.flush();
1318
+ }, 1e3);
1319
+ }
1320
+ async flushNow() {
1017
1321
  if (!await this.ensureAuthority()) return;
1018
1322
  if (this.stateValue !== "active" || !this.current) return;
1019
- const ids = Object.values(this.current.gtag_ids).filter(
1323
+ const config = this.current;
1324
+ const generation = this.authorityGeneration;
1325
+ const ids = Object.values(config.gtag_ids).filter(
1020
1326
  (id) => typeof id === "string" && isValidGtagId(id)
1021
1327
  );
1022
1328
  if (ids.length === 0) return;
1023
1329
  await ensureScript(ids[0]);
1330
+ if (this.authorityGeneration !== generation || this.stateValue !== "active" || this.current !== config) {
1331
+ return;
1332
+ }
1024
1333
  const gtag = window.gtag;
1025
1334
  if (typeof gtag !== "function") return;
1026
- if (!jsInitialized) {
1027
- gtag("js", /* @__PURE__ */ new Date());
1028
- jsInitialized = true;
1029
- }
1030
- for (const id of ids) {
1031
- if (configuredIds.has(id)) continue;
1032
- gtag("config", id, { send_page_view: false });
1033
- configuredIds.add(id);
1034
- }
1035
- while (this.pageQueue.length) {
1036
- const page = this.pageQueue.shift();
1037
- gtag("event", "page_view", {
1038
- page_location: page.href,
1039
- page_title: page.title ?? void 0,
1040
- page_referrer: page.referrer ?? void 0
1041
- });
1335
+ try {
1336
+ if (!jsInitialized) {
1337
+ gtag("js", /* @__PURE__ */ new Date());
1338
+ jsInitialized = true;
1339
+ }
1340
+ for (const id of ids) {
1341
+ if (configuredIds.has(id)) continue;
1342
+ gtag("config", id, { send_page_view: false });
1343
+ configuredIds.add(id);
1344
+ }
1345
+ } catch {
1346
+ this.scheduleRetry();
1347
+ return;
1348
+ }
1349
+ while (this.pageQueue.length > 0) {
1350
+ const page = this.pageQueue[0];
1351
+ try {
1352
+ gtag("event", "page_view", {
1353
+ page_location: page.href,
1354
+ page_title: page.title ?? void 0,
1355
+ page_referrer: page.referrer ?? void 0
1356
+ });
1357
+ this.pageQueue.shift();
1358
+ } catch {
1359
+ this.scheduleRetry();
1360
+ return;
1361
+ }
1042
1362
  }
1043
- while (this.automaticQueue.length) {
1044
- const event = this.automaticQueue.shift();
1045
- for (const goal of this.current.goals) {
1363
+ while (this.automaticQueue.length > 0) {
1364
+ const event = this.automaticQueue[0];
1365
+ for (const goal of config.goals) {
1046
1366
  if (goal.kind !== "event" || !goal.firing) continue;
1047
1367
  if (!automaticThresholdMet(goal, event.eventType, event.metadata)) continue;
1048
1368
  this.conversionQueue.push({
1049
1369
  key: goal.key,
1050
- transactionId: `auto:${goal.key}:${event.transactionPath}`
1370
+ transactionId: getAutomaticTransactionId(
1371
+ event.transactionScope,
1372
+ goal.key,
1373
+ event.transactionPath
1374
+ )
1051
1375
  });
1052
1376
  }
1377
+ this.automaticQueue.shift();
1053
1378
  }
1054
- while (this.conversionQueue.length) {
1055
- const item = this.conversionQueue.shift();
1056
- const goal = this.current.goals.find((g) => g.key === item.key);
1379
+ while (this.conversionQueue.length > 0) {
1380
+ const item = this.conversionQueue[0];
1381
+ const goal = config.goals.find((g) => g.key === item.key);
1057
1382
  const firing = goal?.firing;
1058
- if (!firing || getConsentState() === "denied") continue;
1383
+ if (!firing) {
1384
+ this.conversionQueue.shift();
1385
+ continue;
1386
+ }
1059
1387
  const currency = item.currency ?? firing.currency ?? null;
1060
1388
  const value = item.value ?? (firing.value_cents != null && currency ? fromMinor(firing.value_cents, currency) : null);
1061
- fireGtagConversion({
1389
+ const outcome = fireConversionWithConsent({
1062
1390
  sendTo: firing.send_to,
1063
1391
  value,
1064
1392
  currency,
1065
1393
  transactionId: item.transactionId ?? null
1066
1394
  });
1395
+ if (outcome === "retryable") {
1396
+ this.scheduleRetry();
1397
+ return;
1398
+ }
1399
+ this.conversionQueue.shift();
1067
1400
  }
1068
1401
  }
1069
1402
  };
1070
- function automaticThresholdMet(goal, eventType, metadata) {
1071
- const t = goal.trigger;
1072
- if (!t || t.event_type !== eventType) return false;
1073
- switch (eventType) {
1074
- case "scroll_depth":
1075
- return typeof metadata.depth_percent === "number" && t.threshold_percent != null && metadata.depth_percent >= t.threshold_percent;
1076
- case "time_on_site":
1077
- return typeof metadata.duration_ms === "number" && t.threshold_seconds != null && metadata.duration_ms >= t.threshold_seconds * 1e3;
1078
- case "multi_page_session":
1079
- return typeof metadata.page_count === "number" && t.page_threshold != null && metadata.page_count >= t.page_threshold;
1080
- case "specific_page_visit":
1081
- return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
1082
- case "page_view":
1083
- case "form_start":
1084
- case "phone_click":
1085
- return true;
1086
- default:
1087
- return false;
1088
- }
1089
- }
1090
1403
  function getTrackingConfigRuntime(ref, fetchImpl) {
1091
1404
  const key = `${ref.cdnUrl}|${ref.businessId}|${ref.environment}`;
1092
1405
  const existing = runtimes.get(key);
@@ -1097,26 +1410,6 @@ function getTrackingConfigRuntime(ref, fetchImpl) {
1097
1410
  }
1098
1411
 
1099
1412
  // ../tracking-core/src/resources/conversion-autofire.ts
1100
- function thresholdMet(goal, eventType, metadata) {
1101
- const t = goal.trigger;
1102
- if (!t || t.event_type !== eventType) return false;
1103
- switch (eventType) {
1104
- case "scroll_depth":
1105
- return typeof metadata.depth_percent === "number" && t.threshold_percent != null && metadata.depth_percent >= t.threshold_percent;
1106
- case "time_on_site":
1107
- return typeof metadata.duration_ms === "number" && t.threshold_seconds != null && metadata.duration_ms >= t.threshold_seconds * 1e3;
1108
- case "multi_page_session":
1109
- return typeof metadata.page_count === "number" && t.page_threshold != null && metadata.page_count >= t.page_threshold;
1110
- case "specific_page_visit":
1111
- return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
1112
- case "page_view":
1113
- case "form_start":
1114
- case "phone_click":
1115
- return true;
1116
- default:
1117
- return false;
1118
- }
1119
- }
1120
1413
  function currentPath() {
1121
1414
  return typeof window === "undefined" ? "" : window.location.pathname;
1122
1415
  }
@@ -1124,13 +1417,14 @@ var MAX_BUFFERED_EVENTS = 50;
1124
1417
  function createConversionAutoFire(store) {
1125
1418
  const pending = [];
1126
1419
  let subscribed = false;
1127
- function fireMatching(eventType, metadata) {
1420
+ function fireMatching(eventType, metadata, transactionScope) {
1128
1421
  for (const goal of store.listGoals()) {
1129
1422
  if (goal.kind !== "event" || !goal.firing) continue;
1130
- if (!thresholdMet(goal, eventType, metadata)) continue;
1423
+ if (!automaticThresholdMet(goal, eventType, metadata)) continue;
1424
+ const transactionId = getAutomaticTransactionId(transactionScope, goal.key, currentPath());
1131
1425
  if ("queueAutomaticEvent" in store) {
1132
1426
  store.fireConversion(goal.key, {
1133
- transactionId: `auto:${goal.key}:${currentPath()}`
1427
+ transactionId
1134
1428
  });
1135
1429
  continue;
1136
1430
  }
@@ -1141,28 +1435,30 @@ function createConversionAutoFire(store) {
1141
1435
  sendTo: firing.send_to,
1142
1436
  value: cents != null && currency ? fromMinor(cents, currency) : null,
1143
1437
  currency,
1144
- // Page-scoped txn id → fire once per (goal, path) per session; engagement conversions
1145
- // shouldn't re-fire as the visitor scrolls back and forth or re-enters a page.
1146
- transactionId: `auto:${goal.key}:${currentPath()}`
1438
+ transactionId
1147
1439
  });
1148
1440
  }
1149
1441
  }
1150
1442
  return {
1151
- onAutomaticEvent(eventType, metadata) {
1443
+ onAutomaticEvent(eventType, metadata, transactionScope) {
1152
1444
  if ("queueAutomaticEvent" in store) {
1153
- store.queueAutomaticEvent(eventType, metadata, currentPath());
1445
+ store.queueAutomaticEvent(eventType, metadata, currentPath(), transactionScope);
1154
1446
  return;
1155
1447
  }
1156
1448
  if (store.isReady()) {
1157
- fireMatching(eventType, metadata);
1449
+ fireMatching(eventType, metadata, transactionScope);
1158
1450
  return;
1159
1451
  }
1160
- if (pending.length < MAX_BUFFERED_EVENTS) pending.push({ eventType, metadata });
1452
+ if (pending.length < MAX_BUFFERED_EVENTS) {
1453
+ pending.push({ eventType, metadata, transactionScope });
1454
+ }
1161
1455
  if (!subscribed) {
1162
1456
  subscribed = true;
1163
1457
  store.onResolve(() => {
1164
1458
  const buffered = pending.splice(0);
1165
- for (const event of buffered) fireMatching(event.eventType, event.metadata);
1459
+ for (const event of buffered) {
1460
+ fireMatching(event.eventType, event.metadata, event.transactionScope);
1461
+ }
1166
1462
  });
1167
1463
  }
1168
1464
  }
@@ -1174,7 +1470,7 @@ function withConversionAutoFire(client, autoFire) {
1174
1470
  trackEvent: (input) => {
1175
1471
  client.trackEvent(input);
1176
1472
  try {
1177
- autoFire.onAutomaticEvent(input.eventType, input.metadata ?? {});
1473
+ autoFire.onAutomaticEvent(input.eventType, input.metadata ?? {}, client.getSessionId());
1178
1474
  } catch {
1179
1475
  }
1180
1476
  }
@@ -1589,6 +1885,8 @@ function createTrackingClient(config) {
1589
1885
  session_id: sessionId,
1590
1886
  visitor_id: visitorId,
1591
1887
  gclid: params.gclid,
1888
+ wbraid: params.wbraid,
1889
+ gbraid: params.gbraid,
1592
1890
  fbclid: params.fbclid,
1593
1891
  fbc: getFbcCookie(),
1594
1892
  fbp: getFbpCookie(),
@@ -1635,6 +1933,13 @@ function createTrackingClient(config) {
1635
1933
  function trackEvent(input) {
1636
1934
  if (destroyed) return;
1637
1935
  if (!input || typeof input.eventType !== "string" || input.eventType.length === 0) return;
1936
+ if (input.eventType === "form_submit" && getConsentState() !== "denied") {
1937
+ try {
1938
+ const fields = input.metadata?.form?.fields;
1939
+ if (fields) stashUserDataFromFormFields(fields, config.phone?.defaultCountry);
1940
+ } catch {
1941
+ }
1942
+ }
1638
1943
  const occurredAt = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : (/* @__PURE__ */ new Date()).toISOString();
1639
1944
  queue.push({
1640
1945
  event_type: input.eventType,
@@ -2298,7 +2603,7 @@ function attachScrollDepth(client, config) {
2298
2603
  }
2299
2604
 
2300
2605
  // ../tracking-core/src/triggers/multi-page-session.ts
2301
- var STORAGE_KEY = "aranova_tracking_mps_paths";
2606
+ var STORAGE_KEY2 = "aranova_tracking_mps_paths";
2302
2607
  var SESSION_KEY = "aranova_tracking_mps_session";
2303
2608
  var FIRED_KEY = "aranova_tracking_mps_fired";
2304
2609
  function getSessionStorage() {
@@ -2320,7 +2625,7 @@ function attachMultiPageSession(client, config) {
2320
2625
  let lastCheckedPath = "";
2321
2626
  function getDistinctPaths() {
2322
2627
  try {
2323
- const raw = storage.getItem(STORAGE_KEY);
2628
+ const raw = storage.getItem(STORAGE_KEY2);
2324
2629
  return raw ? new Set(JSON.parse(raw)) : /* @__PURE__ */ new Set();
2325
2630
  } catch {
2326
2631
  return /* @__PURE__ */ new Set();
@@ -2328,7 +2633,7 @@ function attachMultiPageSession(client, config) {
2328
2633
  }
2329
2634
  function saveDistinctPaths(paths) {
2330
2635
  try {
2331
- storage.setItem(STORAGE_KEY, JSON.stringify([...paths]));
2636
+ storage.setItem(STORAGE_KEY2, JSON.stringify([...paths]));
2332
2637
  } catch {
2333
2638
  }
2334
2639
  }
@@ -2337,7 +2642,7 @@ function attachMultiPageSession(client, config) {
2337
2642
  const storedSession = storage.getItem(SESSION_KEY);
2338
2643
  if (storedSession !== currentSession) {
2339
2644
  storage.setItem(SESSION_KEY, currentSession);
2340
- storage.removeItem(STORAGE_KEY);
2645
+ storage.removeItem(STORAGE_KEY2);
2341
2646
  storage.removeItem(FIRED_KEY);
2342
2647
  }
2343
2648
  }
@@ -2598,46 +2903,6 @@ function attachCtaClickCapture(client, config) {
2598
2903
  };
2599
2904
  }
2600
2905
 
2601
- // ../tracking-core/src/phone.ts
2602
- var import_libphonenumber_js = require("libphonenumber-js");
2603
- var DEFAULT_PHONE_COUNTRY = "CA";
2604
- function parsePhone(raw, country) {
2605
- const region = country ?? DEFAULT_PHONE_COUNTRY;
2606
- const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(raw ?? "", region);
2607
- if (!parsed) {
2608
- return { e164: null, national: "", international: "", country: region, isValid: false };
2609
- }
2610
- const isValid = parsed.isValid();
2611
- return {
2612
- // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
2613
- // (e.g. too few digits) still parses but must not be transmitted.
2614
- e164: isValid ? parsed.number : null,
2615
- national: parsed.formatNational(),
2616
- international: parsed.formatInternational(),
2617
- country: parsed.country ?? region,
2618
- isValid
2619
- };
2620
- }
2621
- function toE164(raw, country) {
2622
- return parsePhone(raw, country).e164;
2623
- }
2624
- function formatPhone(value, format = "national", country) {
2625
- const parsed = parsePhone(value, country);
2626
- if (typeof format === "function") return format(parsed);
2627
- switch (format) {
2628
- case "international":
2629
- return parsed.international || value;
2630
- case "e164":
2631
- return parsed.e164 ?? value;
2632
- case "national":
2633
- default:
2634
- return parsed.national || value;
2635
- }
2636
- }
2637
- function formatPhoneAsTyped(raw, country) {
2638
- return new import_libphonenumber_js.AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
2639
- }
2640
-
2641
2906
  // ../tracking-core/src/triggers/phone-click-capture.ts
2642
2907
  var DEFAULT_TEL_SELECTOR = 'a[href^="tel:"]';
2643
2908
  function safeDecodeURIComponent(value) {
@@ -2747,6 +3012,8 @@ async function salesRequest(config, method, path, body) {
2747
3012
  // ../tracking-core/src/resources/sales/client.ts
2748
3013
  function fireRecordedConversions(firing, input, recorded, sale, currency) {
2749
3014
  if (!firing) return;
3015
+ const userData = input.customer_email || input.customer_phone ? { email: input.customer_email ?? null, phone: input.customer_phone ?? null } : null;
3016
+ if (userData && getConsentState() !== "denied") stashUserData(userData);
2750
3017
  const txnBase = input.external_id ?? sale.id;
2751
3018
  for (const item of recorded) {
2752
3019
  if (!item.service) continue;
@@ -2761,12 +3028,15 @@ function fireRecordedConversions(firing, input, recorded, sale, currency) {
2761
3028
  const config = firing.getFiring(item.service);
2762
3029
  if (!config) continue;
2763
3030
  const cents = item.amount_cents ?? config.value_cents ?? null;
2764
- fireConversionWithConsent({
2765
- sendTo: config.send_to,
2766
- value: cents != null ? fromMinor(cents, currency) : null,
2767
- currency: config.currency ?? currency,
2768
- transactionId: `${txnBase}:${item.service}`
2769
- });
3031
+ fireConversionWithConsent(
3032
+ {
3033
+ sendTo: config.send_to,
3034
+ value: cents != null ? fromMinor(cents, currency) : null,
3035
+ currency: config.currency ?? currency,
3036
+ transactionId: `${txnBase}:${item.service}`
3037
+ },
3038
+ { userData }
3039
+ );
2770
3040
  }
2771
3041
  }
2772
3042
  function createSalesClient(config) {
@@ -2801,6 +3071,7 @@ function createSalesClient(config) {
2801
3071
  recordSale: record,
2802
3072
  trackConversion(key, options) {
2803
3073
  if (config.firing && "fireConversion" in config.firing) {
3074
+ if (options?.userData && getConsentState() !== "denied") stashUserData(options.userData);
2804
3075
  config.firing.fireConversion(key, {
2805
3076
  value: options?.value ?? void 0,
2806
3077
  currency: options?.currency ?? config.defaultCurrency ?? void 0,
@@ -2813,12 +3084,15 @@ function createSalesClient(config) {
2813
3084
  const currency = firing.currency ?? options?.currency ?? config.defaultCurrency ?? null;
2814
3085
  const cents = firing.value_cents ?? null;
2815
3086
  const value = options?.value ?? (cents != null && currency ? fromMinor(cents, currency) : null);
2816
- fireConversionWithConsent({
2817
- sendTo: firing.send_to,
2818
- value,
2819
- currency,
2820
- transactionId: options?.transactionId ?? null
2821
- });
3087
+ fireConversionWithConsent(
3088
+ {
3089
+ sendTo: firing.send_to,
3090
+ value,
3091
+ currency,
3092
+ transactionId: options?.transactionId ?? null
3093
+ },
3094
+ { userData: options?.userData }
3095
+ );
2822
3096
  },
2823
3097
  async list(query) {
2824
3098
  const { cursor, limit, sort, order, want_total, ...filters } = query ?? {};
@@ -3363,7 +3637,7 @@ function GoogleAdsTracking(props) {
3363
3637
  var import_react6 = require("react");
3364
3638
 
3365
3639
  // package.json
3366
- var version = "0.18.1";
3640
+ var version = "0.19.1";
3367
3641
 
3368
3642
  // ../tracking-core/src/phone-react.tsx
3369
3643
  var import_react5 = require("react");
@@ -3494,31 +3768,46 @@ function createTracking(options) {
3494
3768
  metaPixelIds,
3495
3769
  children
3496
3770
  }) {
3497
- const resolvedGtagIds = gtagIds ? Object.fromEntries(
3498
- Object.entries(gtagIds).filter((e) => e[1] != null)
3499
- ) : gtagId ? { default: gtagId } : void 0;
3500
- const client = (0, import_react6.useMemo)(
3501
- () => createTypedClient(
3502
- getOrCreateTrackingClient({
3503
- apiKey,
3504
- endpoint,
3505
- surface: "react",
3506
- packageName: "@aranova/tracking-react",
3507
- sdkVersion: version,
3508
- triggers,
3509
- environment,
3510
- activeGtagIds: resolvedGtagIds,
3511
- debug
3512
- }),
3771
+ const gtagIdsKey = (0, import_react6.useMemo)(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
3772
+ const resolvedGtagIds = (0, import_react6.useMemo)(
3773
+ () => gtagIds ? Object.fromEntries(
3774
+ Object.entries(gtagIds).filter((e) => e[1] != null)
3775
+ ) : gtagId ? { default: gtagId } : void 0,
3776
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- gtagIdsKey is stable proxy
3777
+ [gtagId, gtagIdsKey]
3778
+ );
3779
+ const rawClient = (0, import_react6.useMemo)(
3780
+ () => getOrCreateTrackingClient({
3781
+ apiKey,
3782
+ endpoint,
3783
+ surface: "react",
3784
+ packageName: "@aranova/tracking-react",
3785
+ sdkVersion: version,
3513
3786
  triggers,
3514
- { debug }
3515
- ),
3787
+ environment,
3788
+ activeGtagIds: resolvedGtagIds,
3789
+ debug
3790
+ }),
3791
+ [resolvedGtagIds]
3792
+ );
3793
+ const conversionStore = (0, import_react6.useMemo)(
3794
+ () => trackingConfig ? getTrackingConfigRuntime(trackingConfig) : conversionConfig ? resolveConversionConfig({
3795
+ cdnUrl: conversionConfig.cdnUrl,
3796
+ baked: conversionConfig.baked
3797
+ }) : null,
3516
3798
  []
3517
3799
  );
3518
- const gtagIdsKey = (0, import_react6.useMemo)(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
3800
+ const conversionClient = (0, import_react6.useMemo)(
3801
+ () => conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient,
3802
+ [rawClient, conversionStore]
3803
+ );
3804
+ const client = (0, import_react6.useMemo)(
3805
+ () => createTypedClient(conversionClient, triggers, { debug }),
3806
+ [conversionClient]
3807
+ );
3519
3808
  (0, import_react6.useEffect)(() => {
3520
3809
  if (trackingConfig) {
3521
- void getTrackingConfigRuntime(trackingConfig).revalidate();
3810
+ getTrackingConfigRuntime(trackingConfig).start();
3522
3811
  } else if (gtagIds && Object.keys(gtagIds).length > 0) {
3523
3812
  bootstrapMultipleGtags(gtagIds);
3524
3813
  } else if (gtagId) {
@@ -3537,23 +3826,9 @@ function createTracking(options) {
3537
3826
  }
3538
3827
  }, [metaPixelId, metaPixelIdsKey]);
3539
3828
  (0, import_react6.useEffect)(() => {
3540
- const rawClient = getOrCreateTrackingClient({
3541
- apiKey,
3542
- endpoint,
3543
- surface: "react",
3544
- packageName: "@aranova/tracking-react",
3545
- triggers,
3546
- environment,
3547
- activeGtagIds: resolvedGtagIds,
3548
- debug
3549
- });
3550
3829
  return attachClientCapturesOnce(rawClient, () => {
3551
3830
  const detachers = [];
3552
- const conversionStore = trackingConfig ? getTrackingConfigRuntime(trackingConfig) : conversionConfig ? resolveConversionConfig({
3553
- cdnUrl: conversionConfig.cdnUrl,
3554
- baked: conversionConfig.baked
3555
- }) : null;
3556
- const detectorClient = conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient;
3831
+ const detectorClient = conversionStore ? conversionClient : rawClient;
3557
3832
  const pageClient = trackingConfig && conversionStore && "queuePageView" in conversionStore ? {
3558
3833
  ...detectorClient,
3559
3834
  trackEvent: (input) => {
@@ -3598,7 +3873,7 @@ function createTracking(options) {
3598
3873
  }
3599
3874
  };
3600
3875
  });
3601
- }, []);
3876
+ }, [conversionClient, conversionStore, rawClient]);
3602
3877
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TrackingContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PhoneConfigProvider, { value: phone ?? null, children }) });
3603
3878
  }
3604
3879
  function useTracking() {
@@ -3627,6 +3902,7 @@ function createTracking(options) {
3627
3902
  TRACKING_PARAM_KEYS,
3628
3903
  TRACKING_RANGES,
3629
3904
  captureTrackingParamsFromLocation,
3905
+ clearStashedUserData,
3630
3906
  createSalesClient,
3631
3907
  createTracking,
3632
3908
  createTrackingClientContext,
@@ -3655,6 +3931,7 @@ function createTracking(options) {
3655
3931
  saleUpdateSchema,
3656
3932
  salesRequest,
3657
3933
  setConsentState,
3934
+ stashUserData,
3658
3935
  toE164,
3659
3936
  toMinor,
3660
3937
  useConsent,