@aranova/tracking-next 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
@@ -43,6 +43,7 @@ __export(src_exports, {
43
43
  TRACKING_PARAM_KEYS: () => TRACKING_PARAM_KEYS,
44
44
  TRACKING_RANGES: () => TRACKING_RANGES,
45
45
  captureTrackingParamsFromLocation: () => captureTrackingParamsFromLocation,
46
+ clearStashedUserData: () => clearStashedUserData,
46
47
  createSalesClient: () => createSalesClient,
47
48
  createTracking: () => createTracking,
48
49
  createTrackingClientContext: () => createTrackingClientContext,
@@ -71,6 +72,7 @@ __export(src_exports, {
71
72
  saleUpdateSchema: () => saleUpdateSchema,
72
73
  salesRequest: () => salesRequest,
73
74
  setConsentState: () => setConsentState,
75
+ stashUserData: () => stashUserData,
74
76
  toE164: () => toE164,
75
77
  toMinor: () => toMinor,
76
78
  useConsent: () => useConsent,
@@ -83,6 +85,140 @@ __export(src_exports, {
83
85
  });
84
86
  module.exports = __toCommonJS(src_exports);
85
87
 
88
+ // ../tracking-core/src/phone.ts
89
+ var import_libphonenumber_js = require("libphonenumber-js");
90
+ var DEFAULT_PHONE_COUNTRY = "CA";
91
+ function parsePhone(raw, country) {
92
+ const region = country ?? DEFAULT_PHONE_COUNTRY;
93
+ const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(raw ?? "", region);
94
+ if (!parsed) {
95
+ return { e164: null, national: "", international: "", country: region, isValid: false };
96
+ }
97
+ const isValid = parsed.isValid();
98
+ return {
99
+ // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
100
+ // (e.g. too few digits) still parses but must not be transmitted.
101
+ e164: isValid ? parsed.number : null,
102
+ national: parsed.formatNational(),
103
+ international: parsed.formatInternational(),
104
+ country: parsed.country ?? region,
105
+ isValid
106
+ };
107
+ }
108
+ function toE164(raw, country) {
109
+ return parsePhone(raw, country).e164;
110
+ }
111
+ function formatPhone(value, format = "national", country) {
112
+ const parsed = parsePhone(value, country);
113
+ if (typeof format === "function") return format(parsed);
114
+ switch (format) {
115
+ case "international":
116
+ return parsed.international || value;
117
+ case "e164":
118
+ return parsed.e164 ?? value;
119
+ case "national":
120
+ default:
121
+ return parsed.national || value;
122
+ }
123
+ }
124
+ function formatPhoneAsTyped(raw, country) {
125
+ return new import_libphonenumber_js.AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
126
+ }
127
+
128
+ // ../tracking-core/src/user-data.ts
129
+ var EMAIL_SHAPE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
130
+ var EMAIL_NAME_HINT = /e[-_]?mail/i;
131
+ var PHONE_NAME_HINT = /(^|[^a-z])(phone|tel|mobile|cell)/i;
132
+ var stash = { email: null, phoneNumber: null };
133
+ function normalizeEmail(raw) {
134
+ if (typeof raw !== "string") return null;
135
+ const cleaned = raw.trim().toLowerCase();
136
+ return EMAIL_SHAPE.test(cleaned) ? cleaned : null;
137
+ }
138
+ function fieldText(field, key) {
139
+ const value = field[key];
140
+ return typeof value === "string" ? value : "";
141
+ }
142
+ function extractUserDataFromFormFields(fields, country) {
143
+ const result = { email: null, phoneNumber: null };
144
+ if (!Array.isArray(fields)) return result;
145
+ const passes = [
146
+ (field, _hint, type) => fieldText(field, "type").toLowerCase() === type,
147
+ (field, hint) => hint.test(fieldText(field, "name")) || hint.test(fieldText(field, "label"))
148
+ ];
149
+ for (const matches of passes) {
150
+ for (const raw of fields) {
151
+ if (raw === null || typeof raw !== "object") continue;
152
+ const field = raw;
153
+ if (typeof field.value !== "string" || field.value.length === 0) continue;
154
+ if (result.email === null && matches(field, EMAIL_NAME_HINT, "email")) {
155
+ result.email = normalizeEmail(field.value);
156
+ }
157
+ if (result.phoneNumber === null && matches(field, PHONE_NAME_HINT, "tel")) {
158
+ try {
159
+ result.phoneNumber = toE164(field.value, country);
160
+ } catch {
161
+ }
162
+ }
163
+ }
164
+ if (result.email !== null && result.phoneNumber !== null) break;
165
+ }
166
+ return result;
167
+ }
168
+ function stashUserData(data, country) {
169
+ const email = normalizeEmail(data.email);
170
+ let phoneNumber = null;
171
+ if (typeof data.phone === "string" && data.phone.length > 0) {
172
+ try {
173
+ phoneNumber = toE164(data.phone, country);
174
+ } catch {
175
+ phoneNumber = null;
176
+ }
177
+ }
178
+ stash = {
179
+ email: email ?? stash.email,
180
+ phoneNumber: phoneNumber ?? stash.phoneNumber
181
+ };
182
+ }
183
+ function stashUserDataFromFormFields(fields, country) {
184
+ try {
185
+ const extracted = extractUserDataFromFormFields(fields, country);
186
+ stash = {
187
+ email: extracted.email ?? stash.email,
188
+ phoneNumber: extracted.phoneNumber ?? stash.phoneNumber
189
+ };
190
+ } catch {
191
+ }
192
+ }
193
+ function clearStashedUserData() {
194
+ stash = { email: null, phoneNumber: null };
195
+ }
196
+ function applyUserDataForConversion(explicit, country) {
197
+ if (typeof window === "undefined" || typeof window.gtag !== "function") return false;
198
+ let email = stash.email;
199
+ let phoneNumber = stash.phoneNumber;
200
+ if (explicit) {
201
+ const normalizedEmail = normalizeEmail(explicit.email);
202
+ if (normalizedEmail) email = normalizedEmail;
203
+ if (typeof explicit.phone === "string" && explicit.phone.length > 0) {
204
+ try {
205
+ phoneNumber = toE164(explicit.phone, country) ?? phoneNumber;
206
+ } catch {
207
+ }
208
+ }
209
+ }
210
+ if (email === null && phoneNumber === null) return false;
211
+ try {
212
+ window.gtag("set", "user_data", {
213
+ ...email !== null ? { email } : {},
214
+ ...phoneNumber !== null ? { phone_number: phoneNumber } : {}
215
+ });
216
+ return true;
217
+ } catch {
218
+ return false;
219
+ }
220
+ }
221
+
86
222
  // ../tracking-core/src/consent.ts
87
223
  var CONSENT_STATE_KEY = "consent_state";
88
224
  var CONSENT_TIMESTAMP_KEY = "consent_timestamp";
@@ -168,6 +304,13 @@ function setConsentState(state, options) {
168
304
  } catch {
169
305
  }
170
306
  pushConsentToPlatforms(state);
307
+ if (state === "denied") {
308
+ clearStashedUserData();
309
+ try {
310
+ if (typeof window.gtag === "function") window.gtag("set", "user_data", null);
311
+ } catch {
312
+ }
313
+ }
171
314
  notifyConsentChanged({ state, source: "explicit", updatedAt, expiresAt });
172
315
  }
173
316
  function optIn() {
@@ -206,6 +349,11 @@ var aranovaConsent = (function () {
206
349
  var TRACKING_COOKIE_MAX_AGE_SECONDS = 7776e3;
207
350
  var TRACKING_PARAM_KEYS = [
208
351
  "gclid",
352
+ // Google's iOS/Safari replacement click IDs — issued when privacy features
353
+ // withhold gclid (wbraid: web-to-web, gbraid: app-to-web). First-class
354
+ // citizens: captured, persisted, and attributed exactly like gclid.
355
+ "wbraid",
356
+ "gbraid",
209
357
  "fbclid",
210
358
  "utm_source",
211
359
  "utm_medium",
@@ -216,6 +364,8 @@ var TRACKING_PARAM_KEYS = [
216
364
  function createEmptyTrackingParams() {
217
365
  return {
218
366
  gclid: null,
367
+ wbraid: null,
368
+ gbraid: null,
219
369
  fbclid: null,
220
370
  utm_source: null,
221
371
  utm_medium: null,
@@ -518,6 +668,8 @@ function buildLandingPayloadFields(sessionId, override) {
518
668
  if (params === null) return {};
519
669
  return {
520
670
  landing_gclid: params.gclid ?? null,
671
+ landing_wbraid: params.wbraid ?? null,
672
+ landing_gbraid: params.gbraid ?? null,
521
673
  landing_fbclid: params.fbclid ?? null,
522
674
  landing_utm_source: params.utm_source ?? null,
523
675
  landing_utm_medium: params.utm_medium ?? null,
@@ -545,6 +697,8 @@ function createTrackingSessionUpsertPayload(trackingParams, input, context) {
545
697
  session_id: input.sessionId,
546
698
  visitor_id: input.visitorId ?? null,
547
699
  gclid: trackingParams.gclid,
700
+ wbraid: trackingParams.wbraid,
701
+ gbraid: trackingParams.gbraid,
548
702
  fbclid: trackingParams.fbclid,
549
703
  fbc: getFbcCookie(),
550
704
  fbp: getFbpCookie(),
@@ -566,6 +720,8 @@ function createTrackingEventCreatePayload(trackingParams, input, context) {
566
720
  session_id: input.sessionId,
567
721
  event_type: input.eventType,
568
722
  gclid: trackingParams.gclid,
723
+ wbraid: trackingParams.wbraid,
724
+ gbraid: trackingParams.gbraid,
569
725
  fbclid: trackingParams.fbclid,
570
726
  fbc: getFbcCookie(),
571
727
  fbp: getFbpCookie(),
@@ -595,13 +751,18 @@ function markFired(input) {
595
751
  } catch {
596
752
  }
597
753
  }
598
- function fireOnce(input) {
599
- if (alreadyFired(input)) return;
600
- if (fireGtagConversion(input)) markFired(input);
601
- }
602
- function fireConversionWithConsent(input) {
603
- if (getConsentState() === "denied") return;
604
- fireOnce(input);
754
+ function fireConversionWithConsent(input, options) {
755
+ if (getConsentState() === "denied") return "denied";
756
+ if (alreadyFired(input)) return "duplicate";
757
+ if (!isValidSendTo(input.sendTo)) return "invalid";
758
+ try {
759
+ applyUserDataForConversion(options?.userData);
760
+ if (!fireGtagConversion(input)) return "retryable";
761
+ markFired(input);
762
+ return "fired";
763
+ } catch {
764
+ return "retryable";
765
+ }
605
766
  }
606
767
 
607
768
  // ../tracking-core/src/resources/conversion-config.ts
@@ -775,6 +936,103 @@ function resolveConversionConfig(options) {
775
936
  };
776
937
  }
777
938
 
939
+ // ../tracking-core/src/resources/automatic-transaction.ts
940
+ var STORAGE_KEY = "_aranova_auto_txn_map";
941
+ var transactionIds = /* @__PURE__ */ new Map();
942
+ var legacyCounter = 0;
943
+ function scopeKey(sessionId, goalKey, path) {
944
+ return JSON.stringify([sessionId, goalKey, path]);
945
+ }
946
+ function randomId() {
947
+ const cryptoApi = globalThis.crypto;
948
+ if (typeof cryptoApi?.randomUUID === "function") return cryptoApi.randomUUID();
949
+ if (typeof cryptoApi?.getRandomValues === "function") {
950
+ const bytes = cryptoApi.getRandomValues(new Uint8Array(16));
951
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
952
+ }
953
+ legacyCounter = (legacyCounter + 1) % 4294967296;
954
+ const timestamp = Date.now().toString(16).padStart(12, "0");
955
+ const counter = legacyCounter.toString(16).padStart(8, "0");
956
+ const random = Math.floor(Math.random() * 281474976710655).toString(16).padStart(12, "0");
957
+ return `${timestamp}${counter}${random}`.slice(0, 32);
958
+ }
959
+ function isValidTransactionId(value) {
960
+ 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(
961
+ value
962
+ ) && value.length <= 64;
963
+ }
964
+ function readStoredMap(sessionId) {
965
+ if (typeof window === "undefined") return { sessionId, entries: {} };
966
+ try {
967
+ const raw = window.localStorage.getItem(STORAGE_KEY);
968
+ if (!raw) return { sessionId, entries: {} };
969
+ const parsed = JSON.parse(raw);
970
+ if (parsed.sessionId !== sessionId || !parsed.entries || typeof parsed.entries !== "object" || Array.isArray(parsed.entries)) {
971
+ return { sessionId, entries: {} };
972
+ }
973
+ return { sessionId, entries: parsed.entries };
974
+ } catch {
975
+ return { sessionId, entries: {} };
976
+ }
977
+ }
978
+ function writeStoredMap(stored) {
979
+ if (typeof window === "undefined") return;
980
+ try {
981
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
982
+ } catch {
983
+ }
984
+ }
985
+ function getAutomaticTransactionId(sessionId, goalKey, path) {
986
+ const key = scopeKey(sessionId, goalKey, path);
987
+ const existing = transactionIds.get(key);
988
+ if (existing) return existing;
989
+ const stored = readStoredMap(sessionId);
990
+ const storedId = stored.entries[key];
991
+ if (isValidTransactionId(storedId)) {
992
+ transactionIds.set(key, storedId);
993
+ return storedId;
994
+ }
995
+ const transactionId = `auto:${randomId()}`;
996
+ transactionIds.set(key, transactionId);
997
+ const latest = readStoredMap(sessionId);
998
+ const concurrentId = latest.entries[key];
999
+ if (isValidTransactionId(concurrentId)) {
1000
+ transactionIds.set(key, concurrentId);
1001
+ return concurrentId;
1002
+ }
1003
+ latest.entries[key] = transactionId;
1004
+ writeStoredMap(latest);
1005
+ return transactionId;
1006
+ }
1007
+
1008
+ // ../tracking-core/src/resources/automatic-trigger.ts
1009
+ function hasNumberField(metadata, key) {
1010
+ return typeof metadata[key] === "number";
1011
+ }
1012
+ function hasStringField(metadata, key) {
1013
+ return typeof metadata[key] === "string";
1014
+ }
1015
+ function automaticThresholdMet(goal, eventType, metadata) {
1016
+ const trigger = goal.trigger;
1017
+ if (!trigger || trigger.event_type !== eventType) return false;
1018
+ switch (eventType) {
1019
+ case "scroll_depth":
1020
+ return hasNumberField(metadata, "depth_percent") && trigger.threshold_percent != null && metadata.depth_percent >= trigger.threshold_percent;
1021
+ case "time_on_site":
1022
+ return hasNumberField(metadata, "duration_ms") && trigger.threshold_seconds != null && metadata.duration_ms >= trigger.threshold_seconds * 1e3;
1023
+ case "multi_page_session":
1024
+ return hasNumberField(metadata, "page_count") && trigger.page_threshold != null && metadata.page_count >= trigger.page_threshold;
1025
+ case "specific_page_visit":
1026
+ return hasStringField(metadata, "page_name") && metadata.page_name === trigger.page_name;
1027
+ case "page_view":
1028
+ case "form_start":
1029
+ case "phone_click":
1030
+ return true;
1031
+ default:
1032
+ return false;
1033
+ }
1034
+ }
1035
+
778
1036
  // ../tracking-core/src/resources/sales/money.ts
779
1037
  var MINOR_UNIT_EXPONENT = {
780
1038
  USD: 2,
@@ -893,7 +1151,12 @@ var TrackingConfigRuntime = class {
893
1151
  this.etag = null;
894
1152
  this.stateValue = "unconfirmed";
895
1153
  this.confirmedAt = 0;
1154
+ this.authorityGeneration = 0;
896
1155
  this.inFlight = null;
1156
+ this.flushInFlight = null;
1157
+ this.flushRequested = false;
1158
+ this.retryTimer = null;
1159
+ this.started = false;
897
1160
  this.conversionQueue = [];
898
1161
  this.automaticQueue = [];
899
1162
  this.pageQueue = [];
@@ -901,6 +1164,20 @@ var TrackingConfigRuntime = class {
901
1164
  const cached = readCache2(ref.cdnUrl);
902
1165
  this.current = cached?.config ?? null;
903
1166
  this.etag = cached?.etag ?? null;
1167
+ this.start();
1168
+ }
1169
+ /**
1170
+ * Explicitly start authority resolution and Google-tag bootstrap.
1171
+ *
1172
+ * Idempotent so framework effects can call it after hydration without
1173
+ * depending on constructor timing.
1174
+ */
1175
+ start() {
1176
+ if (this.started) {
1177
+ void this.flush();
1178
+ return;
1179
+ }
1180
+ this.started = true;
904
1181
  void this.revalidate();
905
1182
  if (typeof window !== "undefined") {
906
1183
  window.addEventListener("visibilitychange", () => {
@@ -916,7 +1193,7 @@ var TrackingConfigRuntime = class {
916
1193
  return this.stateValue === "active" || this.stateValue === "tombstone" ? this.current : null;
917
1194
  }
918
1195
  __unsafeExpireAuthorityForTests() {
919
- this.confirmedAt = 0;
1196
+ this.confirmedAt = Number.NEGATIVE_INFINITY;
920
1197
  }
921
1198
  subscribe(listener) {
922
1199
  this.listeners.add(listener);
@@ -926,10 +1203,14 @@ var TrackingConfigRuntime = class {
926
1203
  if (this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS) {
927
1204
  return true;
928
1205
  }
929
- await this.revalidate();
1206
+ await this.revalidateAuthority();
930
1207
  return this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS;
931
1208
  }
932
1209
  async revalidate() {
1210
+ await this.revalidateAuthority();
1211
+ await this.flush();
1212
+ }
1213
+ async revalidateAuthority() {
933
1214
  if (typeof window === "undefined" || !this.fetchImpl) return;
934
1215
  if (this.inFlight) return this.inFlight;
935
1216
  this.inFlight = this.revalidateNow().finally(() => {
@@ -946,8 +1227,8 @@ var TrackingConfigRuntime = class {
946
1227
  this.conversionQueue.push({ key, ...options });
947
1228
  void this.flush();
948
1229
  }
949
- queueAutomaticEvent(eventType, metadata, transactionPath) {
950
- this.automaticQueue.push({ eventType, metadata, transactionPath });
1230
+ queueAutomaticEvent(eventType, metadata, transactionPath, transactionScope) {
1231
+ this.automaticQueue.push({ eventType, metadata, transactionPath, transactionScope });
951
1232
  void this.flush();
952
1233
  }
953
1234
  listGoals() {
@@ -964,7 +1245,6 @@ var TrackingConfigRuntime = class {
964
1245
  });
965
1246
  if (response.status === 304 && this.current && validateConfig(this.current, this.ref)) {
966
1247
  this.confirm(this.current, this.etag);
967
- await this.flush();
968
1248
  return;
969
1249
  }
970
1250
  if (!response.ok) {
@@ -981,21 +1261,26 @@ var TrackingConfigRuntime = class {
981
1261
  return;
982
1262
  }
983
1263
  this.confirm(next, response.headers.get("ETag"));
984
- await this.flush();
985
1264
  } catch {
986
1265
  this.expireAuthority();
987
1266
  }
988
1267
  }
989
1268
  expireAuthority() {
1269
+ this.authorityGeneration += 1;
990
1270
  if (this.stateValue !== "unconfirmed") this.stateValue = "unconfirmed";
991
1271
  }
992
1272
  confirm(config, etag) {
1273
+ this.authorityGeneration += 1;
993
1274
  this.current = config;
994
1275
  this.etag = etag;
995
1276
  this.confirmedAt = Date.now();
996
1277
  this.stateValue = isTombstone(config) ? "tombstone" : "active";
997
1278
  writeCache2(this.ref.cdnUrl, { etag, config });
998
1279
  if (this.stateValue === "tombstone") {
1280
+ if (this.retryTimer !== null) {
1281
+ window.clearTimeout(this.retryTimer);
1282
+ this.retryTimer = null;
1283
+ }
999
1284
  this.conversionQueue.length = 0;
1000
1285
  this.automaticQueue.length = 0;
1001
1286
  this.pageQueue.length = 0;
@@ -1003,79 +1288,107 @@ var TrackingConfigRuntime = class {
1003
1288
  for (const listener of this.listeners) listener();
1004
1289
  }
1005
1290
  async flush() {
1291
+ if (this.flushInFlight) {
1292
+ this.flushRequested = true;
1293
+ return this.flushInFlight;
1294
+ }
1295
+ this.flushRequested = false;
1296
+ this.flushInFlight = this.flushNow().finally(() => {
1297
+ this.flushInFlight = null;
1298
+ if (this.flushRequested && this.stateValue === "active") void this.flush();
1299
+ });
1300
+ return this.flushInFlight;
1301
+ }
1302
+ scheduleRetry() {
1303
+ if (this.retryTimer !== null || typeof window === "undefined") return;
1304
+ this.retryTimer = window.setTimeout(() => {
1305
+ this.retryTimer = null;
1306
+ void this.flush();
1307
+ }, 1e3);
1308
+ }
1309
+ async flushNow() {
1006
1310
  if (!await this.ensureAuthority()) return;
1007
1311
  if (this.stateValue !== "active" || !this.current) return;
1008
- const ids = Object.values(this.current.gtag_ids).filter(
1312
+ const config = this.current;
1313
+ const generation = this.authorityGeneration;
1314
+ const ids = Object.values(config.gtag_ids).filter(
1009
1315
  (id) => typeof id === "string" && isValidGtagId(id)
1010
1316
  );
1011
1317
  if (ids.length === 0) return;
1012
1318
  await ensureScript(ids[0]);
1319
+ if (this.authorityGeneration !== generation || this.stateValue !== "active" || this.current !== config) {
1320
+ return;
1321
+ }
1013
1322
  const gtag = window.gtag;
1014
1323
  if (typeof gtag !== "function") return;
1015
- if (!jsInitialized) {
1016
- gtag("js", /* @__PURE__ */ new Date());
1017
- jsInitialized = true;
1018
- }
1019
- for (const id of ids) {
1020
- if (configuredIds.has(id)) continue;
1021
- gtag("config", id, { send_page_view: false });
1022
- configuredIds.add(id);
1023
- }
1024
- while (this.pageQueue.length) {
1025
- const page = this.pageQueue.shift();
1026
- gtag("event", "page_view", {
1027
- page_location: page.href,
1028
- page_title: page.title ?? void 0,
1029
- page_referrer: page.referrer ?? void 0
1030
- });
1324
+ try {
1325
+ if (!jsInitialized) {
1326
+ gtag("js", /* @__PURE__ */ new Date());
1327
+ jsInitialized = true;
1328
+ }
1329
+ for (const id of ids) {
1330
+ if (configuredIds.has(id)) continue;
1331
+ gtag("config", id, { send_page_view: false });
1332
+ configuredIds.add(id);
1333
+ }
1334
+ } catch {
1335
+ this.scheduleRetry();
1336
+ return;
1031
1337
  }
1032
- while (this.automaticQueue.length) {
1033
- const event = this.automaticQueue.shift();
1034
- for (const goal of this.current.goals) {
1338
+ while (this.pageQueue.length > 0) {
1339
+ const page = this.pageQueue[0];
1340
+ try {
1341
+ gtag("event", "page_view", {
1342
+ page_location: page.href,
1343
+ page_title: page.title ?? void 0,
1344
+ page_referrer: page.referrer ?? void 0
1345
+ });
1346
+ this.pageQueue.shift();
1347
+ } catch {
1348
+ this.scheduleRetry();
1349
+ return;
1350
+ }
1351
+ }
1352
+ while (this.automaticQueue.length > 0) {
1353
+ const event = this.automaticQueue[0];
1354
+ for (const goal of config.goals) {
1035
1355
  if (goal.kind !== "event" || !goal.firing) continue;
1036
1356
  if (!automaticThresholdMet(goal, event.eventType, event.metadata)) continue;
1037
1357
  this.conversionQueue.push({
1038
1358
  key: goal.key,
1039
- transactionId: `auto:${goal.key}:${event.transactionPath}`
1359
+ transactionId: getAutomaticTransactionId(
1360
+ event.transactionScope,
1361
+ goal.key,
1362
+ event.transactionPath
1363
+ )
1040
1364
  });
1041
1365
  }
1366
+ this.automaticQueue.shift();
1042
1367
  }
1043
- while (this.conversionQueue.length) {
1044
- const item = this.conversionQueue.shift();
1045
- const goal = this.current.goals.find((g) => g.key === item.key);
1368
+ while (this.conversionQueue.length > 0) {
1369
+ const item = this.conversionQueue[0];
1370
+ const goal = config.goals.find((g) => g.key === item.key);
1046
1371
  const firing = goal?.firing;
1047
- if (!firing || getConsentState() === "denied") continue;
1372
+ if (!firing) {
1373
+ this.conversionQueue.shift();
1374
+ continue;
1375
+ }
1048
1376
  const currency = item.currency ?? firing.currency ?? null;
1049
1377
  const value = item.value ?? (firing.value_cents != null && currency ? fromMinor(firing.value_cents, currency) : null);
1050
- fireGtagConversion({
1378
+ const outcome = fireConversionWithConsent({
1051
1379
  sendTo: firing.send_to,
1052
1380
  value,
1053
1381
  currency,
1054
1382
  transactionId: item.transactionId ?? null
1055
1383
  });
1384
+ if (outcome === "retryable") {
1385
+ this.scheduleRetry();
1386
+ return;
1387
+ }
1388
+ this.conversionQueue.shift();
1056
1389
  }
1057
1390
  }
1058
1391
  };
1059
- function automaticThresholdMet(goal, eventType, metadata) {
1060
- const t = goal.trigger;
1061
- if (!t || t.event_type !== eventType) return false;
1062
- switch (eventType) {
1063
- case "scroll_depth":
1064
- return typeof metadata.depth_percent === "number" && t.threshold_percent != null && metadata.depth_percent >= t.threshold_percent;
1065
- case "time_on_site":
1066
- return typeof metadata.duration_ms === "number" && t.threshold_seconds != null && metadata.duration_ms >= t.threshold_seconds * 1e3;
1067
- case "multi_page_session":
1068
- return typeof metadata.page_count === "number" && t.page_threshold != null && metadata.page_count >= t.page_threshold;
1069
- case "specific_page_visit":
1070
- return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
1071
- case "page_view":
1072
- case "form_start":
1073
- case "phone_click":
1074
- return true;
1075
- default:
1076
- return false;
1077
- }
1078
- }
1079
1392
  function getTrackingConfigRuntime(ref, fetchImpl) {
1080
1393
  const key = `${ref.cdnUrl}|${ref.businessId}|${ref.environment}`;
1081
1394
  const existing = runtimes.get(key);
@@ -1086,26 +1399,6 @@ function getTrackingConfigRuntime(ref, fetchImpl) {
1086
1399
  }
1087
1400
 
1088
1401
  // ../tracking-core/src/resources/conversion-autofire.ts
1089
- function thresholdMet(goal, eventType, metadata) {
1090
- const t = goal.trigger;
1091
- if (!t || t.event_type !== eventType) return false;
1092
- switch (eventType) {
1093
- case "scroll_depth":
1094
- return typeof metadata.depth_percent === "number" && t.threshold_percent != null && metadata.depth_percent >= t.threshold_percent;
1095
- case "time_on_site":
1096
- return typeof metadata.duration_ms === "number" && t.threshold_seconds != null && metadata.duration_ms >= t.threshold_seconds * 1e3;
1097
- case "multi_page_session":
1098
- return typeof metadata.page_count === "number" && t.page_threshold != null && metadata.page_count >= t.page_threshold;
1099
- case "specific_page_visit":
1100
- return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
1101
- case "page_view":
1102
- case "form_start":
1103
- case "phone_click":
1104
- return true;
1105
- default:
1106
- return false;
1107
- }
1108
- }
1109
1402
  function currentPath() {
1110
1403
  return typeof window === "undefined" ? "" : window.location.pathname;
1111
1404
  }
@@ -1113,13 +1406,14 @@ var MAX_BUFFERED_EVENTS = 50;
1113
1406
  function createConversionAutoFire(store) {
1114
1407
  const pending = [];
1115
1408
  let subscribed = false;
1116
- function fireMatching(eventType, metadata) {
1409
+ function fireMatching(eventType, metadata, transactionScope) {
1117
1410
  for (const goal of store.listGoals()) {
1118
1411
  if (goal.kind !== "event" || !goal.firing) continue;
1119
- if (!thresholdMet(goal, eventType, metadata)) continue;
1412
+ if (!automaticThresholdMet(goal, eventType, metadata)) continue;
1413
+ const transactionId = getAutomaticTransactionId(transactionScope, goal.key, currentPath());
1120
1414
  if ("queueAutomaticEvent" in store) {
1121
1415
  store.fireConversion(goal.key, {
1122
- transactionId: `auto:${goal.key}:${currentPath()}`
1416
+ transactionId
1123
1417
  });
1124
1418
  continue;
1125
1419
  }
@@ -1130,28 +1424,30 @@ function createConversionAutoFire(store) {
1130
1424
  sendTo: firing.send_to,
1131
1425
  value: cents != null && currency ? fromMinor(cents, currency) : null,
1132
1426
  currency,
1133
- // Page-scoped txn id → fire once per (goal, path) per session; engagement conversions
1134
- // shouldn't re-fire as the visitor scrolls back and forth or re-enters a page.
1135
- transactionId: `auto:${goal.key}:${currentPath()}`
1427
+ transactionId
1136
1428
  });
1137
1429
  }
1138
1430
  }
1139
1431
  return {
1140
- onAutomaticEvent(eventType, metadata) {
1432
+ onAutomaticEvent(eventType, metadata, transactionScope) {
1141
1433
  if ("queueAutomaticEvent" in store) {
1142
- store.queueAutomaticEvent(eventType, metadata, currentPath());
1434
+ store.queueAutomaticEvent(eventType, metadata, currentPath(), transactionScope);
1143
1435
  return;
1144
1436
  }
1145
1437
  if (store.isReady()) {
1146
- fireMatching(eventType, metadata);
1438
+ fireMatching(eventType, metadata, transactionScope);
1147
1439
  return;
1148
1440
  }
1149
- if (pending.length < MAX_BUFFERED_EVENTS) pending.push({ eventType, metadata });
1441
+ if (pending.length < MAX_BUFFERED_EVENTS) {
1442
+ pending.push({ eventType, metadata, transactionScope });
1443
+ }
1150
1444
  if (!subscribed) {
1151
1445
  subscribed = true;
1152
1446
  store.onResolve(() => {
1153
1447
  const buffered = pending.splice(0);
1154
- for (const event of buffered) fireMatching(event.eventType, event.metadata);
1448
+ for (const event of buffered) {
1449
+ fireMatching(event.eventType, event.metadata, event.transactionScope);
1450
+ }
1155
1451
  });
1156
1452
  }
1157
1453
  }
@@ -1163,7 +1459,7 @@ function withConversionAutoFire(client, autoFire) {
1163
1459
  trackEvent: (input) => {
1164
1460
  client.trackEvent(input);
1165
1461
  try {
1166
- autoFire.onAutomaticEvent(input.eventType, input.metadata ?? {});
1462
+ autoFire.onAutomaticEvent(input.eventType, input.metadata ?? {}, client.getSessionId());
1167
1463
  } catch {
1168
1464
  }
1169
1465
  }
@@ -1540,6 +1836,8 @@ function createTrackingClient(config) {
1540
1836
  session_id: sessionId,
1541
1837
  visitor_id: visitorId,
1542
1838
  gclid: params.gclid,
1839
+ wbraid: params.wbraid,
1840
+ gbraid: params.gbraid,
1543
1841
  fbclid: params.fbclid,
1544
1842
  fbc: getFbcCookie(),
1545
1843
  fbp: getFbpCookie(),
@@ -1586,6 +1884,13 @@ function createTrackingClient(config) {
1586
1884
  function trackEvent(input) {
1587
1885
  if (destroyed) return;
1588
1886
  if (!input || typeof input.eventType !== "string" || input.eventType.length === 0) return;
1887
+ if (input.eventType === "form_submit" && getConsentState() !== "denied") {
1888
+ try {
1889
+ const fields = input.metadata?.form?.fields;
1890
+ if (fields) stashUserDataFromFormFields(fields, config.phone?.defaultCountry);
1891
+ } catch {
1892
+ }
1893
+ }
1589
1894
  const occurredAt = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : (/* @__PURE__ */ new Date()).toISOString();
1590
1895
  queue.push({
1591
1896
  event_type: input.eventType,
@@ -2414,46 +2719,6 @@ function attachCtaClickCapture(client, config) {
2414
2719
  };
2415
2720
  }
2416
2721
 
2417
- // ../tracking-core/src/phone.ts
2418
- var import_libphonenumber_js = require("libphonenumber-js");
2419
- var DEFAULT_PHONE_COUNTRY = "CA";
2420
- function parsePhone(raw, country) {
2421
- const region = country ?? DEFAULT_PHONE_COUNTRY;
2422
- const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(raw ?? "", region);
2423
- if (!parsed) {
2424
- return { e164: null, national: "", international: "", country: region, isValid: false };
2425
- }
2426
- const isValid = parsed.isValid();
2427
- return {
2428
- // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
2429
- // (e.g. too few digits) still parses but must not be transmitted.
2430
- e164: isValid ? parsed.number : null,
2431
- national: parsed.formatNational(),
2432
- international: parsed.formatInternational(),
2433
- country: parsed.country ?? region,
2434
- isValid
2435
- };
2436
- }
2437
- function toE164(raw, country) {
2438
- return parsePhone(raw, country).e164;
2439
- }
2440
- function formatPhone(value, format = "national", country) {
2441
- const parsed = parsePhone(value, country);
2442
- if (typeof format === "function") return format(parsed);
2443
- switch (format) {
2444
- case "international":
2445
- return parsed.international || value;
2446
- case "e164":
2447
- return parsed.e164 ?? value;
2448
- case "national":
2449
- default:
2450
- return parsed.national || value;
2451
- }
2452
- }
2453
- function formatPhoneAsTyped(raw, country) {
2454
- return new import_libphonenumber_js.AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
2455
- }
2456
-
2457
2722
  // ../tracking-core/src/triggers/phone-click-capture.ts
2458
2723
  var DEFAULT_TEL_SELECTOR = 'a[href^="tel:"]';
2459
2724
  function safeDecodeURIComponent(value) {
@@ -2563,6 +2828,8 @@ async function salesRequest(config, method, path, body) {
2563
2828
  // ../tracking-core/src/resources/sales/client.ts
2564
2829
  function fireRecordedConversions(firing, input, recorded, sale, currency) {
2565
2830
  if (!firing) return;
2831
+ const userData = input.customer_email || input.customer_phone ? { email: input.customer_email ?? null, phone: input.customer_phone ?? null } : null;
2832
+ if (userData && getConsentState() !== "denied") stashUserData(userData);
2566
2833
  const txnBase = input.external_id ?? sale.id;
2567
2834
  for (const item of recorded) {
2568
2835
  if (!item.service) continue;
@@ -2577,12 +2844,15 @@ function fireRecordedConversions(firing, input, recorded, sale, currency) {
2577
2844
  const config = firing.getFiring(item.service);
2578
2845
  if (!config) continue;
2579
2846
  const cents = item.amount_cents ?? config.value_cents ?? null;
2580
- fireConversionWithConsent({
2581
- sendTo: config.send_to,
2582
- value: cents != null ? fromMinor(cents, currency) : null,
2583
- currency: config.currency ?? currency,
2584
- transactionId: `${txnBase}:${item.service}`
2585
- });
2847
+ fireConversionWithConsent(
2848
+ {
2849
+ sendTo: config.send_to,
2850
+ value: cents != null ? fromMinor(cents, currency) : null,
2851
+ currency: config.currency ?? currency,
2852
+ transactionId: `${txnBase}:${item.service}`
2853
+ },
2854
+ { userData }
2855
+ );
2586
2856
  }
2587
2857
  }
2588
2858
  function createSalesClient(config) {
@@ -2617,6 +2887,7 @@ function createSalesClient(config) {
2617
2887
  recordSale: record,
2618
2888
  trackConversion(key, options) {
2619
2889
  if (config.firing && "fireConversion" in config.firing) {
2890
+ if (options?.userData && getConsentState() !== "denied") stashUserData(options.userData);
2620
2891
  config.firing.fireConversion(key, {
2621
2892
  value: options?.value ?? void 0,
2622
2893
  currency: options?.currency ?? config.defaultCurrency ?? void 0,
@@ -2629,12 +2900,15 @@ function createSalesClient(config) {
2629
2900
  const currency = firing.currency ?? options?.currency ?? config.defaultCurrency ?? null;
2630
2901
  const cents = firing.value_cents ?? null;
2631
2902
  const value = options?.value ?? (cents != null && currency ? fromMinor(cents, currency) : null);
2632
- fireConversionWithConsent({
2633
- sendTo: firing.send_to,
2634
- value,
2635
- currency,
2636
- transactionId: options?.transactionId ?? null
2637
- });
2903
+ fireConversionWithConsent(
2904
+ {
2905
+ sendTo: firing.send_to,
2906
+ value,
2907
+ currency,
2908
+ transactionId: options?.transactionId ?? null
2909
+ },
2910
+ { userData: options?.userData }
2911
+ );
2638
2912
  },
2639
2913
  async list(query) {
2640
2914
  const { cursor, limit, sort, order, want_total, ...filters } = query ?? {};
@@ -3144,7 +3418,7 @@ function AdPlatformTracking({
3144
3418
  if (!trackingConfig) return;
3145
3419
  const runtime = getTrackingConfigRuntime(trackingConfig);
3146
3420
  if (standalonePageView) runtime.queuePageView();
3147
- else void runtime.revalidate();
3421
+ else runtime.start();
3148
3422
  }, [trackingConfigKey, standalonePageView]);
3149
3423
  const gtagAll = trackingConfig ? [] : gtagIds ? Object.values(gtagIds).filter((id) => id != null) : gtagId ? [gtagId] : [];
3150
3424
  const metaAll = metaPixelIds ? Object.values(metaPixelIds).filter((id) => id != null) : metaPixelId ? [metaPixelId] : [];
@@ -3196,7 +3470,7 @@ function GoogleAdsTracking(props) {
3196
3470
  var import_navigation3 = require("next/navigation");
3197
3471
 
3198
3472
  // package.json
3199
- var version = "0.18.1";
3473
+ var version = "0.19.1";
3200
3474
 
3201
3475
  // src/factory.tsx
3202
3476
  var import_react5 = require("react");
@@ -3333,22 +3607,18 @@ function createTracking(options) {
3333
3607
  const timeOnSiteConfig = triggers.automatic.time_on_site ?? null;
3334
3608
  function TrackingProvider({ children }) {
3335
3609
  const pathname = (0, import_navigation3.usePathname)();
3336
- const client = (0, import_react5.useMemo)(
3337
- () => createTypedClient(
3338
- getOrCreateTrackingClient({
3339
- apiKey,
3340
- endpoint,
3341
- surface: "next",
3342
- packageName: "@aranova/tracking-next",
3343
- sdkVersion: version,
3344
- triggers,
3345
- environment,
3346
- activeGtagIds: gtagIds,
3347
- debug
3348
- }),
3610
+ const rawClient = (0, import_react5.useMemo)(
3611
+ () => getOrCreateTrackingClient({
3612
+ apiKey,
3613
+ endpoint,
3614
+ surface: "next",
3615
+ packageName: "@aranova/tracking-next",
3616
+ sdkVersion: version,
3349
3617
  triggers,
3350
- { debug }
3351
- ),
3618
+ environment,
3619
+ activeGtagIds: gtagIds,
3620
+ debug
3621
+ }),
3352
3622
  []
3353
3623
  );
3354
3624
  const conversionStore = (0, import_react5.useMemo)(
@@ -3362,33 +3632,22 @@ function createTracking(options) {
3362
3632
  const autoFire = conversionStore ? createConversionAutoFire(conversionStore) : null;
3363
3633
  return (c) => autoFire ? withConversionAutoFire(c, autoFire) : c;
3364
3634
  }, [conversionStore]);
3635
+ const client = (0, import_react5.useMemo)(
3636
+ () => createTypedClient(wrapClient(rawClient), triggers, { debug }),
3637
+ [rawClient, wrapClient]
3638
+ );
3639
+ (0, import_react5.useEffect)(() => {
3640
+ if (trackingConfig && conversionStore && "start" in conversionStore) {
3641
+ conversionStore.start();
3642
+ }
3643
+ }, [conversionStore]);
3365
3644
  const firePageView = (0, import_react5.useCallback)(() => {
3366
- const rawClient = getOrCreateTrackingClient({
3367
- apiKey,
3368
- endpoint,
3369
- surface: "next",
3370
- packageName: "@aranova/tracking-next",
3371
- triggers,
3372
- environment,
3373
- activeGtagIds: gtagIds,
3374
- debug
3375
- });
3376
3645
  fireManualPageView(wrapClient(rawClient));
3377
3646
  if (trackingConfig) getTrackingConfigRuntime(trackingConfig).queuePageView();
3378
- }, [wrapClient]);
3647
+ }, [rawClient, wrapClient]);
3379
3648
  const firedPagesRef = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
3380
3649
  (0, import_react5.useEffect)(() => {
3381
3650
  if (!specificPageVisitConfig || pathname === null) return;
3382
- const rawClient = getOrCreateTrackingClient({
3383
- apiKey,
3384
- endpoint,
3385
- surface: "next",
3386
- packageName: "@aranova/tracking-next",
3387
- triggers,
3388
- environment,
3389
- activeGtagIds: gtagIds,
3390
- debug
3391
- });
3392
3651
  const detectorClient = wrapClient(rawClient);
3393
3652
  for (const { name, pathPattern } of specificPageVisitConfig.pages) {
3394
3653
  if (!pathPattern.test(pathname)) continue;
@@ -3405,7 +3664,7 @@ function createTracking(options) {
3405
3664
  occurredAt: null
3406
3665
  });
3407
3666
  }
3408
- }, [pathname]);
3667
+ }, [pathname, rawClient, wrapClient]);
3409
3668
  const multiPageSessionConfig = triggers.automatic.multi_page_session ?? null;
3410
3669
  const mpsLastPathRef = (0, import_react5.useRef)(null);
3411
3670
  (0, import_react5.useEffect)(() => {
@@ -3431,17 +3690,6 @@ function createTracking(options) {
3431
3690
  store.setItem(MPS_PATHS_KEY, JSON.stringify([...paths]));
3432
3691
  if (paths.size >= multiPageSessionConfig.pageThreshold) {
3433
3692
  store.setItem(MPS_FIRED_KEY, "1");
3434
- const rawClient = getOrCreateTrackingClient({
3435
- apiKey,
3436
- endpoint,
3437
- surface: "next",
3438
- packageName: "@aranova/tracking-next",
3439
- sdkVersion: version,
3440
- triggers,
3441
- environment,
3442
- activeGtagIds: gtagIds,
3443
- debug
3444
- });
3445
3693
  wrapClient(rawClient).trackEvent({
3446
3694
  eventType: "multi_page_session",
3447
3695
  metadata: {
@@ -3454,18 +3702,8 @@ function createTracking(options) {
3454
3702
  }
3455
3703
  } catch {
3456
3704
  }
3457
- }, [pathname]);
3705
+ }, [multiPageSessionConfig, pathname, rawClient, wrapClient]);
3458
3706
  (0, import_react5.useEffect)(() => {
3459
- const rawClient = getOrCreateTrackingClient({
3460
- apiKey,
3461
- endpoint,
3462
- surface: "next",
3463
- packageName: "@aranova/tracking-next",
3464
- triggers,
3465
- environment,
3466
- activeGtagIds: gtagIds,
3467
- debug
3468
- });
3469
3707
  return attachClientCapturesOnce(rawClient, () => {
3470
3708
  const detachers = [];
3471
3709
  const detectorClient = wrapClient(rawClient);
@@ -3496,7 +3734,7 @@ function createTracking(options) {
3496
3734
  }
3497
3735
  };
3498
3736
  });
3499
- }, []);
3737
+ }, [rawClient, wrapClient]);
3500
3738
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(TrackingContext.Provider, { value: client, children: [
3501
3739
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react5.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PageViewTracker, { fire: firePageView }) }),
3502
3740
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PhoneConfigProvider, { value: phone ?? null, children })
@@ -3528,6 +3766,7 @@ function createTracking(options) {
3528
3766
  TRACKING_PARAM_KEYS,
3529
3767
  TRACKING_RANGES,
3530
3768
  captureTrackingParamsFromLocation,
3769
+ clearStashedUserData,
3531
3770
  createSalesClient,
3532
3771
  createTracking,
3533
3772
  createTrackingClientContext,
@@ -3556,6 +3795,7 @@ function createTracking(options) {
3556
3795
  saleUpdateSchema,
3557
3796
  salesRequest,
3558
3797
  setConsentState,
3798
+ stashUserData,
3559
3799
  toE164,
3560
3800
  toMinor,
3561
3801
  useConsent,