@coframe/sdk 1.1.16 → 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) {
@@ -1070,6 +1083,17 @@ 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
+ }
1073
1097
  const MAX_BEACON_BATCH_BYTES = 48 * 1024;
1074
1098
  function getBeaconPayloadBytes(data) {
1075
1099
  return new Blob([JSON.stringify(data)]).size;
@@ -1252,7 +1276,7 @@ function sendPageEngagementEvent(coframe, eventType, event, { projectId, session
1252
1276
  }
1253
1277
  }
1254
1278
  function sendEventToBackend(projectId, data, eventCategory = 'events') {
1255
- const eventsEndpoint = `${"https://ingest.app.coframe.com/ingest/v2"}/${eventCategory}/${projectId}/`;
1279
+ const eventsEndpoint = resolveIngestUrl(`${"https://ingest.app.coframe.com/ingest/v2"}/${eventCategory}/${projectId}/`);
1256
1280
  if (navigator.sendBeacon) {
1257
1281
  const textData = JSON.stringify(data);
1258
1282
  const blob = new Blob([textData], { type: 'text/plain' });
@@ -1265,7 +1289,7 @@ function sendEventToBackend(projectId, data, eventCategory = 'events') {
1265
1289
  }
1266
1290
  }
1267
1291
  function createSession(data) {
1268
- const sessionApiUrl = `${"https://ingest.app.coframe.com/ingest/v2"}/session_result/`;
1292
+ const sessionApiUrl = resolveIngestUrl(`${"https://ingest.app.coframe.com/ingest/v2"}/session_result/`);
1269
1293
  if (navigator.sendBeacon) {
1270
1294
  const textData = JSON.stringify(data);
1271
1295
  const blob = new Blob([textData], { type: 'text/plain' });
@@ -1305,6 +1329,26 @@ async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext,
1305
1329
  return null;
1306
1330
  }
1307
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
+ }
1308
1352
 
1309
1353
  const isPageElement = (element) => {
1310
1354
  return (element === document.body ||
@@ -2156,6 +2200,78 @@ function resolveLaneWinners(claims, reachableByLane, lanes, userToken) {
2156
2200
  return excluded;
2157
2201
  }
2158
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
+
2159
2275
  function isElementVisible(el) {
2160
2276
  const rect = el.getBoundingClientRect();
2161
2277
  return rect.top >= 0 && rect.bottom <= window.innerHeight;
@@ -2195,11 +2311,11 @@ function checkActivationCondition(condition, personalizationHeaders, currentUrl,
2195
2311
  }
2196
2312
  if (condition.url &&
2197
2313
  condition.url !== '*' &&
2198
- !urlsMatch(condition.url, currentUrl)) {
2314
+ !urlConditionMatches(condition.url, currentUrl, false, logger)) {
2199
2315
  return false;
2200
2316
  }
2201
2317
  if (condition.base_url_matches &&
2202
- !urlsMatch(condition.base_url_matches, currentUrl, true)) {
2318
+ !urlConditionMatches(condition.base_url_matches, currentUrl, true, logger)) {
2203
2319
  return false;
2204
2320
  }
2205
2321
  if (condition.url_regex) {
@@ -2313,6 +2429,18 @@ const normalizeUrl = (url, baseUrlMatch) => {
2313
2429
  }
2314
2430
  return `${hostname}${pathname}${parsedUrl.search}${parsedUrl.hash}`;
2315
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
+ }
2316
2444
  function urlsMatch(conditionUrl, currentUrl, baseUrlMatch) {
2317
2445
  if (currentUrl.includes('localhost') || currentUrl.includes('0.0.0.0')) {
2318
2446
  const conditionUrlObj = new URL(conditionUrl);
@@ -2423,15 +2551,15 @@ const inRange = (bins, scale, seed, uid) => {
2423
2551
  }
2424
2552
  return false;
2425
2553
  };
2426
- function checkAllAudiences(metadata, project, personalizationHeaders, currentPageUrl, languages, browserPlatform, userToken, userVisitCount, nonce, logger) {
2554
+ function checkAllAudiences(metadata, ctx) {
2427
2555
  var _a;
2428
2556
  const audienceIds = (_a = metadata.activation_condition.audience_ids) !== null && _a !== void 0 ? _a : [];
2429
2557
  let result = true;
2430
2558
  for (const audienceId of audienceIds) {
2431
- const audience = project.audiences[audienceId];
2559
+ const audience = ctx.project.audiences[audienceId];
2432
2560
  if (!audience)
2433
2561
  continue;
2434
- 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);
2435
2563
  if (activated === false)
2436
2564
  return false;
2437
2565
  if (activated !== true)
@@ -2440,9 +2568,8 @@ function checkAllAudiences(metadata, project, personalizationHeaders, currentPag
2440
2568
  return result;
2441
2569
  }
2442
2570
  async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}, silentFallback = false) {
2443
- var _a, _b;
2571
+ var _a;
2444
2572
  const experimentsInfo = [];
2445
- const variantsAndElements = [];
2446
2573
  const cloakBot = !!((_a = project.settings) === null || _a === void 0 ? void 0 : _a.bot_cloaking_enabled) &&
2447
2574
  personalizationHeaders['x-is-bot'] === 'true';
2448
2575
  const buildResponse = ({ variantsAndElements, experimentsInfo, hasActivatedFrames: frameActivated = false, laneClaims: meClaims = laneClaims, }) => {
@@ -2453,39 +2580,30 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2453
2580
  laneClaims: meClaims,
2454
2581
  };
2455
2582
  };
2456
- 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);
2457
2597
  if (previewVariantIds.length > 0) {
2458
- const filteredVariantsAndElements = project.frames
2459
- .flatMap((frame) => frame.experiments.flatMap((experiment) => experiment.variants.map((variant) => ({
2460
- variant,
2461
- elements: frame.elements,
2462
- }))))
2463
- .filter((item) => previewVariantIds.includes(item.variant.id));
2464
- const skipAlert = previewVariantIds.some((id) => id === project.holdout_info.holdout_variant_id || id === GLOBAL_HOLDOUT);
2465
- if (filteredVariantsAndElements.length > 0) {
2598
+ const variantsAndElements = previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback);
2599
+ if (variantsAndElements) {
2466
2600
  return buildResponse({
2467
- variantsAndElements: filteredVariantsAndElements,
2468
- hasActivatedFrames: true,
2601
+ variantsAndElements,
2602
+ hasActivatedFrames: variantsAndElements.length > 0,
2469
2603
  });
2470
2604
  }
2471
- if (!skipAlert && !silentFallback) {
2472
- window.alert(`[Coframe] Variant preview unavailable: ${previewVariantId}.`);
2473
- }
2474
- if (silentFallback) ;
2475
- else {
2476
- return buildResponse({});
2477
- }
2478
2605
  }
2479
- const userVisitCount = Number.parseInt(getCookie(VISIT_COUNT_COOKIE_KEY) || '0', 10);
2480
- const validatedClaims = validateClaims(laneClaims, project.lanes);
2481
- const activeFrames = project.frames.filter((frame) => {
2482
- if (!frame.experiments)
2483
- return false;
2484
- const frameActivated = checkActivationCondition(frame.activation_condition, personalizationHeaders, currentPageUrl, languages, browserPlatform, userToken, userVisitCount, (js, args) => executeJs(js, args, { isExpression: true, nonce }), logger);
2485
- if (!frameActivated)
2486
- return false;
2487
- return true;
2488
- });
2606
+ const activeFrames = activateFrames(ctx);
2489
2607
  if (activeFrames.length === 0) {
2490
2608
  return buildResponse({ laneClaims: validatedClaims });
2491
2609
  }
@@ -2503,7 +2621,6 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2503
2621
  level: GLOBAL_HOLDOUT,
2504
2622
  });
2505
2623
  return buildResponse({
2506
- variantsAndElements,
2507
2624
  experimentsInfo,
2508
2625
  hasActivatedFrames: true,
2509
2626
  laneClaims: validatedClaims,
@@ -2518,7 +2635,27 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2518
2635
  final_state: false,
2519
2636
  level: GLOBAL_HOLDOUT,
2520
2637
  });
2521
- 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;
2522
2659
  const audiencePass = new Map();
2523
2660
  const reachableByLane = {};
2524
2661
  for (const frame of activeFrames) {
@@ -2611,50 +2748,49 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2611
2748
  variantsAndElements.push({ variant, elements });
2612
2749
  }
2613
2750
  }
2614
- return buildResponse({
2751
+ return {
2615
2752
  variantsAndElements,
2616
2753
  experimentsInfo,
2617
2754
  hasActivatedFrames,
2618
2755
  laneClaims: updatedClaims,
2619
- });
2756
+ };
2620
2757
  }
2621
-
2622
- const COFRAME_PREVIEW_SESSION_STORAGE_KEY = 'coframe.preview_variant';
2623
- const COFRAME_PREVIEW_VARIANT_QUERY_PARAM = 'coframe_variant_id';
2624
- function normalizePreviewVariantId(raw) {
2625
- 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) {
2626
2764
  return null;
2627
- const ids = raw
2628
- .split(',')
2629
- .map((id) => id.trim())
2630
- .filter(Boolean);
2631
- return ids.length > 0 ? ids.join(',') : null;
2632
- }
2633
- function readPreviewVariantIdFromUrl(url = window.location.href) {
2634
- try {
2635
- return normalizePreviewVariantId(getUrlSearchParams(url).get(COFRAME_PREVIEW_VARIANT_QUERY_PARAM));
2636
2765
  }
2637
- catch (_a) {
2638
- 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(',')}.`);
2639
2769
  }
2770
+ const rolledOutVariants = previewRollouts && !previewVariantIds.includes(GLOBAL_HOLDOUT)
2771
+ ? assignFrames(keepRolledOutExperiments(activateFrames(ctx)), validatedClaims, ctx).variantsAndElements
2772
+ : [];
2773
+ return uniqueByVariantId([...rolledOutVariants, ...requestedVariants]);
2640
2774
  }
2641
- function readStoredPreviewVariantId() {
2642
- try {
2643
- return normalizePreviewVariantId(window.sessionStorage.getItem(COFRAME_PREVIEW_SESSION_STORAGE_KEY));
2644
- }
2645
- catch (_a) {
2646
- return null;
2647
- }
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 }))));
2648
2779
  }
2649
- function resolvePreviewSource(url = window.location.href) {
2650
- const urlVariantId = readPreviewVariantIdFromUrl(url);
2651
- const storedVariantId = readStoredPreviewVariantId();
2652
- return {
2653
- effectiveVariantId: urlVariantId || storedVariantId,
2654
- isKeptForSession: Boolean(storedVariantId && storedVariantId === (urlVariantId || storedVariantId)),
2655
- storedVariantId,
2656
- urlVariantId,
2657
- };
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
+ }));
2658
2794
  }
2659
2795
 
2660
2796
  function buildSessionContext(isInitialApply) {
@@ -2940,29 +3076,27 @@ function createTreatmentAnalytics(Coframe, previewVariantId) {
2940
3076
  });
2941
3077
  }
2942
3078
  }
2943
- if (previewVariantId) {
2944
- const previewIds = previewVariantId.split(',');
2945
- for (const id of previewIds) {
2946
- treatmentsData.push({
2947
- impressionId: sessionId,
2948
- userToken,
2949
- experimentId: `preview_id_${id}`,
2950
- holdoutExperimentId: `preview_holdout_id_${id}`,
2951
- variantId: id,
2952
- inGlobalHoldout: false,
2953
- isBaseline: false,
2954
- });
2955
- treatmentsDataV2.push({
2956
- coframe_experiment_id: `preview_id_${id}`,
2957
- coframe_experiment_slug: `preview_id_${id}`,
2958
- coframe_variant_id: id,
2959
- coframe_variant_slug: id,
2960
- coframe_user_token: userToken,
2961
- coframe_impression_id: sessionId,
2962
- coframe_in_global_holdout: false,
2963
- coframe_in_exp_baseline: false,
2964
- });
2965
- }
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
+ });
2966
3100
  }
2967
3101
  Coframe.treatmentsData = treatmentsData;
2968
3102
  Coframe.treatmentsDataV2 = treatmentsDataV2;
@@ -3207,12 +3341,12 @@ async function ensureBody(Coframe, cdnProject, timeoutMs, nonce) {
3207
3341
  }
3208
3342
  const FLUSH_EVENTS_TIME = 1000 * 3;
3209
3343
  let flushInterval = null;
3210
- function setupFlushInterval(projectId) {
3211
- flushQueuedEvents(projectId);
3344
+ function setupFlushInterval(Coframe) {
3345
+ flushQueuedEvents(Coframe.projectId);
3212
3346
  if (flushInterval !== null) {
3213
3347
  clearInterval(flushInterval);
3214
3348
  }
3215
- flushInterval = setInterval(() => flushQueuedEvents(projectId), FLUSH_EVENTS_TIME);
3349
+ flushInterval = setInterval(() => flushQueuedEvents(Coframe.projectId), FLUSH_EVENTS_TIME);
3216
3350
  }
3217
3351
  function setupSPANavigation(Coframe, project, config, previewVariantId, isSessionVariant, applyVariants) {
3218
3352
  var _a;
@@ -3260,7 +3394,7 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
3260
3394
  else {
3261
3395
  try {
3262
3396
  const preview = resolvePreview(newPageUrl, previewVariantId, isSessionVariant);
3263
- const result = await fetchProjectFromEdge(Coframe.projectId, newPageUrl, signal, config.userContext, preview.previewVariantId || undefined, undefined, (_a = config.referrer) !== null && _a !== void 0 ? _a : getBrowserReferrer());
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());
3264
3398
  if (signal.aborted)
3265
3399
  return;
3266
3400
  if (result) {
@@ -3358,7 +3492,7 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
3358
3492
  }
3359
3493
  reportInitTiming(Coframe, cdnProject, integrationStartTime, executionStartTime);
3360
3494
  setupSPANavigation(Coframe, cdnProject, config, previewVariantId, isSessionVariant, applyVariants);
3361
- setupFlushInterval(Coframe.projectId);
3495
+ setupFlushInterval(Coframe);
3362
3496
  }
3363
3497
 
3364
3498
  const TIMEOUT_MAX_MS = 5000;
@@ -3394,6 +3528,7 @@ async function resolveConfig(Coframe, revealPage) {
3394
3528
  : null;
3395
3529
  const executionStartTime = new Date();
3396
3530
  Coframe.batchSize = config.batchSize || 32;
3531
+ configureIngestUrl(config.ingestUrl);
3397
3532
  const gracefulExit = (msg) => {
3398
3533
  console.warn(`[Coframe] ${msg}`);
3399
3534
  revealPage();
@@ -3521,7 +3656,7 @@ async function resolveProject(Coframe, previewVariantId, isSessionVariant, confi
3521
3656
  let personalizationHeaders = (projectData === null || projectData === void 0 ? void 0 : projectData.personalizationHeaders) || null;
3522
3657
  let project = isSessionVariant ? undefined : projectData === null || projectData === void 0 ? void 0 : projectData.project;
3523
3658
  if (!project) {
3524
- const edgeResult = await fetchProjectFromEdge(Coframe.projectId, config.currentUrl || getCurrentPageUrl(), undefined, config.userContext, previewVariantId || undefined, undefined, (_b = config.referrer) !== null && _b !== void 0 ? _b : getBrowserReferrer());
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());
3525
3660
  personalizationHeaders = (edgeResult === null || edgeResult === void 0 ? void 0 : edgeResult.personalizationHeaders) || null;
3526
3661
  project = (edgeResult === null || edgeResult === void 0 ? void 0 : edgeResult.project) || undefined;
3527
3662
  }
@@ -22,6 +22,19 @@ if (!Array.prototype.find) {
22
22
  enumerable: false,
23
23
  });
24
24
  }
25
+ if (!Array.prototype.includes) {
26
+ Object.defineProperty(Array.prototype, 'includes', {
27
+ value: function (searchElement) {
28
+ if (this == null) {
29
+ throw new TypeError('Array.prototype.includes called on null or undefined');
30
+ }
31
+ return Array.prototype.indexOf.call(this, searchElement) !== -1;
32
+ },
33
+ writable: true,
34
+ configurable: true,
35
+ enumerable: false,
36
+ });
37
+ }
25
38
  if (!Array.prototype.flatMap) {
26
39
  Object.defineProperty(Array.prototype, 'flatMap', {
27
40
  value: function (callback, thisArg) {
@@ -1068,6 +1081,17 @@ const SDK_VERSION = '2.7.0';
1068
1081
  const queuedEvents = [];
1069
1082
  const queuedMonitoringEvents = [];
1070
1083
  const queuedSessions = [];
1084
+ let ingestUrl;
1085
+ const COMPILED_INGEST_BASE = "https://ingest.app.coframe.com/ingest/v2";
1086
+ function configureIngestUrl(url) {
1087
+ const trimmed = url === null || url === void 0 ? void 0 : url.trim().replace(/\/+$/, '');
1088
+ ingestUrl = trimmed || undefined;
1089
+ }
1090
+ function resolveIngestUrl(defaultUrl) {
1091
+ if (!ingestUrl || !defaultUrl.startsWith(COMPILED_INGEST_BASE))
1092
+ return defaultUrl;
1093
+ return ingestUrl + defaultUrl.slice(COMPILED_INGEST_BASE.length);
1094
+ }
1071
1095
  const MAX_BEACON_BATCH_BYTES = 48 * 1024;
1072
1096
  function getBeaconPayloadBytes(data) {
1073
1097
  return new Blob([JSON.stringify(data)]).size;
@@ -1250,7 +1274,7 @@ function sendPageEngagementEvent(coframe, eventType, event, { projectId, session
1250
1274
  }
1251
1275
  }
1252
1276
  function sendEventToBackend(projectId, data, eventCategory = 'events') {
1253
- const eventsEndpoint = `${"https://ingest.app.coframe.com/ingest/v2"}/${eventCategory}/${projectId}/`;
1277
+ const eventsEndpoint = resolveIngestUrl(`${"https://ingest.app.coframe.com/ingest/v2"}/${eventCategory}/${projectId}/`);
1254
1278
  if (navigator.sendBeacon) {
1255
1279
  const textData = JSON.stringify(data);
1256
1280
  const blob = new Blob([textData], { type: 'text/plain' });
@@ -1263,7 +1287,7 @@ function sendEventToBackend(projectId, data, eventCategory = 'events') {
1263
1287
  }
1264
1288
  }
1265
1289
  function createSession(data) {
1266
- const sessionApiUrl = `${"https://ingest.app.coframe.com/ingest/v2"}/session_result/`;
1290
+ const sessionApiUrl = resolveIngestUrl(`${"https://ingest.app.coframe.com/ingest/v2"}/session_result/`);
1267
1291
  if (navigator.sendBeacon) {
1268
1292
  const textData = JSON.stringify(data);
1269
1293
  const blob = new Blob([textData], { type: 'text/plain' });
@@ -1303,6 +1327,26 @@ async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext,
1303
1327
  return null;
1304
1328
  }
1305
1329
  }
1330
+ async function fetchProject(projectUrl, projectId, currentUrl, signal, userContext, previewVariantId, headers, referrer) {
1331
+ if (!projectUrl) {
1332
+ return fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers, referrer);
1333
+ }
1334
+ try {
1335
+ const response = await fetch(projectUrl, { headers, signal });
1336
+ if (!response.ok)
1337
+ return null;
1338
+ return {
1339
+ project: (await response.json()),
1340
+ personalizationHeaders: {},
1341
+ };
1342
+ }
1343
+ catch (error) {
1344
+ if (error instanceof DOMException && error.name === 'AbortError') {
1345
+ return null;
1346
+ }
1347
+ return null;
1348
+ }
1349
+ }
1306
1350
 
1307
1351
  const isPageElement = (element) => {
1308
1352
  return (element === document.body ||
@@ -2154,6 +2198,78 @@ function resolveLaneWinners(claims, reachableByLane, lanes, userToken) {
2154
2198
  return excluded;
2155
2199
  }
2156
2200
 
2201
+ var CoInspectorMessageType;
2202
+ (function (CoInspectorMessageType) {
2203
+ CoInspectorMessageType["onReady"] = "onReady";
2204
+ CoInspectorMessageType["onHoverElement"] = "onHoverElement";
2205
+ CoInspectorMessageType["onClickElement"] = "onClickElement";
2206
+ CoInspectorMessageType["onPageUrlChange"] = "onPageUrlChange";
2207
+ CoInspectorMessageType["onCreateNewElement"] = "onCreateNewElement";
2208
+ CoInspectorMessageType["onCancelNewElement"] = "onCancelNewElement";
2209
+ CoInspectorMessageType["onCopy"] = "onCopy";
2210
+ CoInspectorMessageType["onCloseDomInspectorPanel"] = "onCloseDomInspectorPanel";
2211
+ CoInspectorMessageType["onEditElement"] = "onEditElement";
2212
+ CoInspectorMessageType["onDeleteElement"] = "onDeleteElement";
2213
+ CoInspectorMessageType["onScroll"] = "onScroll";
2214
+ CoInspectorMessageType["setAllRects"] = "setAllRects";
2215
+ CoInspectorMessageType["onUpdateRects"] = "onUpdateRects";
2216
+ CoInspectorMessageType["DisableMouseTracking"] = "DisableMouseTracking";
2217
+ CoInspectorMessageType["EnableMouseTracking"] = "EnableMouseTracking";
2218
+ CoInspectorMessageType["onLeaveElement"] = "onLeaveElement";
2219
+ })(CoInspectorMessageType || (CoInspectorMessageType = {}));
2220
+ var CoMainSiteMessageType;
2221
+ (function (CoMainSiteMessageType) {
2222
+ CoMainSiteMessageType["ActiveInspect"] = "ActiveInspect";
2223
+ CoMainSiteMessageType["FreezeInspect"] = "FreezeInspect";
2224
+ CoMainSiteMessageType["SetElementChanges"] = "SetElementChanges";
2225
+ CoMainSiteMessageType["ShowDomInspector"] = "ShowDomInspector";
2226
+ CoMainSiteMessageType["RestoreOriginal"] = "RestoreOriginal";
2227
+ CoMainSiteMessageType["CreateNewElement"] = "CreateNewElement";
2228
+ CoMainSiteMessageType["ClickHoverElement"] = "ClickHoverElement";
2229
+ CoMainSiteMessageType["ReloadIframe"] = "ReloadIframe";
2230
+ })(CoMainSiteMessageType || (CoMainSiteMessageType = {}));
2231
+
2232
+ const COFRAME_PREVIEW_SESSION_STORAGE_KEY = 'coframe.preview_variant';
2233
+ const COFRAME_PREVIEW_VARIANT_QUERY_PARAM = 'coframe_variant_id';
2234
+ const ROLLOUTS_PREVIEW_ID = 'rollouts';
2235
+ function splitPreviewVariantIds(raw) {
2236
+ var _a;
2237
+ return ((_a = raw === null || raw === void 0 ? void 0 : raw.split(',').map((id) => id.trim()).filter(Boolean)) !== null && _a !== void 0 ? _a : []);
2238
+ }
2239
+ function isPreviewSentinel(id) {
2240
+ return id === GLOBAL_HOLDOUT || id === ROLLOUTS_PREVIEW_ID;
2241
+ }
2242
+ function normalizePreviewVariantId(raw) {
2243
+ const ids = splitPreviewVariantIds(raw);
2244
+ return ids.length > 0 ? ids.join(',') : null;
2245
+ }
2246
+ function readPreviewVariantIdFromUrl(url = window.location.href) {
2247
+ try {
2248
+ return normalizePreviewVariantId(getUrlSearchParams(url).get(COFRAME_PREVIEW_VARIANT_QUERY_PARAM));
2249
+ }
2250
+ catch (_a) {
2251
+ return null;
2252
+ }
2253
+ }
2254
+ function readStoredPreviewVariantId() {
2255
+ try {
2256
+ return normalizePreviewVariantId(window.sessionStorage.getItem(COFRAME_PREVIEW_SESSION_STORAGE_KEY));
2257
+ }
2258
+ catch (_a) {
2259
+ return null;
2260
+ }
2261
+ }
2262
+ function resolvePreviewSource(url = window.location.href) {
2263
+ const urlVariantId = readPreviewVariantIdFromUrl(url);
2264
+ const storedVariantId = readStoredPreviewVariantId();
2265
+ return {
2266
+ effectiveVariantId: urlVariantId || storedVariantId,
2267
+ isKeptForSession: Boolean(storedVariantId && storedVariantId === (urlVariantId || storedVariantId)),
2268
+ storedVariantId,
2269
+ urlVariantId,
2270
+ };
2271
+ }
2272
+
2157
2273
  function isElementVisible(el) {
2158
2274
  const rect = el.getBoundingClientRect();
2159
2275
  return rect.top >= 0 && rect.bottom <= window.innerHeight;
@@ -2193,11 +2309,11 @@ function checkActivationCondition(condition, personalizationHeaders, currentUrl,
2193
2309
  }
2194
2310
  if (condition.url &&
2195
2311
  condition.url !== '*' &&
2196
- !urlsMatch(condition.url, currentUrl)) {
2312
+ !urlConditionMatches(condition.url, currentUrl, false, logger)) {
2197
2313
  return false;
2198
2314
  }
2199
2315
  if (condition.base_url_matches &&
2200
- !urlsMatch(condition.base_url_matches, currentUrl, true)) {
2316
+ !urlConditionMatches(condition.base_url_matches, currentUrl, true, logger)) {
2201
2317
  return false;
2202
2318
  }
2203
2319
  if (condition.url_regex) {
@@ -2311,6 +2427,18 @@ const normalizeUrl = (url, baseUrlMatch) => {
2311
2427
  }
2312
2428
  return `${hostname}${pathname}${parsedUrl.search}${parsedUrl.hash}`;
2313
2429
  };
2430
+ function urlConditionMatches(conditionUrl, currentUrl, baseUrlMatch, logger) {
2431
+ try {
2432
+ return urlsMatch(conditionUrl, currentUrl, baseUrlMatch);
2433
+ }
2434
+ catch (_a) {
2435
+ logger.error('Error parsing URL condition', 'url_condition_error', {
2436
+ conditionUrl,
2437
+ currentUrl,
2438
+ });
2439
+ return false;
2440
+ }
2441
+ }
2314
2442
  function urlsMatch(conditionUrl, currentUrl, baseUrlMatch) {
2315
2443
  if (currentUrl.includes('localhost') || currentUrl.includes('0.0.0.0')) {
2316
2444
  const conditionUrlObj = new URL(conditionUrl);
@@ -2421,15 +2549,15 @@ const inRange = (bins, scale, seed, uid) => {
2421
2549
  }
2422
2550
  return false;
2423
2551
  };
2424
- function checkAllAudiences(metadata, project, personalizationHeaders, currentPageUrl, languages, browserPlatform, userToken, userVisitCount, nonce, logger) {
2552
+ function checkAllAudiences(metadata, ctx) {
2425
2553
  var _a;
2426
2554
  const audienceIds = (_a = metadata.activation_condition.audience_ids) !== null && _a !== void 0 ? _a : [];
2427
2555
  let result = true;
2428
2556
  for (const audienceId of audienceIds) {
2429
- const audience = project.audiences[audienceId];
2557
+ const audience = ctx.project.audiences[audienceId];
2430
2558
  if (!audience)
2431
2559
  continue;
2432
- const activated = checkActivationCondition(audience.selector, personalizationHeaders, currentPageUrl, languages, browserPlatform, userToken, userVisitCount, (js, args) => executeJs(js, args, { isExpression: true, nonce }), logger);
2560
+ 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);
2433
2561
  if (activated === false)
2434
2562
  return false;
2435
2563
  if (activated !== true)
@@ -2438,9 +2566,8 @@ function checkAllAudiences(metadata, project, personalizationHeaders, currentPag
2438
2566
  return result;
2439
2567
  }
2440
2568
  async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}, silentFallback = false) {
2441
- var _a, _b;
2569
+ var _a;
2442
2570
  const experimentsInfo = [];
2443
- const variantsAndElements = [];
2444
2571
  const cloakBot = !!((_a = project.settings) === null || _a === void 0 ? void 0 : _a.bot_cloaking_enabled) &&
2445
2572
  personalizationHeaders['x-is-bot'] === 'true';
2446
2573
  const buildResponse = ({ variantsAndElements, experimentsInfo, hasActivatedFrames: frameActivated = false, laneClaims: meClaims = laneClaims, }) => {
@@ -2451,39 +2578,30 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2451
2578
  laneClaims: meClaims,
2452
2579
  };
2453
2580
  };
2454
- const previewVariantIds = (_b = previewVariantId === null || previewVariantId === void 0 ? void 0 : previewVariantId.split(',').map((id) => id.trim()).filter(Boolean)) !== null && _b !== void 0 ? _b : [];
2581
+ const userVisitCount = Number.parseInt(getCookie(VISIT_COUNT_COOKIE_KEY) || '0', 10);
2582
+ const ctx = {
2583
+ project,
2584
+ personalizationHeaders,
2585
+ userToken,
2586
+ currentPageUrl,
2587
+ languages,
2588
+ browserPlatform,
2589
+ userVisitCount,
2590
+ nonce,
2591
+ logger,
2592
+ };
2593
+ const validatedClaims = validateClaims(laneClaims, project.lanes);
2594
+ const previewVariantIds = splitPreviewVariantIds(previewVariantId);
2455
2595
  if (previewVariantIds.length > 0) {
2456
- const filteredVariantsAndElements = project.frames
2457
- .flatMap((frame) => frame.experiments.flatMap((experiment) => experiment.variants.map((variant) => ({
2458
- variant,
2459
- elements: frame.elements,
2460
- }))))
2461
- .filter((item) => previewVariantIds.includes(item.variant.id));
2462
- const skipAlert = previewVariantIds.some((id) => id === project.holdout_info.holdout_variant_id || id === GLOBAL_HOLDOUT);
2463
- if (filteredVariantsAndElements.length > 0) {
2596
+ const variantsAndElements = previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback);
2597
+ if (variantsAndElements) {
2464
2598
  return buildResponse({
2465
- variantsAndElements: filteredVariantsAndElements,
2466
- hasActivatedFrames: true,
2599
+ variantsAndElements,
2600
+ hasActivatedFrames: variantsAndElements.length > 0,
2467
2601
  });
2468
2602
  }
2469
- if (!skipAlert && !silentFallback) {
2470
- window.alert(`[Coframe] Variant preview unavailable: ${previewVariantId}.`);
2471
- }
2472
- if (silentFallback) ;
2473
- else {
2474
- return buildResponse({});
2475
- }
2476
2603
  }
2477
- const userVisitCount = Number.parseInt(getCookie(VISIT_COUNT_COOKIE_KEY) || '0', 10);
2478
- const validatedClaims = validateClaims(laneClaims, project.lanes);
2479
- const activeFrames = project.frames.filter((frame) => {
2480
- if (!frame.experiments)
2481
- return false;
2482
- const frameActivated = checkActivationCondition(frame.activation_condition, personalizationHeaders, currentPageUrl, languages, browserPlatform, userToken, userVisitCount, (js, args) => executeJs(js, args, { isExpression: true, nonce }), logger);
2483
- if (!frameActivated)
2484
- return false;
2485
- return true;
2486
- });
2604
+ const activeFrames = activateFrames(ctx);
2487
2605
  if (activeFrames.length === 0) {
2488
2606
  return buildResponse({ laneClaims: validatedClaims });
2489
2607
  }
@@ -2501,7 +2619,6 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2501
2619
  level: GLOBAL_HOLDOUT,
2502
2620
  });
2503
2621
  return buildResponse({
2504
- variantsAndElements,
2505
2622
  experimentsInfo,
2506
2623
  hasActivatedFrames: true,
2507
2624
  laneClaims: validatedClaims,
@@ -2516,7 +2633,27 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2516
2633
  final_state: false,
2517
2634
  level: GLOBAL_HOLDOUT,
2518
2635
  });
2519
- const evalAudiences = (metadata) => checkAllAudiences(metadata, project, personalizationHeaders, currentPageUrl, languages, browserPlatform, userToken, userVisitCount, nonce, logger) !== false;
2636
+ const assignment = assignFrames(activeFrames, validatedClaims, ctx);
2637
+ return buildResponse({
2638
+ ...assignment,
2639
+ experimentsInfo: [...experimentsInfo, ...assignment.experimentsInfo],
2640
+ });
2641
+ }
2642
+ function activateFrames(ctx) {
2643
+ return ctx.project.frames.filter((frame) => {
2644
+ if (!frame.experiments)
2645
+ return false;
2646
+ 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);
2647
+ if (!frameActivated)
2648
+ return false;
2649
+ return true;
2650
+ });
2651
+ }
2652
+ function assignFrames(activeFrames, validatedClaims, ctx) {
2653
+ const { project, userToken, logger } = ctx;
2654
+ const experimentsInfo = [];
2655
+ const variantsAndElements = [];
2656
+ const evalAudiences = (metadata) => checkAllAudiences(metadata, ctx) !== false;
2520
2657
  const audiencePass = new Map();
2521
2658
  const reachableByLane = {};
2522
2659
  for (const frame of activeFrames) {
@@ -2609,50 +2746,49 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2609
2746
  variantsAndElements.push({ variant, elements });
2610
2747
  }
2611
2748
  }
2612
- return buildResponse({
2749
+ return {
2613
2750
  variantsAndElements,
2614
2751
  experimentsInfo,
2615
2752
  hasActivatedFrames,
2616
2753
  laneClaims: updatedClaims,
2617
- });
2754
+ };
2618
2755
  }
2619
-
2620
- const COFRAME_PREVIEW_SESSION_STORAGE_KEY = 'coframe.preview_variant';
2621
- const COFRAME_PREVIEW_VARIANT_QUERY_PARAM = 'coframe_variant_id';
2622
- function normalizePreviewVariantId(raw) {
2623
- if (!raw)
2756
+ function previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback) {
2757
+ const previewRollouts = previewVariantIds.includes(ROLLOUTS_PREVIEW_ID);
2758
+ const requestedVariantIds = previewVariantIds.filter((id) => !isPreviewSentinel(id));
2759
+ const requestedVariants = findVariants(ctx.project.frames, requestedVariantIds);
2760
+ const requestedVariantsMissing = requestedVariantIds.length > 0 && requestedVariants.length === 0;
2761
+ if (requestedVariantsMissing && !previewRollouts && silentFallback) {
2624
2762
  return null;
2625
- const ids = raw
2626
- .split(',')
2627
- .map((id) => id.trim())
2628
- .filter(Boolean);
2629
- return ids.length > 0 ? ids.join(',') : null;
2630
- }
2631
- function readPreviewVariantIdFromUrl(url = window.location.href) {
2632
- try {
2633
- return normalizePreviewVariantId(getUrlSearchParams(url).get(COFRAME_PREVIEW_VARIANT_QUERY_PARAM));
2634
2763
  }
2635
- catch (_a) {
2636
- return null;
2764
+ const requestedHoldout = requestedVariantIds.includes(ctx.project.holdout_info.holdout_variant_id);
2765
+ if (requestedVariantsMissing && !requestedHoldout && !silentFallback) {
2766
+ window.alert(`[Coframe] Variant preview unavailable: ${previewVariantIds.join(',')}.`);
2637
2767
  }
2768
+ const rolledOutVariants = previewRollouts && !previewVariantIds.includes(GLOBAL_HOLDOUT)
2769
+ ? assignFrames(keepRolledOutExperiments(activateFrames(ctx)), validatedClaims, ctx).variantsAndElements
2770
+ : [];
2771
+ return uniqueByVariantId([...rolledOutVariants, ...requestedVariants]);
2638
2772
  }
2639
- function readStoredPreviewVariantId() {
2640
- try {
2641
- return normalizePreviewVariantId(window.sessionStorage.getItem(COFRAME_PREVIEW_SESSION_STORAGE_KEY));
2642
- }
2643
- catch (_a) {
2644
- return null;
2645
- }
2773
+ function findVariants(frames, variantIds) {
2774
+ return frames.flatMap((frame) => frame.experiments.flatMap((experiment) => experiment.variants
2775
+ .filter((variant) => variantIds.includes(variant.id))
2776
+ .map((variant) => ({ variant, elements: frame.elements }))));
2646
2777
  }
2647
- function resolvePreviewSource(url = window.location.href) {
2648
- const urlVariantId = readPreviewVariantIdFromUrl(url);
2649
- const storedVariantId = readStoredPreviewVariantId();
2650
- return {
2651
- effectiveVariantId: urlVariantId || storedVariantId,
2652
- isKeptForSession: Boolean(storedVariantId && storedVariantId === (urlVariantId || storedVariantId)),
2653
- storedVariantId,
2654
- urlVariantId,
2655
- };
2778
+ function uniqueByVariantId(items) {
2779
+ const seen = {};
2780
+ return items.filter(({ variant }) => {
2781
+ if (seen[variant.id])
2782
+ return false;
2783
+ seen[variant.id] = true;
2784
+ return true;
2785
+ });
2786
+ }
2787
+ function keepRolledOutExperiments(frames) {
2788
+ return frames.map((frame) => ({
2789
+ ...frame,
2790
+ experiments: frame.experiments.filter((experiment) => experiment.metadata.serving_mode === 'winner'),
2791
+ }));
2656
2792
  }
2657
2793
 
2658
2794
  function buildSessionContext(isInitialApply) {
@@ -2938,29 +3074,27 @@ function createTreatmentAnalytics(Coframe, previewVariantId) {
2938
3074
  });
2939
3075
  }
2940
3076
  }
2941
- if (previewVariantId) {
2942
- const previewIds = previewVariantId.split(',');
2943
- for (const id of previewIds) {
2944
- treatmentsData.push({
2945
- impressionId: sessionId,
2946
- userToken,
2947
- experimentId: `preview_id_${id}`,
2948
- holdoutExperimentId: `preview_holdout_id_${id}`,
2949
- variantId: id,
2950
- inGlobalHoldout: false,
2951
- isBaseline: false,
2952
- });
2953
- treatmentsDataV2.push({
2954
- coframe_experiment_id: `preview_id_${id}`,
2955
- coframe_experiment_slug: `preview_id_${id}`,
2956
- coframe_variant_id: id,
2957
- coframe_variant_slug: id,
2958
- coframe_user_token: userToken,
2959
- coframe_impression_id: sessionId,
2960
- coframe_in_global_holdout: false,
2961
- coframe_in_exp_baseline: false,
2962
- });
2963
- }
3077
+ const previewIds = splitPreviewVariantIds(previewVariantId).filter((id) => !isPreviewSentinel(id));
3078
+ for (const id of previewIds) {
3079
+ treatmentsData.push({
3080
+ impressionId: sessionId,
3081
+ userToken,
3082
+ experimentId: `preview_id_${id}`,
3083
+ holdoutExperimentId: `preview_holdout_id_${id}`,
3084
+ variantId: id,
3085
+ inGlobalHoldout: false,
3086
+ isBaseline: false,
3087
+ });
3088
+ treatmentsDataV2.push({
3089
+ coframe_experiment_id: `preview_id_${id}`,
3090
+ coframe_experiment_slug: `preview_id_${id}`,
3091
+ coframe_variant_id: id,
3092
+ coframe_variant_slug: id,
3093
+ coframe_user_token: userToken,
3094
+ coframe_impression_id: sessionId,
3095
+ coframe_in_global_holdout: false,
3096
+ coframe_in_exp_baseline: false,
3097
+ });
2964
3098
  }
2965
3099
  Coframe.treatmentsData = treatmentsData;
2966
3100
  Coframe.treatmentsDataV2 = treatmentsDataV2;
@@ -3205,12 +3339,12 @@ async function ensureBody(Coframe, cdnProject, timeoutMs, nonce) {
3205
3339
  }
3206
3340
  const FLUSH_EVENTS_TIME = 1000 * 3;
3207
3341
  let flushInterval = null;
3208
- function setupFlushInterval(projectId) {
3209
- flushQueuedEvents(projectId);
3342
+ function setupFlushInterval(Coframe) {
3343
+ flushQueuedEvents(Coframe.projectId);
3210
3344
  if (flushInterval !== null) {
3211
3345
  clearInterval(flushInterval);
3212
3346
  }
3213
- flushInterval = setInterval(() => flushQueuedEvents(projectId), FLUSH_EVENTS_TIME);
3347
+ flushInterval = setInterval(() => flushQueuedEvents(Coframe.projectId), FLUSH_EVENTS_TIME);
3214
3348
  }
3215
3349
  function setupSPANavigation(Coframe, project, config, previewVariantId, isSessionVariant, applyVariants) {
3216
3350
  var _a;
@@ -3258,7 +3392,7 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
3258
3392
  else {
3259
3393
  try {
3260
3394
  const preview = resolvePreview(newPageUrl, previewVariantId, isSessionVariant);
3261
- const result = await fetchProjectFromEdge(Coframe.projectId, newPageUrl, signal, config.userContext, preview.previewVariantId || undefined, undefined, (_a = config.referrer) !== null && _a !== void 0 ? _a : getBrowserReferrer());
3395
+ 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());
3262
3396
  if (signal.aborted)
3263
3397
  return;
3264
3398
  if (result) {
@@ -3356,7 +3490,7 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
3356
3490
  }
3357
3491
  reportInitTiming(Coframe, cdnProject, integrationStartTime, executionStartTime);
3358
3492
  setupSPANavigation(Coframe, cdnProject, config, previewVariantId, isSessionVariant, applyVariants);
3359
- setupFlushInterval(Coframe.projectId);
3493
+ setupFlushInterval(Coframe);
3360
3494
  }
3361
3495
 
3362
3496
  const TIMEOUT_MAX_MS = 5000;
@@ -3392,6 +3526,7 @@ async function resolveConfig(Coframe, revealPage) {
3392
3526
  : null;
3393
3527
  const executionStartTime = new Date();
3394
3528
  Coframe.batchSize = config.batchSize || 32;
3529
+ configureIngestUrl(config.ingestUrl);
3395
3530
  const gracefulExit = (msg) => {
3396
3531
  console.warn(`[Coframe] ${msg}`);
3397
3532
  revealPage();
@@ -3519,7 +3654,7 @@ async function resolveProject(Coframe, previewVariantId, isSessionVariant, confi
3519
3654
  let personalizationHeaders = (projectData === null || projectData === void 0 ? void 0 : projectData.personalizationHeaders) || null;
3520
3655
  let project = isSessionVariant ? undefined : projectData === null || projectData === void 0 ? void 0 : projectData.project;
3521
3656
  if (!project) {
3522
- const edgeResult = await fetchProjectFromEdge(Coframe.projectId, config.currentUrl || getCurrentPageUrl(), undefined, config.userContext, previewVariantId || undefined, undefined, (_b = config.referrer) !== null && _b !== void 0 ? _b : getBrowserReferrer());
3657
+ 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());
3523
3658
  personalizationHeaders = (edgeResult === null || edgeResult === void 0 ? void 0 : edgeResult.personalizationHeaders) || null;
3524
3659
  project = (edgeResult === null || edgeResult === void 0 ? void 0 : edgeResult.project) || undefined;
3525
3660
  }
@@ -172,7 +172,9 @@ interface ExperimentMetadata {
172
172
  audience_ids?: string[];
173
173
  };
174
174
  lane_ids?: string[];
175
+ serving_mode?: ServingMode;
175
176
  }
177
+ type ServingMode = 'off' | 'split' | 'winner';
176
178
  interface Allocation {
177
179
  [variantId: string]: number[][];
178
180
  }
@@ -246,6 +248,8 @@ type CoframeConfig = {
246
248
  timeoutMs?: number;
247
249
  timeoutBehavior?: 'exclude';
248
250
  currentUrl?: string;
251
+ projectUrl?: string;
252
+ ingestUrl?: string;
249
253
  referrer?: string;
250
254
  userContext?: Record<string, unknown>;
251
255
  nonce?: string;
@@ -175,7 +175,9 @@ interface ExperimentMetadata {
175
175
  audience_ids?: string[];
176
176
  };
177
177
  lane_ids?: string[];
178
+ serving_mode?: ServingMode;
178
179
  }
180
+ type ServingMode = 'off' | 'split' | 'winner';
179
181
  interface Allocation {
180
182
  [variantId: string]: number[][];
181
183
  }
@@ -249,6 +251,8 @@ type CoframeConfig = {
249
251
  timeoutMs?: number;
250
252
  timeoutBehavior?: 'exclude';
251
253
  currentUrl?: string;
254
+ projectUrl?: string;
255
+ ingestUrl?: string;
252
256
  referrer?: string;
253
257
  userContext?: Record<string, unknown>;
254
258
  nonce?: string;
@@ -138,7 +138,9 @@ interface ExperimentMetadata {
138
138
  audience_ids?: string[];
139
139
  };
140
140
  lane_ids?: string[];
141
+ serving_mode?: ServingMode;
141
142
  }
143
+ type ServingMode = 'off' | 'split' | 'winner';
142
144
  interface Allocation {
143
145
  [variantId: string]: number[][];
144
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coframe/sdk",
3
- "version": "1.1.16",
3
+ "version": "1.1.21",
4
4
  "description": "Coframe optimizes your website using generative AI to deliver maximum impact to your audience and allow your site to improve itself as you sleep. Learn more at coframe.com.",
5
5
  "main": "dist/pkg/cf.cjs.js",
6
6
  "module": "dist/pkg/cf.esm.mjs",