@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.cjs
CHANGED
|
@@ -548,7 +548,7 @@ var XeniaApi = class {
|
|
|
548
548
|
* ({@link personalizeContent}) calls this once per round with the runtime
|
|
549
549
|
* targets it found; callers rarely need it directly.
|
|
550
550
|
*/
|
|
551
|
-
async resolveEntries(entryIds, opts
|
|
551
|
+
async resolveEntries(entryIds, opts) {
|
|
552
552
|
const { data } = await this.http.request({
|
|
553
553
|
method: "POST",
|
|
554
554
|
path: "/api/personalization/resolve-entries",
|
|
@@ -556,7 +556,8 @@ var XeniaApi = class {
|
|
|
556
556
|
body: compact({
|
|
557
557
|
entryIds,
|
|
558
558
|
data: opts.data,
|
|
559
|
-
locale: opts.locale
|
|
559
|
+
locale: opts.locale,
|
|
560
|
+
contentKey: opts.contentKey
|
|
560
561
|
})
|
|
561
562
|
});
|
|
562
563
|
return data;
|
|
@@ -680,7 +681,7 @@ var DEFAULT_TIMEOUT_MS = 300;
|
|
|
680
681
|
var DEFAULT_MAX_ROUNDS = 3;
|
|
681
682
|
var copyOf = (v) => v && typeof v === "object" && typeof structuredClone === "function" ? structuredClone(v) : v;
|
|
682
683
|
async function personalizeContent(payload, options) {
|
|
683
|
-
const
|
|
684
|
+
const cms = scopedAdapter(options.cms, options.contentKey);
|
|
684
685
|
const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
|
|
685
686
|
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
686
687
|
const applied = [];
|
|
@@ -879,7 +880,8 @@ function withDeadline(promise, deadline) {
|
|
|
879
880
|
})
|
|
880
881
|
]).finally(() => clearTimeout(timer));
|
|
881
882
|
}
|
|
882
|
-
function pendingTargets(payload,
|
|
883
|
+
function pendingTargets(payload, adapter, contentKey) {
|
|
884
|
+
const cms = scopedAdapter(adapter, contentKey);
|
|
883
885
|
try {
|
|
884
886
|
let content = typeof structuredClone === "function" ? structuredClone(payload) : payload;
|
|
885
887
|
content = cms.prepare ? cms.prepare(content) : content;
|
|
@@ -890,6 +892,15 @@ function pendingTargets(payload, cms) {
|
|
|
890
892
|
return [];
|
|
891
893
|
}
|
|
892
894
|
}
|
|
895
|
+
function scopedAdapter(cms, contentKey) {
|
|
896
|
+
return {
|
|
897
|
+
...cms,
|
|
898
|
+
marker(entry) {
|
|
899
|
+
const m = cms.marker(entry);
|
|
900
|
+
return m && contentKey && m.keys?.includes(contentKey) ? m : null;
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
}
|
|
893
904
|
async function personalizePage(ref, options) {
|
|
894
905
|
let page;
|
|
895
906
|
try {
|
|
@@ -1079,17 +1090,22 @@ function parseDataAttribute(value) {
|
|
|
1079
1090
|
return { eventId, interactionType };
|
|
1080
1091
|
}
|
|
1081
1092
|
function readSlotMeta(el) {
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
const contentEntryId =
|
|
1085
|
-
|
|
1086
|
-
const personalized =
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
+
const attr = (name) => el.getAttribute(`data-xenia-${name}`) ?? void 0;
|
|
1094
|
+
const contentKey = attr("content-key");
|
|
1095
|
+
const contentEntryId = attr("content-entry-id");
|
|
1096
|
+
if (!contentKey && !contentEntryId) return null;
|
|
1097
|
+
const personalized = attr("personalized");
|
|
1098
|
+
const meta2 = {};
|
|
1099
|
+
const slot = attr("slot");
|
|
1100
|
+
if (slot) meta2.slot = slot;
|
|
1101
|
+
if (contentKey) meta2.contentKey = contentKey;
|
|
1102
|
+
if (contentEntryId) meta2.contentEntryId = contentEntryId;
|
|
1103
|
+
const baseEntryId = attr("base-entry-id");
|
|
1104
|
+
if (baseEntryId) meta2.baseEntryId = baseEntryId;
|
|
1105
|
+
if (personalized != null) meta2.personalized = personalized === "true";
|
|
1106
|
+
const bucket = attr("bucket");
|
|
1107
|
+
if (bucket) meta2.bucket = bucket;
|
|
1108
|
+
return meta2;
|
|
1093
1109
|
}
|
|
1094
1110
|
var AutoTracker = class {
|
|
1095
1111
|
opts;
|
|
@@ -1150,8 +1166,12 @@ var AutoTracker = class {
|
|
|
1150
1166
|
if (this.opts.fireSlot) {
|
|
1151
1167
|
const slot = target.closest("[data-xenia-slot]");
|
|
1152
1168
|
if (slot) {
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1169
|
+
const meta2 = readSlotMeta(slot);
|
|
1170
|
+
const found = target.closest("a, button");
|
|
1171
|
+
const control = found && slot.contains(found) ? found : null;
|
|
1172
|
+
if (!control && this.opts.slotClicks === "controls") return;
|
|
1173
|
+
const label = control ? (control.textContent ?? "").trim().slice(0, 60) : "";
|
|
1174
|
+
if (meta2 && !this.throttled(slot)) this.opts.fireSlot("click", label ? { ...meta2, label } : meta2);
|
|
1155
1175
|
}
|
|
1156
1176
|
}
|
|
1157
1177
|
};
|
|
@@ -1159,24 +1179,24 @@ var AutoTracker = class {
|
|
|
1159
1179
|
}
|
|
1160
1180
|
observeSlot(el) {
|
|
1161
1181
|
if (!this.opts.fireSlot || this.slotObservers.has(el)) return;
|
|
1162
|
-
const
|
|
1163
|
-
if (!
|
|
1182
|
+
const meta2 = readSlotMeta(el);
|
|
1183
|
+
if (!meta2) return;
|
|
1164
1184
|
const fireSlot = this.opts.fireSlot;
|
|
1165
1185
|
const { threshold, rootMargin } = this.opts.viewport;
|
|
1166
1186
|
const observer = new IntersectionObserver(
|
|
1167
1187
|
(entries) => {
|
|
1168
1188
|
for (const entry of entries) {
|
|
1169
|
-
|
|
1170
|
-
if (this.slotImpressed.has(el)) continue;
|
|
1189
|
+
const inView = entry.isIntersecting && (entry.intersectionRatio >= threshold || entry.intersectionRect.height >= window.innerHeight / 2);
|
|
1190
|
+
if (!inView || this.slotImpressed.has(el)) continue;
|
|
1171
1191
|
this.slotImpressed.add(el);
|
|
1172
|
-
fireSlot("impression",
|
|
1192
|
+
fireSlot("impression", meta2);
|
|
1173
1193
|
observer.disconnect();
|
|
1174
1194
|
this.slotObservers.delete(el);
|
|
1175
1195
|
}
|
|
1176
1196
|
},
|
|
1177
|
-
{ threshold, rootMargin }
|
|
1197
|
+
{ threshold: [.../* @__PURE__ */ new Set([0.1, 0.25, threshold])].sort(), rootMargin }
|
|
1178
1198
|
);
|
|
1179
|
-
observer.observe(el);
|
|
1199
|
+
observer.observe(boxOf(el));
|
|
1180
1200
|
this.slotObservers.set(el, observer);
|
|
1181
1201
|
}
|
|
1182
1202
|
observeViewport(el, eventId) {
|
|
@@ -1230,6 +1250,235 @@ var AutoTracker = class {
|
|
|
1230
1250
|
this.mutationObserver.observe(document.body, { childList: true, subtree: true });
|
|
1231
1251
|
}
|
|
1232
1252
|
};
|
|
1253
|
+
function boxOf(el) {
|
|
1254
|
+
if (typeof getComputedStyle !== "function") return el;
|
|
1255
|
+
return getComputedStyle(el).display === "contents" ? el.firstElementChild ?? el : el;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/edge/trail.ts
|
|
1259
|
+
var MAX_TRAIL = 8;
|
|
1260
|
+
var TRAIL_MAX_AGE_S = 30 * 60;
|
|
1261
|
+
function trailCookieName(prefix = DEFAULT_PREFIX) {
|
|
1262
|
+
return `${prefix}_trail`;
|
|
1263
|
+
}
|
|
1264
|
+
var decode = (v) => {
|
|
1265
|
+
try {
|
|
1266
|
+
return decodeURIComponent(v);
|
|
1267
|
+
} catch {
|
|
1268
|
+
return v;
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
function readTrail(value) {
|
|
1272
|
+
if (!value) return [];
|
|
1273
|
+
try {
|
|
1274
|
+
const parsed = JSON.parse(decode(value));
|
|
1275
|
+
if (!Array.isArray(parsed)) return [];
|
|
1276
|
+
return parsed.filter((t) => Array.isArray(t) && typeof t[0] === "string" && typeof t[1] === "number").slice(0, MAX_TRAIL).map(
|
|
1277
|
+
([path, at, seconds]) => typeof seconds === "number" && seconds >= 0 && seconds <= 86400 ? { path, at, seconds } : { path, at }
|
|
1278
|
+
);
|
|
1279
|
+
} catch {
|
|
1280
|
+
return [];
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function withView(trail, path, now = Date.now()) {
|
|
1284
|
+
if (trail[0]?.path === path) return trail;
|
|
1285
|
+
const earlier = trail.find((t) => t.path === path);
|
|
1286
|
+
return [{ path, at: earlier?.at ?? now }, ...trail.filter((t) => t.path !== path)].slice(0, MAX_TRAIL);
|
|
1287
|
+
}
|
|
1288
|
+
function trailCookieValue(trail) {
|
|
1289
|
+
return JSON.stringify(
|
|
1290
|
+
trail.slice(0, MAX_TRAIL).map((t) => t.seconds === void 0 ? [t.path, t.at] : [t.path, t.at, t.seconds])
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/client/page-tracking.ts
|
|
1295
|
+
var SETTLE_MS = 3e3;
|
|
1296
|
+
var POLL_MS = 50;
|
|
1297
|
+
var MIN_VISIT_MS = 250;
|
|
1298
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
1299
|
+
var restore = null;
|
|
1300
|
+
function watchNavigation(listener) {
|
|
1301
|
+
listeners.add(listener);
|
|
1302
|
+
if (!restore) {
|
|
1303
|
+
const { pushState, replaceState } = history;
|
|
1304
|
+
const notify = () => listeners.forEach((l) => {
|
|
1305
|
+
try {
|
|
1306
|
+
l();
|
|
1307
|
+
} catch {
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
const ownPush = function(...args) {
|
|
1311
|
+
pushState.apply(this, args);
|
|
1312
|
+
notify();
|
|
1313
|
+
};
|
|
1314
|
+
const ownReplace = function(...args) {
|
|
1315
|
+
replaceState.apply(this, args);
|
|
1316
|
+
notify();
|
|
1317
|
+
};
|
|
1318
|
+
history.pushState = ownPush;
|
|
1319
|
+
history.replaceState = ownReplace;
|
|
1320
|
+
window.addEventListener("popstate", notify);
|
|
1321
|
+
restore = () => {
|
|
1322
|
+
if (history.pushState !== ownPush || history.replaceState !== ownReplace) return false;
|
|
1323
|
+
history.pushState = pushState;
|
|
1324
|
+
history.replaceState = replaceState;
|
|
1325
|
+
window.removeEventListener("popstate", notify);
|
|
1326
|
+
return true;
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
return () => {
|
|
1330
|
+
listeners.delete(listener);
|
|
1331
|
+
if (!listeners.size && restore?.()) restore = null;
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
var meta = (name) => document.querySelector(`meta[name="${name}"]`)?.content || void 0;
|
|
1335
|
+
function startPageTracking(client, opts) {
|
|
1336
|
+
let path = location.pathname;
|
|
1337
|
+
let lastTitle = null;
|
|
1338
|
+
let timer;
|
|
1339
|
+
let view = { path, since: visibleNow(), sent: false };
|
|
1340
|
+
function visibleNow() {
|
|
1341
|
+
return document.visibilityState === "visible" ? Date.now() : null;
|
|
1342
|
+
}
|
|
1343
|
+
function sendView() {
|
|
1344
|
+
clearTimeout(timer);
|
|
1345
|
+
const started = Date.now();
|
|
1346
|
+
const tick = () => {
|
|
1347
|
+
const settled = document.title !== "" && document.title !== lastTitle;
|
|
1348
|
+
if (!settled && Date.now() - started < SETTLE_MS) {
|
|
1349
|
+
timer = setTimeout(tick, POLL_MS);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const params = new URLSearchParams(location.search);
|
|
1353
|
+
const searchTerm = (params.get("utm_term") ?? params.get("q") ?? "").slice(0, 120);
|
|
1354
|
+
const category = meta("xenia-category");
|
|
1355
|
+
lastTitle = document.title;
|
|
1356
|
+
client.track("pageView", {
|
|
1357
|
+
title: (meta("xenia-title") ?? document.title).slice(0, 160),
|
|
1358
|
+
...searchTerm ? { searchTerm } : {},
|
|
1359
|
+
...category ? { category } : {}
|
|
1360
|
+
});
|
|
1361
|
+
};
|
|
1362
|
+
timer = setTimeout(tick, 0);
|
|
1363
|
+
}
|
|
1364
|
+
function leave() {
|
|
1365
|
+
if (view.sent || view.since === null || Date.now() - view.since < MIN_VISIT_MS) return;
|
|
1366
|
+
view.sent = true;
|
|
1367
|
+
client.track("page_leave", { path: view.path, durationMs: Date.now() - view.since });
|
|
1368
|
+
}
|
|
1369
|
+
const onNavigate = () => {
|
|
1370
|
+
if (location.pathname === path) return;
|
|
1371
|
+
if (opts.pageLeave) leave();
|
|
1372
|
+
path = location.pathname;
|
|
1373
|
+
view = { path, since: visibleNow(), sent: false };
|
|
1374
|
+
if (opts.pageViews) sendView();
|
|
1375
|
+
};
|
|
1376
|
+
const onVisibility = () => {
|
|
1377
|
+
if (document.visibilityState === "hidden") leave();
|
|
1378
|
+
else if (view.since === null) view.since = Date.now();
|
|
1379
|
+
};
|
|
1380
|
+
const unwatch = watchNavigation(onNavigate);
|
|
1381
|
+
if (opts.pageLeave) document.addEventListener("visibilitychange", onVisibility);
|
|
1382
|
+
if (opts.pageViews) sendView();
|
|
1383
|
+
return () => {
|
|
1384
|
+
clearTimeout(timer);
|
|
1385
|
+
unwatch();
|
|
1386
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/client/engagement.ts
|
|
1391
|
+
var rootOf = (opt, fallback) => typeof opt === "object" && opt.root || fallback;
|
|
1392
|
+
function readCookie(name) {
|
|
1393
|
+
return document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))?.[1];
|
|
1394
|
+
}
|
|
1395
|
+
function writeCookie(name, value) {
|
|
1396
|
+
try {
|
|
1397
|
+
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${TRAIL_MAX_AGE_S}; samesite=lax`;
|
|
1398
|
+
} catch {
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function sectionOf(link) {
|
|
1402
|
+
if (!link || link.origin !== location.origin) return void 0;
|
|
1403
|
+
return link.pathname.split("/").filter(Boolean)[0];
|
|
1404
|
+
}
|
|
1405
|
+
function cardImage(target, root) {
|
|
1406
|
+
let el = target;
|
|
1407
|
+
for (let depth = 0; el && depth < 5 && el !== root; depth++, el = el.parentElement) {
|
|
1408
|
+
const imgs = [...el.querySelectorAll("img")].filter((i) => i.alt.trim());
|
|
1409
|
+
if (!imgs.length) continue;
|
|
1410
|
+
return new Set(imgs.map((i) => i.alt.trim())).size === 1 ? imgs[0] : null;
|
|
1411
|
+
}
|
|
1412
|
+
return null;
|
|
1413
|
+
}
|
|
1414
|
+
function startEngagement(client, opts) {
|
|
1415
|
+
const trailName = trailCookieName(opts.prefix);
|
|
1416
|
+
const clicksName = `${opts.prefix}_clicks`;
|
|
1417
|
+
const visibleNow = () => document.visibilityState === "visible" ? Date.now() : null;
|
|
1418
|
+
let current = { path: location.pathname, since: visibleNow(), stamped: false };
|
|
1419
|
+
const stamp = () => {
|
|
1420
|
+
if (current.stamped || !opts.trail || !opts.analyticsAllowed() || current.since === null) return;
|
|
1421
|
+
current.stamped = true;
|
|
1422
|
+
const seconds = Math.round((Date.now() - current.since) / 100) / 10;
|
|
1423
|
+
const trail = readTrail(readCookie(trailName));
|
|
1424
|
+
const i = trail.findIndex((t) => t.path === current.path);
|
|
1425
|
+
if (i < 0) return;
|
|
1426
|
+
trail[i] = { ...trail[i], seconds };
|
|
1427
|
+
writeCookie(trailName, trailCookieValue(trail));
|
|
1428
|
+
};
|
|
1429
|
+
const onNavigate = () => {
|
|
1430
|
+
if (location.pathname === current.path) return;
|
|
1431
|
+
stamp();
|
|
1432
|
+
current = { path: location.pathname, since: visibleNow(), stamped: false };
|
|
1433
|
+
if (!opts.trail || !opts.analyticsAllowed()) return;
|
|
1434
|
+
const trail = readTrail(readCookie(trailName));
|
|
1435
|
+
const next = withView(trail, current.path);
|
|
1436
|
+
if (next !== trail) writeCookie(trailName, trailCookieValue(next));
|
|
1437
|
+
};
|
|
1438
|
+
const onVisible = () => {
|
|
1439
|
+
if (document.visibilityState === "visible" && current.since === null) current.since = Date.now();
|
|
1440
|
+
};
|
|
1441
|
+
const onLeaveClick = (e) => {
|
|
1442
|
+
const link = e.target?.closest?.("a[href]");
|
|
1443
|
+
if (link && link.origin === location.origin && link.pathname !== location.pathname) stamp();
|
|
1444
|
+
};
|
|
1445
|
+
const bump = () => {
|
|
1446
|
+
if (!opts.analyticsAllowed()) return;
|
|
1447
|
+
writeCookie(clicksName, String((Number(readCookie(clicksName)) || 0) + 1));
|
|
1448
|
+
};
|
|
1449
|
+
const navRoot = rootOf(opts.navClicks, "header");
|
|
1450
|
+
const contentRoot = rootOf(opts.contentClicks, "main");
|
|
1451
|
+
const onClick = (e) => {
|
|
1452
|
+
const target = e.target;
|
|
1453
|
+
if (!target?.closest) return;
|
|
1454
|
+
const nav = opts.navClicks ? target.closest(`${navRoot} a[href]`) : null;
|
|
1455
|
+
if (nav) {
|
|
1456
|
+
const category2 = sectionOf(nav);
|
|
1457
|
+
if (!category2) return;
|
|
1458
|
+
client.track("nav_click", { category: category2, label: (nav.textContent ?? "").trim().slice(0, 60) });
|
|
1459
|
+
bump();
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const root = opts.contentClicks ? target.closest(contentRoot) : null;
|
|
1463
|
+
if (!root) return;
|
|
1464
|
+
const img = target.closest("img") ?? cardImage(target, root);
|
|
1465
|
+
const label = (img?.alt ?? "").trim().slice(0, 160);
|
|
1466
|
+
if (!label) return;
|
|
1467
|
+
const category = sectionOf(target.closest("a[href]"));
|
|
1468
|
+
client.track("content_click", category ? { label, category } : { label });
|
|
1469
|
+
bump();
|
|
1470
|
+
};
|
|
1471
|
+
const unwatch = watchNavigation(onNavigate);
|
|
1472
|
+
window.addEventListener("click", onLeaveClick, { capture: true });
|
|
1473
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
1474
|
+
if (opts.contentClicks || opts.navClicks) document.addEventListener("click", onClick, { capture: true });
|
|
1475
|
+
return () => {
|
|
1476
|
+
unwatch();
|
|
1477
|
+
window.removeEventListener("click", onLeaveClick, { capture: true });
|
|
1478
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
1479
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1233
1482
|
|
|
1234
1483
|
// src/server/cookies.ts
|
|
1235
1484
|
function parseCookies(header) {
|
|
@@ -1240,7 +1489,12 @@ function parseCookies(header) {
|
|
|
1240
1489
|
if (idx === -1) continue;
|
|
1241
1490
|
const key = part.slice(0, idx).trim();
|
|
1242
1491
|
const value = part.slice(idx + 1).trim();
|
|
1243
|
-
if (key)
|
|
1492
|
+
if (!key) continue;
|
|
1493
|
+
try {
|
|
1494
|
+
out[key] = decodeURIComponent(value);
|
|
1495
|
+
} catch {
|
|
1496
|
+
out[key] = value;
|
|
1497
|
+
}
|
|
1244
1498
|
}
|
|
1245
1499
|
return out;
|
|
1246
1500
|
}
|
|
@@ -1314,6 +1568,14 @@ var Identity = class {
|
|
|
1314
1568
|
const storageKey = this.key("device_id");
|
|
1315
1569
|
try {
|
|
1316
1570
|
const cached = localStorage.getItem(storageKey);
|
|
1571
|
+
const fromServer = this.cookie?.authoritative ? this.readVisitorCookie() : null;
|
|
1572
|
+
const effective = localStorage.getItem(this.key("visitor_id")) ?? cached;
|
|
1573
|
+
if (cached && fromServer && fromServer !== effective) {
|
|
1574
|
+
this.deviceId = fromServer;
|
|
1575
|
+
localStorage.setItem(storageKey, fromServer);
|
|
1576
|
+
if (localStorage.getItem(this.key("visitor_id"))) localStorage.setItem(this.key("visitor_id"), fromServer);
|
|
1577
|
+
return fromServer;
|
|
1578
|
+
}
|
|
1317
1579
|
if (cached) {
|
|
1318
1580
|
this.deviceId = cached;
|
|
1319
1581
|
this.writeVisitorCookie(this.getVisitorId() ?? cached);
|
|
@@ -1493,7 +1755,7 @@ function resolveCookieConfig(opt) {
|
|
|
1493
1755
|
const o = opt === true ? {} : opt;
|
|
1494
1756
|
const sameSite = o.sameSite ?? "Lax";
|
|
1495
1757
|
const secure = sameSite === "None" ? true : o.secure ?? false;
|
|
1496
|
-
return { sameSite, secure, maxAgeDays: o.maxAgeDays ?? 365, domain: o.domain };
|
|
1758
|
+
return { sameSite, secure, maxAgeDays: o.maxAgeDays ?? 365, domain: o.domain, authoritative: o.authoritative ?? false };
|
|
1497
1759
|
}
|
|
1498
1760
|
|
|
1499
1761
|
// src/client/widgets/form.ts
|
|
@@ -2177,6 +2439,8 @@ var XeniaClient = class {
|
|
|
2177
2439
|
renderer;
|
|
2178
2440
|
analyticsQueue;
|
|
2179
2441
|
tracker = null;
|
|
2442
|
+
stopPageTracking = null;
|
|
2443
|
+
stopEngagement = null;
|
|
2180
2444
|
/** The in-flight (or finished) boot; shared so concurrent `init()` calls start one tracker. */
|
|
2181
2445
|
booting = null;
|
|
2182
2446
|
/** Bumped by `destroy()`, so a boot still waiting on the device id knows to stop. */
|
|
@@ -2228,17 +2492,21 @@ var XeniaClient = class {
|
|
|
2228
2492
|
rootMargin: this.options.viewport?.rootMargin,
|
|
2229
2493
|
triggerOnce: this.options.viewport?.triggerOnce ?? true
|
|
2230
2494
|
},
|
|
2495
|
+
slotClicks: this.options.slotClicks,
|
|
2231
2496
|
// Declarative `data-xenia` elements fire journey triggers (immediate).
|
|
2232
2497
|
fire: (eventId, interaction) => this.track(eventId, { interactionType: interaction }, { trigger: true }),
|
|
2233
2498
|
// `[data-xenia-slot]` elements fire analytics-only personalization events.
|
|
2234
|
-
fireSlot: (interaction,
|
|
2499
|
+
fireSlot: (interaction, meta2) => {
|
|
2235
2500
|
this.track(
|
|
2236
2501
|
"customEvent",
|
|
2237
2502
|
{
|
|
2238
2503
|
customEventName: interaction === "impression" ? "personalization_impression" : "personalization_click",
|
|
2239
|
-
contentKey:
|
|
2240
|
-
contentEntryId:
|
|
2241
|
-
baseEntryId:
|
|
2504
|
+
...meta2.contentKey ? { contentKey: meta2.contentKey } : {},
|
|
2505
|
+
contentEntryId: meta2.contentEntryId ?? null,
|
|
2506
|
+
baseEntryId: meta2.baseEntryId ?? null,
|
|
2507
|
+
...meta2.bucket ? { bucket: meta2.bucket } : {},
|
|
2508
|
+
...meta2.slot ? { slot: meta2.slot } : {},
|
|
2509
|
+
...meta2.label ? { label: meta2.label } : {}
|
|
2242
2510
|
},
|
|
2243
2511
|
{ trigger: false }
|
|
2244
2512
|
);
|
|
@@ -2246,11 +2514,32 @@ var XeniaClient = class {
|
|
|
2246
2514
|
});
|
|
2247
2515
|
this.tracker.start();
|
|
2248
2516
|
}
|
|
2517
|
+
const { pageViews = false, pageLeave = false } = this.options;
|
|
2518
|
+
if (pageViews || pageLeave) {
|
|
2519
|
+
this.stopPageTracking = startPageTracking({ track: (name, props) => this.track(name, props) }, { pageViews, pageLeave });
|
|
2520
|
+
}
|
|
2521
|
+
const { trail = false, contentClicks = false, navClicks = false } = this.options;
|
|
2522
|
+
if (trail || contentClicks || navClicks) {
|
|
2523
|
+
this.stopEngagement = startEngagement(
|
|
2524
|
+
{ track: (name, props) => this.track(name, props) },
|
|
2525
|
+
{
|
|
2526
|
+
trail,
|
|
2527
|
+
contentClicks,
|
|
2528
|
+
navClicks,
|
|
2529
|
+
prefix: this.options.prefix ?? "xenia",
|
|
2530
|
+
analyticsAllowed: () => this.analyticsAllowed() && !this.dntBlocked()
|
|
2531
|
+
}
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2249
2534
|
}
|
|
2250
2535
|
/** Tear down listeners, observers, and injected UI. */
|
|
2251
2536
|
destroy() {
|
|
2252
2537
|
this.tracker?.stop();
|
|
2253
2538
|
this.tracker = null;
|
|
2539
|
+
this.stopPageTracking?.();
|
|
2540
|
+
this.stopPageTracking = null;
|
|
2541
|
+
this.stopEngagement?.();
|
|
2542
|
+
this.stopEngagement = null;
|
|
2254
2543
|
this.renderer.destroy();
|
|
2255
2544
|
this.analyticsQueue.stop();
|
|
2256
2545
|
this.booting = null;
|
|
@@ -2401,7 +2690,7 @@ var XeniaClient = class {
|
|
|
2401
2690
|
return personalizePage(ref, { ...opts, resolve: this.entryResolver(opts) });
|
|
2402
2691
|
}
|
|
2403
2692
|
entryResolver(opts) {
|
|
2404
|
-
const resolveOpts = this.withVisitor({ data: opts.data, locale: opts.locale });
|
|
2693
|
+
const resolveOpts = { ...this.withVisitor({ data: opts.data, locale: opts.locale }), contentKey: opts.contentKey };
|
|
2405
2694
|
return async (ids) => (await this.api.resolveEntries(ids, resolveOpts)).decisions ?? [];
|
|
2406
2695
|
}
|
|
2407
2696
|
/** The SDK-owned visitor anchor: the `identify()`/`config.visitorId` id, else the device id. */
|