@aranova/tracking-next 0.14.2 → 0.16.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
@@ -33,6 +33,7 @@ __export(src_exports, {
33
33
  AdPlatformTracking: () => AdPlatformTracking,
34
34
  AranovaApiError: () => AranovaApiError,
35
35
  ConsentBanner: () => ConsentBanner,
36
+ DEFAULT_DECLINE_TTL_DAYS: () => DEFAULT_DECLINE_TTL_DAYS,
36
37
  DEFAULT_PHONE_COUNTRY: () => DEFAULT_PHONE_COUNTRY,
37
38
  GoogleAdsTracking: () => GoogleAdsTracking,
38
39
  NAMED_RANGES: () => NAMED_RANGES,
@@ -52,7 +53,11 @@ __export(src_exports, {
52
53
  formatPhone: () => formatPhone,
53
54
  formatPhoneAsTyped: () => formatPhoneAsTyped,
54
55
  fromMinor: () => fromMinor,
56
+ getConsentChoice: () => getConsentChoice,
55
57
  getConsentState: () => getConsentState,
58
+ onConsentChange: () => onConsentChange,
59
+ optIn: () => optIn,
60
+ optOut: () => optOut,
56
61
  parsePhone: () => parsePhone,
57
62
  phoneField: () => phoneField,
58
63
  resetConsent: () => resetConsent,
@@ -67,6 +72,7 @@ __export(src_exports, {
67
72
  toMinor: () => toMinor,
68
73
  useConsent: () => useConsent,
69
74
  useConsentState: () => useConsentState,
75
+ useCookiePreferences: () => useCookiePreferences,
70
76
  useGclid: () => useGclid,
71
77
  usePhoneConfig: () => usePhoneConfig,
72
78
  usePhoneField: () => usePhoneField,
@@ -77,13 +83,30 @@ module.exports = __toCommonJS(src_exports);
77
83
  // ../tracking-core/src/consent.ts
78
84
  var CONSENT_STATE_KEY = "consent_state";
79
85
  var CONSENT_TIMESTAMP_KEY = "consent_timestamp";
80
- var grantedListeners = /* @__PURE__ */ new Set();
81
- function onConsentGranted(listener) {
82
- grantedListeners.add(listener);
86
+ var CONSENT_EXPIRES_AT_KEY = "consent_expires_at";
87
+ var DEFAULT_DECLINE_TTL_DAYS = 90;
88
+ var DAY_MS = 864e5;
89
+ var DEFAULT_CHOICE = {
90
+ state: "granted",
91
+ source: "default",
92
+ updatedAt: null,
93
+ expiresAt: null
94
+ };
95
+ var changeListeners = /* @__PURE__ */ new Set();
96
+ function onConsentChange(listener) {
97
+ changeListeners.add(listener);
83
98
  return () => {
84
- grantedListeners.delete(listener);
99
+ changeListeners.delete(listener);
85
100
  };
86
101
  }
102
+ function notifyConsentChanged(choice) {
103
+ for (const listener of changeListeners) {
104
+ try {
105
+ listener(choice);
106
+ } catch {
107
+ }
108
+ }
109
+ }
87
110
  function buildConsentPayload(state) {
88
111
  return {
89
112
  ad_storage: state,
@@ -92,42 +115,88 @@ function buildConsentPayload(state) {
92
115
  analytics_storage: state
93
116
  };
94
117
  }
95
- function getConsentState() {
96
- if (typeof window === "undefined") return "pending";
118
+ function getConsentChoice() {
119
+ if (typeof window === "undefined") return DEFAULT_CHOICE;
97
120
  try {
98
- const storedState = window.localStorage.getItem(CONSENT_STATE_KEY);
99
- if (storedState === "granted" || storedState === "denied") return storedState;
121
+ const stored = window.localStorage.getItem(CONSENT_STATE_KEY);
122
+ const updatedAt = window.localStorage.getItem(CONSENT_TIMESTAMP_KEY);
123
+ if (stored === "granted")
124
+ return { state: "granted", source: "explicit", updatedAt, expiresAt: null };
125
+ if (stored === "denied") {
126
+ let expiresAt = window.localStorage.getItem(CONSENT_EXPIRES_AT_KEY);
127
+ if (!expiresAt) {
128
+ expiresAt = new Date(Date.now() + DEFAULT_DECLINE_TTL_DAYS * DAY_MS).toISOString();
129
+ try {
130
+ window.localStorage.setItem(CONSENT_EXPIRES_AT_KEY, expiresAt);
131
+ } catch {
132
+ }
133
+ }
134
+ if (!(Date.parse(expiresAt) <= Date.now()))
135
+ return { state: "denied", source: "explicit", updatedAt, expiresAt };
136
+ }
100
137
  } catch {
101
138
  }
102
- return "pending";
139
+ return DEFAULT_CHOICE;
103
140
  }
104
- function setConsentState(state) {
141
+ function getConsentState() {
142
+ return getConsentChoice().state;
143
+ }
144
+ function pushConsentToPlatforms(state) {
105
145
  if (typeof window === "undefined") return;
106
- try {
107
- window.localStorage.setItem(CONSENT_STATE_KEY, state);
108
- window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, (/* @__PURE__ */ new Date()).toISOString());
109
- } catch {
110
- }
111
146
  if (typeof window.gtag === "function")
112
147
  window.gtag("consent", "update", buildConsentPayload(state));
113
148
  if (typeof window.fbq === "function")
114
149
  window.fbq("consent", state === "granted" ? "grant" : "revoke");
115
- if (state === "granted") {
116
- for (const listener of grantedListeners) {
117
- try {
118
- listener();
119
- } catch {
120
- }
150
+ }
151
+ function setConsentState(state, options) {
152
+ if (typeof window === "undefined") return;
153
+ const requestedTtl = options?.declineTtlDays;
154
+ const ttlDays = typeof requestedTtl === "number" && Number.isFinite(requestedTtl) && requestedTtl > 0 ? requestedTtl : DEFAULT_DECLINE_TTL_DAYS;
155
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
156
+ const expiresAt = state === "denied" ? new Date(Date.now() + ttlDays * DAY_MS).toISOString() : null;
157
+ try {
158
+ window.localStorage.setItem(CONSENT_STATE_KEY, state);
159
+ window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, updatedAt);
160
+ if (expiresAt !== null) {
161
+ window.localStorage.setItem(CONSENT_EXPIRES_AT_KEY, expiresAt);
162
+ } else {
163
+ window.localStorage.removeItem(CONSENT_EXPIRES_AT_KEY);
121
164
  }
165
+ } catch {
122
166
  }
167
+ pushConsentToPlatforms(state);
168
+ notifyConsentChanged({ state, source: "explicit", updatedAt, expiresAt });
169
+ }
170
+ function optIn() {
171
+ setConsentState("granted");
172
+ }
173
+ function optOut(options) {
174
+ setConsentState("denied", options);
123
175
  }
124
176
  function resetConsent() {
125
177
  if (typeof window === "undefined") return;
126
178
  try {
127
179
  window.localStorage.removeItem(CONSENT_STATE_KEY);
128
180
  window.localStorage.removeItem(CONSENT_TIMESTAMP_KEY);
181
+ window.localStorage.removeItem(CONSENT_EXPIRES_AT_KEY);
129
182
  } catch {
130
183
  }
184
+ pushConsentToPlatforms("granted");
185
+ notifyConsentChanged(DEFAULT_CHOICE);
186
+ }
187
+ function createEffectiveConsentSnippet() {
188
+ return `
189
+ var aranovaConsent = (function () {
190
+ try {
191
+ if (window.localStorage.getItem('${CONSENT_STATE_KEY}') === 'denied') {
192
+ var exp = window.localStorage.getItem('${CONSENT_EXPIRES_AT_KEY}');
193
+ var expMs = exp ? Date.parse(exp) : NaN;
194
+ if (!(expMs <= Date.now())) return 'denied';
195
+ }
196
+ } catch (e) {}
197
+ return 'granted';
198
+ })();
199
+ `.trim();
131
200
  }
132
201
 
133
202
  // ../tracking-core/src/tracking.ts
@@ -242,12 +311,12 @@ function createConsentDefaultScript() {
242
311
  window.dataLayer = window.dataLayer || [];
243
312
  function gtag(){dataLayer.push(arguments);}
244
313
  window.gtag = gtag;
314
+ ${createEffectiveConsentSnippet()}
245
315
  gtag('consent', 'default', {
246
- ad_storage: 'denied',
247
- ad_user_data: 'denied',
248
- ad_personalization: 'denied',
249
- analytics_storage: 'denied',
250
- wait_for_update: 500,
316
+ ad_storage: aranovaConsent,
317
+ ad_user_data: aranovaConsent,
318
+ ad_personalization: aranovaConsent,
319
+ analytics_storage: aranovaConsent
251
320
  });
252
321
  `.trim();
253
322
  }
@@ -261,21 +330,6 @@ gtag('js', new Date());
261
330
  gtag('config', '${gtagId}');
262
331
  `.trim();
263
332
  }
264
- function createConsentRestoreScript() {
265
- return `
266
- try {
267
- var consentState = window.localStorage.getItem('consent_state');
268
- if (window.gtag && (consentState === 'granted' || consentState === 'denied')) {
269
- window.gtag('consent', 'update', {
270
- ad_storage: consentState,
271
- ad_user_data: consentState,
272
- ad_personalization: consentState,
273
- analytics_storage: consentState,
274
- });
275
- }
276
- } catch (e) {}
277
- `.trim();
278
- }
279
333
  var SEND_TO_RE = /^AW-[A-Za-z0-9]+\/[A-Za-z0-9_-]+$/;
280
334
  function isValidSendTo(sendTo) {
281
335
  return SEND_TO_RE.test(sendTo);
@@ -354,7 +408,8 @@ document,'script','${FB_EVENTS_SCRIPT_HOST}');`.trim();
354
408
  }
355
409
  function createMetaConsentDefaultScript() {
356
410
  return `${fbqStubScript()}
357
- fbq('consent', 'revoke');`;
411
+ ${createEffectiveConsentSnippet()}
412
+ fbq('consent', aranovaConsent === 'denied' ? 'revoke' : 'grant');`;
358
413
  }
359
414
  function createMetaPixelInitScript(pixelId) {
360
415
  if (!isValidMetaPixelId(pixelId)) return "";
@@ -367,14 +422,71 @@ function createMetaPixelInitScriptMulti(pixelIds) {
367
422
  return `${inits.join("\n")}
368
423
  fbq('track', 'PageView');`;
369
424
  }
370
- function createMetaConsentRestoreScript() {
371
- return `
372
- try {
373
- var consentState = window.localStorage.getItem('consent_state');
374
- if (window.fbq && (consentState === 'granted' || consentState === 'denied')) {
375
- window.fbq('consent', consentState === 'granted' ? 'grant' : 'revoke');
425
+
426
+ // ../tracking-core/src/landing.ts
427
+ var LANDING_STORAGE_KEY = "_aranova_track_landing";
428
+ var memoryRecord = null;
429
+ function sanitizeParams(value) {
430
+ if (typeof value !== "object" || value === null) return {};
431
+ const source = value;
432
+ return TRACKING_PARAM_KEYS.reduce((params, key) => {
433
+ const entry = source[key];
434
+ if (typeof entry === "string" && entry.length > 0) params[key] = entry;
435
+ return params;
436
+ }, {});
437
+ }
438
+ function readStoredRecord() {
439
+ try {
440
+ const raw = window.localStorage.getItem(LANDING_STORAGE_KEY);
441
+ if (!raw) return null;
442
+ const parsed = JSON.parse(raw);
443
+ if (typeof parsed.session_id !== "string" || parsed.session_id.length === 0) return null;
444
+ return { session_id: parsed.session_id, params: sanitizeParams(parsed.params) };
445
+ } catch {
446
+ return null;
376
447
  }
377
- } catch (e) {}`.trim();
448
+ }
449
+ function writeRecord(record) {
450
+ memoryRecord = record;
451
+ try {
452
+ window.localStorage.setItem(LANDING_STORAGE_KEY, JSON.stringify(record));
453
+ } catch {
454
+ }
455
+ }
456
+ function captureFromUrl(url) {
457
+ try {
458
+ const resolved = new URL(url ?? window.location.href, window.location.origin);
459
+ return getTrackingQueryValues(resolved.searchParams);
460
+ } catch {
461
+ return {};
462
+ }
463
+ }
464
+ function getOrCaptureLandingParams(sessionId, url) {
465
+ if (typeof window === "undefined") return {};
466
+ const stored = readStoredRecord();
467
+ if (stored && stored.session_id === sessionId) {
468
+ memoryRecord = stored;
469
+ return stored.params;
470
+ }
471
+ if (memoryRecord && memoryRecord.session_id === sessionId) {
472
+ return memoryRecord.params;
473
+ }
474
+ const record = { session_id: sessionId, params: captureFromUrl(url) };
475
+ writeRecord(record);
476
+ return record.params;
477
+ }
478
+ function buildLandingPayloadFields(sessionId, override) {
479
+ const params = override ?? (typeof window === "undefined" ? null : getOrCaptureLandingParams(sessionId));
480
+ if (params === null) return {};
481
+ return {
482
+ landing_gclid: params.gclid ?? null,
483
+ landing_fbclid: params.fbclid ?? null,
484
+ landing_utm_source: params.utm_source ?? null,
485
+ landing_utm_medium: params.utm_medium ?? null,
486
+ landing_utm_campaign: params.utm_campaign ?? null,
487
+ landing_utm_term: params.utm_term ?? null,
488
+ landing_utm_content: params.utm_content ?? null
489
+ };
378
490
  }
379
491
 
380
492
  // ../tracking-core/src/payloads.ts
@@ -403,6 +515,9 @@ function createTrackingSessionUpsertPayload(trackingParams, input, context) {
403
515
  utm_campaign: trackingParams.utm_campaign,
404
516
  utm_term: trackingParams.utm_term,
405
517
  utm_content: trackingParams.utm_content,
518
+ // Omitted entirely when the landing isn't observable (SSR, no override) —
519
+ // present-as-null would wrongly tell the backend "landed with no params".
520
+ ...buildLandingPayloadFields(input.sessionId, input.landingParams),
406
521
  first_page: input.firstPage ?? null,
407
522
  consent_state: input.consentState ?? null,
408
523
  context
@@ -424,8 +539,6 @@ function createTrackingEventCreatePayload(trackingParams, input, context) {
424
539
 
425
540
  // ../tracking-core/src/resources/conversion-firing.ts
426
541
  var DEDUP_PREFIX = "_aranova_conv_";
427
- var MAX_PENDING = 100;
428
- var pendingQueue = [];
429
542
  function dedupKey(input) {
430
543
  return `${DEDUP_PREFIX}${input.transactionId ?? ""}:${input.sendTo}`;
431
544
  }
@@ -449,23 +562,9 @@ function fireOnce(input) {
449
562
  if (fireGtagConversion(input)) markFired(input);
450
563
  }
451
564
  function fireConversionWithConsent(input) {
452
- const state = getConsentState();
453
- if (state === "denied") return;
454
- if (state === "pending") {
455
- if (pendingQueue.length >= MAX_PENDING) pendingQueue.shift();
456
- pendingQueue.push(input);
457
- return;
458
- }
565
+ if (getConsentState() === "denied") return;
459
566
  fireOnce(input);
460
567
  }
461
- function flushPendingConversions() {
462
- if (getConsentState() !== "granted") return;
463
- while (pendingQueue.length > 0) {
464
- const input = pendingQueue.shift();
465
- if (input) fireOnce(input);
466
- }
467
- }
468
- if (typeof window !== "undefined") onConsentGranted(flushPendingConversions);
469
568
 
470
569
  // ../tracking-core/src/resources/conversion-config.ts
471
570
  function isStringMap(value) {
@@ -973,7 +1072,13 @@ function readTrackingParams() {
973
1072
  }
974
1073
  function consentSnapshot() {
975
1074
  try {
976
- return { state: getConsentState() };
1075
+ const choice = getConsentChoice();
1076
+ return {
1077
+ state: choice.state,
1078
+ source: choice.source,
1079
+ updated_at: choice.updatedAt,
1080
+ expires_at: choice.expiresAt
1081
+ };
977
1082
  } catch {
978
1083
  return null;
979
1084
  }
@@ -1046,6 +1151,7 @@ function createTrackingClient(config) {
1046
1151
  initialParams = captureTrackingParamsFromLocation();
1047
1152
  } catch {
1048
1153
  }
1154
+ getOrCaptureLandingParams(sessionId);
1049
1155
  try {
1050
1156
  captureFbc();
1051
1157
  } catch {
@@ -1095,6 +1201,11 @@ function createTrackingClient(config) {
1095
1201
  utm_campaign: params.utm_campaign,
1096
1202
  utm_term: params.utm_term,
1097
1203
  utm_content: params.utm_content,
1204
+ // Landing params for the CURRENT session id — captured on the spot when
1205
+ // the session just rotated (the current URL is the rotated session's
1206
+ // landing), reused from the stored record otherwise. Keys are omitted
1207
+ // entirely when the landing isn't observable (SSR).
1208
+ ...buildLandingPayloadFields(sessionId),
1098
1209
  first_page: firstPage,
1099
1210
  consent_state: consentSnapshot(),
1100
1211
  context
@@ -1162,6 +1273,7 @@ function createTrackingClient(config) {
1162
1273
  return {
1163
1274
  trackEvent,
1164
1275
  flush,
1276
+ flushBeacon: flushOnUnload,
1165
1277
  getSessionId: () => sessionId,
1166
1278
  getVisitorId: () => visitorId,
1167
1279
  destroy: () => {
@@ -1186,9 +1298,17 @@ var ctaClickMetadataSchema = import_zod2.z.object({
1186
1298
  path: import_zod2.z.string()
1187
1299
  }).strict(),
1188
1300
  section: import_zod2.z.string().nullable().optional(),
1189
- destination_url: import_zod2.z.string().nullable().optional()
1301
+ destination_url: import_zod2.z.string().nullable().optional(),
1302
+ // Set by auto-capture (and available to manual callers): the link target
1303
+ // and a short element descriptor (tag#id) for tying clicks to specific UI.
1304
+ href: import_zod2.z.string().nullable().optional(),
1305
+ element: import_zod2.z.string().nullable().optional()
1306
+ }).strict();
1307
+ var ctaClickConfigSchema = import_zod2.z.object({
1308
+ autoCapture: import_zod2.z.object({
1309
+ selector: import_zod2.z.string().optional()
1310
+ }).strict().optional()
1190
1311
  }).strict();
1191
- var ctaClickConfigSchema = import_zod2.z.object({}).strict();
1192
1312
 
1193
1313
  // ../tracking-core/src/events/sdk-heartbeat.ts
1194
1314
  var import_zod3 = require("zod");
@@ -1264,31 +1384,44 @@ var multiPageSessionConfigSchema = import_zod6.z.object({
1264
1384
  pageThreshold: import_zod6.z.number().int().min(2)
1265
1385
  }).strict();
1266
1386
 
1267
- // ../tracking-core/src/events/phone-click.ts
1387
+ // ../tracking-core/src/events/page-exit.ts
1268
1388
  var import_zod7 = require("zod");
1269
- var phoneClickMetadataSchema = import_zod7.z.object({
1270
- phone_number: import_zod7.z.string(),
1389
+ var pageExitMetadataSchema = import_zod7.z.object({
1390
+ dwell_ms: import_zod7.z.number().int().min(0),
1391
+ // null = left without any scroll signal; floor is 0 so a valid 0% is never
1392
+ // rejected (a single bad field 422s the whole keepalive beacon batch).
1393
+ max_scroll_percent: import_zod7.z.number().int().min(0).max(100).nullable(),
1271
1394
  page: import_zod7.z.object({
1272
1395
  path: import_zod7.z.string()
1273
- }).strict(),
1274
- section: import_zod7.z.string().nullable().optional()
1396
+ }).strict()
1275
1397
  }).strict();
1276
- var phoneClickConfigSchema = import_zod7.z.object({}).strict();
1398
+ var pageExitConfigSchema = import_zod7.z.object({}).strict();
1277
1399
 
1278
- // ../tracking-core/src/events/scroll-depth.ts
1400
+ // ../tracking-core/src/events/phone-click.ts
1279
1401
  var import_zod8 = require("zod");
1280
- var scrollDepthMetadataSchema = import_zod8.z.object({
1281
- depth_percent: import_zod8.z.number().int().min(1).max(100),
1402
+ var phoneClickMetadataSchema = import_zod8.z.object({
1403
+ phone_number: import_zod8.z.string(),
1282
1404
  page: import_zod8.z.object({
1283
1405
  path: import_zod8.z.string()
1406
+ }).strict(),
1407
+ section: import_zod8.z.string().nullable().optional()
1408
+ }).strict();
1409
+ var phoneClickConfigSchema = import_zod8.z.object({}).strict();
1410
+
1411
+ // ../tracking-core/src/events/scroll-depth.ts
1412
+ var import_zod9 = require("zod");
1413
+ var scrollDepthMetadataSchema = import_zod9.z.object({
1414
+ depth_percent: import_zod9.z.number().int().min(1).max(100),
1415
+ page: import_zod9.z.object({
1416
+ path: import_zod9.z.string()
1284
1417
  }).strict()
1285
1418
  }).strict();
1286
- var scrollDepthConfigSchema = import_zod8.z.object({
1287
- thresholds: import_zod8.z.array(import_zod8.z.number().int().min(1).max(100)).min(1)
1419
+ var scrollDepthConfigSchema = import_zod9.z.object({
1420
+ thresholds: import_zod9.z.array(import_zod9.z.number().int().min(1).max(100)).min(1)
1288
1421
  }).strict();
1289
1422
 
1290
1423
  // ../tracking-core/src/events/specific-page-visit.ts
1291
- var import_zod9 = require("zod");
1424
+ var import_zod10 = require("zod");
1292
1425
  var SPECIFIC_PAGE_NAMES = [
1293
1426
  "contact_page",
1294
1427
  "about_page",
@@ -1299,18 +1432,18 @@ var SPECIFIC_PAGE_NAMES = [
1299
1432
  "faq_page",
1300
1433
  "testimonials_page"
1301
1434
  ];
1302
- var specificPageNameSchema = import_zod9.z.enum(SPECIFIC_PAGE_NAMES);
1303
- var specificPageVisitMetadataSchema = import_zod9.z.object({
1435
+ var specificPageNameSchema = import_zod10.z.enum(SPECIFIC_PAGE_NAMES);
1436
+ var specificPageVisitMetadataSchema = import_zod10.z.object({
1304
1437
  page_name: specificPageNameSchema,
1305
- page: import_zod9.z.object({
1306
- path: import_zod9.z.string()
1438
+ page: import_zod10.z.object({
1439
+ path: import_zod10.z.string()
1307
1440
  }).strict()
1308
1441
  }).strict();
1309
- var specificPageVisitConfigSchema = import_zod9.z.object({
1310
- pages: import_zod9.z.array(
1311
- import_zod9.z.object({
1442
+ var specificPageVisitConfigSchema = import_zod10.z.object({
1443
+ pages: import_zod10.z.array(
1444
+ import_zod10.z.object({
1312
1445
  name: specificPageNameSchema,
1313
- pathPattern: import_zod9.z.custom((value) => value instanceof RegExp, {
1446
+ pathPattern: import_zod10.z.custom((value) => value instanceof RegExp, {
1314
1447
  message: "pathPattern must be a RegExp"
1315
1448
  })
1316
1449
  }).strict()
@@ -1318,15 +1451,15 @@ var specificPageVisitConfigSchema = import_zod9.z.object({
1318
1451
  }).strict();
1319
1452
 
1320
1453
  // ../tracking-core/src/events/time-on-site.ts
1321
- var import_zod10 = require("zod");
1322
- var timeOnSiteMetadataSchema = import_zod10.z.object({
1323
- duration_ms: import_zod10.z.number().int().nonnegative(),
1324
- page: import_zod10.z.object({
1325
- path: import_zod10.z.string()
1454
+ var import_zod11 = require("zod");
1455
+ var timeOnSiteMetadataSchema = import_zod11.z.object({
1456
+ duration_ms: import_zod11.z.number().int().nonnegative(),
1457
+ page: import_zod11.z.object({
1458
+ path: import_zod11.z.string()
1326
1459
  }).strict()
1327
1460
  }).strict();
1328
- var timeOnSiteConfigSchema = import_zod10.z.object({
1329
- thresholdSeconds: import_zod10.z.number().int().positive()
1461
+ var timeOnSiteConfigSchema = import_zod11.z.object({
1462
+ thresholdSeconds: import_zod11.z.number().int().positive()
1330
1463
  }).strict();
1331
1464
 
1332
1465
  // ../tracking-core/src/events/registry.ts
@@ -1368,6 +1501,11 @@ var EVENT_REGISTRY = {
1368
1501
  metadataSchema: sdkHeartbeatMetadataSchema,
1369
1502
  configSchema: sdkHeartbeatConfigSchema
1370
1503
  },
1504
+ page_exit: {
1505
+ kind: "automatic",
1506
+ metadataSchema: pageExitMetadataSchema,
1507
+ configSchema: pageExitConfigSchema
1508
+ },
1371
1509
  // --- manual triggers ---
1372
1510
  form_submit: {
1373
1511
  kind: "manual",
@@ -1611,6 +1749,163 @@ function attachFormStart(client, config) {
1611
1749
  };
1612
1750
  }
1613
1751
 
1752
+ // ../tracking-core/src/triggers/page-exit.ts
1753
+ function attachPageExit(client) {
1754
+ if (typeof window === "undefined" || typeof document === "undefined") {
1755
+ return () => {
1756
+ };
1757
+ }
1758
+ let currentPath2 = window.location.pathname;
1759
+ let activeSince = document.visibilityState === "visible" ? Date.now() : null;
1760
+ let accumulatedMs = 0;
1761
+ let maxScrollPercent = null;
1762
+ let rafId = null;
1763
+ function getScrollPercent() {
1764
+ const doc = document.documentElement;
1765
+ const scrollTop = window.scrollY || doc.scrollTop;
1766
+ const scrollHeight = doc.scrollHeight;
1767
+ const clientHeight = doc.clientHeight;
1768
+ if (scrollHeight <= clientHeight) return 100;
1769
+ return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1770
+ }
1771
+ function onScroll() {
1772
+ if (rafId !== null) return;
1773
+ rafId = requestAnimationFrame(() => {
1774
+ rafId = null;
1775
+ const percent = getScrollPercent();
1776
+ if (percent >= 1 && (maxScrollPercent === null || percent > maxScrollPercent)) {
1777
+ maxScrollPercent = Math.min(percent, 100);
1778
+ }
1779
+ });
1780
+ }
1781
+ function settledDwellMs() {
1782
+ let total = accumulatedMs;
1783
+ if (activeSince !== null) {
1784
+ total += Date.now() - activeSince;
1785
+ }
1786
+ return Math.max(0, Math.round(total));
1787
+ }
1788
+ function emitSegment(path, flush) {
1789
+ const dwell = settledDwellMs();
1790
+ if (dwell === 0) return;
1791
+ client.trackEvent({
1792
+ eventType: "page_exit",
1793
+ metadata: {
1794
+ dwell_ms: dwell,
1795
+ max_scroll_percent: maxScrollPercent,
1796
+ page: { path }
1797
+ },
1798
+ pageUrl: window.location.href,
1799
+ occurredAt: null
1800
+ });
1801
+ if (flush) {
1802
+ client.flushBeacon();
1803
+ }
1804
+ accumulatedMs = 0;
1805
+ activeSince = document.visibilityState === "visible" ? Date.now() : null;
1806
+ }
1807
+ function onNavigate() {
1808
+ const newPath = window.location.pathname;
1809
+ if (newPath === currentPath2) return;
1810
+ emitSegment(currentPath2, false);
1811
+ currentPath2 = newPath;
1812
+ maxScrollPercent = null;
1813
+ accumulatedMs = 0;
1814
+ activeSince = document.visibilityState === "visible" ? Date.now() : null;
1815
+ }
1816
+ function onVisibilityChange() {
1817
+ if (document.visibilityState === "hidden") {
1818
+ emitSegment(currentPath2, true);
1819
+ } else {
1820
+ activeSince = Date.now();
1821
+ }
1822
+ }
1823
+ function onPageHide() {
1824
+ emitSegment(currentPath2, true);
1825
+ }
1826
+ const originalPushState = history.pushState.bind(history);
1827
+ const originalReplaceState = history.replaceState.bind(history);
1828
+ function patchedPushState(...args) {
1829
+ originalPushState(...args);
1830
+ setTimeout(onNavigate, 0);
1831
+ }
1832
+ function patchedReplaceState(...args) {
1833
+ originalReplaceState(...args);
1834
+ setTimeout(onNavigate, 0);
1835
+ }
1836
+ history.pushState = patchedPushState;
1837
+ history.replaceState = patchedReplaceState;
1838
+ window.addEventListener("popstate", onNavigate);
1839
+ window.addEventListener("scroll", onScroll, { passive: true });
1840
+ document.addEventListener("visibilitychange", onVisibilityChange);
1841
+ window.addEventListener("pagehide", onPageHide);
1842
+ return () => {
1843
+ if (rafId !== null) cancelAnimationFrame(rafId);
1844
+ history.pushState = originalPushState;
1845
+ history.replaceState = originalReplaceState;
1846
+ window.removeEventListener("popstate", onNavigate);
1847
+ window.removeEventListener("scroll", onScroll);
1848
+ document.removeEventListener("visibilitychange", onVisibilityChange);
1849
+ window.removeEventListener("pagehide", onPageHide);
1850
+ };
1851
+ }
1852
+
1853
+ // ../tracking-core/src/triggers/cta-click-capture.ts
1854
+ var DEFAULT_CTA_SELECTOR = "[data-aranova-cta]";
1855
+ var CTA_NAME_MAX_LENGTH = 120;
1856
+ function describeElement(el) {
1857
+ const tag = el.tagName.toLowerCase();
1858
+ return el.id ? `${tag}#${el.id}` : tag;
1859
+ }
1860
+ function resolveCtaName(el) {
1861
+ const explicit = el.getAttribute("data-aranova-cta");
1862
+ if (explicit && explicit.trim().length > 0) return explicit.trim();
1863
+ const text = (el.textContent ?? "").trim().replace(/\s+/g, " ");
1864
+ if (text.length > 0) return text.slice(0, CTA_NAME_MAX_LENGTH);
1865
+ return describeElement(el);
1866
+ }
1867
+ function attachCtaClickCapture(client, config) {
1868
+ if (typeof window === "undefined" || typeof document === "undefined") {
1869
+ return () => {
1870
+ };
1871
+ }
1872
+ const autoCapture = config.autoCapture;
1873
+ if (!autoCapture) {
1874
+ return () => {
1875
+ };
1876
+ }
1877
+ const selector = autoCapture.selector ?? DEFAULT_CTA_SELECTOR;
1878
+ function onClick(event) {
1879
+ const target = event.target;
1880
+ if (!(target instanceof Element)) return;
1881
+ let matched = null;
1882
+ try {
1883
+ matched = target.closest(selector);
1884
+ } catch {
1885
+ return;
1886
+ }
1887
+ if (matched === null) return;
1888
+ const href = matched instanceof HTMLAnchorElement ? matched.href || null : matched.getAttribute("href");
1889
+ client.trackEvent({
1890
+ eventType: "cta_click",
1891
+ metadata: {
1892
+ cta_name: resolveCtaName(matched),
1893
+ page: { path: window.location.pathname },
1894
+ section: matched.getAttribute("data-aranova-section"),
1895
+ destination_url: href,
1896
+ href,
1897
+ element: describeElement(matched)
1898
+ },
1899
+ pageUrl: window.location.href,
1900
+ occurredAt: null
1901
+ });
1902
+ }
1903
+ document.addEventListener("click", onClick, true);
1904
+ return () => {
1905
+ document.removeEventListener("click", onClick, true);
1906
+ };
1907
+ }
1908
+
1614
1909
  // ../tracking-core/src/resources/sales/errors.ts
1615
1910
  var AranovaApiError = class extends Error {
1616
1911
  constructor(message, options) {
@@ -1817,42 +2112,42 @@ function createSalesClient(config) {
1817
2112
  }
1818
2113
 
1819
2114
  // ../tracking-core/src/resources/sales/schema.ts
1820
- var import_zod11 = require("zod");
2115
+ var import_zod12 = require("zod");
1821
2116
  var SUPPORTED_CURRENCIES = ["USD", "CAD"];
1822
2117
  var TRACKING_ENVIRONMENTS = ["production", "development"];
1823
- var currencySchema = import_zod11.z.enum(SUPPORTED_CURRENCIES);
1824
- var centsSchema = import_zod11.z.number().int().nonnegative();
1825
- var quantitySchema = import_zod11.z.string().regex(/^\d+(\.\d{1,3})?$/);
1826
- var metadataSchema = import_zod11.z.record(import_zod11.z.unknown());
1827
- var saleItemSchema = import_zod11.z.object({
1828
- external_item_id: import_zod11.z.string().nullable().optional(),
1829
- name: import_zod11.z.string().nullable().optional(),
1830
- category: import_zod11.z.string().nullable().optional(),
2118
+ var currencySchema = import_zod12.z.enum(SUPPORTED_CURRENCIES);
2119
+ var centsSchema = import_zod12.z.number().int().nonnegative();
2120
+ var quantitySchema = import_zod12.z.string().regex(/^\d+(\.\d{1,3})?$/);
2121
+ var metadataSchema = import_zod12.z.record(import_zod12.z.unknown());
2122
+ var saleItemSchema = import_zod12.z.object({
2123
+ external_item_id: import_zod12.z.string().nullable().optional(),
2124
+ name: import_zod12.z.string().nullable().optional(),
2125
+ category: import_zod12.z.string().nullable().optional(),
1831
2126
  quantity: quantitySchema,
1832
2127
  unit_price_cents: centsSchema,
1833
2128
  // Non-negativity validated on the wire — same contract as the other cents
1834
2129
  // fields — and backstopped by the DB CHECK.
1835
2130
  unit_cost_cents: centsSchema.nullable().optional()
1836
2131
  }).strict();
1837
- var saleServiceSchema = import_zod11.z.object({
1838
- service: import_zod11.z.string(),
2132
+ var saleServiceSchema = import_zod12.z.object({
2133
+ service: import_zod12.z.string(),
1839
2134
  amount_cents: centsSchema
1840
2135
  }).strict();
1841
- var customerNameSchema = import_zod11.z.string().max(200);
1842
- var customerPhoneSchema = import_zod11.z.string().max(64);
1843
- var customerEmailSchema = import_zod11.z.string().max(320).email();
2136
+ var customerNameSchema = import_zod12.z.string().max(200);
2137
+ var customerPhoneSchema = import_zod12.z.string().max(64);
2138
+ var customerEmailSchema = import_zod12.z.string().max(320).email();
1844
2139
  function refineServiceXor(val, ctx, { requireAmount }) {
1845
2140
  if (val.services != null) {
1846
2141
  if (val.service != null) {
1847
2142
  ctx.addIssue({
1848
- code: import_zod11.z.ZodIssueCode.custom,
2143
+ code: import_zod12.z.ZodIssueCode.custom,
1849
2144
  message: "pass either `service` or `services`, not both",
1850
2145
  path: ["services"]
1851
2146
  });
1852
2147
  }
1853
2148
  if (val.services.length === 0) {
1854
2149
  ctx.addIssue({
1855
- code: import_zod11.z.ZodIssueCode.custom,
2150
+ code: import_zod12.z.ZodIssueCode.custom,
1856
2151
  message: "`services` must not be empty",
1857
2152
  path: ["services"]
1858
2153
  });
@@ -1860,7 +2155,7 @@ function refineServiceXor(val, ctx, { requireAmount }) {
1860
2155
  const keys = val.services.map((s) => s.service);
1861
2156
  if (new Set(keys).size !== keys.length) {
1862
2157
  ctx.addIssue({
1863
- code: import_zod11.z.ZodIssueCode.custom,
2158
+ code: import_zod12.z.ZodIssueCode.custom,
1864
2159
  message: "`services` must not list the same service more than once",
1865
2160
  path: ["services"]
1866
2161
  });
@@ -1869,7 +2164,7 @@ function refineServiceXor(val, ctx, { requireAmount }) {
1869
2164
  const sum = val.services.reduce((acc, s) => acc + s.amount_cents, 0);
1870
2165
  if (val.amount_total_cents !== sum) {
1871
2166
  ctx.addIssue({
1872
- code: import_zod11.z.ZodIssueCode.custom,
2167
+ code: import_zod12.z.ZodIssueCode.custom,
1873
2168
  message: "amount_total_cents must equal the sum of the services amounts (omit it to derive it automatically)",
1874
2169
  path: ["amount_total_cents"]
1875
2170
  });
@@ -1877,37 +2172,37 @@ function refineServiceXor(val, ctx, { requireAmount }) {
1877
2172
  }
1878
2173
  } else if (requireAmount && val.amount_total_cents == null) {
1879
2174
  ctx.addIssue({
1880
- code: import_zod11.z.ZodIssueCode.custom,
2175
+ code: import_zod12.z.ZodIssueCode.custom,
1881
2176
  message: "amount_total_cents is required unless `services` is provided",
1882
2177
  path: ["amount_total_cents"]
1883
2178
  });
1884
2179
  }
1885
2180
  }
1886
- var saleCreateSchema = import_zod11.z.object({
1887
- external_id: import_zod11.z.string().nullable().optional(),
1888
- description: import_zod11.z.string().nullable().optional(),
1889
- service: import_zod11.z.string().nullable().optional(),
1890
- services: import_zod11.z.array(saleServiceSchema).nullable().optional(),
2181
+ var saleCreateSchema = import_zod12.z.object({
2182
+ external_id: import_zod12.z.string().nullable().optional(),
2183
+ description: import_zod12.z.string().nullable().optional(),
2184
+ service: import_zod12.z.string().nullable().optional(),
2185
+ services: import_zod12.z.array(saleServiceSchema).nullable().optional(),
1891
2186
  currency: currencySchema,
1892
2187
  // Optional only because the plural `services` form derives it from the sum
1893
2188
  // (see refineServiceXor); the singular/serviceless path still requires it.
1894
2189
  amount_total_cents: centsSchema.nullable().optional(),
1895
- occurred_at: import_zod11.z.string().datetime(),
1896
- environment: import_zod11.z.enum(TRACKING_ENVIRONMENTS).default("production"),
1897
- items: import_zod11.z.array(saleItemSchema).default([]),
2190
+ occurred_at: import_zod12.z.string().datetime(),
2191
+ environment: import_zod12.z.enum(TRACKING_ENVIRONMENTS).default("production"),
2192
+ items: import_zod12.z.array(saleItemSchema).default([]),
1898
2193
  metadata: metadataSchema.nullable().optional(),
1899
2194
  customer_name: customerNameSchema.nullable().optional(),
1900
2195
  customer_phone: customerPhoneSchema.nullable().optional(),
1901
2196
  customer_email: customerEmailSchema.nullable().optional()
1902
2197
  }).strict().superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: true }));
1903
- var saleUpdateSchema = import_zod11.z.object({
1904
- description: import_zod11.z.string().nullable().optional(),
1905
- service: import_zod11.z.string().nullable().optional(),
1906
- services: import_zod11.z.array(saleServiceSchema).nullable().optional(),
2198
+ var saleUpdateSchema = import_zod12.z.object({
2199
+ description: import_zod12.z.string().nullable().optional(),
2200
+ service: import_zod12.z.string().nullable().optional(),
2201
+ services: import_zod12.z.array(saleServiceSchema).nullable().optional(),
1907
2202
  currency: currencySchema.optional(),
1908
2203
  amount_total_cents: centsSchema.optional(),
1909
- occurred_at: import_zod11.z.string().datetime().optional(),
1910
- items: import_zod11.z.array(saleItemSchema).optional(),
2204
+ occurred_at: import_zod12.z.string().datetime().optional(),
2205
+ items: import_zod12.z.array(saleItemSchema).optional(),
1911
2206
  metadata: metadataSchema.nullable().optional(),
1912
2207
  customer_name: customerNameSchema.nullable().optional(),
1913
2208
  customer_phone: customerPhoneSchema.nullable().optional(),
@@ -1997,36 +2292,68 @@ function useTrackingParams() {
1997
2292
  }, []);
1998
2293
  return trackingParams;
1999
2294
  }
2000
- function useConsentState() {
2001
- return useConsent().state;
2002
- }
2003
- function useConsent() {
2004
- const [state, setState] = (0, import_react.useState)("pending");
2295
+ var DEFAULT_CHOICE2 = {
2296
+ state: "granted",
2297
+ source: "default",
2298
+ updatedAt: null,
2299
+ expiresAt: null
2300
+ };
2301
+ function useCookiePreferences(options) {
2302
+ const [choice, setChoice] = (0, import_react.useState)(DEFAULT_CHOICE2);
2303
+ const ttlDays = options?.declineTtlDays;
2005
2304
  (0, import_react.useEffect)(() => {
2006
- setState(getConsentState());
2305
+ const sync = () => setChoice(getConsentChoice());
2306
+ sync();
2007
2307
  const handleStorage = (event) => {
2008
- if (event.key === CONSENT_STATE_KEY) setState(getConsentState());
2308
+ if (event.key === null || event.key === CONSENT_STATE_KEY || event.key === CONSENT_EXPIRES_AT_KEY || event.key === CONSENT_TIMESTAMP_KEY)
2309
+ sync();
2009
2310
  };
2010
2311
  window.addEventListener("storage", handleStorage);
2011
- return () => window.removeEventListener("storage", handleStorage);
2012
- }, []);
2013
- const accept = (0, import_react.useCallback)(() => {
2014
- setConsentState("granted");
2015
- setState("granted");
2312
+ const unsubscribe = onConsentChange(sync);
2313
+ return () => {
2314
+ window.removeEventListener("storage", handleStorage);
2315
+ unsubscribe();
2316
+ };
2016
2317
  }, []);
2017
- const decline = (0, import_react.useCallback)(() => {
2018
- setConsentState("denied");
2019
- setState("denied");
2318
+ const optOutAction = (0, import_react.useCallback)(() => {
2319
+ optOut(ttlDays != null ? { declineTtlDays: ttlDays } : void 0);
2320
+ }, [ttlDays]);
2321
+ const optInAction = (0, import_react.useCallback)(() => {
2322
+ optIn();
2020
2323
  }, []);
2021
2324
  const reset = (0, import_react.useCallback)(() => {
2022
2325
  resetConsent();
2023
- setState("pending");
2024
2326
  }, []);
2327
+ return {
2328
+ state: choice.state,
2329
+ source: choice.source,
2330
+ isDefault: choice.source === "default",
2331
+ isGranted: choice.state === "granted",
2332
+ isDenied: choice.state === "denied",
2333
+ updatedAt: choice.updatedAt,
2334
+ expiresAt: choice.expiresAt,
2335
+ optOut: optOutAction,
2336
+ optIn: optInAction,
2337
+ reset
2338
+ };
2339
+ }
2340
+ function useConsentState() {
2341
+ return useConsent().state;
2342
+ }
2343
+ function useConsent() {
2344
+ const {
2345
+ state,
2346
+ isGranted,
2347
+ isDenied,
2348
+ optIn: accept,
2349
+ optOut: decline,
2350
+ reset
2351
+ } = useCookiePreferences();
2025
2352
  return {
2026
2353
  state,
2027
- isPending: state === "pending",
2028
- isGranted: state === "granted",
2029
- isDenied: state === "denied",
2354
+ isPending: false,
2355
+ isGranted,
2356
+ isDenied,
2030
2357
  accept,
2031
2358
  decline,
2032
2359
  reset
@@ -2242,13 +2569,11 @@ function AdPlatformTracking({
2242
2569
  strategy: "afterInteractive"
2243
2570
  }
2244
2571
  ),
2245
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_script.default, { id: "gtag-init", strategy: "afterInteractive", children: gtagInitScript }),
2246
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_script.default, { id: "consent-restore", strategy: "afterInteractive", children: createConsentRestoreScript() })
2572
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_script.default, { id: "gtag-init", strategy: "afterInteractive", children: gtagInitScript })
2247
2573
  ] }) : null,
2248
2574
  metaAll.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
2249
2575
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_script.default, { id: "meta-pixel-base", strategy: "afterInteractive", children: createMetaConsentDefaultScript() }),
2250
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_script.default, { id: "meta-pixel-init", strategy: "afterInteractive", children: metaInitScript }),
2251
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_script.default, { id: "meta-pixel-consent-restore", strategy: "afterInteractive", children: createMetaConsentRestoreScript() })
2576
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_script.default, { id: "meta-pixel-init", strategy: "afterInteractive", children: metaInitScript })
2252
2577
  ] }) : null
2253
2578
  ] });
2254
2579
  }
@@ -2271,8 +2596,7 @@ function GoogleAdsTracking(props) {
2271
2596
  strategy: "afterInteractive"
2272
2597
  }
2273
2598
  ),
2274
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_script2.default, { id: "gtag-init", strategy: "afterInteractive", children: initScript }),
2275
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_script2.default, { id: "consent-restore", strategy: "afterInteractive", children: createConsentRestoreScript() })
2599
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_script2.default, { id: "gtag-init", strategy: "afterInteractive", children: initScript })
2276
2600
  ] });
2277
2601
  }
2278
2602
 
@@ -2280,7 +2604,7 @@ function GoogleAdsTracking(props) {
2280
2604
  var import_navigation = require("next/navigation");
2281
2605
 
2282
2606
  // package.json
2283
- var version = "0.14.2";
2607
+ var version = "0.16.0";
2284
2608
 
2285
2609
  // src/factory.tsx
2286
2610
  var import_react4 = require("react");
@@ -2561,6 +2885,7 @@ function createTracking(options) {
2561
2885
  debug
2562
2886
  });
2563
2887
  const detectorClient = wrapClient(rawClient);
2888
+ detachers.push(attachPageExit(detectorClient));
2564
2889
  const scrollDepth = triggers.automatic.scroll_depth;
2565
2890
  if (scrollDepth) {
2566
2891
  detachers.push(attachScrollDepth(detectorClient, scrollDepth));
@@ -2569,6 +2894,10 @@ function createTracking(options) {
2569
2894
  if (formStart) {
2570
2895
  detachers.push(attachFormStart(detectorClient, formStart));
2571
2896
  }
2897
+ const ctaClick = triggers.manual?.cta_click;
2898
+ if (ctaClick) {
2899
+ detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
2900
+ }
2572
2901
  return () => {
2573
2902
  for (let i = detachers.length - 1; i >= 0; i--) {
2574
2903
  detachers[i]();
@@ -2593,6 +2922,7 @@ function createTracking(options) {
2593
2922
  AdPlatformTracking,
2594
2923
  AranovaApiError,
2595
2924
  ConsentBanner,
2925
+ DEFAULT_DECLINE_TTL_DAYS,
2596
2926
  DEFAULT_PHONE_COUNTRY,
2597
2927
  GoogleAdsTracking,
2598
2928
  NAMED_RANGES,
@@ -2612,7 +2942,11 @@ function createTracking(options) {
2612
2942
  formatPhone,
2613
2943
  formatPhoneAsTyped,
2614
2944
  fromMinor,
2945
+ getConsentChoice,
2615
2946
  getConsentState,
2947
+ onConsentChange,
2948
+ optIn,
2949
+ optOut,
2616
2950
  parsePhone,
2617
2951
  phoneField,
2618
2952
  resetConsent,
@@ -2627,6 +2961,7 @@ function createTracking(options) {
2627
2961
  toMinor,
2628
2962
  useConsent,
2629
2963
  useConsentState,
2964
+ useCookiePreferences,
2630
2965
  useGclid,
2631
2966
  usePhoneConfig,
2632
2967
  usePhoneField,