@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/next/index.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { createXeniaServer
|
|
2
|
-
import {
|
|
1
|
+
import { createXeniaServer } from '../chunk-GWRDZIVA.js';
|
|
2
|
+
import { generateUlid, contextFromUrl } from '../chunk-LW5AEJ6K.js';
|
|
3
|
+
import { isBot, trailCookieName, landingContext, viewerGeo, edgeConsent, readTrail, deviceOf, withView, localTime, swapGroups, encodeVariant, fetchManifest, trimManifest, verifyWebhook, isPageLoad, trailCookieValue, TRAIL_MAX_AGE_S, isPrefetch } from '../chunk-OMMIRSPK.js';
|
|
4
|
+
import { getConsent } from '../chunk-QEYONKWY.js';
|
|
5
|
+
import { DEFAULT_PREFIX, visitorCookieName } from '../chunk-BQ5PJCZ7.js';
|
|
3
6
|
import * as React from 'react';
|
|
4
7
|
import { cookies, headers } from 'next/headers';
|
|
5
8
|
import { NextResponse } from 'next/server';
|
|
9
|
+
import { revalidatePath } from 'next/cache';
|
|
6
10
|
|
|
11
|
+
// src/next/shared.ts
|
|
7
12
|
var URL_HEADER = "x-xenia-url";
|
|
8
13
|
function cookieAttributes(c = {}) {
|
|
9
14
|
const sameSite = c.sameSite ?? "Lax";
|
|
@@ -15,16 +20,311 @@ function cookieAttributes(c = {}) {
|
|
|
15
20
|
...c.domain && { domain: c.domain }
|
|
16
21
|
};
|
|
17
22
|
}
|
|
23
|
+
function forwardedHeaders(headers2, url, newCookie) {
|
|
24
|
+
const forwarded = new Headers(headers2);
|
|
25
|
+
forwarded.set(URL_HEADER, url);
|
|
26
|
+
if (newCookie) {
|
|
27
|
+
const header = forwarded.get("cookie");
|
|
28
|
+
forwarded.set("cookie", header ? `${header}; ${newCookie}` : newCookie);
|
|
29
|
+
}
|
|
30
|
+
return forwarded;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/edge/debug-entries.ts
|
|
34
|
+
function readDebugEntries(raw) {
|
|
35
|
+
if (!raw) return {};
|
|
36
|
+
try {
|
|
37
|
+
let text = decodeURIComponent(raw);
|
|
38
|
+
if (text.startsWith("%")) text = decodeURIComponent(text);
|
|
39
|
+
const parsed = JSON.parse(text);
|
|
40
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/next/edge-debug.ts
|
|
47
|
+
var QA_CITIES = {
|
|
48
|
+
minneapolis: { country: "US", region: "MN", regionName: "Minnesota", city: "Minneapolis", timeZone: "America/Chicago", latitude: "44.98", longitude: "-93.27" },
|
|
49
|
+
chicago: { country: "US", region: "IL", regionName: "Illinois", city: "Chicago", timeZone: "America/Chicago", latitude: "41.88", longitude: "-87.63" },
|
|
50
|
+
miami: { country: "US", region: "FL", regionName: "Florida", city: "Miami", timeZone: "America/New_York", latitude: "25.76", longitude: "-80.19" },
|
|
51
|
+
new_york: { country: "US", region: "NY", regionName: "New York", city: "New York", timeZone: "America/New_York", latitude: "40.71", longitude: "-74.01" },
|
|
52
|
+
los_angeles: { country: "US", region: "CA", regionName: "California", city: "Los Angeles", timeZone: "America/Los_Angeles", latitude: "34.05", longitude: "-118.24" },
|
|
53
|
+
seattle: { country: "US", region: "WA", regionName: "Washington", city: "Seattle", timeZone: "America/Los_Angeles", latitude: "47.61", longitude: "-122.33" },
|
|
54
|
+
london: { country: "GB", region: "ENG", regionName: "England", city: "London", timeZone: "Europe/London", latitude: "51.51", longitude: "-0.13" },
|
|
55
|
+
sydney: { country: "AU", region: "NSW", regionName: "New South Wales", city: "Sydney", timeZone: "Australia/Sydney", latitude: "-33.87", longitude: "151.21" }
|
|
56
|
+
};
|
|
57
|
+
var QA_MAX_AGE_S = 60 * 60;
|
|
58
|
+
var validDate = (d) => typeof d === "string" && /^\d{4}-\d{2}-\d{2}$/.test(d) && !Number.isNaN(Date.parse(d));
|
|
59
|
+
function readQa(request, cities, cookie) {
|
|
60
|
+
const isCity = (c) => typeof c === "string" && Object.hasOwn(cities, c);
|
|
61
|
+
let qa;
|
|
62
|
+
try {
|
|
63
|
+
const saved = JSON.parse(request.cookies.get(cookie)?.value ?? "{}");
|
|
64
|
+
qa = {
|
|
65
|
+
...isCity(saved?.geo) ? { geo: saved.geo } : {},
|
|
66
|
+
...validDate(saved?.date) ? { date: saved.date } : {}
|
|
67
|
+
};
|
|
68
|
+
} catch {
|
|
69
|
+
qa = {};
|
|
70
|
+
}
|
|
71
|
+
const p = request.nextUrl.searchParams;
|
|
72
|
+
let changed = false;
|
|
73
|
+
if (p.get("xn_geo") === "off") {
|
|
74
|
+
changed = Object.keys(qa).length > 0;
|
|
75
|
+
qa = {};
|
|
76
|
+
}
|
|
77
|
+
const city = p.get("xn_geo")?.toLowerCase();
|
|
78
|
+
if (isCity(city)) {
|
|
79
|
+
qa = { ...qa, geo: city };
|
|
80
|
+
changed = true;
|
|
81
|
+
}
|
|
82
|
+
const date = p.get("xn_date");
|
|
83
|
+
if (validDate(date)) {
|
|
84
|
+
qa = { ...qa, date };
|
|
85
|
+
changed = true;
|
|
86
|
+
}
|
|
87
|
+
return { qa, changed };
|
|
88
|
+
}
|
|
89
|
+
function rememberQa(response, qa, cookie) {
|
|
90
|
+
if (Object.keys(qa).length) response.cookies.set(cookie, JSON.stringify(qa), { path: "/", maxAge: QA_MAX_AGE_S, sameSite: "lax" });
|
|
91
|
+
else response.cookies.delete(cookie);
|
|
92
|
+
}
|
|
93
|
+
var qaNow = (qa) => qa.date ? /* @__PURE__ */ new Date(`${qa.date}T17:00:00Z`) : void 0;
|
|
94
|
+
var MAX_PAGE = 200;
|
|
95
|
+
var DEBUG_COOKIE_MAX = 3900;
|
|
96
|
+
var DEBUG_MAX_AGE_S = 300;
|
|
97
|
+
var GEO_HEADERS = ["x-xn-geo", "x-nf-geo", "x-country", "x-vercel-ip-country", "cloudfront-viewer-country", "cf-ipcountry"];
|
|
98
|
+
var geoLabel = (g) => g ? `${g.city ? `${g.city}, ` : ""}${g.country ?? "-"}${g.region ? `/${g.region}` : ""} (${g.source ?? "?"})` : "none";
|
|
99
|
+
function sourcesSeen(request) {
|
|
100
|
+
const seen = GEO_HEADERS.filter((n) => request.headers.get(n));
|
|
101
|
+
const g = globalThis;
|
|
102
|
+
if (g.Netlify) seen.push(g.Netlify.context ? "Netlify.context" : "Netlify (no context)");
|
|
103
|
+
return seen;
|
|
104
|
+
}
|
|
105
|
+
function profileSummary(profile) {
|
|
106
|
+
if (!profile) return void 0;
|
|
107
|
+
const answers = Object.fromEntries(
|
|
108
|
+
Object.entries(profile.answers ?? {}).map(([name, a]) => [
|
|
109
|
+
name,
|
|
110
|
+
{ ...a, scores: Object.fromEntries(Object.entries(a?.scores ?? {}).filter(([, v]) => v > 0).sort((x, y) => y[1] - x[1]).slice(0, 5)) }
|
|
111
|
+
])
|
|
112
|
+
);
|
|
113
|
+
return { answers, pageViews: profile.page_views, lastSection: profile.last_section, campaign: profile.last_utm_campaign };
|
|
114
|
+
}
|
|
115
|
+
var SOURCES = { skipped: "site page", "not-targeted": "not targeted", "no-consent": "no consent", error: "error" };
|
|
116
|
+
function debugEntry(info) {
|
|
117
|
+
const { outcome } = info;
|
|
118
|
+
return {
|
|
119
|
+
page: info.ctx.path,
|
|
120
|
+
geo: geoLabel(info.ctx.geo),
|
|
121
|
+
// Structured, for the panel's Request tab.
|
|
122
|
+
request: {
|
|
123
|
+
location: info.ctx.geo,
|
|
124
|
+
via: info.ctx.geoVia,
|
|
125
|
+
seen: sourcesSeen(info.ctx.request),
|
|
126
|
+
...info.ctx.detectedGeo ? { detected: info.ctx.detectedGeo } : {},
|
|
127
|
+
device: info.ctx.device,
|
|
128
|
+
consent: info.ctx.consent
|
|
129
|
+
},
|
|
130
|
+
source: outcome?.source ?? SOURCES[info.exit] ?? info.exit,
|
|
131
|
+
...outcome ? { ms: outcome.ms, profile: profileSummary(outcome.profile) } : {},
|
|
132
|
+
decisions: (outcome?.decisions ?? []).map((d) => ({
|
|
133
|
+
key: d.contentKey,
|
|
134
|
+
result: d.personalized ? d.variant || "personalized" : d.reason?.startsWith("suppressed") ? "suppressed" : d.bucket ?? "base",
|
|
135
|
+
reason: d.reason,
|
|
136
|
+
trace: d.trace
|
|
137
|
+
}))
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function writeDebug(request, response, entry, cookie) {
|
|
141
|
+
if (isPrefetch(request.headers)) return;
|
|
142
|
+
entry = { ...entry, page: entry.page.slice(0, MAX_PAGE) };
|
|
143
|
+
const pages = readDebugEntries(request.cookies.get(cookie)?.value);
|
|
144
|
+
delete pages[entry.page];
|
|
145
|
+
pages[entry.page] = { ...entry, at: Date.now() };
|
|
146
|
+
let entries = Object.entries(pages).slice(-4);
|
|
147
|
+
const encoded = () => encodeURIComponent(JSON.stringify(Object.fromEntries(entries)));
|
|
148
|
+
while (entries.length > 1 && encodeURIComponent(encoded()).length > DEBUG_COOKIE_MAX) entries = entries.slice(1);
|
|
149
|
+
if (encodeURIComponent(encoded()).length > DEBUG_COOKIE_MAX) {
|
|
150
|
+
const { profile: _drop, ...rest } = pages[entry.page];
|
|
151
|
+
entries = [[entry.page, rest]];
|
|
152
|
+
}
|
|
153
|
+
if (encodeURIComponent(encoded()).length > DEBUG_COOKIE_MAX) {
|
|
154
|
+
entries = [[entry.page, { page: entry.page, source: entry.source, note: "entry too large for the debug cookie" }]];
|
|
155
|
+
}
|
|
156
|
+
response.cookies.set(cookie, encoded(), { path: "/", maxAge: DEBUG_MAX_AGE_S, sameSite: "lax" });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/next/edge-proxy.ts
|
|
160
|
+
var MAX_QUERY = 20;
|
|
161
|
+
var MAX_QUERY_VALUE = 500;
|
|
162
|
+
async function xeniaEdgeProxy(request, opts) {
|
|
163
|
+
if (request.method !== "GET" && request.method !== "HEAD") return NextResponse.next();
|
|
164
|
+
if (isBot(request.headers.get("user-agent"))) return NextResponse.next();
|
|
165
|
+
const started = Date.now();
|
|
166
|
+
const prefix = opts.prefix ?? DEFAULT_PREFIX;
|
|
167
|
+
const vidCookie = visitorCookieName(prefix);
|
|
168
|
+
const ctxCookie = `${prefix}_ctx`;
|
|
169
|
+
const trailCookie = trailCookieName(prefix);
|
|
170
|
+
const { pathname: path } = request.nextUrl;
|
|
171
|
+
const existingVisitorId = request.cookies.get(vidCookie)?.value;
|
|
172
|
+
const landing = landingContext(request.nextUrl, request.headers.get("referer"), request.cookies.get(ctxCookie)?.value);
|
|
173
|
+
const qaOn = Boolean(opts.qa);
|
|
174
|
+
const cities = typeof opts.qa === "object" && opts.qa.cities || QA_CITIES;
|
|
175
|
+
const qaCookie = `${prefix}_qa_ctx`;
|
|
176
|
+
const { qa, changed: qaChanged } = qaOn ? readQa(request, cities, qaCookie) : { qa: {}, changed: false };
|
|
177
|
+
const detected = viewerGeo(request, { trust: opts.geoHeaders, devOverride: qaOn && process.env.NODE_ENV === "development" });
|
|
178
|
+
const qaCity = qa.geo && Object.hasOwn(cities, qa.geo) ? cities[qa.geo] : void 0;
|
|
179
|
+
const { geo, via } = qaCity ? { geo: { ...qaCity, source: "qa" }, via: "qa" } : detected;
|
|
180
|
+
const ctx = {
|
|
181
|
+
request,
|
|
182
|
+
host: request.headers.get("host"),
|
|
183
|
+
path,
|
|
184
|
+
visitorId: existingVisitorId ?? generateUlid(),
|
|
185
|
+
existingVisitorId,
|
|
186
|
+
geo,
|
|
187
|
+
geoVia: via,
|
|
188
|
+
...qaCity && detected.geo ? { detectedGeo: detected.geo } : {},
|
|
189
|
+
device: deviceOf(request.headers),
|
|
190
|
+
landing: landing.ctx,
|
|
191
|
+
previousTrail: readTrail(request.cookies.get(trailCookie)?.value),
|
|
192
|
+
trail: [],
|
|
193
|
+
consent: edgeConsent(request.headers, { prefix, respectDnt: opts.respectDnt }),
|
|
194
|
+
now: qaNow(qa) ?? /* @__PURE__ */ new Date()
|
|
195
|
+
};
|
|
196
|
+
const newVisitor = () => ctx.visitorId !== existingVisitorId;
|
|
197
|
+
const forwarded = () => forwardedHeaders(request.headers, request.url, newVisitor() ? `${vidCookie}=${ctx.visitorId}` : void 0);
|
|
198
|
+
const base = () => NextResponse.next({ request: { headers: forwarded() } });
|
|
199
|
+
const respond = (url) => url ? NextResponse.rewrite(url, { request: { headers: forwarded() } }) : base();
|
|
200
|
+
const finish = async (response, exit, desc, rest = {}) => {
|
|
201
|
+
if (newVisitor()) response.cookies.set(vidCookie, ctx.visitorId, cookieAttributes(opts.cookie));
|
|
202
|
+
if (landing.changed || ctx.landing !== landing.ctx) {
|
|
203
|
+
response.cookies.set(ctxCookie, JSON.stringify(ctx.landing), { path: "/", sameSite: "lax", httpOnly: true });
|
|
204
|
+
}
|
|
205
|
+
if (ctx.consent.analytics && isPageLoad(request.headers) && ctx.trail.length && ctx.trail !== ctx.previousTrail) {
|
|
206
|
+
response.cookies.set(trailCookie, trailCookieValue(ctx.trail), { path: "/", maxAge: TRAIL_MAX_AGE_S, sameSite: "lax" });
|
|
207
|
+
}
|
|
208
|
+
response.headers.set("Server-Timing", `xenia;dur=${Date.now() - started};desc="${desc}"`);
|
|
209
|
+
if (qaChanged) rememberQa(response, qa, qaCookie);
|
|
210
|
+
try {
|
|
211
|
+
const info = { ctx, exit, response, ...rest };
|
|
212
|
+
await opts.onDecision?.(info);
|
|
213
|
+
if (opts.debug) writeDebug(request, response, { ...debugEntry(info), ...opts.debugInfo?.(info) }, `${prefix}_debug`);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
opts.onError?.(error);
|
|
216
|
+
}
|
|
217
|
+
return response;
|
|
218
|
+
};
|
|
219
|
+
try {
|
|
220
|
+
return await decideAndRespond();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
opts.onError?.(error);
|
|
223
|
+
return finish(base(), "error", "error");
|
|
224
|
+
}
|
|
225
|
+
async function decideAndRespond() {
|
|
226
|
+
await opts.prepare?.(ctx);
|
|
227
|
+
ctx.trail = withView(ctx.previousTrail, path);
|
|
228
|
+
if (opts.skip?.(path)) return finish(base(), "skipped", "site page");
|
|
229
|
+
const query = {};
|
|
230
|
+
for (const [k, v] of request.nextUrl.searchParams) {
|
|
231
|
+
if (Object.keys(query).length >= MAX_QUERY) break;
|
|
232
|
+
if (v.length <= MAX_QUERY_VALUE) query[k] = v;
|
|
233
|
+
}
|
|
234
|
+
const facts = {
|
|
235
|
+
...ctx.landing,
|
|
236
|
+
...localTime(ctx.geo, ctx.now),
|
|
237
|
+
device: ctx.device,
|
|
238
|
+
path,
|
|
239
|
+
url: request.nextUrl.href,
|
|
240
|
+
query,
|
|
241
|
+
...ctx.geo ? { geo: ctx.geo } : {},
|
|
242
|
+
...ctx.trail[1] ? { previousPath: ctx.trail[1].path } : {},
|
|
243
|
+
...await opts.facts?.(ctx)
|
|
244
|
+
};
|
|
245
|
+
if (!ctx.consent.personalization) {
|
|
246
|
+
return finish(respond(await opts.rewrite?.(ctx, [], void 0, void 0)), "no-consent", "no consent", { facts });
|
|
247
|
+
}
|
|
248
|
+
const keys = await opts.edge.match(ctx.host, path) ?? [{ contentKey: "", mode: "server", pages: [path] }];
|
|
249
|
+
if (!keys.length) {
|
|
250
|
+
return finish(respond(await opts.rewrite?.(ctx, [], void 0, void 0)), "not-targeted", "not targeted", { facts });
|
|
251
|
+
}
|
|
252
|
+
const pending = opts.alongside?.(ctx, keys);
|
|
253
|
+
const cacheFacts = {
|
|
254
|
+
...ctx.landing,
|
|
255
|
+
device: ctx.device,
|
|
256
|
+
country: ctx.geo?.country ?? "",
|
|
257
|
+
city: ctx.geo?.city ?? "",
|
|
258
|
+
// A page the visitor hadn't viewed, a time on page, or a click can change their profile.
|
|
259
|
+
pages: ctx.trail.map((t) => t.path).sort().join(" "),
|
|
260
|
+
timed: ctx.trail.filter((t) => t.seconds !== void 0).map((t) => `${t.path}:${Math.round(t.seconds)}`).sort().join(" "),
|
|
261
|
+
clicks: request.cookies.get(`${prefix}_clicks`)?.value ?? "0",
|
|
262
|
+
// A QA date is another moment: never answer it from today's cached decision.
|
|
263
|
+
...qa.date ? { qaDate: qa.date } : {},
|
|
264
|
+
...await opts.cacheFacts?.(ctx)
|
|
265
|
+
};
|
|
266
|
+
const outcome = await opts.edge.decide(keys, {
|
|
267
|
+
host: ctx.host,
|
|
268
|
+
path,
|
|
269
|
+
visitorId: ctx.visitorId,
|
|
270
|
+
facts,
|
|
271
|
+
cacheFacts,
|
|
272
|
+
...ctx.consent.analytics ? { trail: ctx.trail } : {},
|
|
273
|
+
budgetMs: opts.budgetMs?.(ctx)
|
|
274
|
+
});
|
|
275
|
+
const side = await pending;
|
|
276
|
+
const decisions = opts.filterDecisions ? opts.filterDecisions(outcome.decisions, ctx, side, outcome) : outcome.decisions;
|
|
277
|
+
const groups = swapGroups(decisions);
|
|
278
|
+
let url = await opts.rewrite?.(ctx, groups, side, outcome);
|
|
279
|
+
if (url === void 0 && groups.length) {
|
|
280
|
+
const token = await encodeVariant(groups, opts.variantSecret, await opts.extra?.(ctx, side, outcome));
|
|
281
|
+
url = request.nextUrl.clone();
|
|
282
|
+
url.pathname = `${opts.variantPrefix ?? "/xv"}/${token}${path === "/" ? "" : path}`;
|
|
283
|
+
}
|
|
284
|
+
return finish(respond(url), "decided", outcome.source, { facts, outcome, side });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
var EMPTY = { version: "", pages: [] };
|
|
288
|
+
var MAX_WEBHOOK_BODY = 64 * 1024;
|
|
289
|
+
function createManifestRoute(opts) {
|
|
290
|
+
return async () => {
|
|
291
|
+
if (!opts.token) return Response.json(EMPTY);
|
|
292
|
+
try {
|
|
293
|
+
const manifest = await fetchManifest({
|
|
294
|
+
baseUrl: opts.baseUrl,
|
|
295
|
+
token: opts.token,
|
|
296
|
+
...opts.revalidate !== void 0 ? { init: { next: { revalidate: opts.revalidate } } } : {}
|
|
297
|
+
});
|
|
298
|
+
return Response.json(trimManifest(manifest));
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (process.env.NEXT_PHASE === "phase-production-build") {
|
|
301
|
+
console.warn("[xenia] manifest unavailable at build:", error instanceof Error ? error.message : error);
|
|
302
|
+
return Response.json(EMPTY);
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function createRevalidateRoute(opts) {
|
|
309
|
+
return async (request) => {
|
|
310
|
+
const body = await request.text();
|
|
311
|
+
if (body.length > MAX_WEBHOOK_BODY || !await verifyWebhook({ headers: request.headers, body }, opts.secret ?? "")) {
|
|
312
|
+
return Response.json({ error: "invalid signature" }, { status: 401 });
|
|
313
|
+
}
|
|
314
|
+
const event = request.headers.get("x-xenia-event") ?? "";
|
|
315
|
+
if (event === "ping") return Response.json({ ok: true, event });
|
|
316
|
+
if (event !== "manifest.changed") return Response.json({ ok: true, ignored: event });
|
|
317
|
+
for (const path of opts.paths) revalidatePath(path);
|
|
318
|
+
return Response.json({ ok: true, event, revalidated: opts.paths, ...await opts.onManifestChanged?.() });
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/next/index.ts
|
|
18
323
|
function xeniaProxy(request, opts = {}) {
|
|
19
324
|
const name = visitorCookieName(opts.prefix);
|
|
20
325
|
const existing = request.cookies.get(name)?.value;
|
|
21
326
|
const visitorId = existing ?? generateUlid();
|
|
22
|
-
const forwarded =
|
|
23
|
-
forwarded.set(URL_HEADER, request.url);
|
|
24
|
-
if (!existing) {
|
|
25
|
-
const header = forwarded.get("cookie");
|
|
26
|
-
forwarded.set("cookie", header ? `${header}; ${name}=${visitorId}` : `${name}=${visitorId}`);
|
|
27
|
-
}
|
|
327
|
+
const forwarded = forwardedHeaders(opts.headers ?? request.headers, request.url, existing ? void 0 : `${name}=${visitorId}`);
|
|
28
328
|
const response = NextResponse.next({ request: { headers: forwarded } });
|
|
29
329
|
if (!existing) response.cookies.set(name, visitorId, cookieAttributes(opts.cookie));
|
|
30
330
|
return response;
|
|
@@ -60,6 +360,6 @@ function personalizeOnce(key, personalize) {
|
|
|
60
360
|
return run;
|
|
61
361
|
}
|
|
62
362
|
|
|
63
|
-
export { URL_HEADER, consentFromNext, personalizeOnce, xeniaProxy, xeniaServerFromNext };
|
|
363
|
+
export { QA_CITIES, URL_HEADER, consentFromNext, createManifestRoute, createRevalidateRoute, personalizeOnce, readDebugEntries, xeniaEdgeProxy, xeniaProxy, xeniaServerFromNext };
|
|
64
364
|
//# sourceMappingURL=index.js.map
|
|
65
365
|
//# sourceMappingURL=index.js.map
|
package/dist/next/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/next/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `@xenia-martech/sdk-js/next` — Next.js (App Router, 15+) glue for the server SDK.\n *\n * A Server Component can't see its own URL or set cookies during render, so the two halves split\n * the work: `xeniaProxy()` runs in `proxy.ts`/`middleware.ts` (mints the visitor cookie, stamps the\n * URL onto a request header), and `xeniaServerFromNext()` reads both back to bind a per-request\n * `XeniaServer`. Use them together; each alone degrades gracefully (no URL context / a throwaway\n * visitor id).\n */\n\nimport * as React from \"react\";\nimport { cookies, headers } from \"next/headers\";\nimport { NextResponse, type NextRequest } from \"next/server\";\nimport { visitorCookieName } from \"../core/cookie-names\";\nimport { generateUlid } from \"../core/id\";\nimport { contextFromUrl } from \"../core/request-context\";\nimport type { ConsentState, VisitorCookieOptions, XeniaConfig } from \"../core/types\";\nimport { getConsent } from \"../server/cookies\";\nimport { createXeniaServer, type XeniaServer } from \"../server/xenia-server\";\n\n/** Request header `xeniaProxy()` stamps with the URL being served. */\nexport const URL_HEADER = \"x-xenia-url\";\n\nexport interface XeniaProxyOptions {\n /** Request headers to forward (e.g. your own additions); defaults to the incoming ones. */\n headers?: Headers;\n /** Must match the browser client's `config.prefix`, if you set one. */\n prefix?: string;\n /** Visitor cookie attributes. Default: `path=/`, `SameSite=Lax`, one year. */\n cookie?: VisitorCookieOptions;\n}\n\nfunction cookieAttributes(c: VisitorCookieOptions = {}) {\n const sameSite = c.sameSite ?? \"Lax\";\n return {\n path: \"/\",\n maxAge: Math.floor((c.maxAgeDays ?? 365) * 86400),\n sameSite: sameSite.toLowerCase() as \"lax\" | \"strict\" | \"none\",\n secure: c.secure || sameSite === \"None\",\n ...(c.domain && { domain: c.domain }),\n };\n}\n\n/**\n * Next.js proxy/middleware step: ensure the visitor cookie exists and stamp the URL onto\n * {@link URL_HEADER}, then continue. A new visitor id is also put on the forwarded `Cookie` header,\n * so this very request's render already sees it.\n *\n * ```ts\n * // proxy.ts\n * export const proxy = (request: NextRequest) => xeniaProxy(request);\n * ```\n */\nexport function xeniaProxy(request: NextRequest, opts: XeniaProxyOptions = {}): NextResponse {\n const name = visitorCookieName(opts.prefix);\n const existing = request.cookies.get(name)?.value;\n const visitorId = existing ?? generateUlid();\n\n const forwarded = new Headers(opts.headers ?? request.headers);\n forwarded.set(URL_HEADER, request.url);\n if (!existing) {\n // NextRequest.cookies.set doesn't rewrite the forwarded Cookie header, and cookies() reads that.\n const header = forwarded.get(\"cookie\");\n forwarded.set(\"cookie\", header ? `${header}; ${name}=${visitorId}` : `${name}=${visitorId}`);\n }\n\n const response = NextResponse.next({ request: { headers: forwarded } });\n if (!existing) response.cookies.set(name, visitorId, cookieAttributes(opts.cookie));\n return response;\n}\n\n/**\n * A `XeniaServer` bound to this request's visitor (the cookie `xeniaProxy()` set) and URL context\n * (path, query, UTMs, referrer). For Server Components, Server Actions and Route Handlers.\n * `config.visitorId`/`config.context` still win when passed.\n */\nexport async function xeniaServerFromNext(config: XeniaConfig): Promise<XeniaServer> {\n const [jar, h] = await Promise.all([cookies(), headers()]);\n const name = visitorCookieName(config.prefix);\n let visitorId = config.visitorId ?? jar.get(name)?.value;\n if (!visitorId) {\n visitorId = generateUlid();\n try {\n jar.set(name, visitorId, cookieAttributes());\n } catch {\n // Expected during a Server Component render; xeniaProxy() is the place that persists it.\n }\n }\n return createXeniaServer({\n ...config,\n visitorId,\n context: { ...contextFromUrl(h.get(URL_HEADER), h.get(\"referer\")), ...config.context },\n });\n}\n\n/** This request's consent, from the cookie the browser client's `consent()` (or `consentCookie()`) wrote. */\nexport async function consentFromNext(prefix?: string): Promise<ConsentState> {\n return getConsent((await headers()).get(\"cookie\"), prefix);\n}\n\n// ── CMS page personalization ──────────────────────────────────────────────────\n\n// One store per server request (React.cache scopes it to the render), so every section that asks\n// for the same page shares ONE personalization run. Falls back to \"no sharing\" outside React\n// Server Components, where `cache` is unavailable.\nconst requestStore: () => Map<string, Promise<unknown>> =\n typeof (React as { cache?: unknown }).cache === \"function\"\n ? (React as unknown as { cache: <F extends () => unknown>(fn: F) => F }).cache(() => new Map())\n : () => new Map();\n\n/**\n * Run `personalize` at most once per request for `key`, and share the promise. Lets a page shell\n * render immediately while each personalized section awaits the same run inside its own\n * `<Suspense>`, streaming in as soon as it's ready:\n *\n * ```tsx\n * const run = (ref) => personalizeOnce(`page:${ref}`, async () =>\n * (await xeniaServerFromNext(config)).personalizePage(ref, { cms, timeoutMs: 800 }));\n *\n * export default async function Page({ params }) {\n * const xenia = await xeniaServerFromNext(config);\n * const base = await xenia.personalizePage(ref, { cms, maxRounds: 0 }); // CMS only, no Xenia call\n * const pending = new Set(pendingTargets(base.content, cms)); // from the markers\n * return base.content.fields.sections.map((s) =>\n * pending.has(s.sys.id) ? (\n * <Suspense key={s.sys.id} fallback={<SectionSkeleton />}> // never base content that will change\n * <PersonalizedSection id={s.sys.id} run={() => run(ref)} />\n * </Suspense>\n * ) : (\n * <Section key={s.sys.id} entry={s} />\n * ));\n * }\n * ```\n *\n * To finish deeper levels in the browser instead, pass `maxRounds: 1` here and hand the result's\n * `content` to `usePersonalizedContent` — already-decided entries are not asked about again.\n */\nexport function personalizeOnce<T>(key: string, personalize: () => Promise<T>): Promise<T> {\n const store = requestStore();\n let run = store.get(key) as Promise<T> | undefined;\n if (!run) {\n run = personalize();\n store.set(key, run);\n }\n return run;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/next/shared.ts","../../src/edge/debug-entries.ts","../../src/next/edge-debug.ts","../../src/next/edge-proxy.ts","../../src/next/edge-routes.ts","../../src/next/index.ts"],"names":["headers","NextResponse"],"mappings":"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","file":"index.js","sourcesContent":["import type { VisitorCookieOptions } from \"../core/types\";\n\n/** Request header the proxy stamps with the URL being served (a Server Component can't see its own). */\nexport const URL_HEADER = \"x-xenia-url\";\n\n/** `NextResponse.cookies.set` attributes for the visitor cookie. Default: `path=/`, `SameSite=Lax`, one year. */\nexport function cookieAttributes(c: VisitorCookieOptions = {}) {\n const sameSite = c.sameSite ?? \"Lax\";\n return {\n path: \"/\",\n maxAge: Math.floor((c.maxAgeDays ?? 365) * 86400),\n sameSite: sameSite.toLowerCase() as \"lax\" | \"strict\" | \"none\",\n secure: c.secure || sameSite === \"None\",\n ...(c.domain && { domain: c.domain }),\n };\n}\n\n/**\n * The request headers to hand the render: the URL on {@link URL_HEADER}, and a cookie just minted\n * (`name=value`) added to the forwarded `Cookie` header — `NextRequest.cookies.set` doesn't rewrite\n * it, and `cookies()` reads that — so this very request's render already sees it.\n */\nexport function forwardedHeaders(headers: Headers, url: string, newCookie?: string): Headers {\n const forwarded = new Headers(headers);\n forwarded.set(URL_HEADER, url);\n if (newCookie) {\n const header = forwarded.get(\"cookie\");\n forwarded.set(\"cookie\", header ? `${header}; ${newCookie}` : newCookie);\n }\n return forwarded;\n}\n","/** One page's entry in the edge debug cookie (written by `xeniaEdgeProxy({ debug })`, read by `<XeniaDebugPanel>`). */\nexport type DebugEntry = Record<string, unknown> & { page: string };\n\n/** The debug cookie's pages (it may arrive encoded once or twice); anything unreadable is none. */\nexport function readDebugEntries(raw: string | undefined | null): Record<string, DebugEntry> {\n if (!raw) return {};\n try {\n let text = decodeURIComponent(raw);\n if (text.startsWith(\"%\")) text = decodeURIComponent(text);\n const parsed: unknown = JSON.parse(text);\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? (parsed as Record<string, DebugEntry>) : {};\n } catch {\n return {};\n }\n}\n","/**\n * QA and debugging for the edge flow (`xeniaEdgeProxy`'s `qa` and `debug` options):\n * - QA overrides: `?xn_geo=<city>` places the visitor in a QA city, `?xn_date=YYYY-MM-DD` decides\n * as of that day (midday in the US), `?xn_geo=off` forgets them; kept for the visit in a cookie;\n * - the debug cookie: what was decided for each of the last few pages, read by `<XeniaDebugPanel>`.\n * A cookie rather than the HTML, because variant pages are cached and shared.\n */\nimport type { NextRequest, NextResponse } from \"next/server\";\nimport { isPrefetch } from \"../edge/request-facts\";\nimport { readDebugEntries, type DebugEntry } from \"../edge/debug-entries\";\nimport type { ViewerGeo } from \"../edge/types\";\nimport type { EdgeDecisionInfo } from \"./edge-proxy\";\n\nexport { readDebugEntries, type DebugEntry };\n\n/** Cities a QA visitor can be placed in, with what an edge would report for them. */\nexport const QA_CITIES: Record<string, ViewerGeo> = {\n minneapolis: { country: \"US\", region: \"MN\", regionName: \"Minnesota\", city: \"Minneapolis\", timeZone: \"America/Chicago\", latitude: \"44.98\", longitude: \"-93.27\" },\n chicago: { country: \"US\", region: \"IL\", regionName: \"Illinois\", city: \"Chicago\", timeZone: \"America/Chicago\", latitude: \"41.88\", longitude: \"-87.63\" },\n miami: { country: \"US\", region: \"FL\", regionName: \"Florida\", city: \"Miami\", timeZone: \"America/New_York\", latitude: \"25.76\", longitude: \"-80.19\" },\n new_york: { country: \"US\", region: \"NY\", regionName: \"New York\", city: \"New York\", timeZone: \"America/New_York\", latitude: \"40.71\", longitude: \"-74.01\" },\n los_angeles: { country: \"US\", region: \"CA\", regionName: \"California\", city: \"Los Angeles\", timeZone: \"America/Los_Angeles\", latitude: \"34.05\", longitude: \"-118.24\" },\n seattle: { country: \"US\", region: \"WA\", regionName: \"Washington\", city: \"Seattle\", timeZone: \"America/Los_Angeles\", latitude: \"47.61\", longitude: \"-122.33\" },\n london: { country: \"GB\", region: \"ENG\", regionName: \"England\", city: \"London\", timeZone: \"Europe/London\", latitude: \"51.51\", longitude: \"-0.13\" },\n sydney: { country: \"AU\", region: \"NSW\", regionName: \"New South Wales\", city: \"Sydney\", timeZone: \"Australia/Sydney\", latitude: \"-33.87\", longitude: \"151.21\" },\n};\n\nexport interface QaState {\n geo?: string;\n date?: string;\n}\n\nconst QA_MAX_AGE_S = 60 * 60;\n\nconst validDate = (d: unknown): d is string =>\n typeof d === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(d) && !Number.isNaN(Date.parse(d));\n\n/** The QA overrides for this request: from the URL (then remembered) or the cookie, each checked alike. */\nexport function readQa(request: NextRequest, cities: Record<string, ViewerGeo>, cookie: string): { qa: QaState; changed: boolean } {\n const isCity = (c: unknown): c is string => typeof c === \"string\" && Object.hasOwn(cities, c);\n let qa: QaState;\n try {\n const saved = JSON.parse(request.cookies.get(cookie)?.value ?? \"{}\") as Record<string, unknown> | null;\n qa = {\n ...(isCity(saved?.geo) ? { geo: saved.geo } : {}),\n ...(validDate(saved?.date) ? { date: saved.date } : {}),\n };\n } catch {\n qa = {};\n }\n const p = request.nextUrl.searchParams;\n let changed = false;\n // `off` forgets what was remembered; values on the same URL then apply on a clean slate.\n if (p.get(\"xn_geo\") === \"off\") {\n changed = Object.keys(qa).length > 0;\n qa = {};\n }\n const city = p.get(\"xn_geo\")?.toLowerCase();\n if (isCity(city)) {\n qa = { ...qa, geo: city };\n changed = true;\n }\n const date = p.get(\"xn_date\");\n if (validDate(date)) {\n qa = { ...qa, date };\n changed = true;\n }\n return { qa, changed };\n}\n\nexport function rememberQa(response: NextResponse, qa: QaState, cookie: string) {\n if (Object.keys(qa).length) response.cookies.set(cookie, JSON.stringify(qa), { path: \"/\", maxAge: QA_MAX_AGE_S, sameSite: \"lax\" });\n else response.cookies.delete(cookie);\n}\n\n/** \"Now\" for a QA date: midday in the US (17:00 UTC) that day. */\nexport const qaNow = (qa: QaState): Date | undefined => (qa.date ? new Date(`${qa.date}T17:00:00Z`) : undefined);\n\n// ── debug cookie ─────────────────────────────────────────────────────────────\n\n/** Longest page path kept in an entry (a longer one would make the header itself too large). */\nconst MAX_PAGE = 200;\n/** Room for the cookie's value, under the browser's 4096 bytes for name, value and attributes. */\nconst DEBUG_COOKIE_MAX = 3900;\nconst DEBUG_PAGES = 4;\nconst DEBUG_MAX_AGE_S = 300;\n/** Location headers named on the panel, to show whether the host's geo reached the proxy. */\nconst GEO_HEADERS = [\"x-xn-geo\", \"x-nf-geo\", \"x-country\", \"x-vercel-ip-country\", \"cloudfront-viewer-country\", \"cf-ipcountry\"];\n\nconst geoLabel = (g: ViewerGeo | undefined) =>\n g ? `${g.city ? `${g.city}, ` : \"\"}${g.country ?? \"-\"}${g.region ? `/${g.region}` : \"\"} (${g.source ?? \"?\"})` : \"none\";\n\n/** The location sources that reached the proxy: geo headers, and Netlify's runtime context. */\nfunction sourcesSeen(request: Request): string[] {\n const seen = GEO_HEADERS.filter((n) => request.headers.get(n));\n const g = globalThis as unknown as { Netlify?: { context?: unknown } };\n if (g.Netlify) seen.push(g.Netlify.context ? \"Netlify.context\" : \"Netlify (no context)\");\n return seen;\n}\n\n/** Each Visitor Intent answer with its strongest scores (a step with many options would overflow the cookie). */\nfunction profileSummary(profile: Record<string, unknown> | null | undefined) {\n if (!profile) return undefined;\n const answers = Object.fromEntries(\n Object.entries((profile.answers ?? {}) as Record<string, Record<string, unknown> & { scores?: Record<string, number> }>).map(([name, a]) => [\n name,\n { ...a, scores: Object.fromEntries(Object.entries(a?.scores ?? {}).filter(([, v]) => v > 0).sort((x, y) => y[1] - x[1]).slice(0, 5)) },\n ]),\n );\n return { answers, pageViews: profile.page_views, lastSection: profile.last_section, campaign: profile.last_utm_campaign };\n}\n\nconst SOURCES: Record<string, string> = { skipped: \"site page\", \"not-targeted\": \"not targeted\", \"no-consent\": \"no consent\", error: \"error\" };\n\n/** What the panel shows for one page view. */\nexport function debugEntry(info: EdgeDecisionInfo): DebugEntry {\n const { outcome } = info;\n return {\n page: info.ctx.path,\n geo: geoLabel(info.ctx.geo),\n // Structured, for the panel's Request tab.\n request: {\n location: info.ctx.geo,\n via: info.ctx.geoVia,\n seen: sourcesSeen(info.ctx.request),\n ...(info.ctx.detectedGeo ? { detected: info.ctx.detectedGeo } : {}),\n device: info.ctx.device,\n consent: info.ctx.consent,\n },\n source: outcome?.source ?? SOURCES[info.exit] ?? info.exit,\n ...(outcome ? { ms: outcome.ms, profile: profileSummary(outcome.profile) } : {}),\n decisions: (outcome?.decisions ?? []).map((d) => ({\n key: d.contentKey,\n result: d.personalized ? d.variant || \"personalized\" : d.reason?.startsWith(\"suppressed\") ? \"suppressed\" : (d.bucket ?? \"base\"),\n reason: d.reason,\n trace: d.trace,\n })),\n };\n}\n\n/**\n * Add this page's entry, keyed by page — a link prefetch passes through the proxy too, and must not\n * replace the page's own entry. Keeps the last few pages; drops the oldest until it fits, then this\n * page's profile.\n */\nexport function writeDebug(request: NextRequest, response: NextResponse, entry: DebugEntry, cookie: string) {\n if (isPrefetch(request.headers)) return;\n entry = { ...entry, page: entry.page.slice(0, MAX_PAGE) };\n const pages = readDebugEntries(request.cookies.get(cookie)?.value);\n delete pages[entry.page];\n pages[entry.page] = { ...entry, at: Date.now() };\n // Browsers drop a cookie over 4 KB whole, and the value is encoded twice (here and by `cookies.set`).\n let entries = Object.entries(pages).slice(-DEBUG_PAGES);\n const encoded = () => encodeURIComponent(JSON.stringify(Object.fromEntries(entries)));\n while (entries.length > 1 && encodeURIComponent(encoded()).length > DEBUG_COOKIE_MAX) entries = entries.slice(1);\n if (encodeURIComponent(encoded()).length > DEBUG_COOKIE_MAX) {\n const { profile: _drop, ...rest } = pages[entry.page];\n void _drop;\n entries = [[entry.page, rest as DebugEntry]];\n }\n if (encodeURIComponent(encoded()).length > DEBUG_COOKIE_MAX) {\n entries = [[entry.page, { page: entry.page, source: entry.source, note: \"entry too large for the debug cookie\" } as DebugEntry]];\n }\n response.cookies.set(cookie, encoded(), { path: \"/\", maxAge: DEBUG_MAX_AGE_S, sameSite: \"lax\" });\n}\n","/**\n * The edge decide flow in one proxy call: which page views need Xenia, the facts Xenia decides on,\n * and the rewrite to the variant page. Every failure ends in base content.\n *\n * Cost per request:\n * - page no journey targets → an in-memory manifest lookup, no Xenia call\n * - targeted page, returning visitor → an in-memory decision-cache hit\n * - targeted page, first view this session → one `/decide` call, capped at the budget\n */\nimport { NextResponse, type NextRequest } from \"next/server\";\nimport { DEFAULT_PREFIX, visitorCookieName } from \"../core/cookie-names\";\nimport { generateUlid } from \"../core/id\";\nimport type { VisitorCookieOptions } from \"../core/types\";\nimport type { DecideOutcome, XeniaEdge } from \"../edge/client\";\nimport { edgeConsent, type EdgeConsent } from \"../edge/consent\";\nimport { deviceOf, isBot, isPageLoad, landingContext, localTime, viewerGeo, type GeoHeader } from \"../edge/request-facts\";\nimport { TRAIL_MAX_AGE_S, readTrail, trailCookieName, trailCookieValue, withView } from \"../edge/trail\";\nimport type { EdgeDecision, EdgeFacts, ManifestKey, TrailView, ViewerGeo } from \"../edge/types\";\nimport { encodeVariant, swapGroups, type SwapGroup } from \"../edge/variant\";\nimport { QA_CITIES, debugEntry, qaNow, readQa, rememberQa, writeDebug, type DebugEntry } from \"./edge-debug\";\nimport { cookieAttributes, forwardedHeaders } from \"./shared\";\n\n/** What the proxy knows about this page view. `prepare` may change the fields marked so. */\nexport interface EdgeRequestContext {\n request: NextRequest;\n host: string | null;\n path: string;\n /** The visitor this view is decided for (changeable in `prepare`, e.g. a profile's own visitor). */\n visitorId: string;\n /** The visitor cookie the browser sent, if any. */\n existingVisitorId?: string;\n /** Where the visitor is (changeable in `prepare`, e.g. a QA city). */\n geo?: ViewerGeo;\n /** Which source supplied `geo` (see `viewerGeo`). */\n geoVia: string;\n /** While a QA city stands in for `geo`: what the edge really detected (for the debug panel). */\n detectedGeo?: ViewerGeo;\n device: \"mobile\" | \"tablet\" | \"desktop\";\n /** First-touch campaign and referrer (changeable in `prepare`). */\n landing: Record<string, string>;\n /** Recent views before this one (changeable in `prepare`). */\n previousTrail: TrailView[];\n /** The trail with this view first — set after `prepare`. */\n trail: TrailView[];\n consent: EdgeConsent;\n /** \"Now\" for local-time facts: the real time, or a QA date (changeable in `prepare`). */\n now: Date;\n}\n\n/** `error`: something threw (a hook, the edge client) — base content was served. */\nexport type EdgeExit = \"skipped\" | \"not-targeted\" | \"no-consent\" | \"decided\" | \"error\";\n\nexport interface EdgeDecisionInfo<S = unknown> {\n ctx: EdgeRequestContext;\n exit: EdgeExit;\n /** The facts sent to `/decide` (standard ones with the site's on top); absent when skipped. */\n facts?: EdgeFacts;\n outcome?: DecideOutcome;\n side?: S;\n /** The response about to be returned — set cookies on it here. */\n response: NextResponse;\n}\n\nexport interface XeniaEdgeProxyOptions<S = unknown> {\n /** One client per instance: `createXeniaEdge(…)` from `/edge`. */\n edge: XeniaEdge;\n /** Server-only secret the variant token is signed with. */\n variantSecret: string;\n /** Where variant pages live. Default `/xv` (`/xv/<token>/women`). */\n variantPrefix?: string;\n /** Must match the browser client's `config.prefix`, if you set one. */\n prefix?: string;\n cookie?: VisitorCookieOptions;\n /**\n * The location headers your host (or edge function) sets and overwrites, e.g.\n * `[\"x-vercel-ip-country\"]` — see `viewerGeo`. Default none: a visitor can send any header.\n */\n geoHeaders?: readonly GeoHeader[];\n /** Treat `Sec-GPC` / `DNT` as analytics off. Default true. */\n respectDnt?: boolean;\n /** Paths with no variant page (app routes): their views join the trail, but they're never decided. */\n skip?: (path: string) => boolean;\n /** Adjust the context before facts are built: the visitor, geo, landing or earlier trail. */\n prepare?: (ctx: EdgeRequestContext) => void | Promise<void>;\n /** The site's own facts, on top of the standard ones. */\n facts?: (ctx: EdgeRequestContext) => EdgeFacts | Promise<EdgeFacts>;\n /** What else makes this visitor's decision different (a new value → a fresh decision). */\n cacheFacts?: (ctx: EdgeRequestContext) => EdgeFacts | Promise<EdgeFacts>;\n /** This view's `/decide` budget, instead of the client's (e.g. a journey that calls a back office). */\n budgetMs?: (ctx: EdgeRequestContext) => number | undefined;\n /** Work started before `/decide` and awaited after it; its result reaches the hooks below as `side`. */\n alongside?: (ctx: EdgeRequestContext, keys: ManifestKey[]) => Promise<S>;\n filterDecisions?: (decisions: EdgeDecision[], ctx: EdgeRequestContext, side: S | undefined, outcome: DecideOutcome) => EdgeDecision[];\n /**\n * Where to render this view: a URL to rewrite to, `null` for the page itself, `undefined` for the\n * default (the signed variant page when anything was swapped). Called on every exit but `skipped`;\n * `outcome` is absent when Xenia wasn't asked.\n */\n rewrite?: (\n ctx: EdgeRequestContext,\n groups: SwapGroup[],\n side: S | undefined,\n outcome: DecideOutcome | undefined,\n ) => URL | null | undefined | Promise<URL | null | undefined>;\n /** Carried in the variant token beside the swaps (left out if it would make the token too long). */\n extra?: (ctx: EdgeRequestContext, side: S | undefined, outcome: DecideOutcome) => unknown;\n /** Called on every exit, with the response about to be returned. */\n onDecision?: (info: EdgeDecisionInfo<S>) => void | Promise<void>;\n /** Anything that threw on the way (base content was served instead). Default: nothing. */\n onError?: (error: unknown) => void;\n /**\n * Write what was decided for each page to the `<prefix>_debug` cookie `<XeniaDebugPanel>` reads.\n * For development and QA only — turn it on with `process.env.NODE_ENV === \"development\"` or a flag.\n */\n debug?: boolean;\n /** Fields of your own for the panel (or replacements for the SDK's), per page view. */\n debugInfo?: (info: EdgeDecisionInfo<S>) => Record<string, unknown>;\n /**\n * QA overrides for development and QA only: `?xn_geo=<city>` (`QA_CITIES`, or your own `cities`),\n * `?xn_date=YYYY-MM-DD`, `?xn_geo=off` to forget them, and `?xn_country=GB` under `next dev`.\n */\n qa?: boolean | { cities?: Record<string, ViewerGeo> };\n}\n\nconst MAX_QUERY = 20;\nconst MAX_QUERY_VALUE = 500;\n\n/**\n * Next.js `proxy.ts`/`middleware.ts` step for edge personalization: ensures the visitor cookie\n * (like `xeniaProxy()`), keeps the landing campaign and page trail, decides targeted pages with\n * Xenia within the budget and rewrites a personalized view to its signed variant page\n * (`decodeVariant` there). Respects consent (see `edgeConsent`).\n *\n * ```ts\n * const edge = createXeniaEdge({ baseUrl, token: process.env.XENIA_SDK_TOKEN! });\n * export const proxy = (request: NextRequest) =>\n * xeniaEdgeProxy(request, { edge, variantSecret: process.env.XENIA_VARIANT_SECRET! });\n * ```\n */\nexport async function xeniaEdgeProxy<S = unknown>(request: NextRequest, opts: XeniaEdgeProxyOptions<S>): Promise<NextResponse> {\n if (request.method !== \"GET\" && request.method !== \"HEAD\") return NextResponse.next();\n if (isBot(request.headers.get(\"user-agent\"))) return NextResponse.next();\n\n const started = Date.now();\n const prefix = opts.prefix ?? DEFAULT_PREFIX;\n const vidCookie = visitorCookieName(prefix);\n const ctxCookie = `${prefix}_ctx`;\n const trailCookie = trailCookieName(prefix);\n const { pathname: path } = request.nextUrl;\n const existingVisitorId = request.cookies.get(vidCookie)?.value;\n const landing = landingContext(request.nextUrl, request.headers.get(\"referer\"), request.cookies.get(ctxCookie)?.value);\n const qaOn = Boolean(opts.qa);\n const cities = (typeof opts.qa === \"object\" && opts.qa.cities) || QA_CITIES;\n const qaCookie = `${prefix}_qa_ctx`;\n const { qa, changed: qaChanged } = qaOn ? readQa(request, cities, qaCookie) : { qa: {}, changed: false };\n const detected = viewerGeo(request, { trust: opts.geoHeaders, devOverride: qaOn && process.env.NODE_ENV === \"development\" });\n const qaCity = qa.geo && Object.hasOwn(cities, qa.geo) ? cities[qa.geo] : undefined;\n const { geo, via } = qaCity ? { geo: { ...qaCity, source: \"qa\" }, via: \"qa\" } : detected;\n\n const ctx: EdgeRequestContext = {\n request,\n host: request.headers.get(\"host\"),\n path,\n visitorId: existingVisitorId ?? generateUlid(),\n existingVisitorId,\n geo,\n geoVia: via,\n ...(qaCity && detected.geo ? { detectedGeo: detected.geo } : {}),\n device: deviceOf(request.headers),\n landing: landing.ctx,\n previousTrail: readTrail(request.cookies.get(trailCookie)?.value),\n trail: [],\n consent: edgeConsent(request.headers, { prefix, respectDnt: opts.respectDnt }),\n now: qaNow(qa) ?? new Date(),\n };\n // Read when the response is built, so a visitor `prepare` switched to is the one forwarded.\n const newVisitor = () => ctx.visitorId !== existingVisitorId;\n const forwarded = () =>\n forwardedHeaders(request.headers, request.url, newVisitor() ? `${vidCookie}=${ctx.visitorId}` : undefined);\n const base = () => NextResponse.next({ request: { headers: forwarded() } });\n const respond = (url: URL | null | undefined) =>\n url ? NextResponse.rewrite(url, { request: { headers: forwarded() } }) : base();\n\n const finish = async (\n response: NextResponse,\n exit: EdgeExit,\n desc: string,\n rest: Omit<EdgeDecisionInfo<S>, \"ctx\" | \"exit\" | \"response\"> = {},\n ) => {\n if (newVisitor()) response.cookies.set(vidCookie, ctx.visitorId, cookieAttributes(opts.cookie));\n if (landing.changed || ctx.landing !== landing.ctx) {\n response.cookies.set(ctxCookie, JSON.stringify(ctx.landing), { path: \"/\", sameSite: \"lax\", httpOnly: true });\n }\n if (ctx.consent.analytics && isPageLoad(request.headers) && ctx.trail.length && ctx.trail !== ctx.previousTrail) {\n response.cookies.set(trailCookie, trailCookieValue(ctx.trail), { path: \"/\", maxAge: TRAIL_MAX_AGE_S, sameSite: \"lax\" });\n }\n response.headers.set(\"Server-Timing\", `xenia;dur=${Date.now() - started};desc=\"${desc}\"`);\n if (qaChanged) rememberQa(response, qa, qaCookie);\n try {\n const info: EdgeDecisionInfo<S> = { ctx, exit, response, ...rest };\n await opts.onDecision?.(info);\n if (opts.debug) writeDebug(request, response, { ...debugEntry(info), ...opts.debugInfo?.(info) } as DebugEntry, `${prefix}_debug`);\n } catch (error) {\n opts.onError?.(error);\n }\n return response;\n };\n\n // Every failure — a throwing hook, a bad manifest entry — ends in base content, never an error page.\n try {\n return await decideAndRespond();\n } catch (error) {\n opts.onError?.(error);\n return finish(base(), \"error\", \"error\");\n }\n\n async function decideAndRespond(): Promise<NextResponse> {\n await opts.prepare?.(ctx);\n ctx.trail = withView(ctx.previousTrail, path);\n\n if (opts.skip?.(path)) return finish(base(), \"skipped\", \"site page\");\n\n const query: Record<string, string> = {};\n for (const [k, v] of request.nextUrl.searchParams) {\n if (Object.keys(query).length >= MAX_QUERY) break;\n if (v.length <= MAX_QUERY_VALUE) query[k] = v;\n }\n const facts: EdgeFacts = {\n ...ctx.landing,\n ...localTime(ctx.geo, ctx.now),\n device: ctx.device,\n path,\n url: request.nextUrl.href,\n query,\n ...(ctx.geo ? { geo: ctx.geo as Record<string, string> } : {}),\n ...(ctx.trail[1] ? { previousPath: ctx.trail[1].path } : {}),\n ...(await opts.facts?.(ctx)),\n };\n\n if (!ctx.consent.personalization) {\n return finish(respond(await opts.rewrite?.(ctx, [], undefined, undefined)), \"no-consent\", \"no consent\", { facts });\n }\n\n // No manifest yet (a cold start whose first fetch hasn't come back): decide by path, and Xenia\n // answers \"none\" for pages no journey targets — rather than serving base content to every visitor.\n const keys = (await opts.edge.match(ctx.host, path)) ?? [{ contentKey: \"\", mode: \"server\" as const, pages: [path] }];\n if (!keys.length) {\n return finish(respond(await opts.rewrite?.(ctx, [], undefined, undefined)), \"not-targeted\", \"not targeted\", { facts });\n }\n\n const pending = opts.alongside?.(ctx, keys);\n const cacheFacts: EdgeFacts = {\n ...ctx.landing,\n device: ctx.device,\n country: ctx.geo?.country ?? \"\",\n city: ctx.geo?.city ?? \"\",\n // A page the visitor hadn't viewed, a time on page, or a click can change their profile.\n pages: ctx.trail.map((t) => t.path).sort().join(\" \"),\n timed: ctx.trail.filter((t) => t.seconds !== undefined).map((t) => `${t.path}:${Math.round(t.seconds!)}`).sort().join(\" \"),\n clicks: request.cookies.get(`${prefix}_clicks`)?.value ?? \"0\",\n // A QA date is another moment: never answer it from today's cached decision.\n ...(qa.date ? { qaDate: qa.date } : {}),\n ...(await opts.cacheFacts?.(ctx)),\n };\n const outcome = await opts.edge.decide(keys, {\n host: ctx.host,\n path,\n visitorId: ctx.visitorId,\n facts,\n cacheFacts,\n ...(ctx.consent.analytics ? { trail: ctx.trail } : {}),\n budgetMs: opts.budgetMs?.(ctx),\n });\n const side = await pending;\n const decisions = opts.filterDecisions ? opts.filterDecisions(outcome.decisions, ctx, side, outcome) : outcome.decisions;\n const groups = swapGroups(decisions);\n\n let url = await opts.rewrite?.(ctx, groups, side, outcome);\n if (url === undefined && groups.length) {\n const token = await encodeVariant(groups, opts.variantSecret, await opts.extra?.(ctx, side, outcome));\n url = request.nextUrl.clone();\n url.pathname = `${opts.variantPrefix ?? \"/xv\"}/${token}${path === \"/\" ? \"\" : path}`;\n }\n return finish(respond(url), \"decided\", outcome.source, { facts, outcome, side });\n }\n}\n","/** Route Handlers a Next.js storefront serves for edge personalization. */\nimport { revalidatePath } from \"next/cache\";\nimport { fetchManifest, trimManifest } from \"../edge/manifest\";\nimport type { PublicManifest } from \"../edge/types\";\nimport { verifyWebhook } from \"../edge/webhook\";\n\nconst EMPTY: PublicManifest = { version: \"\", pages: [] };\nconst MAX_WEBHOOK_BODY = 64 * 1024;\n\n/**\n * `GET` for a public, CDN-cached copy of the manifest (version + targeted page patterns only — no\n * content keys or rules), so a cold proxy instance reads it from the nearby CDN instead of waiting on\n * Xenia, and the server token never leaves the server. Point the edge client's `manifestUrl` at it.\n * At build time an unreachable Xenia serves an empty manifest instead of failing the deploy; at\n * runtime it throws, so the cache keeps the last good copy.\n *\n * ```ts\n * // app/api/xenia/manifest/route.ts\n * export const revalidate = 36000; // a safety net — the storefront webhook regenerates it at once\n * export const GET = createManifestRoute({ token: process.env.XENIA_SDK_TOKEN, revalidate });\n * ```\n */\nexport function createManifestRoute(opts: { token?: string; baseUrl?: string; revalidate?: number }): () => Promise<Response> {\n return async () => {\n if (!opts.token) return Response.json(EMPTY);\n try {\n const manifest = await fetchManifest({\n baseUrl: opts.baseUrl,\n token: opts.token,\n ...(opts.revalidate !== undefined ? { init: { next: { revalidate: opts.revalidate } } as RequestInit } : {}),\n });\n return Response.json(trimManifest(manifest));\n } catch (error) {\n if (process.env.NEXT_PHASE === \"phase-production-build\") {\n console.warn(\"[xenia] manifest unavailable at build:\", error instanceof Error ? error.message : error);\n return Response.json(EMPTY);\n }\n throw error;\n }\n };\n}\n\n/**\n * `POST` for Xenia's storefront webhook (Journey Orchestrator → Storefront webhook): verifies the\n * signature (`verifyWebhook`), answers `ping`, and on `manifest.changed` regenerates `paths` (the\n * manifest route) at once, then runs `onManifestChanged` (e.g. a rebuild hook) and merges what it\n * returns into the response.\n */\nexport function createRevalidateRoute(opts: {\n secret?: string;\n paths: string[];\n onManifestChanged?: () => Promise<Record<string, unknown>> | Record<string, unknown>;\n}): (request: Request) => Promise<Response> {\n return async (request) => {\n const body = await request.text();\n if (body.length > MAX_WEBHOOK_BODY || !(await verifyWebhook({ headers: request.headers, body }, opts.secret ?? \"\"))) {\n return Response.json({ error: \"invalid signature\" }, { status: 401 });\n }\n const event = request.headers.get(\"x-xenia-event\") ?? \"\";\n if (event === \"ping\") return Response.json({ ok: true, event });\n if (event !== \"manifest.changed\") return Response.json({ ok: true, ignored: event });\n for (const path of opts.paths) revalidatePath(path);\n return Response.json({ ok: true, event, revalidated: opts.paths, ...(await opts.onManifestChanged?.()) });\n };\n}\n","/**\n * `@xenia-martech/sdk-js/next` — Next.js (App Router, 15+) glue for the server SDK.\n *\n * A Server Component can't see its own URL or set cookies during render, so the two halves split\n * the work: `xeniaProxy()` runs in `proxy.ts`/`middleware.ts` (mints the visitor cookie, stamps the\n * URL onto a request header), and `xeniaServerFromNext()` reads both back to bind a per-request\n * `XeniaServer`. Use them together; each alone degrades gracefully (no URL context / a throwaway\n * visitor id).\n */\n\nimport * as React from \"react\";\nimport { cookies, headers } from \"next/headers\";\nimport { NextResponse, type NextRequest } from \"next/server\";\nimport { visitorCookieName } from \"../core/cookie-names\";\nimport { generateUlid } from \"../core/id\";\nimport { contextFromUrl } from \"../core/request-context\";\nimport type { ConsentState, VisitorCookieOptions, XeniaConfig } from \"../core/types\";\nimport { getConsent } from \"../server/cookies\";\nimport { createXeniaServer, type XeniaServer } from \"../server/xenia-server\";\nimport { URL_HEADER, cookieAttributes, forwardedHeaders } from \"./shared\";\n\nexport { URL_HEADER };\nexport { xeniaEdgeProxy } from \"./edge-proxy\";\nexport type { EdgeDecisionInfo, EdgeExit, EdgeRequestContext, XeniaEdgeProxyOptions } from \"./edge-proxy\";\nexport { createManifestRoute, createRevalidateRoute } from \"./edge-routes\";\nexport { QA_CITIES, readDebugEntries } from \"./edge-debug\";\nexport type { DebugEntry, QaState } from \"./edge-debug\";\n\n\nexport interface XeniaProxyOptions {\n /** Request headers to forward (e.g. your own additions); defaults to the incoming ones. */\n headers?: Headers;\n /** Must match the browser client's `config.prefix`, if you set one. */\n prefix?: string;\n /** Visitor cookie attributes. Default: `path=/`, `SameSite=Lax`, one year. */\n cookie?: VisitorCookieOptions;\n}\n\n/**\n * Next.js proxy/middleware step: ensure the visitor cookie exists and stamp the URL onto\n * {@link URL_HEADER}, then continue. A new visitor id is also put on the forwarded `Cookie` header,\n * so this very request's render already sees it.\n *\n * ```ts\n * // proxy.ts\n * export const proxy = (request: NextRequest) => xeniaProxy(request);\n * ```\n */\nexport function xeniaProxy(request: NextRequest, opts: XeniaProxyOptions = {}): NextResponse {\n const name = visitorCookieName(opts.prefix);\n const existing = request.cookies.get(name)?.value;\n const visitorId = existing ?? generateUlid();\n\n const forwarded = forwardedHeaders(opts.headers ?? request.headers, request.url, existing ? undefined : `${name}=${visitorId}`);\n\n const response = NextResponse.next({ request: { headers: forwarded } });\n if (!existing) response.cookies.set(name, visitorId, cookieAttributes(opts.cookie));\n return response;\n}\n\n/**\n * A `XeniaServer` bound to this request's visitor (the cookie `xeniaProxy()` set) and URL context\n * (path, query, UTMs, referrer). For Server Components, Server Actions and Route Handlers.\n * `config.visitorId`/`config.context` still win when passed.\n */\nexport async function xeniaServerFromNext(config: XeniaConfig): Promise<XeniaServer> {\n const [jar, h] = await Promise.all([cookies(), headers()]);\n const name = visitorCookieName(config.prefix);\n let visitorId = config.visitorId ?? jar.get(name)?.value;\n if (!visitorId) {\n visitorId = generateUlid();\n try {\n jar.set(name, visitorId, cookieAttributes());\n } catch {\n // Expected during a Server Component render; xeniaProxy() is the place that persists it.\n }\n }\n return createXeniaServer({\n ...config,\n visitorId,\n context: { ...contextFromUrl(h.get(URL_HEADER), h.get(\"referer\")), ...config.context },\n });\n}\n\n/** This request's consent, from the cookie the browser client's `consent()` (or `consentCookie()`) wrote. */\nexport async function consentFromNext(prefix?: string): Promise<ConsentState> {\n return getConsent((await headers()).get(\"cookie\"), prefix);\n}\n\n// ── CMS page personalization ──────────────────────────────────────────────────\n\n// One store per server request (React.cache scopes it to the render), so every section that asks\n// for the same page shares ONE personalization run. Falls back to \"no sharing\" outside React\n// Server Components, where `cache` is unavailable.\nconst requestStore: () => Map<string, Promise<unknown>> =\n typeof (React as { cache?: unknown }).cache === \"function\"\n ? (React as unknown as { cache: <F extends () => unknown>(fn: F) => F }).cache(() => new Map())\n : () => new Map();\n\n/**\n * Run `personalize` at most once per request for `key`, and share the promise. Lets a page shell\n * render immediately while each personalized section awaits the same run inside its own\n * `<Suspense>`, streaming in as soon as it's ready:\n *\n * ```tsx\n * const run = (ref) => personalizeOnce(`page:${ref}`, async () =>\n * (await xeniaServerFromNext(config)).personalizePage(ref, { cms, timeoutMs: 800 }));\n *\n * export default async function Page({ params }) {\n * const xenia = await xeniaServerFromNext(config);\n * const base = await xenia.personalizePage(ref, { cms, maxRounds: 0 }); // CMS only, no Xenia call\n * const pending = new Set(pendingTargets(base.content, cms)); // from the markers\n * return base.content.fields.sections.map((s) =>\n * pending.has(s.sys.id) ? (\n * <Suspense key={s.sys.id} fallback={<SectionSkeleton />}> // never base content that will change\n * <PersonalizedSection id={s.sys.id} run={() => run(ref)} />\n * </Suspense>\n * ) : (\n * <Section key={s.sys.id} entry={s} />\n * ));\n * }\n * ```\n *\n * To finish deeper levels in the browser instead, pass `maxRounds: 1` here and hand the result's\n * `content` to `usePersonalizedContent` — already-decided entries are not asked about again.\n */\nexport function personalizeOnce<T>(key: string, personalize: () => Promise<T>): Promise<T> {\n const store = requestStore();\n let run = store.get(key) as Promise<T> | undefined;\n if (!run) {\n run = personalize();\n store.set(key, run);\n }\n return run;\n}\n"]}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { h as EntrySwapOp, g as EntryQuery, e as EntryDecision, p as ResolveOptions } from './types-DgVHS4Qw.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CMS page personalization — walk a fetched CMS page breadth-first, swap the
|
|
5
|
+
* entries Xenia marked as personalized, and do it in as few round trips as
|
|
6
|
+
* the page's nesting allows.
|
|
7
|
+
*
|
|
8
|
+
* Rules (see the "Rounds" section of the feature plan):
|
|
9
|
+
* - **Static** targets carry their replacement in the payload (`xeniaVariants`)
|
|
10
|
+
* and are swapped inline — no network at all.
|
|
11
|
+
* - **Runtime** targets are a barrier: their id is collected, their subtree is
|
|
12
|
+
* NOT walked (it may be replaced). One `resolve-entries` call decides every
|
|
13
|
+
* target found in a round, one parallel CMS fetch loads every replacement,
|
|
14
|
+
* and the next round walks whatever now sits at each position.
|
|
15
|
+
* - Never throws: on error or when `timeoutMs` runs out, whatever is not yet
|
|
16
|
+
* decided keeps its base content.
|
|
17
|
+
*
|
|
18
|
+
* CMS specifics live behind {@link CmsAdapter}; built-in adapters ship as
|
|
19
|
+
* `@xenia-martech/sdk-js/cms/contentful` (and later `/cms/contentstack`). Any
|
|
20
|
+
* other CMS works by implementing the interface.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Any value inside a CMS payload — the walker is shape-agnostic; adapters give it meaning. */
|
|
24
|
+
type Node = any;
|
|
25
|
+
/** The parsed `xenia` marker field of a personalized entry. */
|
|
26
|
+
interface XeniaMarker {
|
|
27
|
+
kind: "static" | "runtime";
|
|
28
|
+
op?: EntrySwapOp;
|
|
29
|
+
/** Array field replaced by `op: "items"`. */
|
|
30
|
+
field?: string;
|
|
31
|
+
limit?: number;
|
|
32
|
+
/**
|
|
33
|
+
* The content keys (Personalization Resolve identifiers) that personalize this entry. A page only
|
|
34
|
+
* acts on markers that list its `contentKey`; a marker without keys is ignored.
|
|
35
|
+
*/
|
|
36
|
+
keys?: string[];
|
|
37
|
+
}
|
|
38
|
+
/** What the page resolver needs to know about a CMS's entries. `N` is the entry shape. */
|
|
39
|
+
interface CmsAdapter<N extends object = Node> {
|
|
40
|
+
/** The entry id when `value` is a CMS entry (not a link stub, asset or plain object). */
|
|
41
|
+
entryId(value: unknown): string | undefined;
|
|
42
|
+
/** The entry's Xenia marker, or null when it is not personalized. */
|
|
43
|
+
marker(entry: N): XeniaMarker | null;
|
|
44
|
+
/** Static replacements pinned on the entry (its resolved `xeniaVariants`). */
|
|
45
|
+
staticVariants(entry: N): N[];
|
|
46
|
+
getField(entry: N, field: string): unknown;
|
|
47
|
+
setField(entry: N, field: string, value: unknown): void;
|
|
48
|
+
/** Legacy `items` without a field: copy the replacement's array fields onto the entry. */
|
|
49
|
+
mergeItems?(entry: N, replacement: N): void;
|
|
50
|
+
/**
|
|
51
|
+
* Remove the marker from an entry that has been dealt with (kept base, or had its items replaced),
|
|
52
|
+
* so personalizing the output again — e.g. the browser continuing a server render — never decides
|
|
53
|
+
* it twice. Optional; without it a re-run re-asks Xenia about those entries.
|
|
54
|
+
*/
|
|
55
|
+
clearMarker?(entry: N): void;
|
|
56
|
+
/** Normalize a fetched payload before walking (e.g. resolve links). Defaults to identity. */
|
|
57
|
+
prepare?(payload: unknown): unknown;
|
|
58
|
+
/** Load entries by id in ONE call; order does not matter. */
|
|
59
|
+
fetchEntries(ids: string[], locale?: string): Promise<N[]>;
|
|
60
|
+
/** Run a content-type query in ONE call, in the query's order. */
|
|
61
|
+
queryEntries(query: EntryQuery, locale?: string): Promise<N[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Load one page with its whole tree (links resolved), or null when there is none. Optional; it
|
|
64
|
+
* is what lets `personalizePage(ref)` do the fetch, so apps never fetch the page themselves.
|
|
65
|
+
*/
|
|
66
|
+
fetchPage?(ref: PageRef, locale?: string): Promise<N | null>;
|
|
67
|
+
}
|
|
68
|
+
/** Identifies a page in the CMS: its entry id, or a content type plus exact field values (a slug). */
|
|
69
|
+
type PageRef = string | {
|
|
70
|
+
contentType: string;
|
|
71
|
+
fields: Record<string, string | number | boolean>;
|
|
72
|
+
};
|
|
73
|
+
/** Resolves a round's runtime targets — `XeniaApi.resolveEntries` bound to the visitor. */
|
|
74
|
+
type EntryResolver = (entryIds: string[]) => Promise<EntryDecision[]>;
|
|
75
|
+
/** Reported for every swap actually applied (for impressions / analytics). */
|
|
76
|
+
interface AppliedSwap {
|
|
77
|
+
target: string;
|
|
78
|
+
kind: "static" | "runtime";
|
|
79
|
+
op: EntrySwapOp;
|
|
80
|
+
/** Ids of the entries now at the target's position (or in its field). */
|
|
81
|
+
entryIds: string[];
|
|
82
|
+
contentKey?: string;
|
|
83
|
+
bucket?: string;
|
|
84
|
+
round: number;
|
|
85
|
+
}
|
|
86
|
+
interface PersonalizeContentOptions {
|
|
87
|
+
cms: CmsAdapter;
|
|
88
|
+
/** Decides runtime targets. Omit to apply static swaps only (zero network). */
|
|
89
|
+
resolve?: EntryResolver;
|
|
90
|
+
locale?: string;
|
|
91
|
+
/**
|
|
92
|
+
* The page's Personalization Resolve identifier (required). Only that journey personalizes the
|
|
93
|
+
* page: a marker that doesn't list this key is plain content, and the backend decides for this key
|
|
94
|
+
* alone — another journey targeting the same section is neither consulted nor run.
|
|
95
|
+
*/
|
|
96
|
+
contentKey: string;
|
|
97
|
+
/** Max network rounds (nesting depth of runtime targets). Default 3. */
|
|
98
|
+
maxRounds?: number;
|
|
99
|
+
/** Total budget for all rounds, in ms. Undecided targets keep their base. Default 300. */
|
|
100
|
+
timeoutMs?: number;
|
|
101
|
+
/** Mutate the payload in place instead of personalizing a copy. Default false. */
|
|
102
|
+
inPlace?: boolean;
|
|
103
|
+
onApplied?(swap: AppliedSwap): void;
|
|
104
|
+
/**
|
|
105
|
+
* Called when a round starts waiting on the network (`pending` = targets being decided) and when
|
|
106
|
+
* it has been applied (`pending` empty). `content` is the live, partially personalized tree —
|
|
107
|
+
* lets a UI render level by level instead of waiting for every round.
|
|
108
|
+
*/
|
|
109
|
+
onProgress?(progress: {
|
|
110
|
+
content: unknown;
|
|
111
|
+
round: number;
|
|
112
|
+
pending: string[];
|
|
113
|
+
}): void;
|
|
114
|
+
}
|
|
115
|
+
/** `personalizeContent` options as taken by `XeniaClient` / `XeniaServer`, which supply `resolve`. */
|
|
116
|
+
type PagePersonalizeOptions = Omit<PersonalizeContentOptions, "resolve"> & Pick<ResolveOptions, "data" | "identity" | "context">;
|
|
117
|
+
interface PersonalizeContentResult<T> {
|
|
118
|
+
content: T;
|
|
119
|
+
applied: AppliedSwap[];
|
|
120
|
+
/** False when the budget ran out, a call failed, or `maxRounds` was hit with targets left. */
|
|
121
|
+
complete: boolean;
|
|
122
|
+
error?: unknown;
|
|
123
|
+
}
|
|
124
|
+
declare function personalizeContent<T>(payload: T, options: PersonalizeContentOptions): Promise<PersonalizeContentResult<T>>;
|
|
125
|
+
/**
|
|
126
|
+
* The entries Xenia still has to decide on this page — runtime targets reachable without crossing
|
|
127
|
+
* another undecided one. Synchronous and network-free (it reads the `xenia` markers), so a page can
|
|
128
|
+
* render a skeleton in exactly those places from the first frame instead of flashing base content.
|
|
129
|
+
* Nested targets inside a pending one are found once it is decided.
|
|
130
|
+
*/
|
|
131
|
+
declare function pendingTargets(payload: unknown, adapter: CmsAdapter, contentKey: string): string[];
|
|
132
|
+
/**
|
|
133
|
+
* Fetch a page through `options.cms` and personalize it — {@link personalizeContent} without the
|
|
134
|
+
* fetch in app code. Never throws: a failed fetch comes back as `content: null` with `error`; a page
|
|
135
|
+
* that does not exist as `content: null`, `complete: true`. The page fetch is not counted against
|
|
136
|
+
* `timeoutMs` — that budget is for personalization only.
|
|
137
|
+
*/
|
|
138
|
+
declare function personalizePage<T>(ref: PageRef, options: PersonalizeContentOptions): Promise<PersonalizeContentResult<T | null>>;
|
|
139
|
+
|
|
140
|
+
export { type AppliedSwap as A, type CmsAdapter as C, type EntryResolver as E, type PagePersonalizeOptions as P, type XeniaMarker as X, type PageRef as a, type PersonalizeContentOptions as b, type PersonalizeContentResult as c, personalizeContent as d, personalizePage as e, pendingTargets as p };
|