@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.mjs CHANGED
@@ -1,13 +1,30 @@
1
1
  // ../tracking-core/src/consent.ts
2
2
  var CONSENT_STATE_KEY = "consent_state";
3
3
  var CONSENT_TIMESTAMP_KEY = "consent_timestamp";
4
- var grantedListeners = /* @__PURE__ */ new Set();
5
- function onConsentGranted(listener) {
6
- grantedListeners.add(listener);
4
+ var CONSENT_EXPIRES_AT_KEY = "consent_expires_at";
5
+ var DEFAULT_DECLINE_TTL_DAYS = 90;
6
+ var DAY_MS = 864e5;
7
+ var DEFAULT_CHOICE = {
8
+ state: "granted",
9
+ source: "default",
10
+ updatedAt: null,
11
+ expiresAt: null
12
+ };
13
+ var changeListeners = /* @__PURE__ */ new Set();
14
+ function onConsentChange(listener) {
15
+ changeListeners.add(listener);
7
16
  return () => {
8
- grantedListeners.delete(listener);
17
+ changeListeners.delete(listener);
9
18
  };
10
19
  }
20
+ function notifyConsentChanged(choice) {
21
+ for (const listener of changeListeners) {
22
+ try {
23
+ listener(choice);
24
+ } catch {
25
+ }
26
+ }
27
+ }
11
28
  function buildConsentPayload(state) {
12
29
  return {
13
30
  ad_storage: state,
@@ -16,42 +33,88 @@ function buildConsentPayload(state) {
16
33
  analytics_storage: state
17
34
  };
18
35
  }
19
- function getConsentState() {
20
- if (typeof window === "undefined") return "pending";
36
+ function getConsentChoice() {
37
+ if (typeof window === "undefined") return DEFAULT_CHOICE;
21
38
  try {
22
- const storedState = window.localStorage.getItem(CONSENT_STATE_KEY);
23
- if (storedState === "granted" || storedState === "denied") return storedState;
39
+ const stored = window.localStorage.getItem(CONSENT_STATE_KEY);
40
+ const updatedAt = window.localStorage.getItem(CONSENT_TIMESTAMP_KEY);
41
+ if (stored === "granted")
42
+ return { state: "granted", source: "explicit", updatedAt, expiresAt: null };
43
+ if (stored === "denied") {
44
+ let expiresAt = window.localStorage.getItem(CONSENT_EXPIRES_AT_KEY);
45
+ if (!expiresAt) {
46
+ expiresAt = new Date(Date.now() + DEFAULT_DECLINE_TTL_DAYS * DAY_MS).toISOString();
47
+ try {
48
+ window.localStorage.setItem(CONSENT_EXPIRES_AT_KEY, expiresAt);
49
+ } catch {
50
+ }
51
+ }
52
+ if (!(Date.parse(expiresAt) <= Date.now()))
53
+ return { state: "denied", source: "explicit", updatedAt, expiresAt };
54
+ }
24
55
  } catch {
25
56
  }
26
- return "pending";
57
+ return DEFAULT_CHOICE;
27
58
  }
28
- function setConsentState(state) {
59
+ function getConsentState() {
60
+ return getConsentChoice().state;
61
+ }
62
+ function pushConsentToPlatforms(state) {
29
63
  if (typeof window === "undefined") return;
30
- try {
31
- window.localStorage.setItem(CONSENT_STATE_KEY, state);
32
- window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, (/* @__PURE__ */ new Date()).toISOString());
33
- } catch {
34
- }
35
64
  if (typeof window.gtag === "function")
36
65
  window.gtag("consent", "update", buildConsentPayload(state));
37
66
  if (typeof window.fbq === "function")
38
67
  window.fbq("consent", state === "granted" ? "grant" : "revoke");
39
- if (state === "granted") {
40
- for (const listener of grantedListeners) {
41
- try {
42
- listener();
43
- } catch {
44
- }
68
+ }
69
+ function setConsentState(state, options) {
70
+ if (typeof window === "undefined") return;
71
+ const requestedTtl = options?.declineTtlDays;
72
+ const ttlDays = typeof requestedTtl === "number" && Number.isFinite(requestedTtl) && requestedTtl > 0 ? requestedTtl : DEFAULT_DECLINE_TTL_DAYS;
73
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
74
+ const expiresAt = state === "denied" ? new Date(Date.now() + ttlDays * DAY_MS).toISOString() : null;
75
+ try {
76
+ window.localStorage.setItem(CONSENT_STATE_KEY, state);
77
+ window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, updatedAt);
78
+ if (expiresAt !== null) {
79
+ window.localStorage.setItem(CONSENT_EXPIRES_AT_KEY, expiresAt);
80
+ } else {
81
+ window.localStorage.removeItem(CONSENT_EXPIRES_AT_KEY);
45
82
  }
83
+ } catch {
46
84
  }
85
+ pushConsentToPlatforms(state);
86
+ notifyConsentChanged({ state, source: "explicit", updatedAt, expiresAt });
87
+ }
88
+ function optIn() {
89
+ setConsentState("granted");
90
+ }
91
+ function optOut(options) {
92
+ setConsentState("denied", options);
47
93
  }
48
94
  function resetConsent() {
49
95
  if (typeof window === "undefined") return;
50
96
  try {
51
97
  window.localStorage.removeItem(CONSENT_STATE_KEY);
52
98
  window.localStorage.removeItem(CONSENT_TIMESTAMP_KEY);
99
+ window.localStorage.removeItem(CONSENT_EXPIRES_AT_KEY);
53
100
  } catch {
54
101
  }
102
+ pushConsentToPlatforms("granted");
103
+ notifyConsentChanged(DEFAULT_CHOICE);
104
+ }
105
+ function createEffectiveConsentSnippet() {
106
+ return `
107
+ var aranovaConsent = (function () {
108
+ try {
109
+ if (window.localStorage.getItem('${CONSENT_STATE_KEY}') === 'denied') {
110
+ var exp = window.localStorage.getItem('${CONSENT_EXPIRES_AT_KEY}');
111
+ var expMs = exp ? Date.parse(exp) : NaN;
112
+ if (!(expMs <= Date.now())) return 'denied';
113
+ }
114
+ } catch (e) {}
115
+ return 'granted';
116
+ })();
117
+ `.trim();
55
118
  }
56
119
 
57
120
  // ../tracking-core/src/tracking.ts
@@ -166,12 +229,12 @@ function createConsentDefaultScript() {
166
229
  window.dataLayer = window.dataLayer || [];
167
230
  function gtag(){dataLayer.push(arguments);}
168
231
  window.gtag = gtag;
232
+ ${createEffectiveConsentSnippet()}
169
233
  gtag('consent', 'default', {
170
- ad_storage: 'denied',
171
- ad_user_data: 'denied',
172
- ad_personalization: 'denied',
173
- analytics_storage: 'denied',
174
- wait_for_update: 500,
234
+ ad_storage: aranovaConsent,
235
+ ad_user_data: aranovaConsent,
236
+ ad_personalization: aranovaConsent,
237
+ analytics_storage: aranovaConsent
175
238
  });
176
239
  `.trim();
177
240
  }
@@ -185,21 +248,6 @@ gtag('js', new Date());
185
248
  gtag('config', '${gtagId}');
186
249
  `.trim();
187
250
  }
188
- function createConsentRestoreScript() {
189
- return `
190
- try {
191
- var consentState = window.localStorage.getItem('consent_state');
192
- if (window.gtag && (consentState === 'granted' || consentState === 'denied')) {
193
- window.gtag('consent', 'update', {
194
- ad_storage: consentState,
195
- ad_user_data: consentState,
196
- ad_personalization: consentState,
197
- analytics_storage: consentState,
198
- });
199
- }
200
- } catch (e) {}
201
- `.trim();
202
- }
203
251
  var SEND_TO_RE = /^AW-[A-Za-z0-9]+\/[A-Za-z0-9_-]+$/;
204
252
  function isValidSendTo(sendTo) {
205
253
  return SEND_TO_RE.test(sendTo);
@@ -278,7 +326,8 @@ document,'script','${FB_EVENTS_SCRIPT_HOST}');`.trim();
278
326
  }
279
327
  function createMetaConsentDefaultScript() {
280
328
  return `${fbqStubScript()}
281
- fbq('consent', 'revoke');`;
329
+ ${createEffectiveConsentSnippet()}
330
+ fbq('consent', aranovaConsent === 'denied' ? 'revoke' : 'grant');`;
282
331
  }
283
332
  function createMetaPixelInitScript(pixelId) {
284
333
  if (!isValidMetaPixelId(pixelId)) return "";
@@ -291,14 +340,71 @@ function createMetaPixelInitScriptMulti(pixelIds) {
291
340
  return `${inits.join("\n")}
292
341
  fbq('track', 'PageView');`;
293
342
  }
294
- function createMetaConsentRestoreScript() {
295
- return `
296
- try {
297
- var consentState = window.localStorage.getItem('consent_state');
298
- if (window.fbq && (consentState === 'granted' || consentState === 'denied')) {
299
- window.fbq('consent', consentState === 'granted' ? 'grant' : 'revoke');
343
+
344
+ // ../tracking-core/src/landing.ts
345
+ var LANDING_STORAGE_KEY = "_aranova_track_landing";
346
+ var memoryRecord = null;
347
+ function sanitizeParams(value) {
348
+ if (typeof value !== "object" || value === null) return {};
349
+ const source = value;
350
+ return TRACKING_PARAM_KEYS.reduce((params, key) => {
351
+ const entry = source[key];
352
+ if (typeof entry === "string" && entry.length > 0) params[key] = entry;
353
+ return params;
354
+ }, {});
355
+ }
356
+ function readStoredRecord() {
357
+ try {
358
+ const raw = window.localStorage.getItem(LANDING_STORAGE_KEY);
359
+ if (!raw) return null;
360
+ const parsed = JSON.parse(raw);
361
+ if (typeof parsed.session_id !== "string" || parsed.session_id.length === 0) return null;
362
+ return { session_id: parsed.session_id, params: sanitizeParams(parsed.params) };
363
+ } catch {
364
+ return null;
300
365
  }
301
- } catch (e) {}`.trim();
366
+ }
367
+ function writeRecord(record) {
368
+ memoryRecord = record;
369
+ try {
370
+ window.localStorage.setItem(LANDING_STORAGE_KEY, JSON.stringify(record));
371
+ } catch {
372
+ }
373
+ }
374
+ function captureFromUrl(url) {
375
+ try {
376
+ const resolved = new URL(url ?? window.location.href, window.location.origin);
377
+ return getTrackingQueryValues(resolved.searchParams);
378
+ } catch {
379
+ return {};
380
+ }
381
+ }
382
+ function getOrCaptureLandingParams(sessionId, url) {
383
+ if (typeof window === "undefined") return {};
384
+ const stored = readStoredRecord();
385
+ if (stored && stored.session_id === sessionId) {
386
+ memoryRecord = stored;
387
+ return stored.params;
388
+ }
389
+ if (memoryRecord && memoryRecord.session_id === sessionId) {
390
+ return memoryRecord.params;
391
+ }
392
+ const record = { session_id: sessionId, params: captureFromUrl(url) };
393
+ writeRecord(record);
394
+ return record.params;
395
+ }
396
+ function buildLandingPayloadFields(sessionId, override) {
397
+ const params = override ?? (typeof window === "undefined" ? null : getOrCaptureLandingParams(sessionId));
398
+ if (params === null) return {};
399
+ return {
400
+ landing_gclid: params.gclid ?? null,
401
+ landing_fbclid: params.fbclid ?? null,
402
+ landing_utm_source: params.utm_source ?? null,
403
+ landing_utm_medium: params.utm_medium ?? null,
404
+ landing_utm_campaign: params.utm_campaign ?? null,
405
+ landing_utm_term: params.utm_term ?? null,
406
+ landing_utm_content: params.utm_content ?? null
407
+ };
302
408
  }
303
409
 
304
410
  // ../tracking-core/src/payloads.ts
@@ -327,6 +433,9 @@ function createTrackingSessionUpsertPayload(trackingParams, input, context) {
327
433
  utm_campaign: trackingParams.utm_campaign,
328
434
  utm_term: trackingParams.utm_term,
329
435
  utm_content: trackingParams.utm_content,
436
+ // Omitted entirely when the landing isn't observable (SSR, no override) —
437
+ // present-as-null would wrongly tell the backend "landed with no params".
438
+ ...buildLandingPayloadFields(input.sessionId, input.landingParams),
330
439
  first_page: input.firstPage ?? null,
331
440
  consent_state: input.consentState ?? null,
332
441
  context
@@ -348,8 +457,6 @@ function createTrackingEventCreatePayload(trackingParams, input, context) {
348
457
 
349
458
  // ../tracking-core/src/resources/conversion-firing.ts
350
459
  var DEDUP_PREFIX = "_aranova_conv_";
351
- var MAX_PENDING = 100;
352
- var pendingQueue = [];
353
460
  function dedupKey(input) {
354
461
  return `${DEDUP_PREFIX}${input.transactionId ?? ""}:${input.sendTo}`;
355
462
  }
@@ -373,23 +480,9 @@ function fireOnce(input) {
373
480
  if (fireGtagConversion(input)) markFired(input);
374
481
  }
375
482
  function fireConversionWithConsent(input) {
376
- const state = getConsentState();
377
- if (state === "denied") return;
378
- if (state === "pending") {
379
- if (pendingQueue.length >= MAX_PENDING) pendingQueue.shift();
380
- pendingQueue.push(input);
381
- return;
382
- }
483
+ if (getConsentState() === "denied") return;
383
484
  fireOnce(input);
384
485
  }
385
- function flushPendingConversions() {
386
- if (getConsentState() !== "granted") return;
387
- while (pendingQueue.length > 0) {
388
- const input = pendingQueue.shift();
389
- if (input) fireOnce(input);
390
- }
391
- }
392
- if (typeof window !== "undefined") onConsentGranted(flushPendingConversions);
393
486
 
394
487
  // ../tracking-core/src/resources/conversion-config.ts
395
488
  function isStringMap(value) {
@@ -897,7 +990,13 @@ function readTrackingParams() {
897
990
  }
898
991
  function consentSnapshot() {
899
992
  try {
900
- return { state: getConsentState() };
993
+ const choice = getConsentChoice();
994
+ return {
995
+ state: choice.state,
996
+ source: choice.source,
997
+ updated_at: choice.updatedAt,
998
+ expires_at: choice.expiresAt
999
+ };
901
1000
  } catch {
902
1001
  return null;
903
1002
  }
@@ -970,6 +1069,7 @@ function createTrackingClient(config) {
970
1069
  initialParams = captureTrackingParamsFromLocation();
971
1070
  } catch {
972
1071
  }
1072
+ getOrCaptureLandingParams(sessionId);
973
1073
  try {
974
1074
  captureFbc();
975
1075
  } catch {
@@ -1019,6 +1119,11 @@ function createTrackingClient(config) {
1019
1119
  utm_campaign: params.utm_campaign,
1020
1120
  utm_term: params.utm_term,
1021
1121
  utm_content: params.utm_content,
1122
+ // Landing params for the CURRENT session id — captured on the spot when
1123
+ // the session just rotated (the current URL is the rotated session's
1124
+ // landing), reused from the stored record otherwise. Keys are omitted
1125
+ // entirely when the landing isn't observable (SSR).
1126
+ ...buildLandingPayloadFields(sessionId),
1022
1127
  first_page: firstPage,
1023
1128
  consent_state: consentSnapshot(),
1024
1129
  context
@@ -1086,6 +1191,7 @@ function createTrackingClient(config) {
1086
1191
  return {
1087
1192
  trackEvent,
1088
1193
  flush,
1194
+ flushBeacon: flushOnUnload,
1089
1195
  getSessionId: () => sessionId,
1090
1196
  getVisitorId: () => visitorId,
1091
1197
  destroy: () => {
@@ -1110,9 +1216,17 @@ var ctaClickMetadataSchema = z2.object({
1110
1216
  path: z2.string()
1111
1217
  }).strict(),
1112
1218
  section: z2.string().nullable().optional(),
1113
- destination_url: z2.string().nullable().optional()
1219
+ destination_url: z2.string().nullable().optional(),
1220
+ // Set by auto-capture (and available to manual callers): the link target
1221
+ // and a short element descriptor (tag#id) for tying clicks to specific UI.
1222
+ href: z2.string().nullable().optional(),
1223
+ element: z2.string().nullable().optional()
1224
+ }).strict();
1225
+ var ctaClickConfigSchema = z2.object({
1226
+ autoCapture: z2.object({
1227
+ selector: z2.string().optional()
1228
+ }).strict().optional()
1114
1229
  }).strict();
1115
- var ctaClickConfigSchema = z2.object({}).strict();
1116
1230
 
1117
1231
  // ../tracking-core/src/events/sdk-heartbeat.ts
1118
1232
  import { z as z3 } from "zod";
@@ -1188,31 +1302,44 @@ var multiPageSessionConfigSchema = z6.object({
1188
1302
  pageThreshold: z6.number().int().min(2)
1189
1303
  }).strict();
1190
1304
 
1191
- // ../tracking-core/src/events/phone-click.ts
1305
+ // ../tracking-core/src/events/page-exit.ts
1192
1306
  import { z as z7 } from "zod";
1193
- var phoneClickMetadataSchema = z7.object({
1194
- phone_number: z7.string(),
1307
+ var pageExitMetadataSchema = z7.object({
1308
+ dwell_ms: z7.number().int().min(0),
1309
+ // null = left without any scroll signal; floor is 0 so a valid 0% is never
1310
+ // rejected (a single bad field 422s the whole keepalive beacon batch).
1311
+ max_scroll_percent: z7.number().int().min(0).max(100).nullable(),
1195
1312
  page: z7.object({
1196
1313
  path: z7.string()
1197
- }).strict(),
1198
- section: z7.string().nullable().optional()
1314
+ }).strict()
1199
1315
  }).strict();
1200
- var phoneClickConfigSchema = z7.object({}).strict();
1316
+ var pageExitConfigSchema = z7.object({}).strict();
1201
1317
 
1202
- // ../tracking-core/src/events/scroll-depth.ts
1318
+ // ../tracking-core/src/events/phone-click.ts
1203
1319
  import { z as z8 } from "zod";
1204
- var scrollDepthMetadataSchema = z8.object({
1205
- depth_percent: z8.number().int().min(1).max(100),
1320
+ var phoneClickMetadataSchema = z8.object({
1321
+ phone_number: z8.string(),
1206
1322
  page: z8.object({
1207
1323
  path: z8.string()
1324
+ }).strict(),
1325
+ section: z8.string().nullable().optional()
1326
+ }).strict();
1327
+ var phoneClickConfigSchema = z8.object({}).strict();
1328
+
1329
+ // ../tracking-core/src/events/scroll-depth.ts
1330
+ import { z as z9 } from "zod";
1331
+ var scrollDepthMetadataSchema = z9.object({
1332
+ depth_percent: z9.number().int().min(1).max(100),
1333
+ page: z9.object({
1334
+ path: z9.string()
1208
1335
  }).strict()
1209
1336
  }).strict();
1210
- var scrollDepthConfigSchema = z8.object({
1211
- thresholds: z8.array(z8.number().int().min(1).max(100)).min(1)
1337
+ var scrollDepthConfigSchema = z9.object({
1338
+ thresholds: z9.array(z9.number().int().min(1).max(100)).min(1)
1212
1339
  }).strict();
1213
1340
 
1214
1341
  // ../tracking-core/src/events/specific-page-visit.ts
1215
- import { z as z9 } from "zod";
1342
+ import { z as z10 } from "zod";
1216
1343
  var SPECIFIC_PAGE_NAMES = [
1217
1344
  "contact_page",
1218
1345
  "about_page",
@@ -1223,18 +1350,18 @@ var SPECIFIC_PAGE_NAMES = [
1223
1350
  "faq_page",
1224
1351
  "testimonials_page"
1225
1352
  ];
1226
- var specificPageNameSchema = z9.enum(SPECIFIC_PAGE_NAMES);
1227
- var specificPageVisitMetadataSchema = z9.object({
1353
+ var specificPageNameSchema = z10.enum(SPECIFIC_PAGE_NAMES);
1354
+ var specificPageVisitMetadataSchema = z10.object({
1228
1355
  page_name: specificPageNameSchema,
1229
- page: z9.object({
1230
- path: z9.string()
1356
+ page: z10.object({
1357
+ path: z10.string()
1231
1358
  }).strict()
1232
1359
  }).strict();
1233
- var specificPageVisitConfigSchema = z9.object({
1234
- pages: z9.array(
1235
- z9.object({
1360
+ var specificPageVisitConfigSchema = z10.object({
1361
+ pages: z10.array(
1362
+ z10.object({
1236
1363
  name: specificPageNameSchema,
1237
- pathPattern: z9.custom((value) => value instanceof RegExp, {
1364
+ pathPattern: z10.custom((value) => value instanceof RegExp, {
1238
1365
  message: "pathPattern must be a RegExp"
1239
1366
  })
1240
1367
  }).strict()
@@ -1242,15 +1369,15 @@ var specificPageVisitConfigSchema = z9.object({
1242
1369
  }).strict();
1243
1370
 
1244
1371
  // ../tracking-core/src/events/time-on-site.ts
1245
- import { z as z10 } from "zod";
1246
- var timeOnSiteMetadataSchema = z10.object({
1247
- duration_ms: z10.number().int().nonnegative(),
1248
- page: z10.object({
1249
- path: z10.string()
1372
+ import { z as z11 } from "zod";
1373
+ var timeOnSiteMetadataSchema = z11.object({
1374
+ duration_ms: z11.number().int().nonnegative(),
1375
+ page: z11.object({
1376
+ path: z11.string()
1250
1377
  }).strict()
1251
1378
  }).strict();
1252
- var timeOnSiteConfigSchema = z10.object({
1253
- thresholdSeconds: z10.number().int().positive()
1379
+ var timeOnSiteConfigSchema = z11.object({
1380
+ thresholdSeconds: z11.number().int().positive()
1254
1381
  }).strict();
1255
1382
 
1256
1383
  // ../tracking-core/src/events/registry.ts
@@ -1292,6 +1419,11 @@ var EVENT_REGISTRY = {
1292
1419
  metadataSchema: sdkHeartbeatMetadataSchema,
1293
1420
  configSchema: sdkHeartbeatConfigSchema
1294
1421
  },
1422
+ page_exit: {
1423
+ kind: "automatic",
1424
+ metadataSchema: pageExitMetadataSchema,
1425
+ configSchema: pageExitConfigSchema
1426
+ },
1295
1427
  // --- manual triggers ---
1296
1428
  form_submit: {
1297
1429
  kind: "manual",
@@ -1535,6 +1667,163 @@ function attachFormStart(client, config) {
1535
1667
  };
1536
1668
  }
1537
1669
 
1670
+ // ../tracking-core/src/triggers/page-exit.ts
1671
+ function attachPageExit(client) {
1672
+ if (typeof window === "undefined" || typeof document === "undefined") {
1673
+ return () => {
1674
+ };
1675
+ }
1676
+ let currentPath2 = window.location.pathname;
1677
+ let activeSince = document.visibilityState === "visible" ? Date.now() : null;
1678
+ let accumulatedMs = 0;
1679
+ let maxScrollPercent = null;
1680
+ let rafId = null;
1681
+ function getScrollPercent() {
1682
+ const doc = document.documentElement;
1683
+ const scrollTop = window.scrollY || doc.scrollTop;
1684
+ const scrollHeight = doc.scrollHeight;
1685
+ const clientHeight = doc.clientHeight;
1686
+ if (scrollHeight <= clientHeight) return 100;
1687
+ return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1688
+ }
1689
+ function onScroll() {
1690
+ if (rafId !== null) return;
1691
+ rafId = requestAnimationFrame(() => {
1692
+ rafId = null;
1693
+ const percent = getScrollPercent();
1694
+ if (percent >= 1 && (maxScrollPercent === null || percent > maxScrollPercent)) {
1695
+ maxScrollPercent = Math.min(percent, 100);
1696
+ }
1697
+ });
1698
+ }
1699
+ function settledDwellMs() {
1700
+ let total = accumulatedMs;
1701
+ if (activeSince !== null) {
1702
+ total += Date.now() - activeSince;
1703
+ }
1704
+ return Math.max(0, Math.round(total));
1705
+ }
1706
+ function emitSegment(path, flush) {
1707
+ const dwell = settledDwellMs();
1708
+ if (dwell === 0) return;
1709
+ client.trackEvent({
1710
+ eventType: "page_exit",
1711
+ metadata: {
1712
+ dwell_ms: dwell,
1713
+ max_scroll_percent: maxScrollPercent,
1714
+ page: { path }
1715
+ },
1716
+ pageUrl: window.location.href,
1717
+ occurredAt: null
1718
+ });
1719
+ if (flush) {
1720
+ client.flushBeacon();
1721
+ }
1722
+ accumulatedMs = 0;
1723
+ activeSince = document.visibilityState === "visible" ? Date.now() : null;
1724
+ }
1725
+ function onNavigate() {
1726
+ const newPath = window.location.pathname;
1727
+ if (newPath === currentPath2) return;
1728
+ emitSegment(currentPath2, false);
1729
+ currentPath2 = newPath;
1730
+ maxScrollPercent = null;
1731
+ accumulatedMs = 0;
1732
+ activeSince = document.visibilityState === "visible" ? Date.now() : null;
1733
+ }
1734
+ function onVisibilityChange() {
1735
+ if (document.visibilityState === "hidden") {
1736
+ emitSegment(currentPath2, true);
1737
+ } else {
1738
+ activeSince = Date.now();
1739
+ }
1740
+ }
1741
+ function onPageHide() {
1742
+ emitSegment(currentPath2, true);
1743
+ }
1744
+ const originalPushState = history.pushState.bind(history);
1745
+ const originalReplaceState = history.replaceState.bind(history);
1746
+ function patchedPushState(...args) {
1747
+ originalPushState(...args);
1748
+ setTimeout(onNavigate, 0);
1749
+ }
1750
+ function patchedReplaceState(...args) {
1751
+ originalReplaceState(...args);
1752
+ setTimeout(onNavigate, 0);
1753
+ }
1754
+ history.pushState = patchedPushState;
1755
+ history.replaceState = patchedReplaceState;
1756
+ window.addEventListener("popstate", onNavigate);
1757
+ window.addEventListener("scroll", onScroll, { passive: true });
1758
+ document.addEventListener("visibilitychange", onVisibilityChange);
1759
+ window.addEventListener("pagehide", onPageHide);
1760
+ return () => {
1761
+ if (rafId !== null) cancelAnimationFrame(rafId);
1762
+ history.pushState = originalPushState;
1763
+ history.replaceState = originalReplaceState;
1764
+ window.removeEventListener("popstate", onNavigate);
1765
+ window.removeEventListener("scroll", onScroll);
1766
+ document.removeEventListener("visibilitychange", onVisibilityChange);
1767
+ window.removeEventListener("pagehide", onPageHide);
1768
+ };
1769
+ }
1770
+
1771
+ // ../tracking-core/src/triggers/cta-click-capture.ts
1772
+ var DEFAULT_CTA_SELECTOR = "[data-aranova-cta]";
1773
+ var CTA_NAME_MAX_LENGTH = 120;
1774
+ function describeElement(el) {
1775
+ const tag = el.tagName.toLowerCase();
1776
+ return el.id ? `${tag}#${el.id}` : tag;
1777
+ }
1778
+ function resolveCtaName(el) {
1779
+ const explicit = el.getAttribute("data-aranova-cta");
1780
+ if (explicit && explicit.trim().length > 0) return explicit.trim();
1781
+ const text = (el.textContent ?? "").trim().replace(/\s+/g, " ");
1782
+ if (text.length > 0) return text.slice(0, CTA_NAME_MAX_LENGTH);
1783
+ return describeElement(el);
1784
+ }
1785
+ function attachCtaClickCapture(client, config) {
1786
+ if (typeof window === "undefined" || typeof document === "undefined") {
1787
+ return () => {
1788
+ };
1789
+ }
1790
+ const autoCapture = config.autoCapture;
1791
+ if (!autoCapture) {
1792
+ return () => {
1793
+ };
1794
+ }
1795
+ const selector = autoCapture.selector ?? DEFAULT_CTA_SELECTOR;
1796
+ function onClick(event) {
1797
+ const target = event.target;
1798
+ if (!(target instanceof Element)) return;
1799
+ let matched = null;
1800
+ try {
1801
+ matched = target.closest(selector);
1802
+ } catch {
1803
+ return;
1804
+ }
1805
+ if (matched === null) return;
1806
+ const href = matched instanceof HTMLAnchorElement ? matched.href || null : matched.getAttribute("href");
1807
+ client.trackEvent({
1808
+ eventType: "cta_click",
1809
+ metadata: {
1810
+ cta_name: resolveCtaName(matched),
1811
+ page: { path: window.location.pathname },
1812
+ section: matched.getAttribute("data-aranova-section"),
1813
+ destination_url: href,
1814
+ href,
1815
+ element: describeElement(matched)
1816
+ },
1817
+ pageUrl: window.location.href,
1818
+ occurredAt: null
1819
+ });
1820
+ }
1821
+ document.addEventListener("click", onClick, true);
1822
+ return () => {
1823
+ document.removeEventListener("click", onClick, true);
1824
+ };
1825
+ }
1826
+
1538
1827
  // ../tracking-core/src/resources/sales/errors.ts
1539
1828
  var AranovaApiError = class extends Error {
1540
1829
  constructor(message, options) {
@@ -1741,42 +2030,42 @@ function createSalesClient(config) {
1741
2030
  }
1742
2031
 
1743
2032
  // ../tracking-core/src/resources/sales/schema.ts
1744
- import { z as z11 } from "zod";
2033
+ import { z as z12 } from "zod";
1745
2034
  var SUPPORTED_CURRENCIES = ["USD", "CAD"];
1746
2035
  var TRACKING_ENVIRONMENTS = ["production", "development"];
1747
- var currencySchema = z11.enum(SUPPORTED_CURRENCIES);
1748
- var centsSchema = z11.number().int().nonnegative();
1749
- var quantitySchema = z11.string().regex(/^\d+(\.\d{1,3})?$/);
1750
- var metadataSchema = z11.record(z11.unknown());
1751
- var saleItemSchema = z11.object({
1752
- external_item_id: z11.string().nullable().optional(),
1753
- name: z11.string().nullable().optional(),
1754
- category: z11.string().nullable().optional(),
2036
+ var currencySchema = z12.enum(SUPPORTED_CURRENCIES);
2037
+ var centsSchema = z12.number().int().nonnegative();
2038
+ var quantitySchema = z12.string().regex(/^\d+(\.\d{1,3})?$/);
2039
+ var metadataSchema = z12.record(z12.unknown());
2040
+ var saleItemSchema = z12.object({
2041
+ external_item_id: z12.string().nullable().optional(),
2042
+ name: z12.string().nullable().optional(),
2043
+ category: z12.string().nullable().optional(),
1755
2044
  quantity: quantitySchema,
1756
2045
  unit_price_cents: centsSchema,
1757
2046
  // Non-negativity validated on the wire — same contract as the other cents
1758
2047
  // fields — and backstopped by the DB CHECK.
1759
2048
  unit_cost_cents: centsSchema.nullable().optional()
1760
2049
  }).strict();
1761
- var saleServiceSchema = z11.object({
1762
- service: z11.string(),
2050
+ var saleServiceSchema = z12.object({
2051
+ service: z12.string(),
1763
2052
  amount_cents: centsSchema
1764
2053
  }).strict();
1765
- var customerNameSchema = z11.string().max(200);
1766
- var customerPhoneSchema = z11.string().max(64);
1767
- var customerEmailSchema = z11.string().max(320).email();
2054
+ var customerNameSchema = z12.string().max(200);
2055
+ var customerPhoneSchema = z12.string().max(64);
2056
+ var customerEmailSchema = z12.string().max(320).email();
1768
2057
  function refineServiceXor(val, ctx, { requireAmount }) {
1769
2058
  if (val.services != null) {
1770
2059
  if (val.service != null) {
1771
2060
  ctx.addIssue({
1772
- code: z11.ZodIssueCode.custom,
2061
+ code: z12.ZodIssueCode.custom,
1773
2062
  message: "pass either `service` or `services`, not both",
1774
2063
  path: ["services"]
1775
2064
  });
1776
2065
  }
1777
2066
  if (val.services.length === 0) {
1778
2067
  ctx.addIssue({
1779
- code: z11.ZodIssueCode.custom,
2068
+ code: z12.ZodIssueCode.custom,
1780
2069
  message: "`services` must not be empty",
1781
2070
  path: ["services"]
1782
2071
  });
@@ -1784,7 +2073,7 @@ function refineServiceXor(val, ctx, { requireAmount }) {
1784
2073
  const keys = val.services.map((s) => s.service);
1785
2074
  if (new Set(keys).size !== keys.length) {
1786
2075
  ctx.addIssue({
1787
- code: z11.ZodIssueCode.custom,
2076
+ code: z12.ZodIssueCode.custom,
1788
2077
  message: "`services` must not list the same service more than once",
1789
2078
  path: ["services"]
1790
2079
  });
@@ -1793,7 +2082,7 @@ function refineServiceXor(val, ctx, { requireAmount }) {
1793
2082
  const sum = val.services.reduce((acc, s) => acc + s.amount_cents, 0);
1794
2083
  if (val.amount_total_cents !== sum) {
1795
2084
  ctx.addIssue({
1796
- code: z11.ZodIssueCode.custom,
2085
+ code: z12.ZodIssueCode.custom,
1797
2086
  message: "amount_total_cents must equal the sum of the services amounts (omit it to derive it automatically)",
1798
2087
  path: ["amount_total_cents"]
1799
2088
  });
@@ -1801,37 +2090,37 @@ function refineServiceXor(val, ctx, { requireAmount }) {
1801
2090
  }
1802
2091
  } else if (requireAmount && val.amount_total_cents == null) {
1803
2092
  ctx.addIssue({
1804
- code: z11.ZodIssueCode.custom,
2093
+ code: z12.ZodIssueCode.custom,
1805
2094
  message: "amount_total_cents is required unless `services` is provided",
1806
2095
  path: ["amount_total_cents"]
1807
2096
  });
1808
2097
  }
1809
2098
  }
1810
- var saleCreateSchema = z11.object({
1811
- external_id: z11.string().nullable().optional(),
1812
- description: z11.string().nullable().optional(),
1813
- service: z11.string().nullable().optional(),
1814
- services: z11.array(saleServiceSchema).nullable().optional(),
2099
+ var saleCreateSchema = z12.object({
2100
+ external_id: z12.string().nullable().optional(),
2101
+ description: z12.string().nullable().optional(),
2102
+ service: z12.string().nullable().optional(),
2103
+ services: z12.array(saleServiceSchema).nullable().optional(),
1815
2104
  currency: currencySchema,
1816
2105
  // Optional only because the plural `services` form derives it from the sum
1817
2106
  // (see refineServiceXor); the singular/serviceless path still requires it.
1818
2107
  amount_total_cents: centsSchema.nullable().optional(),
1819
- occurred_at: z11.string().datetime(),
1820
- environment: z11.enum(TRACKING_ENVIRONMENTS).default("production"),
1821
- items: z11.array(saleItemSchema).default([]),
2108
+ occurred_at: z12.string().datetime(),
2109
+ environment: z12.enum(TRACKING_ENVIRONMENTS).default("production"),
2110
+ items: z12.array(saleItemSchema).default([]),
1822
2111
  metadata: metadataSchema.nullable().optional(),
1823
2112
  customer_name: customerNameSchema.nullable().optional(),
1824
2113
  customer_phone: customerPhoneSchema.nullable().optional(),
1825
2114
  customer_email: customerEmailSchema.nullable().optional()
1826
2115
  }).strict().superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: true }));
1827
- var saleUpdateSchema = z11.object({
1828
- description: z11.string().nullable().optional(),
1829
- service: z11.string().nullable().optional(),
1830
- services: z11.array(saleServiceSchema).nullable().optional(),
2116
+ var saleUpdateSchema = z12.object({
2117
+ description: z12.string().nullable().optional(),
2118
+ service: z12.string().nullable().optional(),
2119
+ services: z12.array(saleServiceSchema).nullable().optional(),
1831
2120
  currency: currencySchema.optional(),
1832
2121
  amount_total_cents: centsSchema.optional(),
1833
- occurred_at: z11.string().datetime().optional(),
1834
- items: z11.array(saleItemSchema).optional(),
2122
+ occurred_at: z12.string().datetime().optional(),
2123
+ items: z12.array(saleItemSchema).optional(),
1835
2124
  metadata: metadataSchema.nullable().optional(),
1836
2125
  customer_name: customerNameSchema.nullable().optional(),
1837
2126
  customer_phone: customerPhoneSchema.nullable().optional(),
@@ -1921,36 +2210,68 @@ function useTrackingParams() {
1921
2210
  }, []);
1922
2211
  return trackingParams;
1923
2212
  }
1924
- function useConsentState() {
1925
- return useConsent().state;
1926
- }
1927
- function useConsent() {
1928
- const [state, setState] = useState("pending");
2213
+ var DEFAULT_CHOICE2 = {
2214
+ state: "granted",
2215
+ source: "default",
2216
+ updatedAt: null,
2217
+ expiresAt: null
2218
+ };
2219
+ function useCookiePreferences(options) {
2220
+ const [choice, setChoice] = useState(DEFAULT_CHOICE2);
2221
+ const ttlDays = options?.declineTtlDays;
1929
2222
  useEffect(() => {
1930
- setState(getConsentState());
2223
+ const sync = () => setChoice(getConsentChoice());
2224
+ sync();
1931
2225
  const handleStorage = (event) => {
1932
- if (event.key === CONSENT_STATE_KEY) setState(getConsentState());
2226
+ if (event.key === null || event.key === CONSENT_STATE_KEY || event.key === CONSENT_EXPIRES_AT_KEY || event.key === CONSENT_TIMESTAMP_KEY)
2227
+ sync();
1933
2228
  };
1934
2229
  window.addEventListener("storage", handleStorage);
1935
- return () => window.removeEventListener("storage", handleStorage);
1936
- }, []);
1937
- const accept = useCallback(() => {
1938
- setConsentState("granted");
1939
- setState("granted");
2230
+ const unsubscribe = onConsentChange(sync);
2231
+ return () => {
2232
+ window.removeEventListener("storage", handleStorage);
2233
+ unsubscribe();
2234
+ };
1940
2235
  }, []);
1941
- const decline = useCallback(() => {
1942
- setConsentState("denied");
1943
- setState("denied");
2236
+ const optOutAction = useCallback(() => {
2237
+ optOut(ttlDays != null ? { declineTtlDays: ttlDays } : void 0);
2238
+ }, [ttlDays]);
2239
+ const optInAction = useCallback(() => {
2240
+ optIn();
1944
2241
  }, []);
1945
2242
  const reset = useCallback(() => {
1946
2243
  resetConsent();
1947
- setState("pending");
1948
2244
  }, []);
2245
+ return {
2246
+ state: choice.state,
2247
+ source: choice.source,
2248
+ isDefault: choice.source === "default",
2249
+ isGranted: choice.state === "granted",
2250
+ isDenied: choice.state === "denied",
2251
+ updatedAt: choice.updatedAt,
2252
+ expiresAt: choice.expiresAt,
2253
+ optOut: optOutAction,
2254
+ optIn: optInAction,
2255
+ reset
2256
+ };
2257
+ }
2258
+ function useConsentState() {
2259
+ return useConsent().state;
2260
+ }
2261
+ function useConsent() {
2262
+ const {
2263
+ state,
2264
+ isGranted,
2265
+ isDenied,
2266
+ optIn: accept,
2267
+ optOut: decline,
2268
+ reset
2269
+ } = useCookiePreferences();
1949
2270
  return {
1950
2271
  state,
1951
- isPending: state === "pending",
1952
- isGranted: state === "granted",
1953
- isDenied: state === "denied",
2272
+ isPending: false,
2273
+ isGranted,
2274
+ isDenied,
1954
2275
  accept,
1955
2276
  decline,
1956
2277
  reset
@@ -2166,13 +2487,11 @@ function AdPlatformTracking({
2166
2487
  strategy: "afterInteractive"
2167
2488
  }
2168
2489
  ),
2169
- /* @__PURE__ */ jsx2(Script, { id: "gtag-init", strategy: "afterInteractive", children: gtagInitScript }),
2170
- /* @__PURE__ */ jsx2(Script, { id: "consent-restore", strategy: "afterInteractive", children: createConsentRestoreScript() })
2490
+ /* @__PURE__ */ jsx2(Script, { id: "gtag-init", strategy: "afterInteractive", children: gtagInitScript })
2171
2491
  ] }) : null,
2172
2492
  metaAll.length > 0 ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
2173
2493
  /* @__PURE__ */ jsx2(Script, { id: "meta-pixel-base", strategy: "afterInteractive", children: createMetaConsentDefaultScript() }),
2174
- /* @__PURE__ */ jsx2(Script, { id: "meta-pixel-init", strategy: "afterInteractive", children: metaInitScript }),
2175
- /* @__PURE__ */ jsx2(Script, { id: "meta-pixel-consent-restore", strategy: "afterInteractive", children: createMetaConsentRestoreScript() })
2494
+ /* @__PURE__ */ jsx2(Script, { id: "meta-pixel-init", strategy: "afterInteractive", children: metaInitScript })
2176
2495
  ] }) : null
2177
2496
  ] });
2178
2497
  }
@@ -2195,8 +2514,7 @@ function GoogleAdsTracking(props) {
2195
2514
  strategy: "afterInteractive"
2196
2515
  }
2197
2516
  ),
2198
- /* @__PURE__ */ jsx3(Script2, { id: "gtag-init", strategy: "afterInteractive", children: initScript }),
2199
- /* @__PURE__ */ jsx3(Script2, { id: "consent-restore", strategy: "afterInteractive", children: createConsentRestoreScript() })
2517
+ /* @__PURE__ */ jsx3(Script2, { id: "gtag-init", strategy: "afterInteractive", children: initScript })
2200
2518
  ] });
2201
2519
  }
2202
2520
 
@@ -2204,7 +2522,7 @@ function GoogleAdsTracking(props) {
2204
2522
  import { usePathname, useSearchParams } from "next/navigation";
2205
2523
 
2206
2524
  // package.json
2207
- var version = "0.14.2";
2525
+ var version = "0.16.0";
2208
2526
 
2209
2527
  // src/factory.tsx
2210
2528
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect3, useMemo as useMemo2, useRef } from "react";
@@ -2492,6 +2810,7 @@ function createTracking(options) {
2492
2810
  debug
2493
2811
  });
2494
2812
  const detectorClient = wrapClient(rawClient);
2813
+ detachers.push(attachPageExit(detectorClient));
2495
2814
  const scrollDepth = triggers.automatic.scroll_depth;
2496
2815
  if (scrollDepth) {
2497
2816
  detachers.push(attachScrollDepth(detectorClient, scrollDepth));
@@ -2500,6 +2819,10 @@ function createTracking(options) {
2500
2819
  if (formStart) {
2501
2820
  detachers.push(attachFormStart(detectorClient, formStart));
2502
2821
  }
2822
+ const ctaClick = triggers.manual?.cta_click;
2823
+ if (ctaClick) {
2824
+ detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
2825
+ }
2503
2826
  return () => {
2504
2827
  for (let i = detachers.length - 1; i >= 0; i--) {
2505
2828
  detachers[i]();
@@ -2523,6 +2846,7 @@ export {
2523
2846
  AdPlatformTracking,
2524
2847
  AranovaApiError,
2525
2848
  ConsentBanner,
2849
+ DEFAULT_DECLINE_TTL_DAYS,
2526
2850
  DEFAULT_PHONE_COUNTRY,
2527
2851
  GoogleAdsTracking,
2528
2852
  NAMED_RANGES,
@@ -2542,7 +2866,11 @@ export {
2542
2866
  formatPhone,
2543
2867
  formatPhoneAsTyped,
2544
2868
  fromMinor,
2869
+ getConsentChoice,
2545
2870
  getConsentState,
2871
+ onConsentChange,
2872
+ optIn,
2873
+ optOut,
2546
2874
  parsePhone,
2547
2875
  phoneField,
2548
2876
  resetConsent,
@@ -2557,6 +2885,7 @@ export {
2557
2885
  toMinor,
2558
2886
  useConsent,
2559
2887
  useConsentState,
2888
+ useCookiePreferences,
2560
2889
  useGclid,
2561
2890
  usePhoneConfig,
2562
2891
  usePhoneField,