@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.
- package/README.md +112 -3
- package/dist/{actions-BcdlokGO.d.cts → actions-C18Kv-4w.d.cts} +50 -2
- package/dist/{actions-BcdlokGO.d.ts → actions-C18Kv-4w.d.ts} +50 -2
- package/dist/{api-CktVQ6Gl.d.ts → api-0IycTYJR.d.cts} +4 -2
- package/dist/{api-BEPW0Uan.d.cts → api-COXWLL5L.d.ts} +4 -2
- package/dist/{chunk-3PYPPIUM.cjs → chunk-2DR5AVPX.cjs} +11 -46
- package/dist/chunk-2DR5AVPX.cjs.map +1 -0
- package/dist/chunk-BQ5PJCZ7.js +179 -0
- package/dist/chunk-BQ5PJCZ7.js.map +1 -0
- package/dist/{chunk-DWFVDNU5.js → chunk-GWRDZIVA.js} +5 -37
- package/dist/chunk-GWRDZIVA.js.map +1 -0
- package/dist/{chunk-7NGJC7CF.js → chunk-LW5AEJ6K.js} +20 -183
- package/dist/chunk-LW5AEJ6K.js.map +1 -0
- package/dist/chunk-N5M4PI7C.cjs +192 -0
- package/dist/chunk-N5M4PI7C.cjs.map +1 -0
- package/dist/chunk-OMMIRSPK.js +336 -0
- package/dist/chunk-OMMIRSPK.js.map +1 -0
- package/dist/chunk-QEYONKWY.js +42 -0
- package/dist/chunk-QEYONKWY.js.map +1 -0
- package/dist/{chunk-IBNIRT7J.cjs → chunk-RWE2OCQL.cjs} +20 -194
- package/dist/chunk-RWE2OCQL.cjs.map +1 -0
- package/dist/chunk-WHGK55JP.cjs +46 -0
- package/dist/chunk-WHGK55JP.cjs.map +1 -0
- package/dist/chunk-YLMMTKQL.cjs +358 -0
- package/dist/chunk-YLMMTKQL.cjs.map +1 -0
- package/dist/client/index.cjs +320 -31
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +15 -4
- package/dist/client/index.d.ts +15 -4
- package/dist/client/index.js +320 -31
- package/dist/client/index.js.map +1 -1
- package/dist/client/react/index.cjs +731 -34
- package/dist/client/react/index.cjs.map +1 -1
- package/dist/client/react/index.d.cts +32 -3
- package/dist/client/react/index.d.ts +32 -3
- package/dist/client/react/index.js +732 -37
- package/dist/client/react/index.js.map +1 -1
- package/dist/cms/contentful.cjs +2 -1
- package/dist/cms/contentful.cjs.map +1 -1
- package/dist/cms/contentful.d.cts +2 -1
- package/dist/cms/contentful.d.ts +2 -1
- package/dist/cms/contentful.js +2 -1
- package/dist/cms/contentful.js.map +1 -1
- package/dist/consent-Cemn91j3.d.ts +248 -0
- package/dist/consent-DgWdjsSy.d.cts +248 -0
- package/dist/{cookie-names-CLqtmGBM.d.ts → cookie-names-BvUaW3uo.d.ts} +1 -1
- package/dist/{cookie-names-BACctagz.d.cts → cookie-names-CnL65Oly.d.cts} +1 -1
- package/dist/edge/index.cjs +280 -0
- package/dist/edge/index.cjs.map +1 -0
- package/dist/edge/index.d.cts +61 -0
- package/dist/edge/index.d.ts +61 -0
- package/dist/edge/index.js +191 -0
- package/dist/edge/index.js.map +1 -0
- package/dist/index.cjs +41 -40
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -1
- package/dist/next/index.cjs +320 -15
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +180 -6
- package/dist/next/index.d.ts +180 -6
- package/dist/next/index.js +309 -9
- package/dist/next/index.js.map +1 -1
- package/dist/personalize-content-c2DDAV6g.d.ts +140 -0
- package/dist/personalize-content-ghZPKlf1.d.cts +140 -0
- package/dist/server/index.cjs +34 -32
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +6 -5
- package/dist/server/index.d.ts +6 -5
- package/dist/server/index.js +5 -3
- package/dist/server/index.js.map +1 -1
- package/dist/{personalize-content-Clc1oMYY.d.ts → types-DgVHS4Qw.d.cts} +7 -127
- package/dist/{personalize-content-Clc1oMYY.d.cts → types-DgVHS4Qw.d.ts} +7 -127
- package/dist/xenia-sdk.iife.js +3 -3
- package/dist/xenia-sdk.iife.js.map +1 -1
- package/dist/{xenia-server-o_wDOcei.d.cts → xenia-server-C3O-cWQT.d.cts} +3 -2
- package/dist/{xenia-server-BWhWD0La.d.ts → xenia-server-DV8Sjmoo.d.ts} +3 -2
- package/package.json +14 -1
- package/dist/chunk-3PYPPIUM.cjs.map +0 -1
- package/dist/chunk-7NGJC7CF.js.map +0 -1
- package/dist/chunk-DWFVDNU5.js.map +0 -1
- package/dist/chunk-IBNIRT7J.cjs.map +0 -1
package/dist/client/index.d.cts
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
1081
|
-
|
|
1082
|
-
const contentEntryId =
|
|
1083
|
-
|
|
1084
|
-
const personalized =
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
|
1152
|
-
|
|
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
|
|
1161
|
-
if (!
|
|
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
|
-
|
|
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",
|
|
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)
|
|
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,
|
|
2497
|
+
fireSlot: (interaction, meta2) => {
|
|
2233
2498
|
this.track(
|
|
2234
2499
|
"customEvent",
|
|
2235
2500
|
{
|
|
2236
2501
|
customEventName: interaction === "impression" ? "personalization_impression" : "personalization_click",
|
|
2237
|
-
contentKey:
|
|
2238
|
-
contentEntryId:
|
|
2239
|
-
baseEntryId:
|
|
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. */
|