@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.mjs CHANGED
@@ -1,3 +1,137 @@
1
+ // ../tracking-core/src/phone.ts
2
+ import { AsYouType, parsePhoneNumberFromString } from "libphonenumber-js";
3
+ var DEFAULT_PHONE_COUNTRY = "CA";
4
+ function parsePhone(raw, country) {
5
+ const region = country ?? DEFAULT_PHONE_COUNTRY;
6
+ const parsed = parsePhoneNumberFromString(raw ?? "", region);
7
+ if (!parsed) {
8
+ return { e164: null, national: "", international: "", country: region, isValid: false };
9
+ }
10
+ const isValid = parsed.isValid();
11
+ return {
12
+ // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
13
+ // (e.g. too few digits) still parses but must not be transmitted.
14
+ e164: isValid ? parsed.number : null,
15
+ national: parsed.formatNational(),
16
+ international: parsed.formatInternational(),
17
+ country: parsed.country ?? region,
18
+ isValid
19
+ };
20
+ }
21
+ function toE164(raw, country) {
22
+ return parsePhone(raw, country).e164;
23
+ }
24
+ function formatPhone(value, format = "national", country) {
25
+ const parsed = parsePhone(value, country);
26
+ if (typeof format === "function") return format(parsed);
27
+ switch (format) {
28
+ case "international":
29
+ return parsed.international || value;
30
+ case "e164":
31
+ return parsed.e164 ?? value;
32
+ case "national":
33
+ default:
34
+ return parsed.national || value;
35
+ }
36
+ }
37
+ function formatPhoneAsTyped(raw, country) {
38
+ return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
39
+ }
40
+
41
+ // ../tracking-core/src/user-data.ts
42
+ var EMAIL_SHAPE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
43
+ var EMAIL_NAME_HINT = /e[-_]?mail/i;
44
+ var PHONE_NAME_HINT = /(^|[^a-z])(phone|tel|mobile|cell)/i;
45
+ var stash = { email: null, phoneNumber: null };
46
+ function normalizeEmail(raw) {
47
+ if (typeof raw !== "string") return null;
48
+ const cleaned = raw.trim().toLowerCase();
49
+ return EMAIL_SHAPE.test(cleaned) ? cleaned : null;
50
+ }
51
+ function fieldText(field, key) {
52
+ const value = field[key];
53
+ return typeof value === "string" ? value : "";
54
+ }
55
+ function extractUserDataFromFormFields(fields, country) {
56
+ const result = { email: null, phoneNumber: null };
57
+ if (!Array.isArray(fields)) return result;
58
+ const passes = [
59
+ (field, _hint, type) => fieldText(field, "type").toLowerCase() === type,
60
+ (field, hint) => hint.test(fieldText(field, "name")) || hint.test(fieldText(field, "label"))
61
+ ];
62
+ for (const matches of passes) {
63
+ for (const raw of fields) {
64
+ if (raw === null || typeof raw !== "object") continue;
65
+ const field = raw;
66
+ if (typeof field.value !== "string" || field.value.length === 0) continue;
67
+ if (result.email === null && matches(field, EMAIL_NAME_HINT, "email")) {
68
+ result.email = normalizeEmail(field.value);
69
+ }
70
+ if (result.phoneNumber === null && matches(field, PHONE_NAME_HINT, "tel")) {
71
+ try {
72
+ result.phoneNumber = toE164(field.value, country);
73
+ } catch {
74
+ }
75
+ }
76
+ }
77
+ if (result.email !== null && result.phoneNumber !== null) break;
78
+ }
79
+ return result;
80
+ }
81
+ function stashUserData(data, country) {
82
+ const email = normalizeEmail(data.email);
83
+ let phoneNumber = null;
84
+ if (typeof data.phone === "string" && data.phone.length > 0) {
85
+ try {
86
+ phoneNumber = toE164(data.phone, country);
87
+ } catch {
88
+ phoneNumber = null;
89
+ }
90
+ }
91
+ stash = {
92
+ email: email ?? stash.email,
93
+ phoneNumber: phoneNumber ?? stash.phoneNumber
94
+ };
95
+ }
96
+ function stashUserDataFromFormFields(fields, country) {
97
+ try {
98
+ const extracted = extractUserDataFromFormFields(fields, country);
99
+ stash = {
100
+ email: extracted.email ?? stash.email,
101
+ phoneNumber: extracted.phoneNumber ?? stash.phoneNumber
102
+ };
103
+ } catch {
104
+ }
105
+ }
106
+ function clearStashedUserData() {
107
+ stash = { email: null, phoneNumber: null };
108
+ }
109
+ function applyUserDataForConversion(explicit, country) {
110
+ if (typeof window === "undefined" || typeof window.gtag !== "function") return false;
111
+ let email = stash.email;
112
+ let phoneNumber = stash.phoneNumber;
113
+ if (explicit) {
114
+ const normalizedEmail = normalizeEmail(explicit.email);
115
+ if (normalizedEmail) email = normalizedEmail;
116
+ if (typeof explicit.phone === "string" && explicit.phone.length > 0) {
117
+ try {
118
+ phoneNumber = toE164(explicit.phone, country) ?? phoneNumber;
119
+ } catch {
120
+ }
121
+ }
122
+ }
123
+ if (email === null && phoneNumber === null) return false;
124
+ try {
125
+ window.gtag("set", "user_data", {
126
+ ...email !== null ? { email } : {},
127
+ ...phoneNumber !== null ? { phone_number: phoneNumber } : {}
128
+ });
129
+ return true;
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+
1
135
  // ../tracking-core/src/consent.ts
2
136
  var CONSENT_STATE_KEY = "consent_state";
3
137
  var CONSENT_TIMESTAMP_KEY = "consent_timestamp";
@@ -83,6 +217,13 @@ function setConsentState(state, options) {
83
217
  } catch {
84
218
  }
85
219
  pushConsentToPlatforms(state);
220
+ if (state === "denied") {
221
+ clearStashedUserData();
222
+ try {
223
+ if (typeof window.gtag === "function") window.gtag("set", "user_data", null);
224
+ } catch {
225
+ }
226
+ }
86
227
  notifyConsentChanged({ state, source: "explicit", updatedAt, expiresAt });
87
228
  }
88
229
  function optIn() {
@@ -121,6 +262,11 @@ var aranovaConsent = (function () {
121
262
  var TRACKING_COOKIE_MAX_AGE_SECONDS = 7776e3;
122
263
  var TRACKING_PARAM_KEYS = [
123
264
  "gclid",
265
+ // Google's iOS/Safari replacement click IDs — issued when privacy features
266
+ // withhold gclid (wbraid: web-to-web, gbraid: app-to-web). First-class
267
+ // citizens: captured, persisted, and attributed exactly like gclid.
268
+ "wbraid",
269
+ "gbraid",
124
270
  "fbclid",
125
271
  "utm_source",
126
272
  "utm_medium",
@@ -131,6 +277,8 @@ var TRACKING_PARAM_KEYS = [
131
277
  function createEmptyTrackingParams() {
132
278
  return {
133
279
  gclid: null,
280
+ wbraid: null,
281
+ gbraid: null,
134
282
  fbclid: null,
135
283
  utm_source: null,
136
284
  utm_medium: null,
@@ -433,6 +581,8 @@ function buildLandingPayloadFields(sessionId, override) {
433
581
  if (params === null) return {};
434
582
  return {
435
583
  landing_gclid: params.gclid ?? null,
584
+ landing_wbraid: params.wbraid ?? null,
585
+ landing_gbraid: params.gbraid ?? null,
436
586
  landing_fbclid: params.fbclid ?? null,
437
587
  landing_utm_source: params.utm_source ?? null,
438
588
  landing_utm_medium: params.utm_medium ?? null,
@@ -460,6 +610,8 @@ function createTrackingSessionUpsertPayload(trackingParams, input, context) {
460
610
  session_id: input.sessionId,
461
611
  visitor_id: input.visitorId ?? null,
462
612
  gclid: trackingParams.gclid,
613
+ wbraid: trackingParams.wbraid,
614
+ gbraid: trackingParams.gbraid,
463
615
  fbclid: trackingParams.fbclid,
464
616
  fbc: getFbcCookie(),
465
617
  fbp: getFbpCookie(),
@@ -481,6 +633,8 @@ function createTrackingEventCreatePayload(trackingParams, input, context) {
481
633
  session_id: input.sessionId,
482
634
  event_type: input.eventType,
483
635
  gclid: trackingParams.gclid,
636
+ wbraid: trackingParams.wbraid,
637
+ gbraid: trackingParams.gbraid,
484
638
  fbclid: trackingParams.fbclid,
485
639
  fbc: getFbcCookie(),
486
640
  fbp: getFbpCookie(),
@@ -510,13 +664,18 @@ function markFired(input) {
510
664
  } catch {
511
665
  }
512
666
  }
513
- function fireOnce(input) {
514
- if (alreadyFired(input)) return;
515
- if (fireGtagConversion(input)) markFired(input);
516
- }
517
- function fireConversionWithConsent(input) {
518
- if (getConsentState() === "denied") return;
519
- fireOnce(input);
667
+ function fireConversionWithConsent(input, options) {
668
+ if (getConsentState() === "denied") return "denied";
669
+ if (alreadyFired(input)) return "duplicate";
670
+ if (!isValidSendTo(input.sendTo)) return "invalid";
671
+ try {
672
+ applyUserDataForConversion(options?.userData);
673
+ if (!fireGtagConversion(input)) return "retryable";
674
+ markFired(input);
675
+ return "fired";
676
+ } catch {
677
+ return "retryable";
678
+ }
520
679
  }
521
680
 
522
681
  // ../tracking-core/src/resources/conversion-config.ts
@@ -690,6 +849,103 @@ function resolveConversionConfig(options) {
690
849
  };
691
850
  }
692
851
 
852
+ // ../tracking-core/src/resources/automatic-transaction.ts
853
+ var STORAGE_KEY = "_aranova_auto_txn_map";
854
+ var transactionIds = /* @__PURE__ */ new Map();
855
+ var legacyCounter = 0;
856
+ function scopeKey(sessionId, goalKey, path) {
857
+ return JSON.stringify([sessionId, goalKey, path]);
858
+ }
859
+ function randomId() {
860
+ const cryptoApi = globalThis.crypto;
861
+ if (typeof cryptoApi?.randomUUID === "function") return cryptoApi.randomUUID();
862
+ if (typeof cryptoApi?.getRandomValues === "function") {
863
+ const bytes = cryptoApi.getRandomValues(new Uint8Array(16));
864
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
865
+ }
866
+ legacyCounter = (legacyCounter + 1) % 4294967296;
867
+ const timestamp = Date.now().toString(16).padStart(12, "0");
868
+ const counter = legacyCounter.toString(16).padStart(8, "0");
869
+ const random = Math.floor(Math.random() * 281474976710655).toString(16).padStart(12, "0");
870
+ return `${timestamp}${counter}${random}`.slice(0, 32);
871
+ }
872
+ function isValidTransactionId(value) {
873
+ 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(
874
+ value
875
+ ) && value.length <= 64;
876
+ }
877
+ function readStoredMap(sessionId) {
878
+ if (typeof window === "undefined") return { sessionId, entries: {} };
879
+ try {
880
+ const raw = window.localStorage.getItem(STORAGE_KEY);
881
+ if (!raw) return { sessionId, entries: {} };
882
+ const parsed = JSON.parse(raw);
883
+ if (parsed.sessionId !== sessionId || !parsed.entries || typeof parsed.entries !== "object" || Array.isArray(parsed.entries)) {
884
+ return { sessionId, entries: {} };
885
+ }
886
+ return { sessionId, entries: parsed.entries };
887
+ } catch {
888
+ return { sessionId, entries: {} };
889
+ }
890
+ }
891
+ function writeStoredMap(stored) {
892
+ if (typeof window === "undefined") return;
893
+ try {
894
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
895
+ } catch {
896
+ }
897
+ }
898
+ function getAutomaticTransactionId(sessionId, goalKey, path) {
899
+ const key = scopeKey(sessionId, goalKey, path);
900
+ const existing = transactionIds.get(key);
901
+ if (existing) return existing;
902
+ const stored = readStoredMap(sessionId);
903
+ const storedId = stored.entries[key];
904
+ if (isValidTransactionId(storedId)) {
905
+ transactionIds.set(key, storedId);
906
+ return storedId;
907
+ }
908
+ const transactionId = `auto:${randomId()}`;
909
+ transactionIds.set(key, transactionId);
910
+ const latest = readStoredMap(sessionId);
911
+ const concurrentId = latest.entries[key];
912
+ if (isValidTransactionId(concurrentId)) {
913
+ transactionIds.set(key, concurrentId);
914
+ return concurrentId;
915
+ }
916
+ latest.entries[key] = transactionId;
917
+ writeStoredMap(latest);
918
+ return transactionId;
919
+ }
920
+
921
+ // ../tracking-core/src/resources/automatic-trigger.ts
922
+ function hasNumberField(metadata, key) {
923
+ return typeof metadata[key] === "number";
924
+ }
925
+ function hasStringField(metadata, key) {
926
+ return typeof metadata[key] === "string";
927
+ }
928
+ function automaticThresholdMet(goal, eventType, metadata) {
929
+ const trigger = goal.trigger;
930
+ if (!trigger || trigger.event_type !== eventType) return false;
931
+ switch (eventType) {
932
+ case "scroll_depth":
933
+ return hasNumberField(metadata, "depth_percent") && trigger.threshold_percent != null && metadata.depth_percent >= trigger.threshold_percent;
934
+ case "time_on_site":
935
+ return hasNumberField(metadata, "duration_ms") && trigger.threshold_seconds != null && metadata.duration_ms >= trigger.threshold_seconds * 1e3;
936
+ case "multi_page_session":
937
+ return hasNumberField(metadata, "page_count") && trigger.page_threshold != null && metadata.page_count >= trigger.page_threshold;
938
+ case "specific_page_visit":
939
+ return hasStringField(metadata, "page_name") && metadata.page_name === trigger.page_name;
940
+ case "page_view":
941
+ case "form_start":
942
+ case "phone_click":
943
+ return true;
944
+ default:
945
+ return false;
946
+ }
947
+ }
948
+
693
949
  // ../tracking-core/src/resources/sales/money.ts
694
950
  var MINOR_UNIT_EXPONENT = {
695
951
  USD: 2,
@@ -808,7 +1064,12 @@ var TrackingConfigRuntime = class {
808
1064
  this.etag = null;
809
1065
  this.stateValue = "unconfirmed";
810
1066
  this.confirmedAt = 0;
1067
+ this.authorityGeneration = 0;
811
1068
  this.inFlight = null;
1069
+ this.flushInFlight = null;
1070
+ this.flushRequested = false;
1071
+ this.retryTimer = null;
1072
+ this.started = false;
812
1073
  this.conversionQueue = [];
813
1074
  this.automaticQueue = [];
814
1075
  this.pageQueue = [];
@@ -816,6 +1077,20 @@ var TrackingConfigRuntime = class {
816
1077
  const cached = readCache2(ref.cdnUrl);
817
1078
  this.current = cached?.config ?? null;
818
1079
  this.etag = cached?.etag ?? null;
1080
+ this.start();
1081
+ }
1082
+ /**
1083
+ * Explicitly start authority resolution and Google-tag bootstrap.
1084
+ *
1085
+ * Idempotent so framework effects can call it after hydration without
1086
+ * depending on constructor timing.
1087
+ */
1088
+ start() {
1089
+ if (this.started) {
1090
+ void this.flush();
1091
+ return;
1092
+ }
1093
+ this.started = true;
819
1094
  void this.revalidate();
820
1095
  if (typeof window !== "undefined") {
821
1096
  window.addEventListener("visibilitychange", () => {
@@ -831,7 +1106,7 @@ var TrackingConfigRuntime = class {
831
1106
  return this.stateValue === "active" || this.stateValue === "tombstone" ? this.current : null;
832
1107
  }
833
1108
  __unsafeExpireAuthorityForTests() {
834
- this.confirmedAt = 0;
1109
+ this.confirmedAt = Number.NEGATIVE_INFINITY;
835
1110
  }
836
1111
  subscribe(listener) {
837
1112
  this.listeners.add(listener);
@@ -841,10 +1116,14 @@ var TrackingConfigRuntime = class {
841
1116
  if (this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS) {
842
1117
  return true;
843
1118
  }
844
- await this.revalidate();
1119
+ await this.revalidateAuthority();
845
1120
  return this.stateValue !== "unconfirmed" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS;
846
1121
  }
847
1122
  async revalidate() {
1123
+ await this.revalidateAuthority();
1124
+ await this.flush();
1125
+ }
1126
+ async revalidateAuthority() {
848
1127
  if (typeof window === "undefined" || !this.fetchImpl) return;
849
1128
  if (this.inFlight) return this.inFlight;
850
1129
  this.inFlight = this.revalidateNow().finally(() => {
@@ -861,8 +1140,8 @@ var TrackingConfigRuntime = class {
861
1140
  this.conversionQueue.push({ key, ...options });
862
1141
  void this.flush();
863
1142
  }
864
- queueAutomaticEvent(eventType, metadata, transactionPath) {
865
- this.automaticQueue.push({ eventType, metadata, transactionPath });
1143
+ queueAutomaticEvent(eventType, metadata, transactionPath, transactionScope) {
1144
+ this.automaticQueue.push({ eventType, metadata, transactionPath, transactionScope });
866
1145
  void this.flush();
867
1146
  }
868
1147
  listGoals() {
@@ -879,7 +1158,6 @@ var TrackingConfigRuntime = class {
879
1158
  });
880
1159
  if (response.status === 304 && this.current && validateConfig(this.current, this.ref)) {
881
1160
  this.confirm(this.current, this.etag);
882
- await this.flush();
883
1161
  return;
884
1162
  }
885
1163
  if (!response.ok) {
@@ -896,21 +1174,26 @@ var TrackingConfigRuntime = class {
896
1174
  return;
897
1175
  }
898
1176
  this.confirm(next, response.headers.get("ETag"));
899
- await this.flush();
900
1177
  } catch {
901
1178
  this.expireAuthority();
902
1179
  }
903
1180
  }
904
1181
  expireAuthority() {
1182
+ this.authorityGeneration += 1;
905
1183
  if (this.stateValue !== "unconfirmed") this.stateValue = "unconfirmed";
906
1184
  }
907
1185
  confirm(config, etag) {
1186
+ this.authorityGeneration += 1;
908
1187
  this.current = config;
909
1188
  this.etag = etag;
910
1189
  this.confirmedAt = Date.now();
911
1190
  this.stateValue = isTombstone(config) ? "tombstone" : "active";
912
1191
  writeCache2(this.ref.cdnUrl, { etag, config });
913
1192
  if (this.stateValue === "tombstone") {
1193
+ if (this.retryTimer !== null) {
1194
+ window.clearTimeout(this.retryTimer);
1195
+ this.retryTimer = null;
1196
+ }
914
1197
  this.conversionQueue.length = 0;
915
1198
  this.automaticQueue.length = 0;
916
1199
  this.pageQueue.length = 0;
@@ -918,79 +1201,107 @@ var TrackingConfigRuntime = class {
918
1201
  for (const listener of this.listeners) listener();
919
1202
  }
920
1203
  async flush() {
1204
+ if (this.flushInFlight) {
1205
+ this.flushRequested = true;
1206
+ return this.flushInFlight;
1207
+ }
1208
+ this.flushRequested = false;
1209
+ this.flushInFlight = this.flushNow().finally(() => {
1210
+ this.flushInFlight = null;
1211
+ if (this.flushRequested && this.stateValue === "active") void this.flush();
1212
+ });
1213
+ return this.flushInFlight;
1214
+ }
1215
+ scheduleRetry() {
1216
+ if (this.retryTimer !== null || typeof window === "undefined") return;
1217
+ this.retryTimer = window.setTimeout(() => {
1218
+ this.retryTimer = null;
1219
+ void this.flush();
1220
+ }, 1e3);
1221
+ }
1222
+ async flushNow() {
921
1223
  if (!await this.ensureAuthority()) return;
922
1224
  if (this.stateValue !== "active" || !this.current) return;
923
- const ids = Object.values(this.current.gtag_ids).filter(
1225
+ const config = this.current;
1226
+ const generation = this.authorityGeneration;
1227
+ const ids = Object.values(config.gtag_ids).filter(
924
1228
  (id) => typeof id === "string" && isValidGtagId(id)
925
1229
  );
926
1230
  if (ids.length === 0) return;
927
1231
  await ensureScript(ids[0]);
1232
+ if (this.authorityGeneration !== generation || this.stateValue !== "active" || this.current !== config) {
1233
+ return;
1234
+ }
928
1235
  const gtag = window.gtag;
929
1236
  if (typeof gtag !== "function") return;
930
- if (!jsInitialized) {
931
- gtag("js", /* @__PURE__ */ new Date());
932
- jsInitialized = true;
933
- }
934
- for (const id of ids) {
935
- if (configuredIds.has(id)) continue;
936
- gtag("config", id, { send_page_view: false });
937
- configuredIds.add(id);
938
- }
939
- while (this.pageQueue.length) {
940
- const page = this.pageQueue.shift();
941
- gtag("event", "page_view", {
942
- page_location: page.href,
943
- page_title: page.title ?? void 0,
944
- page_referrer: page.referrer ?? void 0
945
- });
1237
+ try {
1238
+ if (!jsInitialized) {
1239
+ gtag("js", /* @__PURE__ */ new Date());
1240
+ jsInitialized = true;
1241
+ }
1242
+ for (const id of ids) {
1243
+ if (configuredIds.has(id)) continue;
1244
+ gtag("config", id, { send_page_view: false });
1245
+ configuredIds.add(id);
1246
+ }
1247
+ } catch {
1248
+ this.scheduleRetry();
1249
+ return;
946
1250
  }
947
- while (this.automaticQueue.length) {
948
- const event = this.automaticQueue.shift();
949
- for (const goal of this.current.goals) {
1251
+ while (this.pageQueue.length > 0) {
1252
+ const page = this.pageQueue[0];
1253
+ try {
1254
+ gtag("event", "page_view", {
1255
+ page_location: page.href,
1256
+ page_title: page.title ?? void 0,
1257
+ page_referrer: page.referrer ?? void 0
1258
+ });
1259
+ this.pageQueue.shift();
1260
+ } catch {
1261
+ this.scheduleRetry();
1262
+ return;
1263
+ }
1264
+ }
1265
+ while (this.automaticQueue.length > 0) {
1266
+ const event = this.automaticQueue[0];
1267
+ for (const goal of config.goals) {
950
1268
  if (goal.kind !== "event" || !goal.firing) continue;
951
1269
  if (!automaticThresholdMet(goal, event.eventType, event.metadata)) continue;
952
1270
  this.conversionQueue.push({
953
1271
  key: goal.key,
954
- transactionId: `auto:${goal.key}:${event.transactionPath}`
1272
+ transactionId: getAutomaticTransactionId(
1273
+ event.transactionScope,
1274
+ goal.key,
1275
+ event.transactionPath
1276
+ )
955
1277
  });
956
1278
  }
1279
+ this.automaticQueue.shift();
957
1280
  }
958
- while (this.conversionQueue.length) {
959
- const item = this.conversionQueue.shift();
960
- const goal = this.current.goals.find((g) => g.key === item.key);
1281
+ while (this.conversionQueue.length > 0) {
1282
+ const item = this.conversionQueue[0];
1283
+ const goal = config.goals.find((g) => g.key === item.key);
961
1284
  const firing = goal?.firing;
962
- if (!firing || getConsentState() === "denied") continue;
1285
+ if (!firing) {
1286
+ this.conversionQueue.shift();
1287
+ continue;
1288
+ }
963
1289
  const currency = item.currency ?? firing.currency ?? null;
964
1290
  const value = item.value ?? (firing.value_cents != null && currency ? fromMinor(firing.value_cents, currency) : null);
965
- fireGtagConversion({
1291
+ const outcome = fireConversionWithConsent({
966
1292
  sendTo: firing.send_to,
967
1293
  value,
968
1294
  currency,
969
1295
  transactionId: item.transactionId ?? null
970
1296
  });
1297
+ if (outcome === "retryable") {
1298
+ this.scheduleRetry();
1299
+ return;
1300
+ }
1301
+ this.conversionQueue.shift();
971
1302
  }
972
1303
  }
973
1304
  };
974
- function automaticThresholdMet(goal, eventType, metadata) {
975
- const t = goal.trigger;
976
- if (!t || t.event_type !== eventType) return false;
977
- switch (eventType) {
978
- case "scroll_depth":
979
- return typeof metadata.depth_percent === "number" && t.threshold_percent != null && metadata.depth_percent >= t.threshold_percent;
980
- case "time_on_site":
981
- return typeof metadata.duration_ms === "number" && t.threshold_seconds != null && metadata.duration_ms >= t.threshold_seconds * 1e3;
982
- case "multi_page_session":
983
- return typeof metadata.page_count === "number" && t.page_threshold != null && metadata.page_count >= t.page_threshold;
984
- case "specific_page_visit":
985
- return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
986
- case "page_view":
987
- case "form_start":
988
- case "phone_click":
989
- return true;
990
- default:
991
- return false;
992
- }
993
- }
994
1305
  function getTrackingConfigRuntime(ref, fetchImpl) {
995
1306
  const key = `${ref.cdnUrl}|${ref.businessId}|${ref.environment}`;
996
1307
  const existing = runtimes.get(key);
@@ -1001,26 +1312,6 @@ function getTrackingConfigRuntime(ref, fetchImpl) {
1001
1312
  }
1002
1313
 
1003
1314
  // ../tracking-core/src/resources/conversion-autofire.ts
1004
- function thresholdMet(goal, eventType, metadata) {
1005
- const t = goal.trigger;
1006
- if (!t || t.event_type !== eventType) return false;
1007
- switch (eventType) {
1008
- case "scroll_depth":
1009
- return typeof metadata.depth_percent === "number" && t.threshold_percent != null && metadata.depth_percent >= t.threshold_percent;
1010
- case "time_on_site":
1011
- return typeof metadata.duration_ms === "number" && t.threshold_seconds != null && metadata.duration_ms >= t.threshold_seconds * 1e3;
1012
- case "multi_page_session":
1013
- return typeof metadata.page_count === "number" && t.page_threshold != null && metadata.page_count >= t.page_threshold;
1014
- case "specific_page_visit":
1015
- return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
1016
- case "page_view":
1017
- case "form_start":
1018
- case "phone_click":
1019
- return true;
1020
- default:
1021
- return false;
1022
- }
1023
- }
1024
1315
  function currentPath() {
1025
1316
  return typeof window === "undefined" ? "" : window.location.pathname;
1026
1317
  }
@@ -1028,13 +1319,14 @@ var MAX_BUFFERED_EVENTS = 50;
1028
1319
  function createConversionAutoFire(store) {
1029
1320
  const pending = [];
1030
1321
  let subscribed = false;
1031
- function fireMatching(eventType, metadata) {
1322
+ function fireMatching(eventType, metadata, transactionScope) {
1032
1323
  for (const goal of store.listGoals()) {
1033
1324
  if (goal.kind !== "event" || !goal.firing) continue;
1034
- if (!thresholdMet(goal, eventType, metadata)) continue;
1325
+ if (!automaticThresholdMet(goal, eventType, metadata)) continue;
1326
+ const transactionId = getAutomaticTransactionId(transactionScope, goal.key, currentPath());
1035
1327
  if ("queueAutomaticEvent" in store) {
1036
1328
  store.fireConversion(goal.key, {
1037
- transactionId: `auto:${goal.key}:${currentPath()}`
1329
+ transactionId
1038
1330
  });
1039
1331
  continue;
1040
1332
  }
@@ -1045,28 +1337,30 @@ function createConversionAutoFire(store) {
1045
1337
  sendTo: firing.send_to,
1046
1338
  value: cents != null && currency ? fromMinor(cents, currency) : null,
1047
1339
  currency,
1048
- // Page-scoped txn id → fire once per (goal, path) per session; engagement conversions
1049
- // shouldn't re-fire as the visitor scrolls back and forth or re-enters a page.
1050
- transactionId: `auto:${goal.key}:${currentPath()}`
1340
+ transactionId
1051
1341
  });
1052
1342
  }
1053
1343
  }
1054
1344
  return {
1055
- onAutomaticEvent(eventType, metadata) {
1345
+ onAutomaticEvent(eventType, metadata, transactionScope) {
1056
1346
  if ("queueAutomaticEvent" in store) {
1057
- store.queueAutomaticEvent(eventType, metadata, currentPath());
1347
+ store.queueAutomaticEvent(eventType, metadata, currentPath(), transactionScope);
1058
1348
  return;
1059
1349
  }
1060
1350
  if (store.isReady()) {
1061
- fireMatching(eventType, metadata);
1351
+ fireMatching(eventType, metadata, transactionScope);
1062
1352
  return;
1063
1353
  }
1064
- if (pending.length < MAX_BUFFERED_EVENTS) pending.push({ eventType, metadata });
1354
+ if (pending.length < MAX_BUFFERED_EVENTS) {
1355
+ pending.push({ eventType, metadata, transactionScope });
1356
+ }
1065
1357
  if (!subscribed) {
1066
1358
  subscribed = true;
1067
1359
  store.onResolve(() => {
1068
1360
  const buffered = pending.splice(0);
1069
- for (const event of buffered) fireMatching(event.eventType, event.metadata);
1361
+ for (const event of buffered) {
1362
+ fireMatching(event.eventType, event.metadata, event.transactionScope);
1363
+ }
1070
1364
  });
1071
1365
  }
1072
1366
  }
@@ -1078,7 +1372,7 @@ function withConversionAutoFire(client, autoFire) {
1078
1372
  trackEvent: (input) => {
1079
1373
  client.trackEvent(input);
1080
1374
  try {
1081
- autoFire.onAutomaticEvent(input.eventType, input.metadata ?? {});
1375
+ autoFire.onAutomaticEvent(input.eventType, input.metadata ?? {}, client.getSessionId());
1082
1376
  } catch {
1083
1377
  }
1084
1378
  }
@@ -1455,6 +1749,8 @@ function createTrackingClient(config) {
1455
1749
  session_id: sessionId,
1456
1750
  visitor_id: visitorId,
1457
1751
  gclid: params.gclid,
1752
+ wbraid: params.wbraid,
1753
+ gbraid: params.gbraid,
1458
1754
  fbclid: params.fbclid,
1459
1755
  fbc: getFbcCookie(),
1460
1756
  fbp: getFbpCookie(),
@@ -1501,6 +1797,13 @@ function createTrackingClient(config) {
1501
1797
  function trackEvent(input) {
1502
1798
  if (destroyed) return;
1503
1799
  if (!input || typeof input.eventType !== "string" || input.eventType.length === 0) return;
1800
+ if (input.eventType === "form_submit" && getConsentState() !== "denied") {
1801
+ try {
1802
+ const fields = input.metadata?.form?.fields;
1803
+ if (fields) stashUserDataFromFormFields(fields, config.phone?.defaultCountry);
1804
+ } catch {
1805
+ }
1806
+ }
1504
1807
  const occurredAt = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : (/* @__PURE__ */ new Date()).toISOString();
1505
1808
  queue.push({
1506
1809
  event_type: input.eventType,
@@ -2329,46 +2632,6 @@ function attachCtaClickCapture(client, config) {
2329
2632
  };
2330
2633
  }
2331
2634
 
2332
- // ../tracking-core/src/phone.ts
2333
- import { AsYouType, parsePhoneNumberFromString } from "libphonenumber-js";
2334
- var DEFAULT_PHONE_COUNTRY = "CA";
2335
- function parsePhone(raw, country) {
2336
- const region = country ?? DEFAULT_PHONE_COUNTRY;
2337
- const parsed = parsePhoneNumberFromString(raw ?? "", region);
2338
- if (!parsed) {
2339
- return { e164: null, national: "", international: "", country: region, isValid: false };
2340
- }
2341
- const isValid = parsed.isValid();
2342
- return {
2343
- // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
2344
- // (e.g. too few digits) still parses but must not be transmitted.
2345
- e164: isValid ? parsed.number : null,
2346
- national: parsed.formatNational(),
2347
- international: parsed.formatInternational(),
2348
- country: parsed.country ?? region,
2349
- isValid
2350
- };
2351
- }
2352
- function toE164(raw, country) {
2353
- return parsePhone(raw, country).e164;
2354
- }
2355
- function formatPhone(value, format = "national", country) {
2356
- const parsed = parsePhone(value, country);
2357
- if (typeof format === "function") return format(parsed);
2358
- switch (format) {
2359
- case "international":
2360
- return parsed.international || value;
2361
- case "e164":
2362
- return parsed.e164 ?? value;
2363
- case "national":
2364
- default:
2365
- return parsed.national || value;
2366
- }
2367
- }
2368
- function formatPhoneAsTyped(raw, country) {
2369
- return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
2370
- }
2371
-
2372
2635
  // ../tracking-core/src/triggers/phone-click-capture.ts
2373
2636
  var DEFAULT_TEL_SELECTOR = 'a[href^="tel:"]';
2374
2637
  function safeDecodeURIComponent(value) {
@@ -2478,6 +2741,8 @@ async function salesRequest(config, method, path, body) {
2478
2741
  // ../tracking-core/src/resources/sales/client.ts
2479
2742
  function fireRecordedConversions(firing, input, recorded, sale, currency) {
2480
2743
  if (!firing) return;
2744
+ const userData = input.customer_email || input.customer_phone ? { email: input.customer_email ?? null, phone: input.customer_phone ?? null } : null;
2745
+ if (userData && getConsentState() !== "denied") stashUserData(userData);
2481
2746
  const txnBase = input.external_id ?? sale.id;
2482
2747
  for (const item of recorded) {
2483
2748
  if (!item.service) continue;
@@ -2492,12 +2757,15 @@ function fireRecordedConversions(firing, input, recorded, sale, currency) {
2492
2757
  const config = firing.getFiring(item.service);
2493
2758
  if (!config) continue;
2494
2759
  const cents = item.amount_cents ?? config.value_cents ?? null;
2495
- fireConversionWithConsent({
2496
- sendTo: config.send_to,
2497
- value: cents != null ? fromMinor(cents, currency) : null,
2498
- currency: config.currency ?? currency,
2499
- transactionId: `${txnBase}:${item.service}`
2500
- });
2760
+ fireConversionWithConsent(
2761
+ {
2762
+ sendTo: config.send_to,
2763
+ value: cents != null ? fromMinor(cents, currency) : null,
2764
+ currency: config.currency ?? currency,
2765
+ transactionId: `${txnBase}:${item.service}`
2766
+ },
2767
+ { userData }
2768
+ );
2501
2769
  }
2502
2770
  }
2503
2771
  function createSalesClient(config) {
@@ -2532,6 +2800,7 @@ function createSalesClient(config) {
2532
2800
  recordSale: record,
2533
2801
  trackConversion(key, options) {
2534
2802
  if (config.firing && "fireConversion" in config.firing) {
2803
+ if (options?.userData && getConsentState() !== "denied") stashUserData(options.userData);
2535
2804
  config.firing.fireConversion(key, {
2536
2805
  value: options?.value ?? void 0,
2537
2806
  currency: options?.currency ?? config.defaultCurrency ?? void 0,
@@ -2544,12 +2813,15 @@ function createSalesClient(config) {
2544
2813
  const currency = firing.currency ?? options?.currency ?? config.defaultCurrency ?? null;
2545
2814
  const cents = firing.value_cents ?? null;
2546
2815
  const value = options?.value ?? (cents != null && currency ? fromMinor(cents, currency) : null);
2547
- fireConversionWithConsent({
2548
- sendTo: firing.send_to,
2549
- value,
2550
- currency,
2551
- transactionId: options?.transactionId ?? null
2552
- });
2816
+ fireConversionWithConsent(
2817
+ {
2818
+ sendTo: firing.send_to,
2819
+ value,
2820
+ currency,
2821
+ transactionId: options?.transactionId ?? null
2822
+ },
2823
+ { userData: options?.userData }
2824
+ );
2553
2825
  },
2554
2826
  async list(query) {
2555
2827
  const { cursor, limit, sort, order, want_total, ...filters } = query ?? {};
@@ -3059,7 +3331,7 @@ function AdPlatformTracking({
3059
3331
  if (!trackingConfig) return;
3060
3332
  const runtime = getTrackingConfigRuntime(trackingConfig);
3061
3333
  if (standalonePageView) runtime.queuePageView();
3062
- else void runtime.revalidate();
3334
+ else runtime.start();
3063
3335
  }, [trackingConfigKey, standalonePageView]);
3064
3336
  const gtagAll = trackingConfig ? [] : gtagIds ? Object.values(gtagIds).filter((id) => id != null) : gtagId ? [gtagId] : [];
3065
3337
  const metaAll = metaPixelIds ? Object.values(metaPixelIds).filter((id) => id != null) : metaPixelId ? [metaPixelId] : [];
@@ -3111,7 +3383,7 @@ function GoogleAdsTracking(props) {
3111
3383
  import { usePathname, useSearchParams } from "next/navigation";
3112
3384
 
3113
3385
  // package.json
3114
- var version = "0.18.1";
3386
+ var version = "0.19.1";
3115
3387
 
3116
3388
  // src/factory.tsx
3117
3389
  import {
@@ -3263,22 +3535,18 @@ function createTracking(options) {
3263
3535
  const timeOnSiteConfig = triggers.automatic.time_on_site ?? null;
3264
3536
  function TrackingProvider({ children }) {
3265
3537
  const pathname = usePathname();
3266
- const client = useMemo2(
3267
- () => createTypedClient(
3268
- getOrCreateTrackingClient({
3269
- apiKey,
3270
- endpoint,
3271
- surface: "next",
3272
- packageName: "@aranova/tracking-next",
3273
- sdkVersion: version,
3274
- triggers,
3275
- environment,
3276
- activeGtagIds: gtagIds,
3277
- debug
3278
- }),
3538
+ const rawClient = useMemo2(
3539
+ () => getOrCreateTrackingClient({
3540
+ apiKey,
3541
+ endpoint,
3542
+ surface: "next",
3543
+ packageName: "@aranova/tracking-next",
3544
+ sdkVersion: version,
3279
3545
  triggers,
3280
- { debug }
3281
- ),
3546
+ environment,
3547
+ activeGtagIds: gtagIds,
3548
+ debug
3549
+ }),
3282
3550
  []
3283
3551
  );
3284
3552
  const conversionStore = useMemo2(
@@ -3292,33 +3560,22 @@ function createTracking(options) {
3292
3560
  const autoFire = conversionStore ? createConversionAutoFire(conversionStore) : null;
3293
3561
  return (c) => autoFire ? withConversionAutoFire(c, autoFire) : c;
3294
3562
  }, [conversionStore]);
3563
+ const client = useMemo2(
3564
+ () => createTypedClient(wrapClient(rawClient), triggers, { debug }),
3565
+ [rawClient, wrapClient]
3566
+ );
3567
+ useEffect4(() => {
3568
+ if (trackingConfig && conversionStore && "start" in conversionStore) {
3569
+ conversionStore.start();
3570
+ }
3571
+ }, [conversionStore]);
3295
3572
  const firePageView = useCallback3(() => {
3296
- const rawClient = getOrCreateTrackingClient({
3297
- apiKey,
3298
- endpoint,
3299
- surface: "next",
3300
- packageName: "@aranova/tracking-next",
3301
- triggers,
3302
- environment,
3303
- activeGtagIds: gtagIds,
3304
- debug
3305
- });
3306
3573
  fireManualPageView(wrapClient(rawClient));
3307
3574
  if (trackingConfig) getTrackingConfigRuntime(trackingConfig).queuePageView();
3308
- }, [wrapClient]);
3575
+ }, [rawClient, wrapClient]);
3309
3576
  const firedPagesRef = useRef(/* @__PURE__ */ new Set());
3310
3577
  useEffect4(() => {
3311
3578
  if (!specificPageVisitConfig || pathname === null) return;
3312
- const rawClient = getOrCreateTrackingClient({
3313
- apiKey,
3314
- endpoint,
3315
- surface: "next",
3316
- packageName: "@aranova/tracking-next",
3317
- triggers,
3318
- environment,
3319
- activeGtagIds: gtagIds,
3320
- debug
3321
- });
3322
3579
  const detectorClient = wrapClient(rawClient);
3323
3580
  for (const { name, pathPattern } of specificPageVisitConfig.pages) {
3324
3581
  if (!pathPattern.test(pathname)) continue;
@@ -3335,7 +3592,7 @@ function createTracking(options) {
3335
3592
  occurredAt: null
3336
3593
  });
3337
3594
  }
3338
- }, [pathname]);
3595
+ }, [pathname, rawClient, wrapClient]);
3339
3596
  const multiPageSessionConfig = triggers.automatic.multi_page_session ?? null;
3340
3597
  const mpsLastPathRef = useRef(null);
3341
3598
  useEffect4(() => {
@@ -3361,17 +3618,6 @@ function createTracking(options) {
3361
3618
  store.setItem(MPS_PATHS_KEY, JSON.stringify([...paths]));
3362
3619
  if (paths.size >= multiPageSessionConfig.pageThreshold) {
3363
3620
  store.setItem(MPS_FIRED_KEY, "1");
3364
- const rawClient = getOrCreateTrackingClient({
3365
- apiKey,
3366
- endpoint,
3367
- surface: "next",
3368
- packageName: "@aranova/tracking-next",
3369
- sdkVersion: version,
3370
- triggers,
3371
- environment,
3372
- activeGtagIds: gtagIds,
3373
- debug
3374
- });
3375
3621
  wrapClient(rawClient).trackEvent({
3376
3622
  eventType: "multi_page_session",
3377
3623
  metadata: {
@@ -3384,18 +3630,8 @@ function createTracking(options) {
3384
3630
  }
3385
3631
  } catch {
3386
3632
  }
3387
- }, [pathname]);
3633
+ }, [multiPageSessionConfig, pathname, rawClient, wrapClient]);
3388
3634
  useEffect4(() => {
3389
- const rawClient = getOrCreateTrackingClient({
3390
- apiKey,
3391
- endpoint,
3392
- surface: "next",
3393
- packageName: "@aranova/tracking-next",
3394
- triggers,
3395
- environment,
3396
- activeGtagIds: gtagIds,
3397
- debug
3398
- });
3399
3635
  return attachClientCapturesOnce(rawClient, () => {
3400
3636
  const detachers = [];
3401
3637
  const detectorClient = wrapClient(rawClient);
@@ -3426,7 +3662,7 @@ function createTracking(options) {
3426
3662
  }
3427
3663
  };
3428
3664
  });
3429
- }, []);
3665
+ }, [rawClient, wrapClient]);
3430
3666
  return /* @__PURE__ */ jsxs4(TrackingContext.Provider, { value: client, children: [
3431
3667
  /* @__PURE__ */ jsx5(Suspense, { fallback: null, children: /* @__PURE__ */ jsx5(PageViewTracker, { fire: firePageView }) }),
3432
3668
  /* @__PURE__ */ jsx5(PhoneConfigProvider, { value: phone ?? null, children })
@@ -3457,6 +3693,7 @@ export {
3457
3693
  TRACKING_PARAM_KEYS,
3458
3694
  TRACKING_RANGES,
3459
3695
  captureTrackingParamsFromLocation,
3696
+ clearStashedUserData,
3460
3697
  createSalesClient,
3461
3698
  createTracking,
3462
3699
  createTrackingClientContext,
@@ -3485,6 +3722,7 @@ export {
3485
3722
  saleUpdateSchema,
3486
3723
  salesRequest,
3487
3724
  setConsentState,
3725
+ stashUserData,
3488
3726
  toE164,
3489
3727
  toMinor,
3490
3728
  useConsent,