@coframe/sdk 1.1.14 → 1.1.21

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 CHANGED
@@ -40,13 +40,40 @@ await init({
40
40
 
41
41
  `init()` accepts `config` and optional `projectData`, so npm consumers do not need to manually populate `window.CFQ` first.
42
42
 
43
+ ### Runtime endpoint overrides
44
+
45
+ Private-CDN integrations can override the browser SDK's project and ingest
46
+ endpoints:
47
+
48
+ ```ts
49
+ await init({
50
+ config: {
51
+ projectId: 'your-project-id',
52
+ projectUrl: 'https://cdn.example.com/coframe/project.json',
53
+ ingestUrl: 'https://www.example.com/ingest/v2',
54
+ enableDebug: false,
55
+ },
56
+ })
57
+ ```
58
+
59
+ `projectUrl` must return a raw, complete `Project` JSON object. `ingestUrl`
60
+ replaces the ingest base URL (`https://ingest.app.coframe.com/ingest/v2`); the
61
+ SDK still appends the category and project path, so beacons go to
62
+ `<ingestUrl>/batched_events/<projectId>/`, `<ingestUrl>/batched_monitoring/<projectId>/`,
63
+ `<ingestUrl>/session_result/`, and `<ingestUrl>/identify/<projectId>/`. A proxy
64
+ only has to forward `<ingestUrl>/*` to `https://ingest.app.coframe.com/ingest/v2/*`
65
+ with the path unchanged. Both overrides are optional and retain the compiled
66
+ Coframe endpoints when omitted.
67
+
43
68
  ### URL controls
44
69
 
45
70
  The npm entry uses the same browser URL controls as the script-tag and Edge
46
71
  installations:
47
72
 
48
73
  - `cf_disable` skips initialization and reveals any antiflicker state.
49
- - `coframe_variant_id` previews one or more comma-separated variant IDs.
74
+ - `coframe_variant_id` previews one or more comma-separated variant IDs. Add
75
+ `rollouts` to the list to apply them on top of the rolled-out winners a
76
+ production visitor would get.
50
77
  - `coframe_should_send_events=true` allows events during preview for testing.
51
78
 
52
79
  Debug tools are allowed by default. Open the Launcher with `cf_preview=true` and
@@ -24,6 +24,19 @@ if (!Array.prototype.find) {
24
24
  enumerable: false,
25
25
  });
26
26
  }
27
+ if (!Array.prototype.includes) {
28
+ Object.defineProperty(Array.prototype, 'includes', {
29
+ value: function (searchElement) {
30
+ if (this == null) {
31
+ throw new TypeError('Array.prototype.includes called on null or undefined');
32
+ }
33
+ return Array.prototype.indexOf.call(this, searchElement) !== -1;
34
+ },
35
+ writable: true,
36
+ configurable: true,
37
+ enumerable: false,
38
+ });
39
+ }
27
40
  if (!Array.prototype.flatMap) {
28
41
  Object.defineProperty(Array.prototype, 'flatMap', {
29
42
  value: function (callback, thisArg) {
@@ -1066,10 +1079,41 @@ function warnEventsNotSent() {
1066
1079
  }
1067
1080
  }
1068
1081
 
1069
- const SDK_VERSION = '2.6.0';
1082
+ const SDK_VERSION = '2.7.0';
1070
1083
  const queuedEvents = [];
1071
1084
  const queuedMonitoringEvents = [];
1072
1085
  const queuedSessions = [];
1086
+ let ingestUrl;
1087
+ const COMPILED_INGEST_BASE = "https://ingest.app.coframe.com/ingest/v2";
1088
+ function configureIngestUrl(url) {
1089
+ const trimmed = url === null || url === void 0 ? void 0 : url.trim().replace(/\/+$/, '');
1090
+ ingestUrl = trimmed || undefined;
1091
+ }
1092
+ function resolveIngestUrl(defaultUrl) {
1093
+ if (!ingestUrl || !defaultUrl.startsWith(COMPILED_INGEST_BASE))
1094
+ return defaultUrl;
1095
+ return ingestUrl + defaultUrl.slice(COMPILED_INGEST_BASE.length);
1096
+ }
1097
+ const MAX_BEACON_BATCH_BYTES = 48 * 1024;
1098
+ function getBeaconPayloadBytes(data) {
1099
+ return new Blob([JSON.stringify(data)]).size;
1100
+ }
1101
+ function chunkEventsByByteLimit(events) {
1102
+ const chunks = [];
1103
+ let current = [];
1104
+ for (const event of events) {
1105
+ if (current.length > 0 &&
1106
+ getBeaconPayloadBytes([...current, event]) > MAX_BEACON_BATCH_BYTES) {
1107
+ chunks.push(current);
1108
+ current = [];
1109
+ }
1110
+ current.push(event);
1111
+ }
1112
+ if (current.length > 0) {
1113
+ chunks.push(current);
1114
+ }
1115
+ return chunks;
1116
+ }
1073
1117
  function queueMonitoringEvent(coframe, data) {
1074
1118
  queuedMonitoringEvents.push(data);
1075
1119
  if (queuedMonitoringEvents.length >= coframe.batchSize) {
@@ -1122,11 +1166,15 @@ function flushQueuedEvents(projectId) {
1122
1166
  return;
1123
1167
  }
1124
1168
  if (queuedEvents.length > 0) {
1125
- sendEventToBackend(projectId, queuedEvents, 'batched_events');
1169
+ for (const chunk of chunkEventsByByteLimit(queuedEvents)) {
1170
+ sendEventToBackend(projectId, chunk, 'batched_events');
1171
+ }
1126
1172
  queuedEvents.length = 0;
1127
1173
  }
1128
1174
  if (queuedMonitoringEvents.length > 0) {
1129
- sendEventToBackend(projectId, queuedMonitoringEvents, 'batched_monitoring');
1175
+ for (const chunk of chunkEventsByByteLimit(queuedMonitoringEvents)) {
1176
+ sendEventToBackend(projectId, chunk, 'batched_monitoring');
1177
+ }
1130
1178
  queuedMonitoringEvents.length = 0;
1131
1179
  }
1132
1180
  if (queuedSessions.length > 0) {
@@ -1228,7 +1276,7 @@ function sendPageEngagementEvent(coframe, eventType, event, { projectId, session
1228
1276
  }
1229
1277
  }
1230
1278
  function sendEventToBackend(projectId, data, eventCategory = 'events') {
1231
- const eventsEndpoint = `${"https://ingest.app.coframe.com/ingest/v2"}/${eventCategory}/${projectId}/`;
1279
+ const eventsEndpoint = resolveIngestUrl(`${"https://ingest.app.coframe.com/ingest/v2"}/${eventCategory}/${projectId}/`);
1232
1280
  if (navigator.sendBeacon) {
1233
1281
  const textData = JSON.stringify(data);
1234
1282
  const blob = new Blob([textData], { type: 'text/plain' });
@@ -1241,7 +1289,7 @@ function sendEventToBackend(projectId, data, eventCategory = 'events') {
1241
1289
  }
1242
1290
  }
1243
1291
  function createSession(data) {
1244
- const sessionApiUrl = `${"https://ingest.app.coframe.com/ingest/v2"}/session_result/`;
1292
+ const sessionApiUrl = resolveIngestUrl(`${"https://ingest.app.coframe.com/ingest/v2"}/session_result/`);
1245
1293
  if (navigator.sendBeacon) {
1246
1294
  const textData = JSON.stringify(data);
1247
1295
  const blob = new Blob([textData], { type: 'text/plain' });
@@ -1253,11 +1301,14 @@ function createSession(data) {
1253
1301
  console.warn('[Coframe] cannot send event beacon, feature unavailable');
1254
1302
  }
1255
1303
  }
1256
- async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers) {
1304
+ async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers, referrer) {
1257
1305
  const payload = { projectId, currentUrl };
1258
1306
  if (previewVariantId) {
1259
1307
  payload.previewVariantId = previewVariantId;
1260
1308
  }
1309
+ if (referrer !== undefined) {
1310
+ payload.referrer = referrer;
1311
+ }
1261
1312
  const safe = sanitizeUserContext(userContext);
1262
1313
  if (safe) {
1263
1314
  payload.userContext = safe;
@@ -1278,6 +1329,26 @@ async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext,
1278
1329
  return null;
1279
1330
  }
1280
1331
  }
1332
+ async function fetchProject(projectUrl, projectId, currentUrl, signal, userContext, previewVariantId, headers, referrer) {
1333
+ if (!projectUrl) {
1334
+ return fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers, referrer);
1335
+ }
1336
+ try {
1337
+ const response = await fetch(projectUrl, { headers, signal });
1338
+ if (!response.ok)
1339
+ return null;
1340
+ return {
1341
+ project: (await response.json()),
1342
+ personalizationHeaders: {},
1343
+ };
1344
+ }
1345
+ catch (error) {
1346
+ if (error instanceof DOMException && error.name === 'AbortError') {
1347
+ return null;
1348
+ }
1349
+ return null;
1350
+ }
1351
+ }
1281
1352
 
1282
1353
  const isPageElement = (element) => {
1283
1354
  return (element === document.body ||
@@ -2129,6 +2200,78 @@ function resolveLaneWinners(claims, reachableByLane, lanes, userToken) {
2129
2200
  return excluded;
2130
2201
  }
2131
2202
 
2203
+ var CoInspectorMessageType;
2204
+ (function (CoInspectorMessageType) {
2205
+ CoInspectorMessageType["onReady"] = "onReady";
2206
+ CoInspectorMessageType["onHoverElement"] = "onHoverElement";
2207
+ CoInspectorMessageType["onClickElement"] = "onClickElement";
2208
+ CoInspectorMessageType["onPageUrlChange"] = "onPageUrlChange";
2209
+ CoInspectorMessageType["onCreateNewElement"] = "onCreateNewElement";
2210
+ CoInspectorMessageType["onCancelNewElement"] = "onCancelNewElement";
2211
+ CoInspectorMessageType["onCopy"] = "onCopy";
2212
+ CoInspectorMessageType["onCloseDomInspectorPanel"] = "onCloseDomInspectorPanel";
2213
+ CoInspectorMessageType["onEditElement"] = "onEditElement";
2214
+ CoInspectorMessageType["onDeleteElement"] = "onDeleteElement";
2215
+ CoInspectorMessageType["onScroll"] = "onScroll";
2216
+ CoInspectorMessageType["setAllRects"] = "setAllRects";
2217
+ CoInspectorMessageType["onUpdateRects"] = "onUpdateRects";
2218
+ CoInspectorMessageType["DisableMouseTracking"] = "DisableMouseTracking";
2219
+ CoInspectorMessageType["EnableMouseTracking"] = "EnableMouseTracking";
2220
+ CoInspectorMessageType["onLeaveElement"] = "onLeaveElement";
2221
+ })(CoInspectorMessageType || (CoInspectorMessageType = {}));
2222
+ var CoMainSiteMessageType;
2223
+ (function (CoMainSiteMessageType) {
2224
+ CoMainSiteMessageType["ActiveInspect"] = "ActiveInspect";
2225
+ CoMainSiteMessageType["FreezeInspect"] = "FreezeInspect";
2226
+ CoMainSiteMessageType["SetElementChanges"] = "SetElementChanges";
2227
+ CoMainSiteMessageType["ShowDomInspector"] = "ShowDomInspector";
2228
+ CoMainSiteMessageType["RestoreOriginal"] = "RestoreOriginal";
2229
+ CoMainSiteMessageType["CreateNewElement"] = "CreateNewElement";
2230
+ CoMainSiteMessageType["ClickHoverElement"] = "ClickHoverElement";
2231
+ CoMainSiteMessageType["ReloadIframe"] = "ReloadIframe";
2232
+ })(CoMainSiteMessageType || (CoMainSiteMessageType = {}));
2233
+
2234
+ const COFRAME_PREVIEW_SESSION_STORAGE_KEY = 'coframe.preview_variant';
2235
+ const COFRAME_PREVIEW_VARIANT_QUERY_PARAM = 'coframe_variant_id';
2236
+ const ROLLOUTS_PREVIEW_ID = 'rollouts';
2237
+ function splitPreviewVariantIds(raw) {
2238
+ var _a;
2239
+ return ((_a = raw === null || raw === void 0 ? void 0 : raw.split(',').map((id) => id.trim()).filter(Boolean)) !== null && _a !== void 0 ? _a : []);
2240
+ }
2241
+ function isPreviewSentinel(id) {
2242
+ return id === GLOBAL_HOLDOUT || id === ROLLOUTS_PREVIEW_ID;
2243
+ }
2244
+ function normalizePreviewVariantId(raw) {
2245
+ const ids = splitPreviewVariantIds(raw);
2246
+ return ids.length > 0 ? ids.join(',') : null;
2247
+ }
2248
+ function readPreviewVariantIdFromUrl(url = window.location.href) {
2249
+ try {
2250
+ return normalizePreviewVariantId(getUrlSearchParams(url).get(COFRAME_PREVIEW_VARIANT_QUERY_PARAM));
2251
+ }
2252
+ catch (_a) {
2253
+ return null;
2254
+ }
2255
+ }
2256
+ function readStoredPreviewVariantId() {
2257
+ try {
2258
+ return normalizePreviewVariantId(window.sessionStorage.getItem(COFRAME_PREVIEW_SESSION_STORAGE_KEY));
2259
+ }
2260
+ catch (_a) {
2261
+ return null;
2262
+ }
2263
+ }
2264
+ function resolvePreviewSource(url = window.location.href) {
2265
+ const urlVariantId = readPreviewVariantIdFromUrl(url);
2266
+ const storedVariantId = readStoredPreviewVariantId();
2267
+ return {
2268
+ effectiveVariantId: urlVariantId || storedVariantId,
2269
+ isKeptForSession: Boolean(storedVariantId && storedVariantId === (urlVariantId || storedVariantId)),
2270
+ storedVariantId,
2271
+ urlVariantId,
2272
+ };
2273
+ }
2274
+
2132
2275
  function isElementVisible(el) {
2133
2276
  const rect = el.getBoundingClientRect();
2134
2277
  return rect.top >= 0 && rect.bottom <= window.innerHeight;
@@ -2168,11 +2311,11 @@ function checkActivationCondition(condition, personalizationHeaders, currentUrl,
2168
2311
  }
2169
2312
  if (condition.url &&
2170
2313
  condition.url !== '*' &&
2171
- !urlsMatch(condition.url, currentUrl)) {
2314
+ !urlConditionMatches(condition.url, currentUrl, false, logger)) {
2172
2315
  return false;
2173
2316
  }
2174
2317
  if (condition.base_url_matches &&
2175
- !urlsMatch(condition.base_url_matches, currentUrl, true)) {
2318
+ !urlConditionMatches(condition.base_url_matches, currentUrl, true, logger)) {
2176
2319
  return false;
2177
2320
  }
2178
2321
  if (condition.url_regex) {
@@ -2286,6 +2429,18 @@ const normalizeUrl = (url, baseUrlMatch) => {
2286
2429
  }
2287
2430
  return `${hostname}${pathname}${parsedUrl.search}${parsedUrl.hash}`;
2288
2431
  };
2432
+ function urlConditionMatches(conditionUrl, currentUrl, baseUrlMatch, logger) {
2433
+ try {
2434
+ return urlsMatch(conditionUrl, currentUrl, baseUrlMatch);
2435
+ }
2436
+ catch (_a) {
2437
+ logger.error('Error parsing URL condition', 'url_condition_error', {
2438
+ conditionUrl,
2439
+ currentUrl,
2440
+ });
2441
+ return false;
2442
+ }
2443
+ }
2289
2444
  function urlsMatch(conditionUrl, currentUrl, baseUrlMatch) {
2290
2445
  if (currentUrl.includes('localhost') || currentUrl.includes('0.0.0.0')) {
2291
2446
  const conditionUrlObj = new URL(conditionUrl);
@@ -2396,15 +2551,15 @@ const inRange = (bins, scale, seed, uid) => {
2396
2551
  }
2397
2552
  return false;
2398
2553
  };
2399
- function checkAllAudiences(metadata, project, personalizationHeaders, currentPageUrl, languages, browserPlatform, userToken, userVisitCount, nonce, logger) {
2554
+ function checkAllAudiences(metadata, ctx) {
2400
2555
  var _a;
2401
2556
  const audienceIds = (_a = metadata.activation_condition.audience_ids) !== null && _a !== void 0 ? _a : [];
2402
2557
  let result = true;
2403
2558
  for (const audienceId of audienceIds) {
2404
- const audience = project.audiences[audienceId];
2559
+ const audience = ctx.project.audiences[audienceId];
2405
2560
  if (!audience)
2406
2561
  continue;
2407
- const activated = checkActivationCondition(audience.selector, personalizationHeaders, currentPageUrl, languages, browserPlatform, userToken, userVisitCount, (js, args) => executeJs(js, args, { isExpression: true, nonce }), logger);
2562
+ const activated = checkActivationCondition(audience.selector, ctx.personalizationHeaders, ctx.currentPageUrl, ctx.languages, ctx.browserPlatform, ctx.userToken, ctx.userVisitCount, (js, args) => executeJs(js, args, { isExpression: true, nonce: ctx.nonce }), ctx.logger);
2408
2563
  if (activated === false)
2409
2564
  return false;
2410
2565
  if (activated !== true)
@@ -2413,9 +2568,8 @@ function checkAllAudiences(metadata, project, personalizationHeaders, currentPag
2413
2568
  return result;
2414
2569
  }
2415
2570
  async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}, silentFallback = false) {
2416
- var _a, _b;
2571
+ var _a;
2417
2572
  const experimentsInfo = [];
2418
- const variantsAndElements = [];
2419
2573
  const cloakBot = !!((_a = project.settings) === null || _a === void 0 ? void 0 : _a.bot_cloaking_enabled) &&
2420
2574
  personalizationHeaders['x-is-bot'] === 'true';
2421
2575
  const buildResponse = ({ variantsAndElements, experimentsInfo, hasActivatedFrames: frameActivated = false, laneClaims: meClaims = laneClaims, }) => {
@@ -2426,39 +2580,30 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2426
2580
  laneClaims: meClaims,
2427
2581
  };
2428
2582
  };
2429
- const previewVariantIds = (_b = previewVariantId === null || previewVariantId === void 0 ? void 0 : previewVariantId.split(',').map((id) => id.trim()).filter(Boolean)) !== null && _b !== void 0 ? _b : [];
2583
+ const userVisitCount = Number.parseInt(getCookie(VISIT_COUNT_COOKIE_KEY) || '0', 10);
2584
+ const ctx = {
2585
+ project,
2586
+ personalizationHeaders,
2587
+ userToken,
2588
+ currentPageUrl,
2589
+ languages,
2590
+ browserPlatform,
2591
+ userVisitCount,
2592
+ nonce,
2593
+ logger,
2594
+ };
2595
+ const validatedClaims = validateClaims(laneClaims, project.lanes);
2596
+ const previewVariantIds = splitPreviewVariantIds(previewVariantId);
2430
2597
  if (previewVariantIds.length > 0) {
2431
- const filteredVariantsAndElements = project.frames
2432
- .flatMap((frame) => frame.experiments.flatMap((experiment) => experiment.variants.map((variant) => ({
2433
- variant,
2434
- elements: frame.elements,
2435
- }))))
2436
- .filter((item) => previewVariantIds.includes(item.variant.id));
2437
- const skipAlert = previewVariantIds.some((id) => id === project.holdout_info.holdout_variant_id || id === GLOBAL_HOLDOUT);
2438
- if (filteredVariantsAndElements.length > 0) {
2598
+ const variantsAndElements = previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback);
2599
+ if (variantsAndElements) {
2439
2600
  return buildResponse({
2440
- variantsAndElements: filteredVariantsAndElements,
2441
- hasActivatedFrames: true,
2601
+ variantsAndElements,
2602
+ hasActivatedFrames: variantsAndElements.length > 0,
2442
2603
  });
2443
2604
  }
2444
- if (!skipAlert && !silentFallback) {
2445
- window.alert(`[Coframe] Variant preview unavailable: ${previewVariantId}.`);
2446
- }
2447
- if (silentFallback) ;
2448
- else {
2449
- return buildResponse({});
2450
- }
2451
2605
  }
2452
- const userVisitCount = Number.parseInt(getCookie(VISIT_COUNT_COOKIE_KEY) || '0', 10);
2453
- const validatedClaims = validateClaims(laneClaims, project.lanes);
2454
- const activeFrames = project.frames.filter((frame) => {
2455
- if (!frame.experiments)
2456
- return false;
2457
- const frameActivated = checkActivationCondition(frame.activation_condition, personalizationHeaders, currentPageUrl, languages, browserPlatform, userToken, userVisitCount, (js, args) => executeJs(js, args, { isExpression: true, nonce }), logger);
2458
- if (!frameActivated)
2459
- return false;
2460
- return true;
2461
- });
2606
+ const activeFrames = activateFrames(ctx);
2462
2607
  if (activeFrames.length === 0) {
2463
2608
  return buildResponse({ laneClaims: validatedClaims });
2464
2609
  }
@@ -2476,7 +2621,6 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2476
2621
  level: GLOBAL_HOLDOUT,
2477
2622
  });
2478
2623
  return buildResponse({
2479
- variantsAndElements,
2480
2624
  experimentsInfo,
2481
2625
  hasActivatedFrames: true,
2482
2626
  laneClaims: validatedClaims,
@@ -2491,7 +2635,27 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2491
2635
  final_state: false,
2492
2636
  level: GLOBAL_HOLDOUT,
2493
2637
  });
2494
- const evalAudiences = (metadata) => checkAllAudiences(metadata, project, personalizationHeaders, currentPageUrl, languages, browserPlatform, userToken, userVisitCount, nonce, logger) !== false;
2638
+ const assignment = assignFrames(activeFrames, validatedClaims, ctx);
2639
+ return buildResponse({
2640
+ ...assignment,
2641
+ experimentsInfo: [...experimentsInfo, ...assignment.experimentsInfo],
2642
+ });
2643
+ }
2644
+ function activateFrames(ctx) {
2645
+ return ctx.project.frames.filter((frame) => {
2646
+ if (!frame.experiments)
2647
+ return false;
2648
+ const frameActivated = checkActivationCondition(frame.activation_condition, ctx.personalizationHeaders, ctx.currentPageUrl, ctx.languages, ctx.browserPlatform, ctx.userToken, ctx.userVisitCount, (js, args) => executeJs(js, args, { isExpression: true, nonce: ctx.nonce }), ctx.logger);
2649
+ if (!frameActivated)
2650
+ return false;
2651
+ return true;
2652
+ });
2653
+ }
2654
+ function assignFrames(activeFrames, validatedClaims, ctx) {
2655
+ const { project, userToken, logger } = ctx;
2656
+ const experimentsInfo = [];
2657
+ const variantsAndElements = [];
2658
+ const evalAudiences = (metadata) => checkAllAudiences(metadata, ctx) !== false;
2495
2659
  const audiencePass = new Map();
2496
2660
  const reachableByLane = {};
2497
2661
  for (const frame of activeFrames) {
@@ -2584,50 +2748,49 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2584
2748
  variantsAndElements.push({ variant, elements });
2585
2749
  }
2586
2750
  }
2587
- return buildResponse({
2751
+ return {
2588
2752
  variantsAndElements,
2589
2753
  experimentsInfo,
2590
2754
  hasActivatedFrames,
2591
2755
  laneClaims: updatedClaims,
2592
- });
2756
+ };
2593
2757
  }
2594
-
2595
- const COFRAME_PREVIEW_SESSION_STORAGE_KEY = 'coframe.preview_variant';
2596
- const COFRAME_PREVIEW_VARIANT_QUERY_PARAM = 'coframe_variant_id';
2597
- function normalizePreviewVariantId(raw) {
2598
- if (!raw)
2758
+ function previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback) {
2759
+ const previewRollouts = previewVariantIds.includes(ROLLOUTS_PREVIEW_ID);
2760
+ const requestedVariantIds = previewVariantIds.filter((id) => !isPreviewSentinel(id));
2761
+ const requestedVariants = findVariants(ctx.project.frames, requestedVariantIds);
2762
+ const requestedVariantsMissing = requestedVariantIds.length > 0 && requestedVariants.length === 0;
2763
+ if (requestedVariantsMissing && !previewRollouts && silentFallback) {
2599
2764
  return null;
2600
- const ids = raw
2601
- .split(',')
2602
- .map((id) => id.trim())
2603
- .filter(Boolean);
2604
- return ids.length > 0 ? ids.join(',') : null;
2605
- }
2606
- function readPreviewVariantIdFromUrl(url = window.location.href) {
2607
- try {
2608
- return normalizePreviewVariantId(getUrlSearchParams(url).get(COFRAME_PREVIEW_VARIANT_QUERY_PARAM));
2609
2765
  }
2610
- catch (_a) {
2611
- return null;
2766
+ const requestedHoldout = requestedVariantIds.includes(ctx.project.holdout_info.holdout_variant_id);
2767
+ if (requestedVariantsMissing && !requestedHoldout && !silentFallback) {
2768
+ window.alert(`[Coframe] Variant preview unavailable: ${previewVariantIds.join(',')}.`);
2612
2769
  }
2770
+ const rolledOutVariants = previewRollouts && !previewVariantIds.includes(GLOBAL_HOLDOUT)
2771
+ ? assignFrames(keepRolledOutExperiments(activateFrames(ctx)), validatedClaims, ctx).variantsAndElements
2772
+ : [];
2773
+ return uniqueByVariantId([...rolledOutVariants, ...requestedVariants]);
2613
2774
  }
2614
- function readStoredPreviewVariantId() {
2615
- try {
2616
- return normalizePreviewVariantId(window.sessionStorage.getItem(COFRAME_PREVIEW_SESSION_STORAGE_KEY));
2617
- }
2618
- catch (_a) {
2619
- return null;
2620
- }
2775
+ function findVariants(frames, variantIds) {
2776
+ return frames.flatMap((frame) => frame.experiments.flatMap((experiment) => experiment.variants
2777
+ .filter((variant) => variantIds.includes(variant.id))
2778
+ .map((variant) => ({ variant, elements: frame.elements }))));
2621
2779
  }
2622
- function resolvePreviewSource(url = window.location.href) {
2623
- const urlVariantId = readPreviewVariantIdFromUrl(url);
2624
- const storedVariantId = readStoredPreviewVariantId();
2625
- return {
2626
- effectiveVariantId: urlVariantId || storedVariantId,
2627
- isKeptForSession: Boolean(storedVariantId && storedVariantId === (urlVariantId || storedVariantId)),
2628
- storedVariantId,
2629
- urlVariantId,
2630
- };
2780
+ function uniqueByVariantId(items) {
2781
+ const seen = {};
2782
+ return items.filter(({ variant }) => {
2783
+ if (seen[variant.id])
2784
+ return false;
2785
+ seen[variant.id] = true;
2786
+ return true;
2787
+ });
2788
+ }
2789
+ function keepRolledOutExperiments(frames) {
2790
+ return frames.map((frame) => ({
2791
+ ...frame,
2792
+ experiments: frame.experiments.filter((experiment) => experiment.metadata.serving_mode === 'winner'),
2793
+ }));
2631
2794
  }
2632
2795
 
2633
2796
  function buildSessionContext(isInitialApply) {
@@ -2913,29 +3076,27 @@ function createTreatmentAnalytics(Coframe, previewVariantId) {
2913
3076
  });
2914
3077
  }
2915
3078
  }
2916
- if (previewVariantId) {
2917
- const previewIds = previewVariantId.split(',');
2918
- for (const id of previewIds) {
2919
- treatmentsData.push({
2920
- impressionId: sessionId,
2921
- userToken,
2922
- experimentId: `preview_id_${id}`,
2923
- holdoutExperimentId: `preview_holdout_id_${id}`,
2924
- variantId: id,
2925
- inGlobalHoldout: false,
2926
- isBaseline: false,
2927
- });
2928
- treatmentsDataV2.push({
2929
- coframe_experiment_id: `preview_id_${id}`,
2930
- coframe_experiment_slug: `preview_id_${id}`,
2931
- coframe_variant_id: id,
2932
- coframe_variant_slug: id,
2933
- coframe_user_token: userToken,
2934
- coframe_impression_id: sessionId,
2935
- coframe_in_global_holdout: false,
2936
- coframe_in_exp_baseline: false,
2937
- });
2938
- }
3079
+ const previewIds = splitPreviewVariantIds(previewVariantId).filter((id) => !isPreviewSentinel(id));
3080
+ for (const id of previewIds) {
3081
+ treatmentsData.push({
3082
+ impressionId: sessionId,
3083
+ userToken,
3084
+ experimentId: `preview_id_${id}`,
3085
+ holdoutExperimentId: `preview_holdout_id_${id}`,
3086
+ variantId: id,
3087
+ inGlobalHoldout: false,
3088
+ isBaseline: false,
3089
+ });
3090
+ treatmentsDataV2.push({
3091
+ coframe_experiment_id: `preview_id_${id}`,
3092
+ coframe_experiment_slug: `preview_id_${id}`,
3093
+ coframe_variant_id: id,
3094
+ coframe_variant_slug: id,
3095
+ coframe_user_token: userToken,
3096
+ coframe_impression_id: sessionId,
3097
+ coframe_in_global_holdout: false,
3098
+ coframe_in_exp_baseline: false,
3099
+ });
2939
3100
  }
2940
3101
  Coframe.treatmentsData = treatmentsData;
2941
3102
  Coframe.treatmentsDataV2 = treatmentsDataV2;
@@ -3180,12 +3341,12 @@ async function ensureBody(Coframe, cdnProject, timeoutMs, nonce) {
3180
3341
  }
3181
3342
  const FLUSH_EVENTS_TIME = 1000 * 3;
3182
3343
  let flushInterval = null;
3183
- function setupFlushInterval(projectId) {
3184
- flushQueuedEvents(projectId);
3344
+ function setupFlushInterval(Coframe) {
3345
+ flushQueuedEvents(Coframe.projectId);
3185
3346
  if (flushInterval !== null) {
3186
3347
  clearInterval(flushInterval);
3187
3348
  }
3188
- flushInterval = setInterval(() => flushQueuedEvents(projectId), FLUSH_EVENTS_TIME);
3349
+ flushInterval = setInterval(() => flushQueuedEvents(Coframe.projectId), FLUSH_EVENTS_TIME);
3189
3350
  }
3190
3351
  function setupSPANavigation(Coframe, project, config, previewVariantId, isSessionVariant, applyVariants) {
3191
3352
  var _a;
@@ -3213,6 +3374,7 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
3213
3374
  observer.observe(document.body, { childList: true, subtree: true });
3214
3375
  let navigationAbortController = null;
3215
3376
  observeUrlChanges(false, async (newPageUrl, _oldPageUrl) => {
3377
+ var _a;
3216
3378
  await new Promise((resolve) => requestAnimationFrame(resolve));
3217
3379
  infiniteLoopProtection = 0;
3218
3380
  if (navigationAbortController)
@@ -3220,27 +3382,33 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
3220
3382
  navigationAbortController = new AbortController();
3221
3383
  const { signal } = navigationAbortController;
3222
3384
  let pageProject = project;
3385
+ let pagePersonalizationHeaders = Coframe.personalizationHeaders;
3223
3386
  const cached = getCachedFrames(newPageUrl);
3224
3387
  if (cached) {
3225
3388
  pageProject = cached.project;
3389
+ pagePersonalizationHeaders = {
3390
+ ...cached.personalizationHeaders,
3391
+ 'x-visit-url': newPageUrl,
3392
+ };
3226
3393
  }
3227
3394
  else {
3228
3395
  try {
3229
3396
  const preview = resolvePreview(newPageUrl, previewVariantId, isSessionVariant);
3230
- const result = await fetchProjectFromEdge(Coframe.projectId, newPageUrl, signal, config.userContext, preview.previewVariantId || undefined);
3397
+ const result = await fetchProject(config.projectUrl, Coframe.projectId, newPageUrl, signal, config.userContext, preview.previewVariantId || undefined, undefined, (_a = config.referrer) !== null && _a !== void 0 ? _a : getBrowserReferrer());
3231
3398
  if (signal.aborted)
3232
3399
  return;
3233
3400
  if (result) {
3234
3401
  pageProject = result.project;
3402
+ pagePersonalizationHeaders = result.personalizationHeaders;
3235
3403
  cacheFrames(newPageUrl, result);
3236
3404
  }
3237
3405
  }
3238
- catch (_a) {
3406
+ catch (_b) {
3239
3407
  }
3240
3408
  }
3241
3409
  if (signal.aborted)
3242
3410
  return;
3243
- await applyVariants(newPageUrl, pageProject);
3411
+ await applyVariants(newPageUrl, pageProject, pagePersonalizationHeaders);
3244
3412
  });
3245
3413
  }
3246
3414
  function readLaneClaims() {
@@ -3271,13 +3439,14 @@ function persistLaneClaims(claims, logger) {
3271
3439
  setCookie(LANE_COOKIE_KEY, encodeURIComponent(serialized), logger);
3272
3440
  }
3273
3441
  }
3274
- function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, personalizationHeaders, laneStore) {
3442
+ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore) {
3275
3443
  let isFirstApply = true;
3276
- return async (pageUrl, pageProject) => {
3444
+ return async (pageUrl, pageProject, personalizationHeaders) => {
3277
3445
  const sessionId = isFirstApply ? Coframe.sessionId : generateUUID();
3278
3446
  const sessionContext = buildSessionContext(isFirstApply);
3279
3447
  isFirstApply = false;
3280
3448
  Coframe.sessionId = sessionId;
3449
+ Coframe.personalizationHeaders = personalizationHeaders;
3281
3450
  const preview = resolvePreview(pageUrl, previewVariantId, isSessionVariant);
3282
3451
  setIsPreviewVariant(!!preview.previewVariantId);
3283
3452
  const { variantsAndElements, experimentsInfo, hasActivatedFrames, laneClaims, } = await getVariantsAndElements(personalizationHeaders, pageProject, preview.previewVariantId, Coframe.userToken, pageUrl, getBrowserLanguages(), getBrowserPlatform(), Coframe.logger, config.nonce, laneStore.claims, preview.isSessionVariant);
@@ -3314,8 +3483,8 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
3314
3483
  if (!(await ensureBody(Coframe, cdnProject, timeoutMs, config.nonce)))
3315
3484
  return;
3316
3485
  const laneStore = { claims: readLaneClaims() };
3317
- const applyVariants = createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, personalizationHeaders, laneStore);
3318
- await applyVariants(getCurrentPageUrl(), cdnProject);
3486
+ const applyVariants = createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore);
3487
+ await applyVariants(getCurrentPageUrl(), cdnProject, personalizationHeaders);
3319
3488
  if (!Coframe.variantsAndElements ||
3320
3489
  Coframe.variantsAndElements.length === 0) {
3321
3490
  revealPage();
@@ -3323,7 +3492,7 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
3323
3492
  }
3324
3493
  reportInitTiming(Coframe, cdnProject, integrationStartTime, executionStartTime);
3325
3494
  setupSPANavigation(Coframe, cdnProject, config, previewVariantId, isSessionVariant, applyVariants);
3326
- setupFlushInterval(Coframe.projectId);
3495
+ setupFlushInterval(Coframe);
3327
3496
  }
3328
3497
 
3329
3498
  const TIMEOUT_MAX_MS = 5000;
@@ -3359,6 +3528,7 @@ async function resolveConfig(Coframe, revealPage) {
3359
3528
  : null;
3360
3529
  const executionStartTime = new Date();
3361
3530
  Coframe.batchSize = config.batchSize || 32;
3531
+ configureIngestUrl(config.ingestUrl);
3362
3532
  const gracefulExit = (msg) => {
3363
3533
  console.warn(`[Coframe] ${msg}`);
3364
3534
  revealPage();
@@ -3480,13 +3650,13 @@ async function resolveIdentity(config, Coframe, timeoutMs, gracefulExit) {
3480
3650
  }
3481
3651
 
3482
3652
  async function resolveProject(Coframe, previewVariantId, isSessionVariant, config) {
3483
- var _a;
3653
+ var _a, _b;
3484
3654
  const logger = Coframe.logger;
3485
3655
  const projectData = (_a = findLastCmd((c) => !!c.projectData)) === null || _a === void 0 ? void 0 : _a.projectData;
3486
3656
  let personalizationHeaders = (projectData === null || projectData === void 0 ? void 0 : projectData.personalizationHeaders) || null;
3487
3657
  let project = isSessionVariant ? undefined : projectData === null || projectData === void 0 ? void 0 : projectData.project;
3488
3658
  if (!project) {
3489
- const edgeResult = await fetchProjectFromEdge(Coframe.projectId, config.currentUrl || getCurrentPageUrl(), undefined, config.userContext, previewVariantId || undefined);
3659
+ const edgeResult = await fetchProject(config.projectUrl, Coframe.projectId, config.currentUrl || getCurrentPageUrl(), undefined, config.userContext, previewVariantId || undefined, undefined, (_b = config.referrer) !== null && _b !== void 0 ? _b : getBrowserReferrer());
3490
3660
  personalizationHeaders = (edgeResult === null || edgeResult === void 0 ? void 0 : edgeResult.personalizationHeaders) || null;
3491
3661
  project = (edgeResult === null || edgeResult === void 0 ? void 0 : edgeResult.project) || undefined;
3492
3662
  }