@xenia-martech/sdk-js 1.2.0-dev.2 → 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 (82) hide show
  1. package/README.md +112 -3
  2. package/dist/{actions-BcdlokGO.d.cts → actions-C18Kv-4w.d.cts} +50 -2
  3. package/dist/{actions-BcdlokGO.d.ts → actions-C18Kv-4w.d.ts} +50 -2
  4. package/dist/{api-CktVQ6Gl.d.ts → api-0IycTYJR.d.cts} +4 -2
  5. package/dist/{api-BEPW0Uan.d.cts → api-COXWLL5L.d.ts} +4 -2
  6. package/dist/{chunk-3PYPPIUM.cjs → chunk-2DR5AVPX.cjs} +11 -46
  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-DWFVDNU5.js → chunk-GWRDZIVA.js} +5 -37
  11. package/dist/chunk-GWRDZIVA.js.map +1 -0
  12. package/dist/{chunk-7NGJC7CF.js → chunk-LW5AEJ6K.js} +20 -183
  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-IBNIRT7J.cjs → chunk-RWE2OCQL.cjs} +20 -194
  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 +320 -31
  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 +320 -31
  31. package/dist/client/index.js.map +1 -1
  32. package/dist/client/react/index.cjs +731 -34
  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 +732 -37
  37. package/dist/client/react/index.js.map +1 -1
  38. package/dist/cms/contentful.cjs +2 -1
  39. package/dist/cms/contentful.cjs.map +1 -1
  40. package/dist/cms/contentful.d.cts +2 -1
  41. package/dist/cms/contentful.d.ts +2 -1
  42. package/dist/cms/contentful.js +2 -1
  43. package/dist/cms/contentful.js.map +1 -1
  44. package/dist/consent-Cemn91j3.d.ts +248 -0
  45. package/dist/consent-DgWdjsSy.d.cts +248 -0
  46. package/dist/{cookie-names-CLqtmGBM.d.ts → cookie-names-BvUaW3uo.d.ts} +1 -1
  47. package/dist/{cookie-names-BACctagz.d.cts → cookie-names-CnL65Oly.d.cts} +1 -1
  48. package/dist/edge/index.cjs +280 -0
  49. package/dist/edge/index.cjs.map +1 -0
  50. package/dist/edge/index.d.cts +61 -0
  51. package/dist/edge/index.d.ts +61 -0
  52. package/dist/edge/index.js +191 -0
  53. package/dist/edge/index.js.map +1 -0
  54. package/dist/index.cjs +41 -40
  55. package/dist/index.d.cts +5 -4
  56. package/dist/index.d.ts +5 -4
  57. package/dist/index.js +2 -1
  58. package/dist/next/index.cjs +320 -15
  59. package/dist/next/index.cjs.map +1 -1
  60. package/dist/next/index.d.cts +180 -6
  61. package/dist/next/index.d.ts +180 -6
  62. package/dist/next/index.js +309 -9
  63. package/dist/next/index.js.map +1 -1
  64. package/dist/personalize-content-c2DDAV6g.d.ts +140 -0
  65. package/dist/personalize-content-ghZPKlf1.d.cts +140 -0
  66. package/dist/server/index.cjs +34 -32
  67. package/dist/server/index.cjs.map +1 -1
  68. package/dist/server/index.d.cts +6 -5
  69. package/dist/server/index.d.ts +6 -5
  70. package/dist/server/index.js +5 -3
  71. package/dist/server/index.js.map +1 -1
  72. package/dist/{personalize-content-Clc1oMYY.d.ts → types-DgVHS4Qw.d.cts} +7 -127
  73. package/dist/{personalize-content-Clc1oMYY.d.cts → types-DgVHS4Qw.d.ts} +7 -127
  74. package/dist/xenia-sdk.iife.js +3 -3
  75. package/dist/xenia-sdk.iife.js.map +1 -1
  76. package/dist/{xenia-server-o_wDOcei.d.cts → xenia-server-C3O-cWQT.d.cts} +3 -2
  77. package/dist/{xenia-server-BWhWD0La.d.ts → xenia-server-DV8Sjmoo.d.ts} +3 -2
  78. package/package.json +14 -1
  79. package/dist/chunk-3PYPPIUM.cjs.map +0 -1
  80. package/dist/chunk-7NGJC7CF.js.map +0 -1
  81. package/dist/chunk-DWFVDNU5.js.map +0 -1
  82. package/dist/chunk-IBNIRT7J.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-BcdlokGO.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-BcdlokGO.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-BcdlokGO.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-BcdlokGO.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
@@ -546,7 +546,7 @@ var XeniaApi = class {
546
546
  * ({@link personalizeContent}) calls this once per round with the runtime
547
547
  * targets it found; callers rarely need it directly.
548
548
  */
549
- async resolveEntries(entryIds, opts = {}) {
549
+ async resolveEntries(entryIds, opts) {
550
550
  const { data } = await this.http.request({
551
551
  method: "POST",
552
552
  path: "/api/personalization/resolve-entries",
@@ -554,7 +554,8 @@ var XeniaApi = class {
554
554
  body: compact({
555
555
  entryIds,
556
556
  data: opts.data,
557
- locale: opts.locale
557
+ locale: opts.locale,
558
+ contentKey: opts.contentKey
558
559
  })
559
560
  });
560
561
  return data;
@@ -678,7 +679,7 @@ var DEFAULT_TIMEOUT_MS = 300;
678
679
  var DEFAULT_MAX_ROUNDS = 3;
679
680
  var copyOf = (v) => v && typeof v === "object" && typeof structuredClone === "function" ? structuredClone(v) : v;
680
681
  async function personalizeContent(payload, options) {
681
- const { cms } = options;
682
+ const cms = scopedAdapter(options.cms, options.contentKey);
682
683
  const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
683
684
  const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
684
685
  const applied = [];
@@ -877,7 +878,8 @@ function withDeadline(promise, deadline) {
877
878
  })
878
879
  ]).finally(() => clearTimeout(timer));
879
880
  }
880
- function pendingTargets(payload, cms) {
881
+ function pendingTargets(payload, adapter, contentKey) {
882
+ const cms = scopedAdapter(adapter, contentKey);
881
883
  try {
882
884
  let content = typeof structuredClone === "function" ? structuredClone(payload) : payload;
883
885
  content = cms.prepare ? cms.prepare(content) : content;
@@ -888,6 +890,15 @@ function pendingTargets(payload, cms) {
888
890
  return [];
889
891
  }
890
892
  }
893
+ function scopedAdapter(cms, contentKey) {
894
+ return {
895
+ ...cms,
896
+ marker(entry) {
897
+ const m = cms.marker(entry);
898
+ return m && contentKey && m.keys?.includes(contentKey) ? m : null;
899
+ }
900
+ };
901
+ }
891
902
  async function personalizePage(ref, options) {
892
903
  let page;
893
904
  try {
@@ -1077,17 +1088,22 @@ function parseDataAttribute(value) {
1077
1088
  return { eventId, interactionType };
1078
1089
  }
1079
1090
  function readSlotMeta(el) {
1080
- const contentKey = el.getAttribute("data-xenia-content-key");
1081
- if (!contentKey) return null;
1082
- const contentEntryId = el.getAttribute("data-xenia-content-entry-id");
1083
- const baseEntryId = el.getAttribute("data-xenia-base-entry-id");
1084
- const personalized = el.getAttribute("data-xenia-personalized");
1085
- return {
1086
- contentKey,
1087
- contentEntryId: contentEntryId ?? void 0,
1088
- baseEntryId: baseEntryId ?? void 0,
1089
- personalized: personalized == null ? void 0 : personalized === "true"
1090
- };
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;
1091
1107
  }
1092
1108
  var AutoTracker = class {
1093
1109
  opts;
@@ -1148,8 +1164,12 @@ var AutoTracker = class {
1148
1164
  if (this.opts.fireSlot) {
1149
1165
  const slot = target.closest("[data-xenia-slot]");
1150
1166
  if (slot) {
1151
- const meta = readSlotMeta(slot);
1152
- 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);
1153
1173
  }
1154
1174
  }
1155
1175
  };
@@ -1157,24 +1177,24 @@ var AutoTracker = class {
1157
1177
  }
1158
1178
  observeSlot(el) {
1159
1179
  if (!this.opts.fireSlot || this.slotObservers.has(el)) return;
1160
- const meta = readSlotMeta(el);
1161
- if (!meta) return;
1180
+ const meta2 = readSlotMeta(el);
1181
+ if (!meta2) return;
1162
1182
  const fireSlot = this.opts.fireSlot;
1163
1183
  const { threshold, rootMargin } = this.opts.viewport;
1164
1184
  const observer = new IntersectionObserver(
1165
1185
  (entries) => {
1166
1186
  for (const entry of entries) {
1167
- if (!entry.isIntersecting) continue;
1168
- 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;
1169
1189
  this.slotImpressed.add(el);
1170
- fireSlot("impression", meta);
1190
+ fireSlot("impression", meta2);
1171
1191
  observer.disconnect();
1172
1192
  this.slotObservers.delete(el);
1173
1193
  }
1174
1194
  },
1175
- { threshold, rootMargin }
1195
+ { threshold: [.../* @__PURE__ */ new Set([0.1, 0.25, threshold])].sort(), rootMargin }
1176
1196
  );
1177
- observer.observe(el);
1197
+ observer.observe(boxOf(el));
1178
1198
  this.slotObservers.set(el, observer);
1179
1199
  }
1180
1200
  observeViewport(el, eventId) {
@@ -1228,6 +1248,235 @@ var AutoTracker = class {
1228
1248
  this.mutationObserver.observe(document.body, { childList: true, subtree: true });
1229
1249
  }
1230
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
+ }
1231
1480
 
1232
1481
  // src/server/cookies.ts
1233
1482
  function parseCookies(header) {
@@ -1238,7 +1487,12 @@ function parseCookies(header) {
1238
1487
  if (idx === -1) continue;
1239
1488
  const key = part.slice(0, idx).trim();
1240
1489
  const value = part.slice(idx + 1).trim();
1241
- if (key) out[key] = decodeURIComponent(value);
1490
+ if (!key) continue;
1491
+ try {
1492
+ out[key] = decodeURIComponent(value);
1493
+ } catch {
1494
+ out[key] = value;
1495
+ }
1242
1496
  }
1243
1497
  return out;
1244
1498
  }
@@ -1312,6 +1566,14 @@ var Identity = class {
1312
1566
  const storageKey = this.key("device_id");
1313
1567
  try {
1314
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
+ }
1315
1577
  if (cached) {
1316
1578
  this.deviceId = cached;
1317
1579
  this.writeVisitorCookie(this.getVisitorId() ?? cached);
@@ -1491,7 +1753,7 @@ function resolveCookieConfig(opt) {
1491
1753
  const o = opt === true ? {} : opt;
1492
1754
  const sameSite = o.sameSite ?? "Lax";
1493
1755
  const secure = sameSite === "None" ? true : o.secure ?? false;
1494
- 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 };
1495
1757
  }
1496
1758
 
1497
1759
  // src/client/widgets/form.ts
@@ -2175,6 +2437,8 @@ var XeniaClient = class {
2175
2437
  renderer;
2176
2438
  analyticsQueue;
2177
2439
  tracker = null;
2440
+ stopPageTracking = null;
2441
+ stopEngagement = null;
2178
2442
  /** The in-flight (or finished) boot; shared so concurrent `init()` calls start one tracker. */
2179
2443
  booting = null;
2180
2444
  /** Bumped by `destroy()`, so a boot still waiting on the device id knows to stop. */
@@ -2226,17 +2490,21 @@ var XeniaClient = class {
2226
2490
  rootMargin: this.options.viewport?.rootMargin,
2227
2491
  triggerOnce: this.options.viewport?.triggerOnce ?? true
2228
2492
  },
2493
+ slotClicks: this.options.slotClicks,
2229
2494
  // Declarative `data-xenia` elements fire journey triggers (immediate).
2230
2495
  fire: (eventId, interaction) => this.track(eventId, { interactionType: interaction }, { trigger: true }),
2231
2496
  // `[data-xenia-slot]` elements fire analytics-only personalization events.
2232
- fireSlot: (interaction, meta) => {
2497
+ fireSlot: (interaction, meta2) => {
2233
2498
  this.track(
2234
2499
  "customEvent",
2235
2500
  {
2236
2501
  customEventName: interaction === "impression" ? "personalization_impression" : "personalization_click",
2237
- contentKey: meta.contentKey,
2238
- contentEntryId: meta.contentEntryId ?? null,
2239
- 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 } : {}
2240
2508
  },
2241
2509
  { trigger: false }
2242
2510
  );
@@ -2244,11 +2512,32 @@ var XeniaClient = class {
2244
2512
  });
2245
2513
  this.tracker.start();
2246
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
+ }
2247
2532
  }
2248
2533
  /** Tear down listeners, observers, and injected UI. */
2249
2534
  destroy() {
2250
2535
  this.tracker?.stop();
2251
2536
  this.tracker = null;
2537
+ this.stopPageTracking?.();
2538
+ this.stopPageTracking = null;
2539
+ this.stopEngagement?.();
2540
+ this.stopEngagement = null;
2252
2541
  this.renderer.destroy();
2253
2542
  this.analyticsQueue.stop();
2254
2543
  this.booting = null;
@@ -2399,7 +2688,7 @@ var XeniaClient = class {
2399
2688
  return personalizePage(ref, { ...opts, resolve: this.entryResolver(opts) });
2400
2689
  }
2401
2690
  entryResolver(opts) {
2402
- const resolveOpts = this.withVisitor({ data: opts.data, locale: opts.locale });
2691
+ const resolveOpts = { ...this.withVisitor({ data: opts.data, locale: opts.locale }), contentKey: opts.contentKey };
2403
2692
  return async (ids) => (await this.api.resolveEntries(ids, resolveOpts)).decisions ?? [];
2404
2693
  }
2405
2694
  /** The SDK-owned visitor anchor: the `identify()`/`config.visitorId` id, else the device id. */