@aranova/tracking-next 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,14 +664,12 @@ function markFired(input) {
510
664
  } catch {
511
665
  }
512
666
  }
513
- function fireOnce(input) {
667
+ function fireConversionWithConsent(input, options) {
668
+ if (getConsentState() === "denied") return;
514
669
  if (alreadyFired(input)) return;
670
+ applyUserDataForConversion(options?.userData);
515
671
  if (fireGtagConversion(input)) markFired(input);
516
672
  }
517
- function fireConversionWithConsent(input) {
518
- if (getConsentState() === "denied") return;
519
- fireOnce(input);
520
- }
521
673
 
522
674
  // ../tracking-core/src/resources/conversion-config.ts
523
675
  function isStringMap(value) {
@@ -962,6 +1114,7 @@ var TrackingConfigRuntime = class {
962
1114
  if (!firing || getConsentState() === "denied") continue;
963
1115
  const currency = item.currency ?? firing.currency ?? null;
964
1116
  const value = item.value ?? (firing.value_cents != null && currency ? fromMinor(firing.value_cents, currency) : null);
1117
+ applyUserDataForConversion();
965
1118
  fireGtagConversion({
966
1119
  sendTo: firing.send_to,
967
1120
  value,
@@ -1455,6 +1608,8 @@ function createTrackingClient(config) {
1455
1608
  session_id: sessionId,
1456
1609
  visitor_id: visitorId,
1457
1610
  gclid: params.gclid,
1611
+ wbraid: params.wbraid,
1612
+ gbraid: params.gbraid,
1458
1613
  fbclid: params.fbclid,
1459
1614
  fbc: getFbcCookie(),
1460
1615
  fbp: getFbpCookie(),
@@ -1501,6 +1656,13 @@ function createTrackingClient(config) {
1501
1656
  function trackEvent(input) {
1502
1657
  if (destroyed) return;
1503
1658
  if (!input || typeof input.eventType !== "string" || input.eventType.length === 0) return;
1659
+ if (input.eventType === "form_submit" && getConsentState() !== "denied") {
1660
+ try {
1661
+ const fields = input.metadata?.form?.fields;
1662
+ if (fields) stashUserDataFromFormFields(fields, config.phone?.defaultCountry);
1663
+ } catch {
1664
+ }
1665
+ }
1504
1666
  const occurredAt = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : (/* @__PURE__ */ new Date()).toISOString();
1505
1667
  queue.push({
1506
1668
  event_type: input.eventType,
@@ -2329,46 +2491,6 @@ function attachCtaClickCapture(client, config) {
2329
2491
  };
2330
2492
  }
2331
2493
 
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
2494
  // ../tracking-core/src/triggers/phone-click-capture.ts
2373
2495
  var DEFAULT_TEL_SELECTOR = 'a[href^="tel:"]';
2374
2496
  function safeDecodeURIComponent(value) {
@@ -2478,6 +2600,8 @@ async function salesRequest(config, method, path, body) {
2478
2600
  // ../tracking-core/src/resources/sales/client.ts
2479
2601
  function fireRecordedConversions(firing, input, recorded, sale, currency) {
2480
2602
  if (!firing) return;
2603
+ const userData = input.customer_email || input.customer_phone ? { email: input.customer_email ?? null, phone: input.customer_phone ?? null } : null;
2604
+ if (userData && getConsentState() !== "denied") stashUserData(userData);
2481
2605
  const txnBase = input.external_id ?? sale.id;
2482
2606
  for (const item of recorded) {
2483
2607
  if (!item.service) continue;
@@ -2492,12 +2616,15 @@ function fireRecordedConversions(firing, input, recorded, sale, currency) {
2492
2616
  const config = firing.getFiring(item.service);
2493
2617
  if (!config) continue;
2494
2618
  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
- });
2619
+ fireConversionWithConsent(
2620
+ {
2621
+ sendTo: config.send_to,
2622
+ value: cents != null ? fromMinor(cents, currency) : null,
2623
+ currency: config.currency ?? currency,
2624
+ transactionId: `${txnBase}:${item.service}`
2625
+ },
2626
+ { userData }
2627
+ );
2501
2628
  }
2502
2629
  }
2503
2630
  function createSalesClient(config) {
@@ -2532,6 +2659,7 @@ function createSalesClient(config) {
2532
2659
  recordSale: record,
2533
2660
  trackConversion(key, options) {
2534
2661
  if (config.firing && "fireConversion" in config.firing) {
2662
+ if (options?.userData && getConsentState() !== "denied") stashUserData(options.userData);
2535
2663
  config.firing.fireConversion(key, {
2536
2664
  value: options?.value ?? void 0,
2537
2665
  currency: options?.currency ?? config.defaultCurrency ?? void 0,
@@ -2544,12 +2672,15 @@ function createSalesClient(config) {
2544
2672
  const currency = firing.currency ?? options?.currency ?? config.defaultCurrency ?? null;
2545
2673
  const cents = firing.value_cents ?? null;
2546
2674
  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
- });
2675
+ fireConversionWithConsent(
2676
+ {
2677
+ sendTo: firing.send_to,
2678
+ value,
2679
+ currency,
2680
+ transactionId: options?.transactionId ?? null
2681
+ },
2682
+ { userData: options?.userData }
2683
+ );
2553
2684
  },
2554
2685
  async list(query) {
2555
2686
  const { cursor, limit, sort, order, want_total, ...filters } = query ?? {};
@@ -3111,7 +3242,7 @@ function GoogleAdsTracking(props) {
3111
3242
  import { usePathname, useSearchParams } from "next/navigation";
3112
3243
 
3113
3244
  // package.json
3114
- var version = "0.18.1";
3245
+ var version = "0.19.0";
3115
3246
 
3116
3247
  // src/factory.tsx
3117
3248
  import {
@@ -3457,6 +3588,7 @@ export {
3457
3588
  TRACKING_PARAM_KEYS,
3458
3589
  TRACKING_RANGES,
3459
3590
  captureTrackingParamsFromLocation,
3591
+ clearStashedUserData,
3460
3592
  createSalesClient,
3461
3593
  createTracking,
3462
3594
  createTrackingClientContext,
@@ -3485,6 +3617,7 @@ export {
3485
3617
  saleUpdateSchema,
3486
3618
  salesRequest,
3487
3619
  setConsentState,
3620
+ stashUserData,
3488
3621
  toE164,
3489
3622
  toMinor,
3490
3623
  useConsent,