@aranova/tracking-react 0.14.1 → 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.mjs CHANGED
@@ -7,13 +7,30 @@ import { useCallback, useEffect, useState } from "react";
7
7
  // ../tracking-core/src/consent.ts
8
8
  var CONSENT_STATE_KEY = "consent_state";
9
9
  var CONSENT_TIMESTAMP_KEY = "consent_timestamp";
10
- var grantedListeners = /* @__PURE__ */ new Set();
11
- function onConsentGranted(listener) {
12
- grantedListeners.add(listener);
10
+ var CONSENT_EXPIRES_AT_KEY = "consent_expires_at";
11
+ var DEFAULT_DECLINE_TTL_DAYS = 90;
12
+ var DAY_MS = 864e5;
13
+ var DEFAULT_CHOICE = {
14
+ state: "granted",
15
+ source: "default",
16
+ updatedAt: null,
17
+ expiresAt: null
18
+ };
19
+ var changeListeners = /* @__PURE__ */ new Set();
20
+ function onConsentChange(listener) {
21
+ changeListeners.add(listener);
13
22
  return () => {
14
- grantedListeners.delete(listener);
23
+ changeListeners.delete(listener);
15
24
  };
16
25
  }
26
+ function notifyConsentChanged(choice) {
27
+ for (const listener of changeListeners) {
28
+ try {
29
+ listener(choice);
30
+ } catch {
31
+ }
32
+ }
33
+ }
17
34
  function buildConsentPayload(state) {
18
35
  return {
19
36
  ad_storage: state,
@@ -22,47 +39,74 @@ function buildConsentPayload(state) {
22
39
  analytics_storage: state
23
40
  };
24
41
  }
25
- function getConsentState() {
26
- if (typeof window === "undefined") return "pending";
42
+ function getConsentChoice() {
43
+ if (typeof window === "undefined") return DEFAULT_CHOICE;
27
44
  try {
28
- const storedState = window.localStorage.getItem(CONSENT_STATE_KEY);
29
- if (storedState === "granted" || storedState === "denied") return storedState;
45
+ const stored = window.localStorage.getItem(CONSENT_STATE_KEY);
46
+ const updatedAt = window.localStorage.getItem(CONSENT_TIMESTAMP_KEY);
47
+ if (stored === "granted")
48
+ return { state: "granted", source: "explicit", updatedAt, expiresAt: null };
49
+ if (stored === "denied") {
50
+ let expiresAt = window.localStorage.getItem(CONSENT_EXPIRES_AT_KEY);
51
+ if (!expiresAt) {
52
+ expiresAt = new Date(Date.now() + DEFAULT_DECLINE_TTL_DAYS * DAY_MS).toISOString();
53
+ try {
54
+ window.localStorage.setItem(CONSENT_EXPIRES_AT_KEY, expiresAt);
55
+ } catch {
56
+ }
57
+ }
58
+ if (!(Date.parse(expiresAt) <= Date.now()))
59
+ return { state: "denied", source: "explicit", updatedAt, expiresAt };
60
+ }
30
61
  } catch {
31
62
  }
32
- return "pending";
63
+ return DEFAULT_CHOICE;
33
64
  }
34
- function setConsentState(state) {
65
+ function getConsentState() {
66
+ return getConsentChoice().state;
67
+ }
68
+ function pushConsentToPlatforms(state) {
35
69
  if (typeof window === "undefined") return;
36
- try {
37
- window.localStorage.setItem(CONSENT_STATE_KEY, state);
38
- window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, (/* @__PURE__ */ new Date()).toISOString());
39
- } catch {
40
- }
41
70
  if (typeof window.gtag === "function")
42
71
  window.gtag("consent", "update", buildConsentPayload(state));
43
72
  if (typeof window.fbq === "function")
44
73
  window.fbq("consent", state === "granted" ? "grant" : "revoke");
45
- if (state === "granted") {
46
- for (const listener of grantedListeners) {
47
- try {
48
- listener();
49
- } catch {
50
- }
74
+ }
75
+ function setConsentState(state, options) {
76
+ if (typeof window === "undefined") return;
77
+ const requestedTtl = options?.declineTtlDays;
78
+ const ttlDays = typeof requestedTtl === "number" && Number.isFinite(requestedTtl) && requestedTtl > 0 ? requestedTtl : DEFAULT_DECLINE_TTL_DAYS;
79
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
80
+ const expiresAt = state === "denied" ? new Date(Date.now() + ttlDays * DAY_MS).toISOString() : null;
81
+ try {
82
+ window.localStorage.setItem(CONSENT_STATE_KEY, state);
83
+ window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, updatedAt);
84
+ if (expiresAt !== null) {
85
+ window.localStorage.setItem(CONSENT_EXPIRES_AT_KEY, expiresAt);
86
+ } else {
87
+ window.localStorage.removeItem(CONSENT_EXPIRES_AT_KEY);
51
88
  }
89
+ } catch {
52
90
  }
91
+ pushConsentToPlatforms(state);
92
+ notifyConsentChanged({ state, source: "explicit", updatedAt, expiresAt });
93
+ }
94
+ function optIn() {
95
+ setConsentState("granted");
96
+ }
97
+ function optOut(options) {
98
+ setConsentState("denied", options);
53
99
  }
54
100
  function resetConsent() {
55
101
  if (typeof window === "undefined") return;
56
102
  try {
57
103
  window.localStorage.removeItem(CONSENT_STATE_KEY);
58
104
  window.localStorage.removeItem(CONSENT_TIMESTAMP_KEY);
105
+ window.localStorage.removeItem(CONSENT_EXPIRES_AT_KEY);
59
106
  } catch {
60
107
  }
61
- }
62
- function restoreStoredConsent() {
63
- const consentState = getConsentState();
64
- if (consentState === "granted" || consentState === "denied") setConsentState(consentState);
65
- return consentState;
108
+ pushConsentToPlatforms("granted");
109
+ notifyConsentChanged(DEFAULT_CHOICE);
66
110
  }
67
111
 
68
112
  // ../tracking-core/src/tracking.ts
@@ -206,13 +250,11 @@ function fireGtagConversion(input) {
206
250
  }
207
251
  function applyDefaultConsentState() {
208
252
  const gtag = ensureGtagFunction();
209
- gtag("consent", "default", {
210
- ad_storage: "denied",
211
- ad_user_data: "denied",
212
- ad_personalization: "denied",
213
- analytics_storage: "denied",
214
- wait_for_update: 500
215
- });
253
+ gtag(
254
+ "consent",
255
+ "default",
256
+ buildConsentPayload(getConsentState() === "denied" ? "denied" : "granted")
257
+ );
216
258
  }
217
259
  function loadGtagScript(gtagId) {
218
260
  if (typeof document === "undefined") return;
@@ -238,7 +280,6 @@ function bootstrapGoogleAdsTracking(gtagId) {
238
280
  applyDefaultConsentState();
239
281
  loadGtagScript(gtagId);
240
282
  initializeGtag(gtagId);
241
- restoreStoredConsent();
242
283
  }
243
284
  function bootstrapMultipleGtags(gtagIds) {
244
285
  if (typeof window === "undefined" || typeof document === "undefined") return;
@@ -253,7 +294,6 @@ function bootstrapMultipleGtags(gtagIds) {
253
294
  for (const id of ids) {
254
295
  gtag("config", id);
255
296
  }
256
- restoreStoredConsent();
257
297
  }
258
298
 
259
299
  // ../tracking-core/src/fbq.ts
@@ -313,7 +353,7 @@ function ensureFbqFunction() {
313
353
  return fbq;
314
354
  }
315
355
  function applyDefaultMetaConsentState() {
316
- ensureFbqFunction()("consent", "revoke");
356
+ ensureFbqFunction()("consent", getConsentState() === "denied" ? "revoke" : "grant");
317
357
  }
318
358
  function loadFbeventsScript() {
319
359
  if (typeof document === "undefined") return;
@@ -333,19 +373,12 @@ function initializeMetaPixel(pixelId) {
333
373
  fbq("init", pixelId);
334
374
  fbq("track", "PageView");
335
375
  }
336
- function restoreMetaConsentState() {
337
- if (typeof window === "undefined") return;
338
- const state = getConsentState();
339
- if (state === "granted") window.fbq?.("consent", "grant");
340
- else if (state === "denied") window.fbq?.("consent", "revoke");
341
- }
342
376
  function bootstrapMetaPixel(pixelId) {
343
377
  if (typeof window === "undefined" || typeof document === "undefined") return;
344
378
  if (!isValidMetaPixelId(pixelId)) return;
345
379
  applyDefaultMetaConsentState();
346
380
  loadFbeventsScript();
347
381
  initializeMetaPixel(pixelId);
348
- restoreMetaConsentState();
349
382
  captureFbc();
350
383
  }
351
384
  function bootstrapMultiplePixels(pixelIds) {
@@ -361,10 +394,75 @@ function bootstrapMultiplePixels(pixelIds) {
361
394
  fbq("init", id);
362
395
  }
363
396
  fbq("track", "PageView");
364
- restoreMetaConsentState();
365
397
  captureFbc();
366
398
  }
367
399
 
400
+ // ../tracking-core/src/landing.ts
401
+ var LANDING_STORAGE_KEY = "_aranova_track_landing";
402
+ var memoryRecord = null;
403
+ function sanitizeParams(value) {
404
+ if (typeof value !== "object" || value === null) return {};
405
+ const source = value;
406
+ return TRACKING_PARAM_KEYS.reduce((params, key) => {
407
+ const entry = source[key];
408
+ if (typeof entry === "string" && entry.length > 0) params[key] = entry;
409
+ return params;
410
+ }, {});
411
+ }
412
+ function readStoredRecord() {
413
+ try {
414
+ const raw = window.localStorage.getItem(LANDING_STORAGE_KEY);
415
+ if (!raw) return null;
416
+ const parsed = JSON.parse(raw);
417
+ if (typeof parsed.session_id !== "string" || parsed.session_id.length === 0) return null;
418
+ return { session_id: parsed.session_id, params: sanitizeParams(parsed.params) };
419
+ } catch {
420
+ return null;
421
+ }
422
+ }
423
+ function writeRecord(record) {
424
+ memoryRecord = record;
425
+ try {
426
+ window.localStorage.setItem(LANDING_STORAGE_KEY, JSON.stringify(record));
427
+ } catch {
428
+ }
429
+ }
430
+ function captureFromUrl(url) {
431
+ try {
432
+ const resolved = new URL(url ?? window.location.href, window.location.origin);
433
+ return getTrackingQueryValues(resolved.searchParams);
434
+ } catch {
435
+ return {};
436
+ }
437
+ }
438
+ function getOrCaptureLandingParams(sessionId, url) {
439
+ if (typeof window === "undefined") return {};
440
+ const stored = readStoredRecord();
441
+ if (stored && stored.session_id === sessionId) {
442
+ memoryRecord = stored;
443
+ return stored.params;
444
+ }
445
+ if (memoryRecord && memoryRecord.session_id === sessionId) {
446
+ return memoryRecord.params;
447
+ }
448
+ const record = { session_id: sessionId, params: captureFromUrl(url) };
449
+ writeRecord(record);
450
+ return record.params;
451
+ }
452
+ function buildLandingPayloadFields(sessionId, override) {
453
+ const params = override ?? (typeof window === "undefined" ? null : getOrCaptureLandingParams(sessionId));
454
+ if (params === null) return {};
455
+ return {
456
+ landing_gclid: params.gclid ?? null,
457
+ landing_fbclid: params.fbclid ?? null,
458
+ landing_utm_source: params.utm_source ?? null,
459
+ landing_utm_medium: params.utm_medium ?? null,
460
+ landing_utm_campaign: params.utm_campaign ?? null,
461
+ landing_utm_term: params.utm_term ?? null,
462
+ landing_utm_content: params.utm_content ?? null
463
+ };
464
+ }
465
+
368
466
  // ../tracking-core/src/payloads.ts
369
467
  function createTrackingClientContext(surface, input = {}) {
370
468
  return {
@@ -391,6 +489,9 @@ function createTrackingSessionUpsertPayload(trackingParams, input, context) {
391
489
  utm_campaign: trackingParams.utm_campaign,
392
490
  utm_term: trackingParams.utm_term,
393
491
  utm_content: trackingParams.utm_content,
492
+ // Omitted entirely when the landing isn't observable (SSR, no override) —
493
+ // present-as-null would wrongly tell the backend "landed with no params".
494
+ ...buildLandingPayloadFields(input.sessionId, input.landingParams),
394
495
  first_page: input.firstPage ?? null,
395
496
  consent_state: input.consentState ?? null,
396
497
  context
@@ -412,8 +513,6 @@ function createTrackingEventCreatePayload(trackingParams, input, context) {
412
513
 
413
514
  // ../tracking-core/src/resources/conversion-firing.ts
414
515
  var DEDUP_PREFIX = "_aranova_conv_";
415
- var MAX_PENDING = 100;
416
- var pendingQueue = [];
417
516
  function dedupKey(input) {
418
517
  return `${DEDUP_PREFIX}${input.transactionId ?? ""}:${input.sendTo}`;
419
518
  }
@@ -437,23 +536,9 @@ function fireOnce(input) {
437
536
  if (fireGtagConversion(input)) markFired(input);
438
537
  }
439
538
  function fireConversionWithConsent(input) {
440
- const state = getConsentState();
441
- if (state === "denied") return;
442
- if (state === "pending") {
443
- if (pendingQueue.length >= MAX_PENDING) pendingQueue.shift();
444
- pendingQueue.push(input);
445
- return;
446
- }
539
+ if (getConsentState() === "denied") return;
447
540
  fireOnce(input);
448
541
  }
449
- function flushPendingConversions() {
450
- if (getConsentState() !== "granted") return;
451
- while (pendingQueue.length > 0) {
452
- const input = pendingQueue.shift();
453
- if (input) fireOnce(input);
454
- }
455
- }
456
- if (typeof window !== "undefined") onConsentGranted(flushPendingConversions);
457
542
 
458
543
  // ../tracking-core/src/resources/conversion-config.ts
459
544
  function isStringMap(value) {
@@ -999,7 +1084,13 @@ function readTrackingParams() {
999
1084
  }
1000
1085
  function consentSnapshot() {
1001
1086
  try {
1002
- return { state: getConsentState() };
1087
+ const choice = getConsentChoice();
1088
+ return {
1089
+ state: choice.state,
1090
+ source: choice.source,
1091
+ updated_at: choice.updatedAt,
1092
+ expires_at: choice.expiresAt
1093
+ };
1003
1094
  } catch {
1004
1095
  return null;
1005
1096
  }
@@ -1072,6 +1163,7 @@ function createTrackingClient(config) {
1072
1163
  initialParams = captureTrackingParamsFromLocation();
1073
1164
  } catch {
1074
1165
  }
1166
+ getOrCaptureLandingParams(sessionId);
1075
1167
  try {
1076
1168
  captureFbc();
1077
1169
  } catch {
@@ -1121,6 +1213,11 @@ function createTrackingClient(config) {
1121
1213
  utm_campaign: params.utm_campaign,
1122
1214
  utm_term: params.utm_term,
1123
1215
  utm_content: params.utm_content,
1216
+ // Landing params for the CURRENT session id — captured on the spot when
1217
+ // the session just rotated (the current URL is the rotated session's
1218
+ // landing), reused from the stored record otherwise. Keys are omitted
1219
+ // entirely when the landing isn't observable (SSR).
1220
+ ...buildLandingPayloadFields(sessionId),
1124
1221
  first_page: firstPage,
1125
1222
  consent_state: consentSnapshot(),
1126
1223
  context
@@ -2154,36 +2251,68 @@ function useTrackingParams() {
2154
2251
  }, []);
2155
2252
  return trackingParams;
2156
2253
  }
2157
- function useConsentState() {
2158
- return useConsent().state;
2159
- }
2160
- function useConsent() {
2161
- const [state, setState] = useState("pending");
2254
+ var DEFAULT_CHOICE2 = {
2255
+ state: "granted",
2256
+ source: "default",
2257
+ updatedAt: null,
2258
+ expiresAt: null
2259
+ };
2260
+ function useCookiePreferences(options) {
2261
+ const [choice, setChoice] = useState(DEFAULT_CHOICE2);
2262
+ const ttlDays = options?.declineTtlDays;
2162
2263
  useEffect(() => {
2163
- setState(getConsentState());
2264
+ const sync = () => setChoice(getConsentChoice());
2265
+ sync();
2164
2266
  const handleStorage = (event) => {
2165
- if (event.key === CONSENT_STATE_KEY) setState(getConsentState());
2267
+ if (event.key === null || event.key === CONSENT_STATE_KEY || event.key === CONSENT_EXPIRES_AT_KEY || event.key === CONSENT_TIMESTAMP_KEY)
2268
+ sync();
2166
2269
  };
2167
2270
  window.addEventListener("storage", handleStorage);
2168
- return () => window.removeEventListener("storage", handleStorage);
2169
- }, []);
2170
- const accept = useCallback(() => {
2171
- setConsentState("granted");
2172
- setState("granted");
2271
+ const unsubscribe = onConsentChange(sync);
2272
+ return () => {
2273
+ window.removeEventListener("storage", handleStorage);
2274
+ unsubscribe();
2275
+ };
2173
2276
  }, []);
2174
- const decline = useCallback(() => {
2175
- setConsentState("denied");
2176
- setState("denied");
2277
+ const optOutAction = useCallback(() => {
2278
+ optOut(ttlDays != null ? { declineTtlDays: ttlDays } : void 0);
2279
+ }, [ttlDays]);
2280
+ const optInAction = useCallback(() => {
2281
+ optIn();
2177
2282
  }, []);
2178
2283
  const reset = useCallback(() => {
2179
2284
  resetConsent();
2180
- setState("pending");
2181
2285
  }, []);
2286
+ return {
2287
+ state: choice.state,
2288
+ source: choice.source,
2289
+ isDefault: choice.source === "default",
2290
+ isGranted: choice.state === "granted",
2291
+ isDenied: choice.state === "denied",
2292
+ updatedAt: choice.updatedAt,
2293
+ expiresAt: choice.expiresAt,
2294
+ optOut: optOutAction,
2295
+ optIn: optInAction,
2296
+ reset
2297
+ };
2298
+ }
2299
+ function useConsentState() {
2300
+ return useConsent().state;
2301
+ }
2302
+ function useConsent() {
2303
+ const {
2304
+ state,
2305
+ isGranted,
2306
+ isDenied,
2307
+ optIn: accept,
2308
+ optOut: decline,
2309
+ reset
2310
+ } = useCookiePreferences();
2182
2311
  return {
2183
2312
  state,
2184
- isPending: state === "pending",
2185
- isGranted: state === "granted",
2186
- isDenied: state === "denied",
2313
+ isPending: false,
2314
+ isGranted,
2315
+ isDenied,
2187
2316
  accept,
2188
2317
  decline,
2189
2318
  reset
@@ -2424,7 +2553,7 @@ function GoogleAdsTracking(props) {
2424
2553
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo4 } from "react";
2425
2554
 
2426
2555
  // package.json
2427
- var version = "0.14.1";
2556
+ var version = "0.15.0";
2428
2557
 
2429
2558
  // ../tracking-core/src/phone-react.tsx
2430
2559
  import {
@@ -2655,6 +2784,7 @@ export {
2655
2784
  AdPlatformTracking,
2656
2785
  AranovaApiError,
2657
2786
  ConsentBanner,
2787
+ DEFAULT_DECLINE_TTL_DAYS,
2658
2788
  DEFAULT_PHONE_COUNTRY,
2659
2789
  GoogleAdsTracking,
2660
2790
  NAMED_RANGES,
@@ -2674,7 +2804,11 @@ export {
2674
2804
  formatPhone,
2675
2805
  formatPhoneAsTyped,
2676
2806
  fromMinor,
2807
+ getConsentChoice,
2677
2808
  getConsentState,
2809
+ onConsentChange,
2810
+ optIn,
2811
+ optOut,
2678
2812
  parsePhone,
2679
2813
  phoneField,
2680
2814
  resetConsent,
@@ -2689,6 +2823,7 @@ export {
2689
2823
  toMinor,
2690
2824
  useConsent,
2691
2825
  useConsentState,
2826
+ useCookiePreferences,
2692
2827
  useGclid,
2693
2828
  usePhoneConfig,
2694
2829
  usePhoneField,