@aranova/tracking-react 0.14.2 → 0.15.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.js CHANGED
@@ -23,6 +23,7 @@ __export(src_exports, {
23
23
  AdPlatformTracking: () => AdPlatformTracking,
24
24
  AranovaApiError: () => AranovaApiError,
25
25
  ConsentBanner: () => ConsentBanner,
26
+ DEFAULT_DECLINE_TTL_DAYS: () => DEFAULT_DECLINE_TTL_DAYS,
26
27
  DEFAULT_PHONE_COUNTRY: () => DEFAULT_PHONE_COUNTRY,
27
28
  GoogleAdsTracking: () => GoogleAdsTracking,
28
29
  NAMED_RANGES: () => NAMED_RANGES,
@@ -42,7 +43,11 @@ __export(src_exports, {
42
43
  formatPhone: () => formatPhone,
43
44
  formatPhoneAsTyped: () => formatPhoneAsTyped,
44
45
  fromMinor: () => fromMinor,
46
+ getConsentChoice: () => getConsentChoice,
45
47
  getConsentState: () => getConsentState,
48
+ onConsentChange: () => onConsentChange,
49
+ optIn: () => optIn,
50
+ optOut: () => optOut,
46
51
  parsePhone: () => parsePhone,
47
52
  phoneField: () => phoneField,
48
53
  resetConsent: () => resetConsent,
@@ -57,6 +62,7 @@ __export(src_exports, {
57
62
  toMinor: () => toMinor,
58
63
  useConsent: () => useConsent,
59
64
  useConsentState: () => useConsentState,
65
+ useCookiePreferences: () => useCookiePreferences,
60
66
  useGclid: () => useGclid,
61
67
  usePhoneConfig: () => usePhoneConfig,
62
68
  usePhoneField: () => usePhoneField,
@@ -73,13 +79,30 @@ var import_react = require("react");
73
79
  // ../tracking-core/src/consent.ts
74
80
  var CONSENT_STATE_KEY = "consent_state";
75
81
  var CONSENT_TIMESTAMP_KEY = "consent_timestamp";
76
- var grantedListeners = /* @__PURE__ */ new Set();
77
- function onConsentGranted(listener) {
78
- grantedListeners.add(listener);
82
+ var CONSENT_EXPIRES_AT_KEY = "consent_expires_at";
83
+ var DEFAULT_DECLINE_TTL_DAYS = 90;
84
+ var DAY_MS = 864e5;
85
+ var DEFAULT_CHOICE = {
86
+ state: "granted",
87
+ source: "default",
88
+ updatedAt: null,
89
+ expiresAt: null
90
+ };
91
+ var changeListeners = /* @__PURE__ */ new Set();
92
+ function onConsentChange(listener) {
93
+ changeListeners.add(listener);
79
94
  return () => {
80
- grantedListeners.delete(listener);
95
+ changeListeners.delete(listener);
81
96
  };
82
97
  }
98
+ function notifyConsentChanged(choice) {
99
+ for (const listener of changeListeners) {
100
+ try {
101
+ listener(choice);
102
+ } catch {
103
+ }
104
+ }
105
+ }
83
106
  function buildConsentPayload(state) {
84
107
  return {
85
108
  ad_storage: state,
@@ -88,47 +111,74 @@ function buildConsentPayload(state) {
88
111
  analytics_storage: state
89
112
  };
90
113
  }
91
- function getConsentState() {
92
- if (typeof window === "undefined") return "pending";
114
+ function getConsentChoice() {
115
+ if (typeof window === "undefined") return DEFAULT_CHOICE;
93
116
  try {
94
- const storedState = window.localStorage.getItem(CONSENT_STATE_KEY);
95
- if (storedState === "granted" || storedState === "denied") return storedState;
117
+ const stored = window.localStorage.getItem(CONSENT_STATE_KEY);
118
+ const updatedAt = window.localStorage.getItem(CONSENT_TIMESTAMP_KEY);
119
+ if (stored === "granted")
120
+ return { state: "granted", source: "explicit", updatedAt, expiresAt: null };
121
+ if (stored === "denied") {
122
+ let expiresAt = window.localStorage.getItem(CONSENT_EXPIRES_AT_KEY);
123
+ if (!expiresAt) {
124
+ expiresAt = new Date(Date.now() + DEFAULT_DECLINE_TTL_DAYS * DAY_MS).toISOString();
125
+ try {
126
+ window.localStorage.setItem(CONSENT_EXPIRES_AT_KEY, expiresAt);
127
+ } catch {
128
+ }
129
+ }
130
+ if (!(Date.parse(expiresAt) <= Date.now()))
131
+ return { state: "denied", source: "explicit", updatedAt, expiresAt };
132
+ }
96
133
  } catch {
97
134
  }
98
- return "pending";
135
+ return DEFAULT_CHOICE;
99
136
  }
100
- function setConsentState(state) {
137
+ function getConsentState() {
138
+ return getConsentChoice().state;
139
+ }
140
+ function pushConsentToPlatforms(state) {
101
141
  if (typeof window === "undefined") return;
102
- try {
103
- window.localStorage.setItem(CONSENT_STATE_KEY, state);
104
- window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, (/* @__PURE__ */ new Date()).toISOString());
105
- } catch {
106
- }
107
142
  if (typeof window.gtag === "function")
108
143
  window.gtag("consent", "update", buildConsentPayload(state));
109
144
  if (typeof window.fbq === "function")
110
145
  window.fbq("consent", state === "granted" ? "grant" : "revoke");
111
- if (state === "granted") {
112
- for (const listener of grantedListeners) {
113
- try {
114
- listener();
115
- } catch {
116
- }
146
+ }
147
+ function setConsentState(state, options) {
148
+ if (typeof window === "undefined") return;
149
+ const requestedTtl = options?.declineTtlDays;
150
+ const ttlDays = typeof requestedTtl === "number" && Number.isFinite(requestedTtl) && requestedTtl > 0 ? requestedTtl : DEFAULT_DECLINE_TTL_DAYS;
151
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
152
+ const expiresAt = state === "denied" ? new Date(Date.now() + ttlDays * DAY_MS).toISOString() : null;
153
+ try {
154
+ window.localStorage.setItem(CONSENT_STATE_KEY, state);
155
+ window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, updatedAt);
156
+ if (expiresAt !== null) {
157
+ window.localStorage.setItem(CONSENT_EXPIRES_AT_KEY, expiresAt);
158
+ } else {
159
+ window.localStorage.removeItem(CONSENT_EXPIRES_AT_KEY);
117
160
  }
161
+ } catch {
118
162
  }
163
+ pushConsentToPlatforms(state);
164
+ notifyConsentChanged({ state, source: "explicit", updatedAt, expiresAt });
165
+ }
166
+ function optIn() {
167
+ setConsentState("granted");
168
+ }
169
+ function optOut(options) {
170
+ setConsentState("denied", options);
119
171
  }
120
172
  function resetConsent() {
121
173
  if (typeof window === "undefined") return;
122
174
  try {
123
175
  window.localStorage.removeItem(CONSENT_STATE_KEY);
124
176
  window.localStorage.removeItem(CONSENT_TIMESTAMP_KEY);
177
+ window.localStorage.removeItem(CONSENT_EXPIRES_AT_KEY);
125
178
  } catch {
126
179
  }
127
- }
128
- function restoreStoredConsent() {
129
- const consentState = getConsentState();
130
- if (consentState === "granted" || consentState === "denied") setConsentState(consentState);
131
- return consentState;
180
+ pushConsentToPlatforms("granted");
181
+ notifyConsentChanged(DEFAULT_CHOICE);
132
182
  }
133
183
 
134
184
  // ../tracking-core/src/tracking.ts
@@ -272,13 +322,11 @@ function fireGtagConversion(input) {
272
322
  }
273
323
  function applyDefaultConsentState() {
274
324
  const gtag = ensureGtagFunction();
275
- gtag("consent", "default", {
276
- ad_storage: "denied",
277
- ad_user_data: "denied",
278
- ad_personalization: "denied",
279
- analytics_storage: "denied",
280
- wait_for_update: 500
281
- });
325
+ gtag(
326
+ "consent",
327
+ "default",
328
+ buildConsentPayload(getConsentState() === "denied" ? "denied" : "granted")
329
+ );
282
330
  }
283
331
  function loadGtagScript(gtagId) {
284
332
  if (typeof document === "undefined") return;
@@ -304,7 +352,6 @@ function bootstrapGoogleAdsTracking(gtagId) {
304
352
  applyDefaultConsentState();
305
353
  loadGtagScript(gtagId);
306
354
  initializeGtag(gtagId);
307
- restoreStoredConsent();
308
355
  }
309
356
  function bootstrapMultipleGtags(gtagIds) {
310
357
  if (typeof window === "undefined" || typeof document === "undefined") return;
@@ -319,7 +366,6 @@ function bootstrapMultipleGtags(gtagIds) {
319
366
  for (const id of ids) {
320
367
  gtag("config", id);
321
368
  }
322
- restoreStoredConsent();
323
369
  }
324
370
 
325
371
  // ../tracking-core/src/fbq.ts
@@ -379,7 +425,7 @@ function ensureFbqFunction() {
379
425
  return fbq;
380
426
  }
381
427
  function applyDefaultMetaConsentState() {
382
- ensureFbqFunction()("consent", "revoke");
428
+ ensureFbqFunction()("consent", getConsentState() === "denied" ? "revoke" : "grant");
383
429
  }
384
430
  function loadFbeventsScript() {
385
431
  if (typeof document === "undefined") return;
@@ -399,19 +445,12 @@ function initializeMetaPixel(pixelId) {
399
445
  fbq("init", pixelId);
400
446
  fbq("track", "PageView");
401
447
  }
402
- function restoreMetaConsentState() {
403
- if (typeof window === "undefined") return;
404
- const state = getConsentState();
405
- if (state === "granted") window.fbq?.("consent", "grant");
406
- else if (state === "denied") window.fbq?.("consent", "revoke");
407
- }
408
448
  function bootstrapMetaPixel(pixelId) {
409
449
  if (typeof window === "undefined" || typeof document === "undefined") return;
410
450
  if (!isValidMetaPixelId(pixelId)) return;
411
451
  applyDefaultMetaConsentState();
412
452
  loadFbeventsScript();
413
453
  initializeMetaPixel(pixelId);
414
- restoreMetaConsentState();
415
454
  captureFbc();
416
455
  }
417
456
  function bootstrapMultiplePixels(pixelIds) {
@@ -427,10 +466,75 @@ function bootstrapMultiplePixels(pixelIds) {
427
466
  fbq("init", id);
428
467
  }
429
468
  fbq("track", "PageView");
430
- restoreMetaConsentState();
431
469
  captureFbc();
432
470
  }
433
471
 
472
+ // ../tracking-core/src/landing.ts
473
+ var LANDING_STORAGE_KEY = "_aranova_track_landing";
474
+ var memoryRecord = null;
475
+ function sanitizeParams(value) {
476
+ if (typeof value !== "object" || value === null) return {};
477
+ const source = value;
478
+ return TRACKING_PARAM_KEYS.reduce((params, key) => {
479
+ const entry = source[key];
480
+ if (typeof entry === "string" && entry.length > 0) params[key] = entry;
481
+ return params;
482
+ }, {});
483
+ }
484
+ function readStoredRecord() {
485
+ try {
486
+ const raw = window.localStorage.getItem(LANDING_STORAGE_KEY);
487
+ if (!raw) return null;
488
+ const parsed = JSON.parse(raw);
489
+ if (typeof parsed.session_id !== "string" || parsed.session_id.length === 0) return null;
490
+ return { session_id: parsed.session_id, params: sanitizeParams(parsed.params) };
491
+ } catch {
492
+ return null;
493
+ }
494
+ }
495
+ function writeRecord(record) {
496
+ memoryRecord = record;
497
+ try {
498
+ window.localStorage.setItem(LANDING_STORAGE_KEY, JSON.stringify(record));
499
+ } catch {
500
+ }
501
+ }
502
+ function captureFromUrl(url) {
503
+ try {
504
+ const resolved = new URL(url ?? window.location.href, window.location.origin);
505
+ return getTrackingQueryValues(resolved.searchParams);
506
+ } catch {
507
+ return {};
508
+ }
509
+ }
510
+ function getOrCaptureLandingParams(sessionId, url) {
511
+ if (typeof window === "undefined") return {};
512
+ const stored = readStoredRecord();
513
+ if (stored && stored.session_id === sessionId) {
514
+ memoryRecord = stored;
515
+ return stored.params;
516
+ }
517
+ if (memoryRecord && memoryRecord.session_id === sessionId) {
518
+ return memoryRecord.params;
519
+ }
520
+ const record = { session_id: sessionId, params: captureFromUrl(url) };
521
+ writeRecord(record);
522
+ return record.params;
523
+ }
524
+ function buildLandingPayloadFields(sessionId, override) {
525
+ const params = override ?? (typeof window === "undefined" ? null : getOrCaptureLandingParams(sessionId));
526
+ if (params === null) return {};
527
+ return {
528
+ landing_gclid: params.gclid ?? null,
529
+ landing_fbclid: params.fbclid ?? null,
530
+ landing_utm_source: params.utm_source ?? null,
531
+ landing_utm_medium: params.utm_medium ?? null,
532
+ landing_utm_campaign: params.utm_campaign ?? null,
533
+ landing_utm_term: params.utm_term ?? null,
534
+ landing_utm_content: params.utm_content ?? null
535
+ };
536
+ }
537
+
434
538
  // ../tracking-core/src/payloads.ts
435
539
  function createTrackingClientContext(surface, input = {}) {
436
540
  return {
@@ -457,6 +561,9 @@ function createTrackingSessionUpsertPayload(trackingParams, input, context) {
457
561
  utm_campaign: trackingParams.utm_campaign,
458
562
  utm_term: trackingParams.utm_term,
459
563
  utm_content: trackingParams.utm_content,
564
+ // Omitted entirely when the landing isn't observable (SSR, no override) —
565
+ // present-as-null would wrongly tell the backend "landed with no params".
566
+ ...buildLandingPayloadFields(input.sessionId, input.landingParams),
460
567
  first_page: input.firstPage ?? null,
461
568
  consent_state: input.consentState ?? null,
462
569
  context
@@ -478,8 +585,6 @@ function createTrackingEventCreatePayload(trackingParams, input, context) {
478
585
 
479
586
  // ../tracking-core/src/resources/conversion-firing.ts
480
587
  var DEDUP_PREFIX = "_aranova_conv_";
481
- var MAX_PENDING = 100;
482
- var pendingQueue = [];
483
588
  function dedupKey(input) {
484
589
  return `${DEDUP_PREFIX}${input.transactionId ?? ""}:${input.sendTo}`;
485
590
  }
@@ -503,23 +608,9 @@ function fireOnce(input) {
503
608
  if (fireGtagConversion(input)) markFired(input);
504
609
  }
505
610
  function fireConversionWithConsent(input) {
506
- const state = getConsentState();
507
- if (state === "denied") return;
508
- if (state === "pending") {
509
- if (pendingQueue.length >= MAX_PENDING) pendingQueue.shift();
510
- pendingQueue.push(input);
511
- return;
512
- }
611
+ if (getConsentState() === "denied") return;
513
612
  fireOnce(input);
514
613
  }
515
- function flushPendingConversions() {
516
- if (getConsentState() !== "granted") return;
517
- while (pendingQueue.length > 0) {
518
- const input = pendingQueue.shift();
519
- if (input) fireOnce(input);
520
- }
521
- }
522
- if (typeof window !== "undefined") onConsentGranted(flushPendingConversions);
523
614
 
524
615
  // ../tracking-core/src/resources/conversion-config.ts
525
616
  function isStringMap(value) {
@@ -1065,7 +1156,13 @@ function readTrackingParams() {
1065
1156
  }
1066
1157
  function consentSnapshot() {
1067
1158
  try {
1068
- return { state: getConsentState() };
1159
+ const choice = getConsentChoice();
1160
+ return {
1161
+ state: choice.state,
1162
+ source: choice.source,
1163
+ updated_at: choice.updatedAt,
1164
+ expires_at: choice.expiresAt
1165
+ };
1069
1166
  } catch {
1070
1167
  return null;
1071
1168
  }
@@ -1138,6 +1235,7 @@ function createTrackingClient(config) {
1138
1235
  initialParams = captureTrackingParamsFromLocation();
1139
1236
  } catch {
1140
1237
  }
1238
+ getOrCaptureLandingParams(sessionId);
1141
1239
  try {
1142
1240
  captureFbc();
1143
1241
  } catch {
@@ -1187,6 +1285,11 @@ function createTrackingClient(config) {
1187
1285
  utm_campaign: params.utm_campaign,
1188
1286
  utm_term: params.utm_term,
1189
1287
  utm_content: params.utm_content,
1288
+ // Landing params for the CURRENT session id — captured on the spot when
1289
+ // the session just rotated (the current URL is the rotated session's
1290
+ // landing), reused from the stored record otherwise. Keys are omitted
1291
+ // entirely when the landing isn't observable (SSR).
1292
+ ...buildLandingPayloadFields(sessionId),
1190
1293
  first_page: firstPage,
1191
1294
  consent_state: consentSnapshot(),
1192
1295
  context
@@ -2220,36 +2323,68 @@ function useTrackingParams() {
2220
2323
  }, []);
2221
2324
  return trackingParams;
2222
2325
  }
2223
- function useConsentState() {
2224
- return useConsent().state;
2225
- }
2226
- function useConsent() {
2227
- const [state, setState] = (0, import_react.useState)("pending");
2326
+ var DEFAULT_CHOICE2 = {
2327
+ state: "granted",
2328
+ source: "default",
2329
+ updatedAt: null,
2330
+ expiresAt: null
2331
+ };
2332
+ function useCookiePreferences(options) {
2333
+ const [choice, setChoice] = (0, import_react.useState)(DEFAULT_CHOICE2);
2334
+ const ttlDays = options?.declineTtlDays;
2228
2335
  (0, import_react.useEffect)(() => {
2229
- setState(getConsentState());
2336
+ const sync = () => setChoice(getConsentChoice());
2337
+ sync();
2230
2338
  const handleStorage = (event) => {
2231
- if (event.key === CONSENT_STATE_KEY) setState(getConsentState());
2339
+ if (event.key === null || event.key === CONSENT_STATE_KEY || event.key === CONSENT_EXPIRES_AT_KEY || event.key === CONSENT_TIMESTAMP_KEY)
2340
+ sync();
2232
2341
  };
2233
2342
  window.addEventListener("storage", handleStorage);
2234
- return () => window.removeEventListener("storage", handleStorage);
2235
- }, []);
2236
- const accept = (0, import_react.useCallback)(() => {
2237
- setConsentState("granted");
2238
- setState("granted");
2343
+ const unsubscribe = onConsentChange(sync);
2344
+ return () => {
2345
+ window.removeEventListener("storage", handleStorage);
2346
+ unsubscribe();
2347
+ };
2239
2348
  }, []);
2240
- const decline = (0, import_react.useCallback)(() => {
2241
- setConsentState("denied");
2242
- setState("denied");
2349
+ const optOutAction = (0, import_react.useCallback)(() => {
2350
+ optOut(ttlDays != null ? { declineTtlDays: ttlDays } : void 0);
2351
+ }, [ttlDays]);
2352
+ const optInAction = (0, import_react.useCallback)(() => {
2353
+ optIn();
2243
2354
  }, []);
2244
2355
  const reset = (0, import_react.useCallback)(() => {
2245
2356
  resetConsent();
2246
- setState("pending");
2247
2357
  }, []);
2358
+ return {
2359
+ state: choice.state,
2360
+ source: choice.source,
2361
+ isDefault: choice.source === "default",
2362
+ isGranted: choice.state === "granted",
2363
+ isDenied: choice.state === "denied",
2364
+ updatedAt: choice.updatedAt,
2365
+ expiresAt: choice.expiresAt,
2366
+ optOut: optOutAction,
2367
+ optIn: optInAction,
2368
+ reset
2369
+ };
2370
+ }
2371
+ function useConsentState() {
2372
+ return useConsent().state;
2373
+ }
2374
+ function useConsent() {
2375
+ const {
2376
+ state,
2377
+ isGranted,
2378
+ isDenied,
2379
+ optIn: accept,
2380
+ optOut: decline,
2381
+ reset
2382
+ } = useCookiePreferences();
2248
2383
  return {
2249
2384
  state,
2250
- isPending: state === "pending",
2251
- isGranted: state === "granted",
2252
- isDenied: state === "denied",
2385
+ isPending: false,
2386
+ isGranted,
2387
+ isDenied,
2253
2388
  accept,
2254
2389
  decline,
2255
2390
  reset
@@ -2490,7 +2625,7 @@ function GoogleAdsTracking(props) {
2490
2625
  var import_react6 = require("react");
2491
2626
 
2492
2627
  // package.json
2493
- var version = "0.14.2";
2628
+ var version = "0.15.0";
2494
2629
 
2495
2630
  // ../tracking-core/src/phone-react.tsx
2496
2631
  var import_react5 = require("react");
@@ -2715,6 +2850,7 @@ function createTracking(options) {
2715
2850
  AdPlatformTracking,
2716
2851
  AranovaApiError,
2717
2852
  ConsentBanner,
2853
+ DEFAULT_DECLINE_TTL_DAYS,
2718
2854
  DEFAULT_PHONE_COUNTRY,
2719
2855
  GoogleAdsTracking,
2720
2856
  NAMED_RANGES,
@@ -2734,7 +2870,11 @@ function createTracking(options) {
2734
2870
  formatPhone,
2735
2871
  formatPhoneAsTyped,
2736
2872
  fromMinor,
2873
+ getConsentChoice,
2737
2874
  getConsentState,
2875
+ onConsentChange,
2876
+ optIn,
2877
+ optOut,
2738
2878
  parsePhone,
2739
2879
  phoneField,
2740
2880
  resetConsent,
@@ -2749,6 +2889,7 @@ function createTracking(options) {
2749
2889
  toMinor,
2750
2890
  useConsent,
2751
2891
  useConsentState,
2892
+ useCookiePreferences,
2752
2893
  useGclid,
2753
2894
  usePhoneConfig,
2754
2895
  usePhoneField,