@coframe/sdk 1.1.3 → 1.1.5

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.
@@ -1105,8 +1105,11 @@ function createSession(data) {
1105
1105
  console.warn('[Coframe] cannot send event beacon, feature unavailable');
1106
1106
  }
1107
1107
  }
1108
- async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, headers) {
1108
+ async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers) {
1109
1109
  const payload = { projectId, currentUrl };
1110
+ if (previewVariantId) {
1111
+ payload.previewVariantId = previewVariantId;
1112
+ }
1110
1113
  const safe = sanitizeUserContext(userContext);
1111
1114
  if (safe) {
1112
1115
  payload.userContext = safe;
@@ -2296,7 +2299,7 @@ function checkAllAudiences(metadata, project, personalizationHeaders, currentPag
2296
2299
  }
2297
2300
  return result;
2298
2301
  }
2299
- async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}) {
2302
+ async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}, silentFallback = false) {
2300
2303
  var _a, _b;
2301
2304
  const experimentsInfo = [];
2302
2305
  const variantsAndElements = [];
@@ -2325,10 +2328,13 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2325
2328
  hasActivatedFrames: true,
2326
2329
  });
2327
2330
  }
2328
- if (!skipAlert) {
2331
+ if (!skipAlert && !silentFallback) {
2329
2332
  window.alert(`[Coframe] Variant preview unavailable: ${previewVariantId}.`);
2330
2333
  }
2331
- return buildResponse({});
2334
+ if (silentFallback) ;
2335
+ else {
2336
+ return buildResponse({});
2337
+ }
2332
2338
  }
2333
2339
  const userVisitCount = Number.parseInt(getCookie(VISIT_COUNT_COOKIE_KEY) || '0', 10);
2334
2340
  const validatedClaims = validateClaims(laneClaims, project.lanes);
@@ -2573,6 +2579,220 @@ function rebindMetrics(coframe, logger, metrics, nonce) {
2573
2579
  }
2574
2580
  }
2575
2581
 
2582
+ function getActivePreviewModes(sessionPreview) {
2583
+ if (!sessionPreview.isSessionPreviewActive ||
2584
+ !sessionPreview.previewVariantId) {
2585
+ return [];
2586
+ }
2587
+ return [
2588
+ {
2589
+ id: 'session-preview',
2590
+ label: 'Preview on',
2591
+ title: `Session preview is active for ${sessionPreview.previewVariantId}`,
2592
+ variantIds: sessionPreview.previewVariantId
2593
+ .split(',')
2594
+ .map((id) => id.trim())
2595
+ .filter(Boolean),
2596
+ },
2597
+ ];
2598
+ }
2599
+
2600
+ const PREVIEW_UI_PROTOCOL_VERSION = 1;
2601
+
2602
+ const SESSION_VARIANT_KEY = 'coframe.preview_variant';
2603
+ const PREVIEW_VARIANT_QUERY_PARAM = 'coframe_variant_id';
2604
+ const KEEP_PREVIEW_FOR_SESSION_QUERY_PARAM = 'keep_preview_for_session';
2605
+ let previewDismissed = false;
2606
+ function dismissSessionPreview() {
2607
+ previewDismissed = true;
2608
+ clearPreviewVariantId();
2609
+ const url = new URL(window.location.href);
2610
+ url.searchParams.delete(PREVIEW_VARIANT_QUERY_PARAM);
2611
+ url.searchParams.delete(KEEP_PREVIEW_FOR_SESSION_QUERY_PARAM);
2612
+ window.history.replaceState(window.history.state, '', url);
2613
+ window.location.reload();
2614
+ }
2615
+ function normalizeVariantIds(raw) {
2616
+ if (!raw)
2617
+ return null;
2618
+ const ids = raw
2619
+ .split(',')
2620
+ .map((id) => id.trim())
2621
+ .filter(Boolean);
2622
+ return ids.length > 0 ? ids.join(',') : null;
2623
+ }
2624
+ function readPreviewVariantId() {
2625
+ try {
2626
+ return sessionStorage.getItem(SESSION_VARIANT_KEY);
2627
+ }
2628
+ catch (_a) {
2629
+ return null;
2630
+ }
2631
+ }
2632
+ function persistPreviewVariantId(variantId) {
2633
+ const normalized = normalizeVariantIds(variantId);
2634
+ if (!normalized)
2635
+ return;
2636
+ try {
2637
+ sessionStorage.setItem(SESSION_VARIANT_KEY, normalized);
2638
+ }
2639
+ catch (_a) {
2640
+ }
2641
+ }
2642
+ function clearPreviewVariantId() {
2643
+ try {
2644
+ sessionStorage.removeItem(SESSION_VARIANT_KEY);
2645
+ }
2646
+ catch (_a) {
2647
+ }
2648
+ }
2649
+ function resolvePreviewSource(urlParams = getCurrentUrlSearchParams()) {
2650
+ return {
2651
+ urlVariantId: urlParams.get(PREVIEW_VARIANT_QUERY_PARAM),
2652
+ storedVariantId: readPreviewVariantId(),
2653
+ keepForSession: urlParams.get(KEEP_PREVIEW_FOR_SESSION_QUERY_PARAM) === 'true',
2654
+ };
2655
+ }
2656
+ function resolveSessionPreviewState() {
2657
+ if (previewDismissed) {
2658
+ return { isSessionPreviewActive: false, previewVariantId: null };
2659
+ }
2660
+ const previewSource = resolvePreviewSource();
2661
+ const urlVariantId = normalizeVariantIds(previewSource.urlVariantId);
2662
+ if (urlVariantId && previewSource.keepForSession) {
2663
+ return {
2664
+ isSessionPreviewActive: true,
2665
+ previewVariantId: urlVariantId,
2666
+ };
2667
+ }
2668
+ if (urlVariantId) {
2669
+ return { isSessionPreviewActive: false, previewVariantId: null };
2670
+ }
2671
+ const storedVariantId = normalizeVariantIds(previewSource.storedVariantId);
2672
+ return {
2673
+ isSessionPreviewActive: !!storedVariantId,
2674
+ previewVariantId: storedVariantId,
2675
+ };
2676
+ }
2677
+
2678
+ const PREVIEW_UI_SCRIPT_ID = 'coframe-preview-ui-script';
2679
+ const PREVIEW_UI_URL = "https://edge.cofra.me/preview-ui/latest.js";
2680
+ let loadPromise = null;
2681
+ let latestState = { modes: [] };
2682
+ let nonce;
2683
+ let logger;
2684
+ let mounted = false;
2685
+ function reportError(message, error) {
2686
+ const errorMessage = error instanceof Error ? error.message : String(error);
2687
+ if (logger) {
2688
+ logger.warn(message, 'preview_ui_unavailable', { error: errorMessage });
2689
+ return;
2690
+ }
2691
+ console.warn(`[Coframe] ${message}`, error);
2692
+ }
2693
+ function getPreviewUI() {
2694
+ const api = window.CoframePreviewUI;
2695
+ return (api === null || api === void 0 ? void 0 : api.protocolVersion) === PREVIEW_UI_PROTOCOL_VERSION ? api : undefined;
2696
+ }
2697
+ function loadPreviewUI() {
2698
+ const existing = getPreviewUI();
2699
+ if (existing)
2700
+ return Promise.resolve(existing);
2701
+ if (loadPromise)
2702
+ return loadPromise;
2703
+ loadPromise = new Promise((resolve, reject) => {
2704
+ const script = document.createElement('script');
2705
+ script.id = PREVIEW_UI_SCRIPT_ID;
2706
+ script.async = true;
2707
+ script.src = PREVIEW_UI_URL;
2708
+ if (nonce)
2709
+ script.nonce = nonce;
2710
+ script.onload = () => {
2711
+ const api = getPreviewUI();
2712
+ if (!api) {
2713
+ reject(new Error('Preview UI protocol is missing or incompatible'));
2714
+ return;
2715
+ }
2716
+ resolve(api);
2717
+ };
2718
+ script.onerror = () => {
2719
+ reject(new Error('Preview UI script failed to load'));
2720
+ };
2721
+ document.head.appendChild(script);
2722
+ });
2723
+ return loadPromise;
2724
+ }
2725
+ function renderPreviewUI(api) {
2726
+ try {
2727
+ if (latestState.modes.length === 0) {
2728
+ api.unmount();
2729
+ mounted = false;
2730
+ }
2731
+ else if (mounted) {
2732
+ api.update(latestState);
2733
+ }
2734
+ else {
2735
+ api.mount({ state: latestState, nonce, onDismiss: dismissPreviewPanel });
2736
+ mounted = true;
2737
+ }
2738
+ }
2739
+ catch (error) {
2740
+ reportError('Preview UI could not be rendered', error);
2741
+ }
2742
+ }
2743
+ function dismissPreviewPanel() {
2744
+ dismissSessionPreview();
2745
+ latestState = { modes: [] };
2746
+ const api = getPreviewUI();
2747
+ if (api)
2748
+ renderPreviewUI(api);
2749
+ }
2750
+ function modesWithVariants(sessionPreview, variantsAndElements, applied = true) {
2751
+ const activeModes = getActivePreviewModes(sessionPreview);
2752
+ if (activeModes.length === 0)
2753
+ return [];
2754
+ const currentPageVariantIds = new Set((variantsAndElements === null || variantsAndElements === void 0 ? void 0 : variantsAndElements.map(({ variant }) => variant.id)) || []);
2755
+ const slugById = new Map((variantsAndElements === null || variantsAndElements === void 0 ? void 0 : variantsAndElements.map(({ variant }) => [variant.id, variant.slug])) || []);
2756
+ return activeModes.map((mode) => ({
2757
+ id: mode.id,
2758
+ label: mode.label,
2759
+ title: mode.title,
2760
+ variants: mode.variantIds.map((id) => ({
2761
+ id,
2762
+ slug: slugById.get(id) || null,
2763
+ status: variantsAndElements
2764
+ ? applied && currentPageVariantIds.has(id)
2765
+ ? 'active'
2766
+ : 'inactive'
2767
+ : 'pending',
2768
+ })),
2769
+ }));
2770
+ }
2771
+ function resolveSessionPreviewPanel(sessionPreview, previewNonce, previewLogger) {
2772
+ const modes = modesWithVariants(sessionPreview);
2773
+ if (modes.length === 0)
2774
+ return;
2775
+ nonce = previewNonce;
2776
+ logger = previewLogger;
2777
+ latestState = { modes };
2778
+ const existing = getPreviewUI();
2779
+ if (existing) {
2780
+ renderPreviewUI(existing);
2781
+ return;
2782
+ }
2783
+ void loadPreviewUI()
2784
+ .then(renderPreviewUI)
2785
+ .catch((error) => reportError('Preview UI could not be loaded', error));
2786
+ }
2787
+ function updatePreviewPanel(variantsAndElements, sessionPreview, applied = true) {
2788
+ latestState = {
2789
+ modes: modesWithVariants(sessionPreview, variantsAndElements, applied),
2790
+ };
2791
+ const api = getPreviewUI();
2792
+ if (api)
2793
+ renderPreviewUI(api);
2794
+ }
2795
+
2576
2796
  let lastUrl = null;
2577
2797
  function observeUrlChanges(baseUrlMatch, onUrlChange) {
2578
2798
  const tryTrack = () => {
@@ -2910,6 +3130,23 @@ function setupTracking(Coframe, projectResult, previewVariantId, executionStartT
2910
3130
  return analytics;
2911
3131
  }
2912
3132
 
3133
+ function resolvePreview(pageUrl, previewVariantId, isSessionVariant) {
3134
+ let urlPreviewVariantId = null;
3135
+ try {
3136
+ urlPreviewVariantId = new URL(pageUrl).searchParams.get('coframe_variant_id');
3137
+ }
3138
+ catch (_a) {
3139
+ urlPreviewVariantId = getCurrentUrlSearchParams().get('coframe_variant_id');
3140
+ }
3141
+ if (urlPreviewVariantId) {
3142
+ return { previewVariantId: urlPreviewVariantId, isSessionVariant: false };
3143
+ }
3144
+ if (previewVariantId) {
3145
+ return { previewVariantId, isSessionVariant };
3146
+ }
3147
+ const stored = readPreviewVariantId();
3148
+ return { previewVariantId: stored, isSessionVariant: !!stored };
3149
+ }
2913
3150
  async function awaitHydration(config, Coframe, project) {
2914
3151
  var _a, _b;
2915
3152
  if (!config.waitForHydration)
@@ -2942,7 +3179,7 @@ function setupFlushInterval(projectId) {
2942
3179
  }
2943
3180
  flushInterval = setInterval(() => flushQueuedEvents(projectId), FLUSH_EVENTS_TIME);
2944
3181
  }
2945
- function setupSPANavigation(Coframe, project, config, applyVariants) {
3182
+ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessionVariant, applyVariants) {
2946
3183
  var _a;
2947
3184
  if (!((_a = project === null || project === void 0 ? void 0 : project.settings) === null || _a === void 0 ? void 0 : _a.mutation_observer_active))
2948
3185
  return;
@@ -2981,7 +3218,8 @@ function setupSPANavigation(Coframe, project, config, applyVariants) {
2981
3218
  }
2982
3219
  else {
2983
3220
  try {
2984
- const result = await fetchProjectFromEdge(Coframe.projectId, newPageUrl, signal, config.userContext);
3221
+ const preview = resolvePreview(newPageUrl, previewVariantId, isSessionVariant);
3222
+ const result = await fetchProjectFromEdge(Coframe.projectId, newPageUrl, signal, config.userContext, preview.previewVariantId || undefined);
2985
3223
  if (signal.aborted)
2986
3224
  return;
2987
3225
  if (result) {
@@ -3025,22 +3263,22 @@ function persistLaneClaims(claims, logger) {
3025
3263
  setCookie(LANE_COOKIE_KEY, encodeURIComponent(serialized), logger);
3026
3264
  }
3027
3265
  }
3028
- function createApplyVariants(Coframe, config, previewVariantId, executionStartTime, analytics, personalizationHeaders, laneStore) {
3266
+ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, personalizationHeaders, laneStore) {
3029
3267
  let isFirstApply = true;
3030
3268
  return async (pageUrl, pageProject) => {
3031
3269
  const sessionId = isFirstApply ? Coframe.sessionId : generateUUID();
3032
3270
  isFirstApply = false;
3033
3271
  Coframe.sessionId = sessionId;
3034
- const urlPreviewVariantId = getCurrentUrlSearchParams().get('coframe_variant_id') || null;
3035
- const updatedPreviewVariantId = urlPreviewVariantId !== previewVariantId
3036
- ? urlPreviewVariantId
3037
- : previewVariantId;
3038
- const { variantsAndElements, experimentsInfo, hasActivatedFrames, laneClaims, } = await getVariantsAndElements(personalizationHeaders, pageProject, updatedPreviewVariantId, Coframe.userToken, pageUrl, getBrowserLanguages(), getBrowserPlatform(), Coframe.logger, config.nonce, laneStore.claims);
3272
+ const preview = resolvePreview(pageUrl, previewVariantId, isSessionVariant);
3273
+ setIsPreviewVariant(!!preview.previewVariantId);
3274
+ const { variantsAndElements, experimentsInfo, hasActivatedFrames, laneClaims, } = await getVariantsAndElements(personalizationHeaders, pageProject, preview.previewVariantId, Coframe.userToken, pageUrl, getBrowserLanguages(), getBrowserPlatform(), Coframe.logger, config.nonce, laneStore.claims, preview.isSessionVariant);
3039
3275
  laneStore.claims = laneClaims;
3040
3276
  persistLaneClaims(laneClaims, Coframe.logger);
3041
3277
  Coframe.experimentInfo = experimentsInfo;
3042
3278
  Coframe.variantsAndElements = variantsAndElements;
3043
3279
  setFrameActivated(hasActivatedFrames);
3280
+ const sessionPreview = resolveSessionPreviewState();
3281
+ updatePreviewPanel(variantsAndElements, sessionPreview, !config.trackingOnly);
3044
3282
  if (Coframe.debug) {
3045
3283
  Coframe.debug.project = pageProject;
3046
3284
  Coframe.debug.frames = pageProject.frames;
@@ -3058,8 +3296,13 @@ function createApplyVariants(Coframe, config, previewVariantId, executionStartTi
3058
3296
  };
3059
3297
  }
3060
3298
  async function modifyPage(Coframe, configResult, projectResult, analytics) {
3061
- const { config, timeoutMs, previewVariantId, integrationStartTime, executionStartTime, } = configResult;
3299
+ const { config, timeoutMs, previewVariantId, isSessionVariant, integrationStartTime, executionStartTime, } = configResult;
3062
3300
  const { project: cdnProject, personalizationHeaders } = projectResult;
3301
+ if (resolveSessionPreviewState().isSessionPreviewActive) {
3302
+ waitForBody(timeoutMs)
3303
+ .then(() => resolveSessionPreviewPanel(resolveSessionPreviewState(), config.nonce, Coframe.logger))
3304
+ .catch(() => { });
3305
+ }
3063
3306
  if (!(await awaitHydration(config, Coframe, cdnProject)))
3064
3307
  return;
3065
3308
  if (cdnProject.tags) {
@@ -3069,7 +3312,7 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
3069
3312
  if (!(await ensureBody(Coframe, cdnProject, timeoutMs, config.nonce)))
3070
3313
  return;
3071
3314
  const laneStore = { claims: readLaneClaims() };
3072
- const applyVariants = createApplyVariants(Coframe, config, previewVariantId, executionStartTime, analytics, personalizationHeaders, laneStore);
3315
+ const applyVariants = createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, personalizationHeaders, laneStore);
3073
3316
  await applyVariants(getCurrentPageUrl(), cdnProject);
3074
3317
  if (!Coframe.variantsAndElements ||
3075
3318
  Coframe.variantsAndElements.length === 0) {
@@ -3077,7 +3320,7 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
3077
3320
  variantRendered();
3078
3321
  }
3079
3322
  reportInitTiming(Coframe, cdnProject, integrationStartTime, executionStartTime);
3080
- setupSPANavigation(Coframe, cdnProject, config, applyVariants);
3323
+ setupSPANavigation(Coframe, cdnProject, config, previewVariantId, isSessionVariant, applyVariants);
3081
3324
  setupFlushInterval(Coframe.projectId);
3082
3325
  }
3083
3326
 
@@ -3152,8 +3395,20 @@ async function resolveConfig(Coframe, revealPage) {
3152
3395
  }
3153
3396
  Coframe.projectId = projectId;
3154
3397
  const urlParams = getCurrentUrlSearchParams();
3155
- const previewVariantId = urlParams.get('coframe_variant_id');
3398
+ const previewSource = resolvePreviewSource(urlParams);
3156
3399
  const shouldSendEventsParam = urlParams.get('coframe_should_send_events');
3400
+ let previewVariantId = null;
3401
+ let isSessionVariant = false;
3402
+ if (previewSource.urlVariantId) {
3403
+ previewVariantId = previewSource.urlVariantId;
3404
+ if (previewSource.keepForSession) {
3405
+ persistPreviewVariantId(previewSource.urlVariantId);
3406
+ }
3407
+ }
3408
+ else if (previewSource.storedVariantId) {
3409
+ previewVariantId = previewSource.storedVariantId;
3410
+ isSessionVariant = true;
3411
+ }
3157
3412
  setForceSendEvents(shouldSendEventsParam === 'true');
3158
3413
  setIsPreviewVariant(!!previewVariantId);
3159
3414
  setWaitingForCookies(!!config.waitForCookiesEnabled);
@@ -3162,6 +3417,7 @@ async function resolveConfig(Coframe, revealPage) {
3162
3417
  timeoutMs,
3163
3418
  projectId,
3164
3419
  previewVariantId,
3420
+ isSessionVariant,
3165
3421
  integrationStartTime,
3166
3422
  executionStartTime,
3167
3423
  };
@@ -3223,14 +3479,14 @@ async function resolveIdentity(config, Coframe, timeoutMs, gracefulExit) {
3223
3479
  return { userToken, applyCookieConsent, onConsentChange };
3224
3480
  }
3225
3481
 
3226
- async function resolveProject(Coframe, previewVariantId, config) {
3482
+ async function resolveProject(Coframe, previewVariantId, isSessionVariant, config) {
3227
3483
  var _a;
3228
3484
  const logger = Coframe.logger;
3229
3485
  const projectData = (_a = findLastCmd((c) => !!c.projectData)) === null || _a === void 0 ? void 0 : _a.projectData;
3230
3486
  let personalizationHeaders = (projectData === null || projectData === void 0 ? void 0 : projectData.personalizationHeaders) || null;
3231
- let project = projectData === null || projectData === void 0 ? void 0 : projectData.project;
3487
+ let project = isSessionVariant ? undefined : projectData === null || projectData === void 0 ? void 0 : projectData.project;
3232
3488
  if (!project) {
3233
- const edgeResult = await fetchProjectFromEdge(Coframe.projectId, config.currentUrl || getCurrentPageUrl(), undefined, config.userContext);
3489
+ const edgeResult = await fetchProjectFromEdge(Coframe.projectId, config.currentUrl || getCurrentPageUrl(), undefined, config.userContext, previewVariantId || undefined);
3234
3490
  personalizationHeaders = (edgeResult === null || edgeResult === void 0 ? void 0 : edgeResult.personalizationHeaders) || null;
3235
3491
  project = (edgeResult === null || edgeResult === void 0 ? void 0 : edgeResult.project) || undefined;
3236
3492
  }
@@ -3322,7 +3578,7 @@ async function init(options) {
3322
3578
  const configResult = await resolveConfig(Coframe, revealPage);
3323
3579
  if (!configResult)
3324
3580
  return;
3325
- const projectResult = await resolveProject(Coframe, configResult.previewVariantId, configResult.config);
3581
+ const projectResult = await resolveProject(Coframe, configResult.previewVariantId, configResult.isSessionVariant, configResult.config);
3326
3582
  if (!projectResult)
3327
3583
  return;
3328
3584
  Coframe.orgId = projectResult.project.org_id || '';
@@ -1103,8 +1103,11 @@ function createSession(data) {
1103
1103
  console.warn('[Coframe] cannot send event beacon, feature unavailable');
1104
1104
  }
1105
1105
  }
1106
- async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, headers) {
1106
+ async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers) {
1107
1107
  const payload = { projectId, currentUrl };
1108
+ if (previewVariantId) {
1109
+ payload.previewVariantId = previewVariantId;
1110
+ }
1108
1111
  const safe = sanitizeUserContext(userContext);
1109
1112
  if (safe) {
1110
1113
  payload.userContext = safe;
@@ -2294,7 +2297,7 @@ function checkAllAudiences(metadata, project, personalizationHeaders, currentPag
2294
2297
  }
2295
2298
  return result;
2296
2299
  }
2297
- async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}) {
2300
+ async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}, silentFallback = false) {
2298
2301
  var _a, _b;
2299
2302
  const experimentsInfo = [];
2300
2303
  const variantsAndElements = [];
@@ -2323,10 +2326,13 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2323
2326
  hasActivatedFrames: true,
2324
2327
  });
2325
2328
  }
2326
- if (!skipAlert) {
2329
+ if (!skipAlert && !silentFallback) {
2327
2330
  window.alert(`[Coframe] Variant preview unavailable: ${previewVariantId}.`);
2328
2331
  }
2329
- return buildResponse({});
2332
+ if (silentFallback) ;
2333
+ else {
2334
+ return buildResponse({});
2335
+ }
2330
2336
  }
2331
2337
  const userVisitCount = Number.parseInt(getCookie(VISIT_COUNT_COOKIE_KEY) || '0', 10);
2332
2338
  const validatedClaims = validateClaims(laneClaims, project.lanes);
@@ -2571,6 +2577,220 @@ function rebindMetrics(coframe, logger, metrics, nonce) {
2571
2577
  }
2572
2578
  }
2573
2579
 
2580
+ function getActivePreviewModes(sessionPreview) {
2581
+ if (!sessionPreview.isSessionPreviewActive ||
2582
+ !sessionPreview.previewVariantId) {
2583
+ return [];
2584
+ }
2585
+ return [
2586
+ {
2587
+ id: 'session-preview',
2588
+ label: 'Preview on',
2589
+ title: `Session preview is active for ${sessionPreview.previewVariantId}`,
2590
+ variantIds: sessionPreview.previewVariantId
2591
+ .split(',')
2592
+ .map((id) => id.trim())
2593
+ .filter(Boolean),
2594
+ },
2595
+ ];
2596
+ }
2597
+
2598
+ const PREVIEW_UI_PROTOCOL_VERSION = 1;
2599
+
2600
+ const SESSION_VARIANT_KEY = 'coframe.preview_variant';
2601
+ const PREVIEW_VARIANT_QUERY_PARAM = 'coframe_variant_id';
2602
+ const KEEP_PREVIEW_FOR_SESSION_QUERY_PARAM = 'keep_preview_for_session';
2603
+ let previewDismissed = false;
2604
+ function dismissSessionPreview() {
2605
+ previewDismissed = true;
2606
+ clearPreviewVariantId();
2607
+ const url = new URL(window.location.href);
2608
+ url.searchParams.delete(PREVIEW_VARIANT_QUERY_PARAM);
2609
+ url.searchParams.delete(KEEP_PREVIEW_FOR_SESSION_QUERY_PARAM);
2610
+ window.history.replaceState(window.history.state, '', url);
2611
+ window.location.reload();
2612
+ }
2613
+ function normalizeVariantIds(raw) {
2614
+ if (!raw)
2615
+ return null;
2616
+ const ids = raw
2617
+ .split(',')
2618
+ .map((id) => id.trim())
2619
+ .filter(Boolean);
2620
+ return ids.length > 0 ? ids.join(',') : null;
2621
+ }
2622
+ function readPreviewVariantId() {
2623
+ try {
2624
+ return sessionStorage.getItem(SESSION_VARIANT_KEY);
2625
+ }
2626
+ catch (_a) {
2627
+ return null;
2628
+ }
2629
+ }
2630
+ function persistPreviewVariantId(variantId) {
2631
+ const normalized = normalizeVariantIds(variantId);
2632
+ if (!normalized)
2633
+ return;
2634
+ try {
2635
+ sessionStorage.setItem(SESSION_VARIANT_KEY, normalized);
2636
+ }
2637
+ catch (_a) {
2638
+ }
2639
+ }
2640
+ function clearPreviewVariantId() {
2641
+ try {
2642
+ sessionStorage.removeItem(SESSION_VARIANT_KEY);
2643
+ }
2644
+ catch (_a) {
2645
+ }
2646
+ }
2647
+ function resolvePreviewSource(urlParams = getCurrentUrlSearchParams()) {
2648
+ return {
2649
+ urlVariantId: urlParams.get(PREVIEW_VARIANT_QUERY_PARAM),
2650
+ storedVariantId: readPreviewVariantId(),
2651
+ keepForSession: urlParams.get(KEEP_PREVIEW_FOR_SESSION_QUERY_PARAM) === 'true',
2652
+ };
2653
+ }
2654
+ function resolveSessionPreviewState() {
2655
+ if (previewDismissed) {
2656
+ return { isSessionPreviewActive: false, previewVariantId: null };
2657
+ }
2658
+ const previewSource = resolvePreviewSource();
2659
+ const urlVariantId = normalizeVariantIds(previewSource.urlVariantId);
2660
+ if (urlVariantId && previewSource.keepForSession) {
2661
+ return {
2662
+ isSessionPreviewActive: true,
2663
+ previewVariantId: urlVariantId,
2664
+ };
2665
+ }
2666
+ if (urlVariantId) {
2667
+ return { isSessionPreviewActive: false, previewVariantId: null };
2668
+ }
2669
+ const storedVariantId = normalizeVariantIds(previewSource.storedVariantId);
2670
+ return {
2671
+ isSessionPreviewActive: !!storedVariantId,
2672
+ previewVariantId: storedVariantId,
2673
+ };
2674
+ }
2675
+
2676
+ const PREVIEW_UI_SCRIPT_ID = 'coframe-preview-ui-script';
2677
+ const PREVIEW_UI_URL = "https://edge.cofra.me/preview-ui/latest.js";
2678
+ let loadPromise = null;
2679
+ let latestState = { modes: [] };
2680
+ let nonce;
2681
+ let logger;
2682
+ let mounted = false;
2683
+ function reportError(message, error) {
2684
+ const errorMessage = error instanceof Error ? error.message : String(error);
2685
+ if (logger) {
2686
+ logger.warn(message, 'preview_ui_unavailable', { error: errorMessage });
2687
+ return;
2688
+ }
2689
+ console.warn(`[Coframe] ${message}`, error);
2690
+ }
2691
+ function getPreviewUI() {
2692
+ const api = window.CoframePreviewUI;
2693
+ return (api === null || api === void 0 ? void 0 : api.protocolVersion) === PREVIEW_UI_PROTOCOL_VERSION ? api : undefined;
2694
+ }
2695
+ function loadPreviewUI() {
2696
+ const existing = getPreviewUI();
2697
+ if (existing)
2698
+ return Promise.resolve(existing);
2699
+ if (loadPromise)
2700
+ return loadPromise;
2701
+ loadPromise = new Promise((resolve, reject) => {
2702
+ const script = document.createElement('script');
2703
+ script.id = PREVIEW_UI_SCRIPT_ID;
2704
+ script.async = true;
2705
+ script.src = PREVIEW_UI_URL;
2706
+ if (nonce)
2707
+ script.nonce = nonce;
2708
+ script.onload = () => {
2709
+ const api = getPreviewUI();
2710
+ if (!api) {
2711
+ reject(new Error('Preview UI protocol is missing or incompatible'));
2712
+ return;
2713
+ }
2714
+ resolve(api);
2715
+ };
2716
+ script.onerror = () => {
2717
+ reject(new Error('Preview UI script failed to load'));
2718
+ };
2719
+ document.head.appendChild(script);
2720
+ });
2721
+ return loadPromise;
2722
+ }
2723
+ function renderPreviewUI(api) {
2724
+ try {
2725
+ if (latestState.modes.length === 0) {
2726
+ api.unmount();
2727
+ mounted = false;
2728
+ }
2729
+ else if (mounted) {
2730
+ api.update(latestState);
2731
+ }
2732
+ else {
2733
+ api.mount({ state: latestState, nonce, onDismiss: dismissPreviewPanel });
2734
+ mounted = true;
2735
+ }
2736
+ }
2737
+ catch (error) {
2738
+ reportError('Preview UI could not be rendered', error);
2739
+ }
2740
+ }
2741
+ function dismissPreviewPanel() {
2742
+ dismissSessionPreview();
2743
+ latestState = { modes: [] };
2744
+ const api = getPreviewUI();
2745
+ if (api)
2746
+ renderPreviewUI(api);
2747
+ }
2748
+ function modesWithVariants(sessionPreview, variantsAndElements, applied = true) {
2749
+ const activeModes = getActivePreviewModes(sessionPreview);
2750
+ if (activeModes.length === 0)
2751
+ return [];
2752
+ const currentPageVariantIds = new Set((variantsAndElements === null || variantsAndElements === void 0 ? void 0 : variantsAndElements.map(({ variant }) => variant.id)) || []);
2753
+ const slugById = new Map((variantsAndElements === null || variantsAndElements === void 0 ? void 0 : variantsAndElements.map(({ variant }) => [variant.id, variant.slug])) || []);
2754
+ return activeModes.map((mode) => ({
2755
+ id: mode.id,
2756
+ label: mode.label,
2757
+ title: mode.title,
2758
+ variants: mode.variantIds.map((id) => ({
2759
+ id,
2760
+ slug: slugById.get(id) || null,
2761
+ status: variantsAndElements
2762
+ ? applied && currentPageVariantIds.has(id)
2763
+ ? 'active'
2764
+ : 'inactive'
2765
+ : 'pending',
2766
+ })),
2767
+ }));
2768
+ }
2769
+ function resolveSessionPreviewPanel(sessionPreview, previewNonce, previewLogger) {
2770
+ const modes = modesWithVariants(sessionPreview);
2771
+ if (modes.length === 0)
2772
+ return;
2773
+ nonce = previewNonce;
2774
+ logger = previewLogger;
2775
+ latestState = { modes };
2776
+ const existing = getPreviewUI();
2777
+ if (existing) {
2778
+ renderPreviewUI(existing);
2779
+ return;
2780
+ }
2781
+ void loadPreviewUI()
2782
+ .then(renderPreviewUI)
2783
+ .catch((error) => reportError('Preview UI could not be loaded', error));
2784
+ }
2785
+ function updatePreviewPanel(variantsAndElements, sessionPreview, applied = true) {
2786
+ latestState = {
2787
+ modes: modesWithVariants(sessionPreview, variantsAndElements, applied),
2788
+ };
2789
+ const api = getPreviewUI();
2790
+ if (api)
2791
+ renderPreviewUI(api);
2792
+ }
2793
+
2574
2794
  let lastUrl = null;
2575
2795
  function observeUrlChanges(baseUrlMatch, onUrlChange) {
2576
2796
  const tryTrack = () => {
@@ -2908,6 +3128,23 @@ function setupTracking(Coframe, projectResult, previewVariantId, executionStartT
2908
3128
  return analytics;
2909
3129
  }
2910
3130
 
3131
+ function resolvePreview(pageUrl, previewVariantId, isSessionVariant) {
3132
+ let urlPreviewVariantId = null;
3133
+ try {
3134
+ urlPreviewVariantId = new URL(pageUrl).searchParams.get('coframe_variant_id');
3135
+ }
3136
+ catch (_a) {
3137
+ urlPreviewVariantId = getCurrentUrlSearchParams().get('coframe_variant_id');
3138
+ }
3139
+ if (urlPreviewVariantId) {
3140
+ return { previewVariantId: urlPreviewVariantId, isSessionVariant: false };
3141
+ }
3142
+ if (previewVariantId) {
3143
+ return { previewVariantId, isSessionVariant };
3144
+ }
3145
+ const stored = readPreviewVariantId();
3146
+ return { previewVariantId: stored, isSessionVariant: !!stored };
3147
+ }
2911
3148
  async function awaitHydration(config, Coframe, project) {
2912
3149
  var _a, _b;
2913
3150
  if (!config.waitForHydration)
@@ -2940,7 +3177,7 @@ function setupFlushInterval(projectId) {
2940
3177
  }
2941
3178
  flushInterval = setInterval(() => flushQueuedEvents(projectId), FLUSH_EVENTS_TIME);
2942
3179
  }
2943
- function setupSPANavigation(Coframe, project, config, applyVariants) {
3180
+ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessionVariant, applyVariants) {
2944
3181
  var _a;
2945
3182
  if (!((_a = project === null || project === void 0 ? void 0 : project.settings) === null || _a === void 0 ? void 0 : _a.mutation_observer_active))
2946
3183
  return;
@@ -2979,7 +3216,8 @@ function setupSPANavigation(Coframe, project, config, applyVariants) {
2979
3216
  }
2980
3217
  else {
2981
3218
  try {
2982
- const result = await fetchProjectFromEdge(Coframe.projectId, newPageUrl, signal, config.userContext);
3219
+ const preview = resolvePreview(newPageUrl, previewVariantId, isSessionVariant);
3220
+ const result = await fetchProjectFromEdge(Coframe.projectId, newPageUrl, signal, config.userContext, preview.previewVariantId || undefined);
2983
3221
  if (signal.aborted)
2984
3222
  return;
2985
3223
  if (result) {
@@ -3023,22 +3261,22 @@ function persistLaneClaims(claims, logger) {
3023
3261
  setCookie(LANE_COOKIE_KEY, encodeURIComponent(serialized), logger);
3024
3262
  }
3025
3263
  }
3026
- function createApplyVariants(Coframe, config, previewVariantId, executionStartTime, analytics, personalizationHeaders, laneStore) {
3264
+ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, personalizationHeaders, laneStore) {
3027
3265
  let isFirstApply = true;
3028
3266
  return async (pageUrl, pageProject) => {
3029
3267
  const sessionId = isFirstApply ? Coframe.sessionId : generateUUID();
3030
3268
  isFirstApply = false;
3031
3269
  Coframe.sessionId = sessionId;
3032
- const urlPreviewVariantId = getCurrentUrlSearchParams().get('coframe_variant_id') || null;
3033
- const updatedPreviewVariantId = urlPreviewVariantId !== previewVariantId
3034
- ? urlPreviewVariantId
3035
- : previewVariantId;
3036
- const { variantsAndElements, experimentsInfo, hasActivatedFrames, laneClaims, } = await getVariantsAndElements(personalizationHeaders, pageProject, updatedPreviewVariantId, Coframe.userToken, pageUrl, getBrowserLanguages(), getBrowserPlatform(), Coframe.logger, config.nonce, laneStore.claims);
3270
+ const preview = resolvePreview(pageUrl, previewVariantId, isSessionVariant);
3271
+ setIsPreviewVariant(!!preview.previewVariantId);
3272
+ const { variantsAndElements, experimentsInfo, hasActivatedFrames, laneClaims, } = await getVariantsAndElements(personalizationHeaders, pageProject, preview.previewVariantId, Coframe.userToken, pageUrl, getBrowserLanguages(), getBrowserPlatform(), Coframe.logger, config.nonce, laneStore.claims, preview.isSessionVariant);
3037
3273
  laneStore.claims = laneClaims;
3038
3274
  persistLaneClaims(laneClaims, Coframe.logger);
3039
3275
  Coframe.experimentInfo = experimentsInfo;
3040
3276
  Coframe.variantsAndElements = variantsAndElements;
3041
3277
  setFrameActivated(hasActivatedFrames);
3278
+ const sessionPreview = resolveSessionPreviewState();
3279
+ updatePreviewPanel(variantsAndElements, sessionPreview, !config.trackingOnly);
3042
3280
  if (Coframe.debug) {
3043
3281
  Coframe.debug.project = pageProject;
3044
3282
  Coframe.debug.frames = pageProject.frames;
@@ -3056,8 +3294,13 @@ function createApplyVariants(Coframe, config, previewVariantId, executionStartTi
3056
3294
  };
3057
3295
  }
3058
3296
  async function modifyPage(Coframe, configResult, projectResult, analytics) {
3059
- const { config, timeoutMs, previewVariantId, integrationStartTime, executionStartTime, } = configResult;
3297
+ const { config, timeoutMs, previewVariantId, isSessionVariant, integrationStartTime, executionStartTime, } = configResult;
3060
3298
  const { project: cdnProject, personalizationHeaders } = projectResult;
3299
+ if (resolveSessionPreviewState().isSessionPreviewActive) {
3300
+ waitForBody(timeoutMs)
3301
+ .then(() => resolveSessionPreviewPanel(resolveSessionPreviewState(), config.nonce, Coframe.logger))
3302
+ .catch(() => { });
3303
+ }
3061
3304
  if (!(await awaitHydration(config, Coframe, cdnProject)))
3062
3305
  return;
3063
3306
  if (cdnProject.tags) {
@@ -3067,7 +3310,7 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
3067
3310
  if (!(await ensureBody(Coframe, cdnProject, timeoutMs, config.nonce)))
3068
3311
  return;
3069
3312
  const laneStore = { claims: readLaneClaims() };
3070
- const applyVariants = createApplyVariants(Coframe, config, previewVariantId, executionStartTime, analytics, personalizationHeaders, laneStore);
3313
+ const applyVariants = createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, personalizationHeaders, laneStore);
3071
3314
  await applyVariants(getCurrentPageUrl(), cdnProject);
3072
3315
  if (!Coframe.variantsAndElements ||
3073
3316
  Coframe.variantsAndElements.length === 0) {
@@ -3075,7 +3318,7 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
3075
3318
  variantRendered();
3076
3319
  }
3077
3320
  reportInitTiming(Coframe, cdnProject, integrationStartTime, executionStartTime);
3078
- setupSPANavigation(Coframe, cdnProject, config, applyVariants);
3321
+ setupSPANavigation(Coframe, cdnProject, config, previewVariantId, isSessionVariant, applyVariants);
3079
3322
  setupFlushInterval(Coframe.projectId);
3080
3323
  }
3081
3324
 
@@ -3150,8 +3393,20 @@ async function resolveConfig(Coframe, revealPage) {
3150
3393
  }
3151
3394
  Coframe.projectId = projectId;
3152
3395
  const urlParams = getCurrentUrlSearchParams();
3153
- const previewVariantId = urlParams.get('coframe_variant_id');
3396
+ const previewSource = resolvePreviewSource(urlParams);
3154
3397
  const shouldSendEventsParam = urlParams.get('coframe_should_send_events');
3398
+ let previewVariantId = null;
3399
+ let isSessionVariant = false;
3400
+ if (previewSource.urlVariantId) {
3401
+ previewVariantId = previewSource.urlVariantId;
3402
+ if (previewSource.keepForSession) {
3403
+ persistPreviewVariantId(previewSource.urlVariantId);
3404
+ }
3405
+ }
3406
+ else if (previewSource.storedVariantId) {
3407
+ previewVariantId = previewSource.storedVariantId;
3408
+ isSessionVariant = true;
3409
+ }
3155
3410
  setForceSendEvents(shouldSendEventsParam === 'true');
3156
3411
  setIsPreviewVariant(!!previewVariantId);
3157
3412
  setWaitingForCookies(!!config.waitForCookiesEnabled);
@@ -3160,6 +3415,7 @@ async function resolveConfig(Coframe, revealPage) {
3160
3415
  timeoutMs,
3161
3416
  projectId,
3162
3417
  previewVariantId,
3418
+ isSessionVariant,
3163
3419
  integrationStartTime,
3164
3420
  executionStartTime,
3165
3421
  };
@@ -3221,14 +3477,14 @@ async function resolveIdentity(config, Coframe, timeoutMs, gracefulExit) {
3221
3477
  return { userToken, applyCookieConsent, onConsentChange };
3222
3478
  }
3223
3479
 
3224
- async function resolveProject(Coframe, previewVariantId, config) {
3480
+ async function resolveProject(Coframe, previewVariantId, isSessionVariant, config) {
3225
3481
  var _a;
3226
3482
  const logger = Coframe.logger;
3227
3483
  const projectData = (_a = findLastCmd((c) => !!c.projectData)) === null || _a === void 0 ? void 0 : _a.projectData;
3228
3484
  let personalizationHeaders = (projectData === null || projectData === void 0 ? void 0 : projectData.personalizationHeaders) || null;
3229
- let project = projectData === null || projectData === void 0 ? void 0 : projectData.project;
3485
+ let project = isSessionVariant ? undefined : projectData === null || projectData === void 0 ? void 0 : projectData.project;
3230
3486
  if (!project) {
3231
- const edgeResult = await fetchProjectFromEdge(Coframe.projectId, config.currentUrl || getCurrentPageUrl(), undefined, config.userContext);
3487
+ const edgeResult = await fetchProjectFromEdge(Coframe.projectId, config.currentUrl || getCurrentPageUrl(), undefined, config.userContext, previewVariantId || undefined);
3232
3488
  personalizationHeaders = (edgeResult === null || edgeResult === void 0 ? void 0 : edgeResult.personalizationHeaders) || null;
3233
3489
  project = (edgeResult === null || edgeResult === void 0 ? void 0 : edgeResult.project) || undefined;
3234
3490
  }
@@ -3320,7 +3576,7 @@ async function init(options) {
3320
3576
  const configResult = await resolveConfig(Coframe, revealPage);
3321
3577
  if (!configResult)
3322
3578
  return;
3323
- const projectResult = await resolveProject(Coframe, configResult.previewVariantId, configResult.config);
3579
+ const projectResult = await resolveProject(Coframe, configResult.previewVariantId, configResult.isSessionVariant, configResult.config);
3324
3580
  if (!projectResult)
3325
3581
  return;
3326
3582
  Coframe.orgId = projectResult.project.org_id || '';
@@ -22,7 +22,7 @@ function sanitizeUserContext(userContext) {
22
22
  return userContext;
23
23
  }
24
24
 
25
- async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, headers) {
25
+ async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers) {
26
26
  const payload = { projectId, currentUrl };
27
27
  const safe = sanitizeUserContext(userContext);
28
28
  if (safe) {
@@ -73,7 +73,7 @@ async function getProjectData(options) {
73
73
  const includeCSS = options.includeCSS !== false;
74
74
  const [projectResult, antiflickerCSS] = await Promise.all([
75
75
  includeProject
76
- ? fetchProjectFromEdge(options.projectId, options.currentUrl, undefined, options.userContext, options.headers)
76
+ ? fetchProjectFromEdge(options.projectId, options.currentUrl, undefined, options.userContext, undefined, options.headers)
77
77
  : Promise.resolve(null),
78
78
  includeCSS
79
79
  ? fetchAntiflickerFromEdge(options.projectId, options.currentUrl, options.userContext, options.enableElementAntiFlicker, options.headers)
@@ -20,7 +20,7 @@ function sanitizeUserContext(userContext) {
20
20
  return userContext;
21
21
  }
22
22
 
23
- async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, headers) {
23
+ async function fetchProjectFromEdge(projectId, currentUrl, signal, userContext, previewVariantId, headers) {
24
24
  const payload = { projectId, currentUrl };
25
25
  const safe = sanitizeUserContext(userContext);
26
26
  if (safe) {
@@ -71,7 +71,7 @@ async function getProjectData(options) {
71
71
  const includeCSS = options.includeCSS !== false;
72
72
  const [projectResult, antiflickerCSS] = await Promise.all([
73
73
  includeProject
74
- ? fetchProjectFromEdge(options.projectId, options.currentUrl, undefined, options.userContext, options.headers)
74
+ ? fetchProjectFromEdge(options.projectId, options.currentUrl, undefined, options.userContext, undefined, options.headers)
75
75
  : Promise.resolve(null),
76
76
  includeCSS
77
77
  ? fetchAntiflickerFromEdge(options.projectId, options.currentUrl, options.userContext, options.enableElementAntiFlicker, options.headers)
@@ -232,6 +232,7 @@ type ActivationCondition = {
232
232
  };
233
233
  type CoframeConfig = {
234
234
  projectId: string;
235
+ previewVariantId?: string;
235
236
  trackingOnly?: boolean;
236
237
  enableAntiFlickering?: boolean;
237
238
  enableElementAntiFlicker?: boolean;
@@ -235,6 +235,7 @@ type ActivationCondition = {
235
235
  };
236
236
  type CoframeConfig = {
237
237
  projectId: string;
238
+ previewVariantId?: string;
238
239
  trackingOnly?: boolean;
239
240
  enableAntiFlickering?: boolean;
240
241
  enableElementAntiFlicker?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coframe/sdk",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
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",