@xenia-martech/sdk-js 1.2.0-dev.3 → 1.2.0-dev.4

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.
Files changed (78) hide show
  1. package/README.md +106 -2
  2. package/dist/{actions-Bhji05Pe.d.cts → actions-C18Kv-4w.d.cts} +35 -0
  3. package/dist/{actions-Bhji05Pe.d.ts → actions-C18Kv-4w.d.ts} +35 -0
  4. package/dist/{api-ByoECRhR.d.ts → api-0IycTYJR.d.cts} +1 -1
  5. package/dist/{api-BqSvWgBZ.d.cts → api-COXWLL5L.d.ts} +1 -1
  6. package/dist/{chunk-TRRMMPBH.cjs → chunk-2DR5AVPX.cjs} +10 -45
  7. package/dist/chunk-2DR5AVPX.cjs.map +1 -0
  8. package/dist/chunk-BQ5PJCZ7.js +179 -0
  9. package/dist/chunk-BQ5PJCZ7.js.map +1 -0
  10. package/dist/{chunk-INM2SPH4.js → chunk-GWRDZIVA.js} +4 -36
  11. package/dist/chunk-GWRDZIVA.js.map +1 -0
  12. package/dist/{chunk-TGG5Y4QD.js → chunk-LW5AEJ6K.js} +5 -179
  13. package/dist/chunk-LW5AEJ6K.js.map +1 -0
  14. package/dist/chunk-N5M4PI7C.cjs +192 -0
  15. package/dist/chunk-N5M4PI7C.cjs.map +1 -0
  16. package/dist/chunk-OMMIRSPK.js +336 -0
  17. package/dist/chunk-OMMIRSPK.js.map +1 -0
  18. package/dist/chunk-QEYONKWY.js +42 -0
  19. package/dist/chunk-QEYONKWY.js.map +1 -0
  20. package/dist/{chunk-YZG6JR4E.cjs → chunk-RWE2OCQL.cjs} +5 -190
  21. package/dist/chunk-RWE2OCQL.cjs.map +1 -0
  22. package/dist/chunk-WHGK55JP.cjs +46 -0
  23. package/dist/chunk-WHGK55JP.cjs.map +1 -0
  24. package/dist/chunk-YLMMTKQL.cjs +358 -0
  25. package/dist/chunk-YLMMTKQL.cjs.map +1 -0
  26. package/dist/client/index.cjs +304 -26
  27. package/dist/client/index.cjs.map +1 -1
  28. package/dist/client/index.d.cts +15 -4
  29. package/dist/client/index.d.ts +15 -4
  30. package/dist/client/index.js +304 -26
  31. package/dist/client/index.js.map +1 -1
  32. package/dist/client/react/index.cjs +712 -26
  33. package/dist/client/react/index.cjs.map +1 -1
  34. package/dist/client/react/index.d.cts +32 -3
  35. package/dist/client/react/index.d.ts +32 -3
  36. package/dist/client/react/index.js +713 -29
  37. package/dist/client/react/index.js.map +1 -1
  38. package/dist/cms/contentful.d.cts +2 -1
  39. package/dist/cms/contentful.d.ts +2 -1
  40. package/dist/consent-Cemn91j3.d.ts +248 -0
  41. package/dist/consent-DgWdjsSy.d.cts +248 -0
  42. package/dist/{cookie-names-Bxd-VNyJ.d.ts → cookie-names-BvUaW3uo.d.ts} +1 -1
  43. package/dist/{cookie-names-RXVfGYPE.d.cts → cookie-names-CnL65Oly.d.cts} +1 -1
  44. package/dist/edge/index.cjs +280 -0
  45. package/dist/edge/index.cjs.map +1 -0
  46. package/dist/edge/index.d.cts +61 -0
  47. package/dist/edge/index.d.ts +61 -0
  48. package/dist/edge/index.js +191 -0
  49. package/dist/edge/index.js.map +1 -0
  50. package/dist/index.cjs +41 -40
  51. package/dist/index.d.cts +5 -4
  52. package/dist/index.d.ts +5 -4
  53. package/dist/index.js +2 -1
  54. package/dist/next/index.cjs +320 -15
  55. package/dist/next/index.cjs.map +1 -1
  56. package/dist/next/index.d.cts +180 -6
  57. package/dist/next/index.d.ts +180 -6
  58. package/dist/next/index.js +309 -9
  59. package/dist/next/index.js.map +1 -1
  60. package/dist/personalize-content-c2DDAV6g.d.ts +140 -0
  61. package/dist/personalize-content-ghZPKlf1.d.cts +140 -0
  62. package/dist/server/index.cjs +34 -32
  63. package/dist/server/index.cjs.map +1 -1
  64. package/dist/server/index.d.cts +6 -5
  65. package/dist/server/index.d.ts +6 -5
  66. package/dist/server/index.js +5 -3
  67. package/dist/server/index.js.map +1 -1
  68. package/dist/{personalize-content-Bf6YZdkZ.d.cts → types-DgVHS4Qw.d.cts} +7 -138
  69. package/dist/{personalize-content-Bf6YZdkZ.d.ts → types-DgVHS4Qw.d.ts} +7 -138
  70. package/dist/xenia-sdk.iife.js +3 -3
  71. package/dist/xenia-sdk.iife.js.map +1 -1
  72. package/dist/{xenia-server-MlI3Khya.d.cts → xenia-server-C3O-cWQT.d.cts} +3 -2
  73. package/dist/{xenia-server-CcjQenru.d.ts → xenia-server-DV8Sjmoo.d.ts} +3 -2
  74. package/package.json +14 -1
  75. package/dist/chunk-INM2SPH4.js.map +0 -1
  76. package/dist/chunk-TGG5Y4QD.js.map +0 -1
  77. package/dist/chunk-TRRMMPBH.cjs.map +0 -1
  78. package/dist/chunk-YZG6JR4E.cjs.map +0 -1
@@ -1,5 +1,5 @@
1
- import { I as InteractionType, X as XeniaConfig, a as XeniaEvent, M as MessageCta, b as MessageTemplateFn, c as MessageComponentRenderer, W as WidgetFormSubmit, d as WidgetSubmitResponse, e as XeniaAction, f as MessagePlacement, C as ConsentState } from '../actions-Bhji05Pe.cjs';
2
- export { A as AnalyticsContext, g as AnalyticsEventName, h as AnalyticsIdentity, i as AnalyticsIngestResponse, j as AppliedSwap, k as ClientEvent, l as CmsAdapter, m as ContentSwap, n as IdentifyRequest, o as IdentifyResponse, p as Identity, J as JsonValue, q as MessageMountContext, r as MessageRenderContext, P as PagePersonalizeOptions, s as PageRef, t as PersonalizationSwap, R as ResolveBatchItem, u as ResolveBatchResult, v as ResolveOptions, w as ResolveResult, T as TrackOptions, x as WebhookResponse, y as WidgetPrize, z as WidgetSegment, B as WidgetType, D as XeniaClient, E as XeniaClientOptions, F as XeniaMarker, G as createXeniaClient, H as extractContentSwaps, K as parseActions, L as pendingTargets, N as personalizeContent, O as personalizePage, Q as spliceSlots } from '../actions-Bhji05Pe.cjs';
1
+ import { I as InteractionType, X as XeniaConfig, a as XeniaEvent, M as MessageCta, b as MessageTemplateFn, c as MessageComponentRenderer, W as WidgetFormSubmit, d as WidgetSubmitResponse, e as XeniaAction, f as MessagePlacement, C as ConsentState } from '../actions-C18Kv-4w.cjs';
2
+ export { A as AnalyticsContext, g as AnalyticsEventName, h as AnalyticsIdentity, i as AnalyticsIngestResponse, j as AppliedSwap, k as ClientEvent, l as CmsAdapter, m as ContentSwap, n as IdentifyRequest, o as IdentifyResponse, p as Identity, J as JsonValue, q as MessageMountContext, r as MessageRenderContext, P as PagePersonalizeOptions, s as PageRef, t as PersonalizationSwap, R as ResolveBatchItem, u as ResolveBatchResult, v as ResolveOptions, w as ResolveResult, T as TrackOptions, x as WebhookResponse, y as WidgetPrize, z as WidgetSegment, B as WidgetType, D as XeniaClient, E as XeniaClientOptions, F as XeniaMarker, G as createXeniaClient, H as extractContentSwaps, K as parseActions, L as pendingTargets, N as personalizeContent, O as personalizePage, Q as spliceSlots } from '../actions-C18Kv-4w.cjs';
3
3
 
4
4
  interface Parsed {
5
5
  eventId: string;
@@ -9,10 +9,16 @@ interface Parsed {
9
9
  declare function parseDataAttribute(value: string): Parsed | null;
10
10
  /** Personalization slot metadata read from `data-xenia-*` attributes. */
11
11
  interface SlotMeta {
12
- contentKey: string;
12
+ /** The `data-xenia-slot` value: which section of the page this is. */
13
+ slot?: string;
14
+ contentKey?: string;
13
15
  contentEntryId?: string;
14
16
  baseEntryId?: string;
15
17
  personalized?: boolean;
18
+ /** `data-xenia-bucket`, e.g. `personalized` — stored by Xenia as the slot event's bucket. */
19
+ bucket?: string;
20
+ /** Click only: the clicked link's or button's text (≤60 chars). */
21
+ label?: string;
16
22
  }
17
23
  interface AutoTrackOptions {
18
24
  prefix: string;
@@ -30,8 +36,13 @@ interface AutoTrackOptions {
30
36
  * scanning is skipped entirely.
31
37
  */
32
38
  fireSlot?: (interaction: "impression" | "click", meta: SlotMeta) => void;
39
+ /** Which clicks in a slot count: `any` (default) or only a link's or button's (`controls`). */
40
+ slotClicks?: "any" | "controls";
33
41
  }
34
- /** Read the resolve metadata attributes off a `[data-xenia-slot]` element. */
42
+ /**
43
+ * Read the resolve metadata attributes off a `[data-xenia-slot]` element. `null` when it carries
44
+ * neither a content key nor an entry id (not a resolved slot).
45
+ */
35
46
  declare function readSlotMeta(el: Element): SlotMeta | null;
36
47
  /**
37
48
  * Wires declarative `data-xenia` auto-tracking: delegated clicks, an
@@ -1,5 +1,5 @@
1
- import { I as InteractionType, X as XeniaConfig, a as XeniaEvent, M as MessageCta, b as MessageTemplateFn, c as MessageComponentRenderer, W as WidgetFormSubmit, d as WidgetSubmitResponse, e as XeniaAction, f as MessagePlacement, C as ConsentState } from '../actions-Bhji05Pe.js';
2
- export { A as AnalyticsContext, g as AnalyticsEventName, h as AnalyticsIdentity, i as AnalyticsIngestResponse, j as AppliedSwap, k as ClientEvent, l as CmsAdapter, m as ContentSwap, n as IdentifyRequest, o as IdentifyResponse, p as Identity, J as JsonValue, q as MessageMountContext, r as MessageRenderContext, P as PagePersonalizeOptions, s as PageRef, t as PersonalizationSwap, R as ResolveBatchItem, u as ResolveBatchResult, v as ResolveOptions, w as ResolveResult, T as TrackOptions, x as WebhookResponse, y as WidgetPrize, z as WidgetSegment, B as WidgetType, D as XeniaClient, E as XeniaClientOptions, F as XeniaMarker, G as createXeniaClient, H as extractContentSwaps, K as parseActions, L as pendingTargets, N as personalizeContent, O as personalizePage, Q as spliceSlots } from '../actions-Bhji05Pe.js';
1
+ import { I as InteractionType, X as XeniaConfig, a as XeniaEvent, M as MessageCta, b as MessageTemplateFn, c as MessageComponentRenderer, W as WidgetFormSubmit, d as WidgetSubmitResponse, e as XeniaAction, f as MessagePlacement, C as ConsentState } from '../actions-C18Kv-4w.js';
2
+ export { A as AnalyticsContext, g as AnalyticsEventName, h as AnalyticsIdentity, i as AnalyticsIngestResponse, j as AppliedSwap, k as ClientEvent, l as CmsAdapter, m as ContentSwap, n as IdentifyRequest, o as IdentifyResponse, p as Identity, J as JsonValue, q as MessageMountContext, r as MessageRenderContext, P as PagePersonalizeOptions, s as PageRef, t as PersonalizationSwap, R as ResolveBatchItem, u as ResolveBatchResult, v as ResolveOptions, w as ResolveResult, T as TrackOptions, x as WebhookResponse, y as WidgetPrize, z as WidgetSegment, B as WidgetType, D as XeniaClient, E as XeniaClientOptions, F as XeniaMarker, G as createXeniaClient, H as extractContentSwaps, K as parseActions, L as pendingTargets, N as personalizeContent, O as personalizePage, Q as spliceSlots } from '../actions-C18Kv-4w.js';
3
3
 
4
4
  interface Parsed {
5
5
  eventId: string;
@@ -9,10 +9,16 @@ interface Parsed {
9
9
  declare function parseDataAttribute(value: string): Parsed | null;
10
10
  /** Personalization slot metadata read from `data-xenia-*` attributes. */
11
11
  interface SlotMeta {
12
- contentKey: string;
12
+ /** The `data-xenia-slot` value: which section of the page this is. */
13
+ slot?: string;
14
+ contentKey?: string;
13
15
  contentEntryId?: string;
14
16
  baseEntryId?: string;
15
17
  personalized?: boolean;
18
+ /** `data-xenia-bucket`, e.g. `personalized` — stored by Xenia as the slot event's bucket. */
19
+ bucket?: string;
20
+ /** Click only: the clicked link's or button's text (≤60 chars). */
21
+ label?: string;
16
22
  }
17
23
  interface AutoTrackOptions {
18
24
  prefix: string;
@@ -30,8 +36,13 @@ interface AutoTrackOptions {
30
36
  * scanning is skipped entirely.
31
37
  */
32
38
  fireSlot?: (interaction: "impression" | "click", meta: SlotMeta) => void;
39
+ /** Which clicks in a slot count: `any` (default) or only a link's or button's (`controls`). */
40
+ slotClicks?: "any" | "controls";
33
41
  }
34
- /** Read the resolve metadata attributes off a `[data-xenia-slot]` element. */
42
+ /**
43
+ * Read the resolve metadata attributes off a `[data-xenia-slot]` element. `null` when it carries
44
+ * neither a content key nor an entry id (not a resolved slot).
45
+ */
35
46
  declare function readSlotMeta(el: Element): SlotMeta | null;
36
47
  /**
37
48
  * Wires declarative `data-xenia` auto-tracking: delegated clicks, an
@@ -1088,17 +1088,22 @@ function parseDataAttribute(value) {
1088
1088
  return { eventId, interactionType };
1089
1089
  }
1090
1090
  function readSlotMeta(el) {
1091
- const contentKey = el.getAttribute("data-xenia-content-key");
1092
- if (!contentKey) return null;
1093
- const contentEntryId = el.getAttribute("data-xenia-content-entry-id");
1094
- const baseEntryId = el.getAttribute("data-xenia-base-entry-id");
1095
- const personalized = el.getAttribute("data-xenia-personalized");
1096
- return {
1097
- contentKey,
1098
- contentEntryId: contentEntryId ?? void 0,
1099
- baseEntryId: baseEntryId ?? void 0,
1100
- personalized: personalized == null ? void 0 : personalized === "true"
1101
- };
1091
+ const attr = (name) => el.getAttribute(`data-xenia-${name}`) ?? void 0;
1092
+ const contentKey = attr("content-key");
1093
+ const contentEntryId = attr("content-entry-id");
1094
+ if (!contentKey && !contentEntryId) return null;
1095
+ const personalized = attr("personalized");
1096
+ const meta2 = {};
1097
+ const slot = attr("slot");
1098
+ if (slot) meta2.slot = slot;
1099
+ if (contentKey) meta2.contentKey = contentKey;
1100
+ if (contentEntryId) meta2.contentEntryId = contentEntryId;
1101
+ const baseEntryId = attr("base-entry-id");
1102
+ if (baseEntryId) meta2.baseEntryId = baseEntryId;
1103
+ if (personalized != null) meta2.personalized = personalized === "true";
1104
+ const bucket = attr("bucket");
1105
+ if (bucket) meta2.bucket = bucket;
1106
+ return meta2;
1102
1107
  }
1103
1108
  var AutoTracker = class {
1104
1109
  opts;
@@ -1159,8 +1164,12 @@ var AutoTracker = class {
1159
1164
  if (this.opts.fireSlot) {
1160
1165
  const slot = target.closest("[data-xenia-slot]");
1161
1166
  if (slot) {
1162
- const meta = readSlotMeta(slot);
1163
- if (meta && !this.throttled(slot)) this.opts.fireSlot("click", meta);
1167
+ const meta2 = readSlotMeta(slot);
1168
+ const found = target.closest("a, button");
1169
+ const control = found && slot.contains(found) ? found : null;
1170
+ if (!control && this.opts.slotClicks === "controls") return;
1171
+ const label = control ? (control.textContent ?? "").trim().slice(0, 60) : "";
1172
+ if (meta2 && !this.throttled(slot)) this.opts.fireSlot("click", label ? { ...meta2, label } : meta2);
1164
1173
  }
1165
1174
  }
1166
1175
  };
@@ -1168,24 +1177,24 @@ var AutoTracker = class {
1168
1177
  }
1169
1178
  observeSlot(el) {
1170
1179
  if (!this.opts.fireSlot || this.slotObservers.has(el)) return;
1171
- const meta = readSlotMeta(el);
1172
- if (!meta) return;
1180
+ const meta2 = readSlotMeta(el);
1181
+ if (!meta2) return;
1173
1182
  const fireSlot = this.opts.fireSlot;
1174
1183
  const { threshold, rootMargin } = this.opts.viewport;
1175
1184
  const observer = new IntersectionObserver(
1176
1185
  (entries) => {
1177
1186
  for (const entry of entries) {
1178
- if (!entry.isIntersecting) continue;
1179
- if (this.slotImpressed.has(el)) continue;
1187
+ const inView = entry.isIntersecting && (entry.intersectionRatio >= threshold || entry.intersectionRect.height >= window.innerHeight / 2);
1188
+ if (!inView || this.slotImpressed.has(el)) continue;
1180
1189
  this.slotImpressed.add(el);
1181
- fireSlot("impression", meta);
1190
+ fireSlot("impression", meta2);
1182
1191
  observer.disconnect();
1183
1192
  this.slotObservers.delete(el);
1184
1193
  }
1185
1194
  },
1186
- { threshold, rootMargin }
1195
+ { threshold: [.../* @__PURE__ */ new Set([0.1, 0.25, threshold])].sort(), rootMargin }
1187
1196
  );
1188
- observer.observe(el);
1197
+ observer.observe(boxOf(el));
1189
1198
  this.slotObservers.set(el, observer);
1190
1199
  }
1191
1200
  observeViewport(el, eventId) {
@@ -1239,6 +1248,235 @@ var AutoTracker = class {
1239
1248
  this.mutationObserver.observe(document.body, { childList: true, subtree: true });
1240
1249
  }
1241
1250
  };
1251
+ function boxOf(el) {
1252
+ if (typeof getComputedStyle !== "function") return el;
1253
+ return getComputedStyle(el).display === "contents" ? el.firstElementChild ?? el : el;
1254
+ }
1255
+
1256
+ // src/edge/trail.ts
1257
+ var MAX_TRAIL = 8;
1258
+ var TRAIL_MAX_AGE_S = 30 * 60;
1259
+ function trailCookieName(prefix = DEFAULT_PREFIX) {
1260
+ return `${prefix}_trail`;
1261
+ }
1262
+ var decode = (v) => {
1263
+ try {
1264
+ return decodeURIComponent(v);
1265
+ } catch {
1266
+ return v;
1267
+ }
1268
+ };
1269
+ function readTrail(value) {
1270
+ if (!value) return [];
1271
+ try {
1272
+ const parsed = JSON.parse(decode(value));
1273
+ if (!Array.isArray(parsed)) return [];
1274
+ return parsed.filter((t) => Array.isArray(t) && typeof t[0] === "string" && typeof t[1] === "number").slice(0, MAX_TRAIL).map(
1275
+ ([path, at, seconds]) => typeof seconds === "number" && seconds >= 0 && seconds <= 86400 ? { path, at, seconds } : { path, at }
1276
+ );
1277
+ } catch {
1278
+ return [];
1279
+ }
1280
+ }
1281
+ function withView(trail, path, now = Date.now()) {
1282
+ if (trail[0]?.path === path) return trail;
1283
+ const earlier = trail.find((t) => t.path === path);
1284
+ return [{ path, at: earlier?.at ?? now }, ...trail.filter((t) => t.path !== path)].slice(0, MAX_TRAIL);
1285
+ }
1286
+ function trailCookieValue(trail) {
1287
+ return JSON.stringify(
1288
+ trail.slice(0, MAX_TRAIL).map((t) => t.seconds === void 0 ? [t.path, t.at] : [t.path, t.at, t.seconds])
1289
+ );
1290
+ }
1291
+
1292
+ // src/client/page-tracking.ts
1293
+ var SETTLE_MS = 3e3;
1294
+ var POLL_MS = 50;
1295
+ var MIN_VISIT_MS = 250;
1296
+ var listeners = /* @__PURE__ */ new Set();
1297
+ var restore = null;
1298
+ function watchNavigation(listener) {
1299
+ listeners.add(listener);
1300
+ if (!restore) {
1301
+ const { pushState, replaceState } = history;
1302
+ const notify = () => listeners.forEach((l) => {
1303
+ try {
1304
+ l();
1305
+ } catch {
1306
+ }
1307
+ });
1308
+ const ownPush = function(...args) {
1309
+ pushState.apply(this, args);
1310
+ notify();
1311
+ };
1312
+ const ownReplace = function(...args) {
1313
+ replaceState.apply(this, args);
1314
+ notify();
1315
+ };
1316
+ history.pushState = ownPush;
1317
+ history.replaceState = ownReplace;
1318
+ window.addEventListener("popstate", notify);
1319
+ restore = () => {
1320
+ if (history.pushState !== ownPush || history.replaceState !== ownReplace) return false;
1321
+ history.pushState = pushState;
1322
+ history.replaceState = replaceState;
1323
+ window.removeEventListener("popstate", notify);
1324
+ return true;
1325
+ };
1326
+ }
1327
+ return () => {
1328
+ listeners.delete(listener);
1329
+ if (!listeners.size && restore?.()) restore = null;
1330
+ };
1331
+ }
1332
+ var meta = (name) => document.querySelector(`meta[name="${name}"]`)?.content || void 0;
1333
+ function startPageTracking(client, opts) {
1334
+ let path = location.pathname;
1335
+ let lastTitle = null;
1336
+ let timer;
1337
+ let view = { path, since: visibleNow(), sent: false };
1338
+ function visibleNow() {
1339
+ return document.visibilityState === "visible" ? Date.now() : null;
1340
+ }
1341
+ function sendView() {
1342
+ clearTimeout(timer);
1343
+ const started = Date.now();
1344
+ const tick = () => {
1345
+ const settled = document.title !== "" && document.title !== lastTitle;
1346
+ if (!settled && Date.now() - started < SETTLE_MS) {
1347
+ timer = setTimeout(tick, POLL_MS);
1348
+ return;
1349
+ }
1350
+ const params = new URLSearchParams(location.search);
1351
+ const searchTerm = (params.get("utm_term") ?? params.get("q") ?? "").slice(0, 120);
1352
+ const category = meta("xenia-category");
1353
+ lastTitle = document.title;
1354
+ client.track("pageView", {
1355
+ title: (meta("xenia-title") ?? document.title).slice(0, 160),
1356
+ ...searchTerm ? { searchTerm } : {},
1357
+ ...category ? { category } : {}
1358
+ });
1359
+ };
1360
+ timer = setTimeout(tick, 0);
1361
+ }
1362
+ function leave() {
1363
+ if (view.sent || view.since === null || Date.now() - view.since < MIN_VISIT_MS) return;
1364
+ view.sent = true;
1365
+ client.track("page_leave", { path: view.path, durationMs: Date.now() - view.since });
1366
+ }
1367
+ const onNavigate = () => {
1368
+ if (location.pathname === path) return;
1369
+ if (opts.pageLeave) leave();
1370
+ path = location.pathname;
1371
+ view = { path, since: visibleNow(), sent: false };
1372
+ if (opts.pageViews) sendView();
1373
+ };
1374
+ const onVisibility = () => {
1375
+ if (document.visibilityState === "hidden") leave();
1376
+ else if (view.since === null) view.since = Date.now();
1377
+ };
1378
+ const unwatch = watchNavigation(onNavigate);
1379
+ if (opts.pageLeave) document.addEventListener("visibilitychange", onVisibility);
1380
+ if (opts.pageViews) sendView();
1381
+ return () => {
1382
+ clearTimeout(timer);
1383
+ unwatch();
1384
+ document.removeEventListener("visibilitychange", onVisibility);
1385
+ };
1386
+ }
1387
+
1388
+ // src/client/engagement.ts
1389
+ var rootOf = (opt, fallback) => typeof opt === "object" && opt.root || fallback;
1390
+ function readCookie(name) {
1391
+ return document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))?.[1];
1392
+ }
1393
+ function writeCookie(name, value) {
1394
+ try {
1395
+ document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${TRAIL_MAX_AGE_S}; samesite=lax`;
1396
+ } catch {
1397
+ }
1398
+ }
1399
+ function sectionOf(link) {
1400
+ if (!link || link.origin !== location.origin) return void 0;
1401
+ return link.pathname.split("/").filter(Boolean)[0];
1402
+ }
1403
+ function cardImage(target, root) {
1404
+ let el = target;
1405
+ for (let depth = 0; el && depth < 5 && el !== root; depth++, el = el.parentElement) {
1406
+ const imgs = [...el.querySelectorAll("img")].filter((i) => i.alt.trim());
1407
+ if (!imgs.length) continue;
1408
+ return new Set(imgs.map((i) => i.alt.trim())).size === 1 ? imgs[0] : null;
1409
+ }
1410
+ return null;
1411
+ }
1412
+ function startEngagement(client, opts) {
1413
+ const trailName = trailCookieName(opts.prefix);
1414
+ const clicksName = `${opts.prefix}_clicks`;
1415
+ const visibleNow = () => document.visibilityState === "visible" ? Date.now() : null;
1416
+ let current = { path: location.pathname, since: visibleNow(), stamped: false };
1417
+ const stamp = () => {
1418
+ if (current.stamped || !opts.trail || !opts.analyticsAllowed() || current.since === null) return;
1419
+ current.stamped = true;
1420
+ const seconds = Math.round((Date.now() - current.since) / 100) / 10;
1421
+ const trail = readTrail(readCookie(trailName));
1422
+ const i = trail.findIndex((t) => t.path === current.path);
1423
+ if (i < 0) return;
1424
+ trail[i] = { ...trail[i], seconds };
1425
+ writeCookie(trailName, trailCookieValue(trail));
1426
+ };
1427
+ const onNavigate = () => {
1428
+ if (location.pathname === current.path) return;
1429
+ stamp();
1430
+ current = { path: location.pathname, since: visibleNow(), stamped: false };
1431
+ if (!opts.trail || !opts.analyticsAllowed()) return;
1432
+ const trail = readTrail(readCookie(trailName));
1433
+ const next = withView(trail, current.path);
1434
+ if (next !== trail) writeCookie(trailName, trailCookieValue(next));
1435
+ };
1436
+ const onVisible = () => {
1437
+ if (document.visibilityState === "visible" && current.since === null) current.since = Date.now();
1438
+ };
1439
+ const onLeaveClick = (e) => {
1440
+ const link = e.target?.closest?.("a[href]");
1441
+ if (link && link.origin === location.origin && link.pathname !== location.pathname) stamp();
1442
+ };
1443
+ const bump = () => {
1444
+ if (!opts.analyticsAllowed()) return;
1445
+ writeCookie(clicksName, String((Number(readCookie(clicksName)) || 0) + 1));
1446
+ };
1447
+ const navRoot = rootOf(opts.navClicks, "header");
1448
+ const contentRoot = rootOf(opts.contentClicks, "main");
1449
+ const onClick = (e) => {
1450
+ const target = e.target;
1451
+ if (!target?.closest) return;
1452
+ const nav = opts.navClicks ? target.closest(`${navRoot} a[href]`) : null;
1453
+ if (nav) {
1454
+ const category2 = sectionOf(nav);
1455
+ if (!category2) return;
1456
+ client.track("nav_click", { category: category2, label: (nav.textContent ?? "").trim().slice(0, 60) });
1457
+ bump();
1458
+ return;
1459
+ }
1460
+ const root = opts.contentClicks ? target.closest(contentRoot) : null;
1461
+ if (!root) return;
1462
+ const img = target.closest("img") ?? cardImage(target, root);
1463
+ const label = (img?.alt ?? "").trim().slice(0, 160);
1464
+ if (!label) return;
1465
+ const category = sectionOf(target.closest("a[href]"));
1466
+ client.track("content_click", category ? { label, category } : { label });
1467
+ bump();
1468
+ };
1469
+ const unwatch = watchNavigation(onNavigate);
1470
+ window.addEventListener("click", onLeaveClick, { capture: true });
1471
+ document.addEventListener("visibilitychange", onVisible);
1472
+ if (opts.contentClicks || opts.navClicks) document.addEventListener("click", onClick, { capture: true });
1473
+ return () => {
1474
+ unwatch();
1475
+ window.removeEventListener("click", onLeaveClick, { capture: true });
1476
+ document.removeEventListener("visibilitychange", onVisible);
1477
+ document.removeEventListener("click", onClick, { capture: true });
1478
+ };
1479
+ }
1242
1480
 
1243
1481
  // src/server/cookies.ts
1244
1482
  function parseCookies(header) {
@@ -1249,7 +1487,12 @@ function parseCookies(header) {
1249
1487
  if (idx === -1) continue;
1250
1488
  const key = part.slice(0, idx).trim();
1251
1489
  const value = part.slice(idx + 1).trim();
1252
- if (key) out[key] = decodeURIComponent(value);
1490
+ if (!key) continue;
1491
+ try {
1492
+ out[key] = decodeURIComponent(value);
1493
+ } catch {
1494
+ out[key] = value;
1495
+ }
1253
1496
  }
1254
1497
  return out;
1255
1498
  }
@@ -1323,6 +1566,14 @@ var Identity = class {
1323
1566
  const storageKey = this.key("device_id");
1324
1567
  try {
1325
1568
  const cached = localStorage.getItem(storageKey);
1569
+ const fromServer = this.cookie?.authoritative ? this.readVisitorCookie() : null;
1570
+ const effective = localStorage.getItem(this.key("visitor_id")) ?? cached;
1571
+ if (cached && fromServer && fromServer !== effective) {
1572
+ this.deviceId = fromServer;
1573
+ localStorage.setItem(storageKey, fromServer);
1574
+ if (localStorage.getItem(this.key("visitor_id"))) localStorage.setItem(this.key("visitor_id"), fromServer);
1575
+ return fromServer;
1576
+ }
1326
1577
  if (cached) {
1327
1578
  this.deviceId = cached;
1328
1579
  this.writeVisitorCookie(this.getVisitorId() ?? cached);
@@ -1502,7 +1753,7 @@ function resolveCookieConfig(opt) {
1502
1753
  const o = opt === true ? {} : opt;
1503
1754
  const sameSite = o.sameSite ?? "Lax";
1504
1755
  const secure = sameSite === "None" ? true : o.secure ?? false;
1505
- return { sameSite, secure, maxAgeDays: o.maxAgeDays ?? 365, domain: o.domain };
1756
+ return { sameSite, secure, maxAgeDays: o.maxAgeDays ?? 365, domain: o.domain, authoritative: o.authoritative ?? false };
1506
1757
  }
1507
1758
 
1508
1759
  // src/client/widgets/form.ts
@@ -2186,6 +2437,8 @@ var XeniaClient = class {
2186
2437
  renderer;
2187
2438
  analyticsQueue;
2188
2439
  tracker = null;
2440
+ stopPageTracking = null;
2441
+ stopEngagement = null;
2189
2442
  /** The in-flight (or finished) boot; shared so concurrent `init()` calls start one tracker. */
2190
2443
  booting = null;
2191
2444
  /** Bumped by `destroy()`, so a boot still waiting on the device id knows to stop. */
@@ -2237,17 +2490,21 @@ var XeniaClient = class {
2237
2490
  rootMargin: this.options.viewport?.rootMargin,
2238
2491
  triggerOnce: this.options.viewport?.triggerOnce ?? true
2239
2492
  },
2493
+ slotClicks: this.options.slotClicks,
2240
2494
  // Declarative `data-xenia` elements fire journey triggers (immediate).
2241
2495
  fire: (eventId, interaction) => this.track(eventId, { interactionType: interaction }, { trigger: true }),
2242
2496
  // `[data-xenia-slot]` elements fire analytics-only personalization events.
2243
- fireSlot: (interaction, meta) => {
2497
+ fireSlot: (interaction, meta2) => {
2244
2498
  this.track(
2245
2499
  "customEvent",
2246
2500
  {
2247
2501
  customEventName: interaction === "impression" ? "personalization_impression" : "personalization_click",
2248
- contentKey: meta.contentKey,
2249
- contentEntryId: meta.contentEntryId ?? null,
2250
- baseEntryId: meta.baseEntryId ?? null
2502
+ ...meta2.contentKey ? { contentKey: meta2.contentKey } : {},
2503
+ contentEntryId: meta2.contentEntryId ?? null,
2504
+ baseEntryId: meta2.baseEntryId ?? null,
2505
+ ...meta2.bucket ? { bucket: meta2.bucket } : {},
2506
+ ...meta2.slot ? { slot: meta2.slot } : {},
2507
+ ...meta2.label ? { label: meta2.label } : {}
2251
2508
  },
2252
2509
  { trigger: false }
2253
2510
  );
@@ -2255,11 +2512,32 @@ var XeniaClient = class {
2255
2512
  });
2256
2513
  this.tracker.start();
2257
2514
  }
2515
+ const { pageViews = false, pageLeave = false } = this.options;
2516
+ if (pageViews || pageLeave) {
2517
+ this.stopPageTracking = startPageTracking({ track: (name, props) => this.track(name, props) }, { pageViews, pageLeave });
2518
+ }
2519
+ const { trail = false, contentClicks = false, navClicks = false } = this.options;
2520
+ if (trail || contentClicks || navClicks) {
2521
+ this.stopEngagement = startEngagement(
2522
+ { track: (name, props) => this.track(name, props) },
2523
+ {
2524
+ trail,
2525
+ contentClicks,
2526
+ navClicks,
2527
+ prefix: this.options.prefix ?? "xenia",
2528
+ analyticsAllowed: () => this.analyticsAllowed() && !this.dntBlocked()
2529
+ }
2530
+ );
2531
+ }
2258
2532
  }
2259
2533
  /** Tear down listeners, observers, and injected UI. */
2260
2534
  destroy() {
2261
2535
  this.tracker?.stop();
2262
2536
  this.tracker = null;
2537
+ this.stopPageTracking?.();
2538
+ this.stopPageTracking = null;
2539
+ this.stopEngagement?.();
2540
+ this.stopEngagement = null;
2263
2541
  this.renderer.destroy();
2264
2542
  this.analyticsQueue.stop();
2265
2543
  this.booting = null;