@coframe/sdk 1.1.22 → 1.1.23

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
@@ -65,6 +65,12 @@ only has to forward `<ingestUrl>/*` to `https://ingest.app.coframe.com/ingest/v2
65
65
  with the path unchanged. Both overrides are optional and retain the compiled
66
66
  Coframe endpoints when omitted.
67
67
 
68
+ Configuring either `projectUrl` or `ingestUrl` makes CMAB experiments use
69
+ deterministic local assignment without calling the Edge assignment API.
70
+ Configure both overrides when the browser must avoid the SDK-compiled Coframe
71
+ project and ingest endpoints entirely. Debug tools remain controlled by
72
+ `enableDebug`.
73
+
68
74
  ### URL controls
69
75
 
70
76
  The npm entry uses the same browser URL controls as the script-tag and Edge
@@ -1079,7 +1079,7 @@ function warnEventsNotSent() {
1079
1079
  }
1080
1080
  }
1081
1081
 
1082
- const SDK_VERSION = '2.7.0';
1082
+ const SDK_VERSION = '2.8.0';
1083
1083
  const queuedEvents = [];
1084
1084
  const queuedMonitoringEvents = [];
1085
1085
  const queuedSessions = [];
@@ -2567,7 +2567,7 @@ function checkAllAudiences(metadata, ctx) {
2567
2567
  }
2568
2568
  return result;
2569
2569
  }
2570
- async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}, silentFallback = false) {
2570
+ async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}, silentFallback = false, chooseCmabVariants) {
2571
2571
  var _a;
2572
2572
  const experimentsInfo = [];
2573
2573
  const cloakBot = !!((_a = project.settings) === null || _a === void 0 ? void 0 : _a.bot_cloaking_enabled) &&
@@ -2595,7 +2595,7 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2595
2595
  const validatedClaims = validateClaims(laneClaims, project.lanes);
2596
2596
  const previewVariantIds = splitPreviewVariantIds(previewVariantId);
2597
2597
  if (previewVariantIds.length > 0) {
2598
- const variantsAndElements = previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback);
2598
+ const variantsAndElements = await previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback);
2599
2599
  if (variantsAndElements) {
2600
2600
  return buildResponse({
2601
2601
  variantsAndElements,
@@ -2635,7 +2635,7 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2635
2635
  final_state: false,
2636
2636
  level: GLOBAL_HOLDOUT,
2637
2637
  });
2638
- const assignment = assignFrames(activeFrames, validatedClaims, ctx);
2638
+ const assignment = await assignFrames(activeFrames, validatedClaims, ctx, chooseCmabVariants);
2639
2639
  return buildResponse({
2640
2640
  ...assignment,
2641
2641
  experimentsInfo: [...experimentsInfo, ...assignment.experimentsInfo],
@@ -2651,7 +2651,8 @@ function activateFrames(ctx) {
2651
2651
  return true;
2652
2652
  });
2653
2653
  }
2654
- function assignFrames(activeFrames, validatedClaims, ctx) {
2654
+ async function assignFrames(activeFrames, validatedClaims, ctx, chooseCmabVariants) {
2655
+ var _a;
2655
2656
  const { project, userToken, logger } = ctx;
2656
2657
  const experimentsInfo = [];
2657
2658
  const variantsAndElements = [];
@@ -2677,6 +2678,7 @@ function assignFrames(activeFrames, validatedClaims, ctx) {
2677
2678
  }
2678
2679
  const excludedExperimentIds = resolveLaneWinners(validatedClaims, reachableByLane, project.lanes, userToken);
2679
2680
  const updatedClaims = { ...validatedClaims };
2681
+ const assignmentEntries = [];
2680
2682
  let hasActivatedFrames = false;
2681
2683
  for (const frame of activeFrames) {
2682
2684
  let atLeastOneExperimentWasActivated = false;
@@ -2702,51 +2704,88 @@ function assignFrames(activeFrames, validatedClaims, ctx) {
2702
2704
  const experimentHoldoutInfo = metadata.holdout_info;
2703
2705
  const experimentUniverseVid = chooseVariant(userToken, experimentHoldoutInfo.variant_bins, experimentHoldoutInfo.seed, experimentHoldoutInfo.holdout_variant_id);
2704
2706
  if (experimentUniverseVid === experimentHoldoutInfo.holdout_variant_id) {
2705
- experimentsInfo.push({
2706
- variant_id: experimentHoldoutInfo.holdout_variant_id,
2707
- variant_slug: experimentHoldoutInfo.holdout_variant_slug,
2707
+ assignmentEntries.push({
2708
+ kind: 'info',
2709
+ info: {
2710
+ variant_id: experimentHoldoutInfo.holdout_variant_id,
2711
+ variant_slug: experimentHoldoutInfo.holdout_variant_slug,
2712
+ experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2713
+ experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2714
+ iteration_id: '',
2715
+ level: EXPERIMENT_HOLDOUT,
2716
+ final_state: true,
2717
+ holdout_experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2718
+ holdout_experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2719
+ },
2720
+ });
2721
+ continue;
2722
+ }
2723
+ assignmentEntries.push({
2724
+ kind: 'info',
2725
+ info: {
2726
+ variant_id: experimentHoldoutInfo.treatment_variant_id,
2727
+ variant_slug: experimentHoldoutInfo.treatment_variant_slug,
2708
2728
  experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2709
2729
  experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2710
2730
  iteration_id: '',
2711
2731
  level: EXPERIMENT_HOLDOUT,
2712
- final_state: true,
2732
+ final_state: false,
2713
2733
  holdout_experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2714
2734
  holdout_experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2715
- });
2716
- continue;
2717
- }
2718
- experimentsInfo.push({
2719
- variant_id: experimentHoldoutInfo.treatment_variant_id,
2720
- variant_slug: experimentHoldoutInfo.treatment_variant_slug,
2721
- experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2722
- experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2723
- iteration_id: '',
2724
- level: EXPERIMENT_HOLDOUT,
2725
- final_state: false,
2726
- holdout_experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2727
- holdout_experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2735
+ },
2728
2736
  });
2729
- const variantId = chooseVariant(userToken, metadata.allocation, metadata.seed, '');
2730
- const variant = variants.find((v) => v.id === variantId);
2731
- if (!variant) {
2732
- logger.error(`Variant not found for experiment ${metadata.experiment_id}`, 'variant_not_found', {
2733
- experiment_id: metadata.experiment_id,
2734
- });
2735
- break;
2736
- }
2737
- experimentsInfo.push({
2738
- variant_id: variant.id,
2739
- variant_slug: variant.slug,
2737
+ assignmentEntries.push({ kind: 'variant', metadata, variants, elements });
2738
+ }
2739
+ }
2740
+ const cmabMetadataById = new Map();
2741
+ for (const entry of assignmentEntries) {
2742
+ if (entry.kind === 'variant' && entry.metadata.type === 'CMAB') {
2743
+ cmabMetadataById.set(entry.metadata.experiment_id, entry.metadata);
2744
+ }
2745
+ }
2746
+ let mlVariantIds = new Map();
2747
+ if (chooseCmabVariants && cmabMetadataById.size > 0) {
2748
+ try {
2749
+ mlVariantIds = await chooseCmabVariants(Array.from(cmabMetadataById.values()), userToken);
2750
+ }
2751
+ catch (_b) {
2752
+ mlVariantIds = new Map();
2753
+ }
2754
+ }
2755
+ for (const entry of assignmentEntries) {
2756
+ if (entry.kind === 'info') {
2757
+ experimentsInfo.push(entry.info);
2758
+ continue;
2759
+ }
2760
+ const { metadata, variants, elements } = entry;
2761
+ let mlVariantId = metadata.type === 'CMAB'
2762
+ ? ((_a = mlVariantIds.get(metadata.experiment_id)) !== null && _a !== void 0 ? _a : null)
2763
+ : null;
2764
+ if (mlVariantId && !variants.some((v) => v.id === mlVariantId)) {
2765
+ logger.warn(`CMAB variant ${mlVariantId} not in experiment ${metadata.experiment_id}`, 'cmab_variant_not_found', { experiment_id: metadata.experiment_id });
2766
+ mlVariantId = null;
2767
+ }
2768
+ const variantId = mlVariantId ||
2769
+ chooseVariant(userToken, metadata.allocation, metadata.seed, '');
2770
+ const variant = variants.find((v) => v.id === variantId);
2771
+ if (!variant) {
2772
+ logger.error(`Variant not found for experiment ${metadata.experiment_id}`, 'variant_not_found', {
2740
2773
  experiment_id: metadata.experiment_id,
2741
- experiment_slug: metadata.slug,
2742
- iteration_id: metadata.iteration_id,
2743
- level: EXPERIMENT,
2744
- final_state: true,
2745
- holdout_experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2746
- holdout_experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2747
2774
  });
2748
- variantsAndElements.push({ variant, elements });
2775
+ continue;
2749
2776
  }
2777
+ experimentsInfo.push({
2778
+ variant_id: variant.id,
2779
+ variant_slug: variant.slug,
2780
+ experiment_id: metadata.experiment_id,
2781
+ experiment_slug: metadata.slug,
2782
+ iteration_id: metadata.iteration_id,
2783
+ level: EXPERIMENT,
2784
+ final_state: true,
2785
+ holdout_experiment_id: metadata.holdout_info.holdout_experiment_id,
2786
+ holdout_experiment_slug: metadata.holdout_info.holdout_experiment_slug,
2787
+ });
2788
+ variantsAndElements.push({ variant, elements });
2750
2789
  }
2751
2790
  return {
2752
2791
  variantsAndElements,
@@ -2755,7 +2794,7 @@ function assignFrames(activeFrames, validatedClaims, ctx) {
2755
2794
  laneClaims: updatedClaims,
2756
2795
  };
2757
2796
  }
2758
- function previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback) {
2797
+ async function previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback) {
2759
2798
  const previewRollouts = previewVariantIds.includes(ROLLOUTS_PREVIEW_ID);
2760
2799
  const requestedVariantIds = previewVariantIds.filter((id) => !isPreviewSentinel(id));
2761
2800
  const requestedVariants = findVariants(ctx.project.frames, requestedVariantIds);
@@ -2768,7 +2807,7 @@ function previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback
2768
2807
  window.alert(`[Coframe] Variant preview unavailable: ${previewVariantIds.join(',')}.`);
2769
2808
  }
2770
2809
  const rolledOutVariants = previewRollouts && !previewVariantIds.includes(GLOBAL_HOLDOUT)
2771
- ? assignFrames(keepRolledOutExperiments(activateFrames(ctx)), validatedClaims, ctx).variantsAndElements
2810
+ ? (await assignFrames(keepRolledOutExperiments(activateFrames(ctx)), validatedClaims, ctx)).variantsAndElements
2772
2811
  : [];
2773
2812
  return uniqueByVariantId([...rolledOutVariants, ...requestedVariants]);
2774
2813
  }
@@ -2846,6 +2885,226 @@ function getNavigationType(isInitialApply) {
2846
2885
  return undefined;
2847
2886
  }
2848
2887
 
2888
+ const CMAB_ASSIGNMENT_TIMEOUT_MS = 500;
2889
+ const CMAB_ASSIGNMENT_MAX_EXPERIMENTS = 64;
2890
+ const ASSIGNMENT_REQUEST_SCHEMA = 'coframe.edge.assign-request/2';
2891
+ const ASSIGNMENT_RESPONSE_SCHEMA = 'coframe.edge.assign-response/2';
2892
+ async function requestAssignmentChunk(options, metadata, userToken) {
2893
+ var _a, _b, _c, _d, _e;
2894
+ const assignments = new Map();
2895
+ const decisions = new Map();
2896
+ let fallbackReason = 'request_failed';
2897
+ let timeoutId;
2898
+ let cancel;
2899
+ try {
2900
+ if (options.disableRemoteAssignment) {
2901
+ fallbackReason = 'network_policy';
2902
+ return { assignments, decisions };
2903
+ }
2904
+ const controller = new AbortController();
2905
+ const budget = Date.now() + CMAB_ASSIGNMENT_TIMEOUT_MS;
2906
+ const deadline = options.deadline !== undefined && options.deadline < budget
2907
+ ? options.deadline
2908
+ : budget;
2909
+ const remaining = deadline - Date.now();
2910
+ if (((_a = options.signal) === null || _a === void 0 ? void 0 : _a.aborted) || remaining <= 0) {
2911
+ fallbackReason = ((_b = options.signal) === null || _b === void 0 ? void 0 : _b.aborted)
2912
+ ? 'cancelled'
2913
+ : 'deadline_exceeded';
2914
+ return { assignments, decisions };
2915
+ }
2916
+ const cancelled = new Promise((resolve) => {
2917
+ var _a;
2918
+ cancel = () => {
2919
+ var _a;
2920
+ fallbackReason = ((_a = options.signal) === null || _a === void 0 ? void 0 : _a.aborted)
2921
+ ? 'cancelled'
2922
+ : 'deadline_exceeded';
2923
+ resolve(null);
2924
+ controller.abort();
2925
+ };
2926
+ timeoutId = setTimeout(cancel, remaining);
2927
+ (_a = options.signal) === null || _a === void 0 ? void 0 : _a.addEventListener('abort', cancel);
2928
+ });
2929
+ const userContext = sanitizeUserContext(options.userContext);
2930
+ const pending = fetch(`${"https://edge.cofra.me"}/api/v2/assign`, {
2931
+ method: 'POST',
2932
+ credentials: 'omit',
2933
+ headers: { 'Content-Type': 'application/json' },
2934
+ signal: controller.signal,
2935
+ body: JSON.stringify({
2936
+ schema: ASSIGNMENT_REQUEST_SCHEMA,
2937
+ project_id: options.projectId,
2938
+ user_token: userToken,
2939
+ current_url: options.currentUrl,
2940
+ referrer: options.referrer,
2941
+ experiment_ids: metadata.map((item) => item.experiment_id),
2942
+ ...(userContext ? { user_context: userContext } : {}),
2943
+ }),
2944
+ }).then((response) => {
2945
+ if (!response.ok)
2946
+ return null;
2947
+ fallbackReason = 'invalid_response';
2948
+ return response.json();
2949
+ });
2950
+ const payload = await Promise.race([pending, cancelled]);
2951
+ if (controller.signal.aborted ||
2952
+ ((_c = options.signal) === null || _c === void 0 ? void 0 : _c.aborted) ||
2953
+ Date.now() >= deadline) {
2954
+ fallbackReason = ((_d = options.signal) === null || _d === void 0 ? void 0 : _d.aborted)
2955
+ ? 'cancelled'
2956
+ : 'deadline_exceeded';
2957
+ return { assignments, decisions };
2958
+ }
2959
+ if ((payload === null || payload === void 0 ? void 0 : payload.schema) !== ASSIGNMENT_RESPONSE_SCHEMA ||
2960
+ !Array.isArray(payload.assignments)) {
2961
+ return { assignments, decisions };
2962
+ }
2963
+ const requested = new Map(metadata.map((item) => [item.experiment_id, item]));
2964
+ const seen = new Set();
2965
+ for (const row of payload.assignments) {
2966
+ if (!row || typeof row.experiment_id !== 'string')
2967
+ continue;
2968
+ const id = row.experiment_id;
2969
+ const expected = requested.get(id);
2970
+ if (!expected)
2971
+ continue;
2972
+ if (seen.has(id)) {
2973
+ assignments.delete(id);
2974
+ decisions.delete(id);
2975
+ continue;
2976
+ }
2977
+ seen.add(id);
2978
+ if (typeof row.iteration_id !== 'string' ||
2979
+ row.iteration_id !== expected.iteration_id ||
2980
+ (row.variant_id !== null && typeof row.variant_id !== 'string'))
2981
+ continue;
2982
+ assignments.set(id, row.variant_id);
2983
+ const decision = {
2984
+ experiment_id: id,
2985
+ iteration_id: row.iteration_id,
2986
+ variant_id: row.variant_id,
2987
+ };
2988
+ for (const key of [
2989
+ 'decision_source',
2990
+ 'fallback_reason',
2991
+ 'model_id',
2992
+ 'feature_spec_version',
2993
+ 'policy_id',
2994
+ 'policy_version',
2995
+ 'policy_route',
2996
+ 'active_model_failure',
2997
+ 'model_load_state',
2998
+ 'scored_by',
2999
+ ]) {
3000
+ if (typeof row[key] === 'string' && row[key].length <= 256)
3001
+ decision[key] = row[key];
3002
+ }
3003
+ if (typeof payload.assignment_request_id === 'string' &&
3004
+ payload.assignment_request_id.length <= 256) {
3005
+ decision.assignment_request_id = payload.assignment_request_id;
3006
+ }
3007
+ if (typeof row.assignment_probability === 'number' &&
3008
+ row.assignment_probability >= 0 &&
3009
+ row.assignment_probability <= 1) {
3010
+ decision.assignment_probability = row.assignment_probability;
3011
+ }
3012
+ if (typeof row.decision_latency_ms === 'number' &&
3013
+ row.decision_latency_ms >= 0 &&
3014
+ row.decision_latency_ms < Infinity) {
3015
+ decision.decision_latency_ms = row.decision_latency_ms;
3016
+ }
3017
+ if (Array.isArray(row.eligible_variant_ids) &&
3018
+ row.eligible_variant_ids.length <= 256 &&
3019
+ row.eligible_variant_ids.every((id) => typeof id === 'string' && id.length <= 256)) {
3020
+ decision.eligible_variant_ids = row.eligible_variant_ids;
3021
+ }
3022
+ if (row.action_probabilities &&
3023
+ typeof row.action_probabilities === 'object' &&
3024
+ !Array.isArray(row.action_probabilities)) {
3025
+ const keys = Object.keys(row.action_probabilities);
3026
+ if (keys.length <= 256 &&
3027
+ keys.every((key) => key.length <= 256 &&
3028
+ typeof row.action_probabilities[key] === 'number' &&
3029
+ row.action_probabilities[key] >= 0 &&
3030
+ row.action_probabilities[key] <= 1)) {
3031
+ decision.action_probabilities = row.action_probabilities;
3032
+ }
3033
+ }
3034
+ decisions.set(id, decision);
3035
+ }
3036
+ return { assignments, decisions };
3037
+ }
3038
+ catch (_f) {
3039
+ return { assignments, decisions };
3040
+ }
3041
+ finally {
3042
+ if (timeoutId !== undefined)
3043
+ clearTimeout(timeoutId);
3044
+ if (cancel)
3045
+ (_e = options.signal) === null || _e === void 0 ? void 0 : _e.removeEventListener('abort', cancel);
3046
+ for (const item of metadata) {
3047
+ if (!decisions.has(item.experiment_id))
3048
+ decisions.set(item.experiment_id, {
3049
+ experiment_id: item.experiment_id,
3050
+ iteration_id: item.iteration_id,
3051
+ variant_id: null,
3052
+ decision_source: 'client_fallback',
3053
+ fallback_reason: fallbackReason,
3054
+ });
3055
+ }
3056
+ }
3057
+ }
3058
+ function createCmabVariantBatchChooser(options) {
3059
+ const decisions = new Map();
3060
+ const choose = async (metadata, userToken) => {
3061
+ decisions.clear();
3062
+ const uniqueMetadata = Array.from(new Map(metadata.map((item) => [item.experiment_id, item])).values());
3063
+ const chunks = [];
3064
+ for (let index = 0; index < uniqueMetadata.length; index += CMAB_ASSIGNMENT_MAX_EXPERIMENTS) {
3065
+ chunks.push(uniqueMetadata.slice(index, index + CMAB_ASSIGNMENT_MAX_EXPERIMENTS));
3066
+ }
3067
+ const results = await Promise.all(chunks.map((chunk) => requestAssignmentChunk(options, chunk, userToken)));
3068
+ const assignments = new Map();
3069
+ for (const result of results) {
3070
+ result.assignments.forEach((value, id) => {
3071
+ assignments.set(id, value);
3072
+ });
3073
+ result.decisions.forEach((value, id) => {
3074
+ decisions.set(id, value);
3075
+ });
3076
+ }
3077
+ return assignments;
3078
+ };
3079
+ choose.getDecisions = (experiments) => {
3080
+ const selected = [];
3081
+ for (const info of experiments) {
3082
+ const decision = decisions.get(info.experiment_id);
3083
+ if (!decision ||
3084
+ decision.iteration_id !== info.iteration_id ||
3085
+ !info.final_state)
3086
+ continue;
3087
+ if (decision.variant_id === info.variant_id)
3088
+ selected.push(decision);
3089
+ else
3090
+ selected.push({
3091
+ experiment_id: info.experiment_id,
3092
+ iteration_id: info.iteration_id,
3093
+ variant_id: info.variant_id,
3094
+ decision_source: 'client_fallback',
3095
+ fallback_reason: decision.variant_id === null
3096
+ ? decision.fallback_reason || 'missing_choice'
3097
+ : 'unsupported_variant',
3098
+ ...(decision.assignment_request_id
3099
+ ? { assignment_request_id: decision.assignment_request_id }
3100
+ : {}),
3101
+ });
3102
+ }
3103
+ return selected;
3104
+ };
3105
+ return choose;
3106
+ }
3107
+
2849
3108
  const CACHE_TTL_MS = 60 * 1000;
2850
3109
  const cache = new Map();
2851
3110
  function getCachedFrames(url) {
@@ -2972,7 +3231,7 @@ function observeUrlChanges(baseUrlMatch, onUrlChange) {
2972
3231
  patchHistoryMethod('replaceState');
2973
3232
  }
2974
3233
 
2975
- function startSession(Coframe, cookiesEnabled, executionStartTime, personalizationHeaders, sessionContext) {
3234
+ function startSession(Coframe, cookiesEnabled, executionStartTime, personalizationHeaders, sessionContext, pageUrl = getCurrentPageUrl()) {
2976
3235
  const sessionPayload = [
2977
3236
  Coframe,
2978
3237
  Coframe.orgId,
@@ -2985,7 +3244,7 @@ function startSession(Coframe, cookiesEnabled, executionStartTime, personalizati
2985
3244
  getBrowserPlatform(),
2986
3245
  getBrowserLanguages(),
2987
3246
  getBrowserUserAgent(),
2988
- getCurrentPageUrl(),
3247
+ pageUrl,
2989
3248
  getBrowserFignerprintInfo(),
2990
3249
  ];
2991
3250
  {
@@ -3375,12 +3634,14 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
3375
3634
  let navigationAbortController = null;
3376
3635
  observeUrlChanges(false, async (newPageUrl, _oldPageUrl) => {
3377
3636
  var _a;
3378
- await new Promise((resolve) => requestAnimationFrame(resolve));
3379
- infiniteLoopProtection = 0;
3380
3637
  if (navigationAbortController)
3381
3638
  navigationAbortController.abort();
3382
3639
  navigationAbortController = new AbortController();
3383
3640
  const { signal } = navigationAbortController;
3641
+ await new Promise((resolve) => requestAnimationFrame(resolve));
3642
+ if (signal.aborted)
3643
+ return;
3644
+ infiniteLoopProtection = 0;
3384
3645
  let pageProject = project;
3385
3646
  let pagePersonalizationHeaders = Coframe.personalizationHeaders;
3386
3647
  const cached = getCachedFrames(newPageUrl);
@@ -3408,7 +3669,7 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
3408
3669
  }
3409
3670
  if (signal.aborted)
3410
3671
  return;
3411
- await applyVariants(newPageUrl, pageProject, pagePersonalizationHeaders);
3672
+ await applyVariants(newPageUrl, pageProject, pagePersonalizationHeaders, signal);
3412
3673
  });
3413
3674
  }
3414
3675
  function readLaneClaims() {
@@ -3441,15 +3702,33 @@ function persistLaneClaims(claims, logger) {
3441
3702
  }
3442
3703
  function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore) {
3443
3704
  let isFirstApply = true;
3444
- return async (pageUrl, pageProject, personalizationHeaders) => {
3705
+ let applicationId = 0;
3706
+ return async (pageUrl, pageProject, personalizationHeaders, signal, deadline) => {
3707
+ var _a;
3708
+ const currentApplication = ++applicationId;
3709
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted)
3710
+ return;
3445
3711
  const sessionId = isFirstApply ? Coframe.sessionId : generateUUID();
3446
3712
  const sessionContext = buildSessionContext(isFirstApply);
3447
3713
  isFirstApply = false;
3714
+ const preview = resolvePreview(pageUrl, previewVariantId, isSessionVariant);
3715
+ const chooseCmabVariants = createCmabVariantBatchChooser({
3716
+ projectId: Coframe.projectId,
3717
+ currentUrl: pageUrl,
3718
+ referrer: (_a = config.referrer) !== null && _a !== void 0 ? _a : getBrowserReferrer(),
3719
+ signal,
3720
+ deadline,
3721
+ disableRemoteAssignment: Boolean(config.projectUrl || config.ingestUrl),
3722
+ userContext: config.userContext,
3723
+ });
3724
+ const { variantsAndElements, experimentsInfo, hasActivatedFrames, laneClaims, } = await getVariantsAndElements(personalizationHeaders, pageProject, preview.previewVariantId, Coframe.userToken, pageUrl, getBrowserLanguages(), getBrowserPlatform(), Coframe.logger, config.nonce, laneStore.claims, preview.isSessionVariant, chooseCmabVariants);
3725
+ if ((signal === null || signal === void 0 ? void 0 : signal.aborted) ||
3726
+ currentApplication !== applicationId ||
3727
+ (signal && !urlsMatch(pageUrl, getCurrentPageUrl())))
3728
+ return;
3448
3729
  Coframe.sessionId = sessionId;
3449
3730
  Coframe.personalizationHeaders = personalizationHeaders;
3450
- const preview = resolvePreview(pageUrl, previewVariantId, isSessionVariant);
3451
3731
  setIsPreviewVariant(!!preview.previewVariantId);
3452
- const { variantsAndElements, experimentsInfo, hasActivatedFrames, laneClaims, } = await getVariantsAndElements(personalizationHeaders, pageProject, preview.previewVariantId, Coframe.userToken, pageUrl, getBrowserLanguages(), getBrowserPlatform(), Coframe.logger, config.nonce, laneStore.claims, preview.isSessionVariant);
3453
3732
  laneStore.claims = laneClaims;
3454
3733
  persistLaneClaims(laneClaims, Coframe.logger);
3455
3734
  Coframe.experimentInfo = experimentsInfo;
@@ -3466,7 +3745,11 @@ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant
3466
3745
  else {
3467
3746
  replaceVariants(variantsAndElements, Coframe.orgId, Coframe.logger, config.nonce);
3468
3747
  }
3469
- startSession(Coframe, isCookiesEnabled, executionStartTime, personalizationHeaders, sessionContext);
3748
+ const cmabDecisions = chooseCmabVariants.getDecisions(experimentsInfo);
3749
+ startSession(Coframe, isCookiesEnabled, executionStartTime, personalizationHeaders, {
3750
+ ...sessionContext,
3751
+ ...(cmabDecisions.length ? { cmab_decisions: cmabDecisions } : {}),
3752
+ }, pageUrl);
3470
3753
  analytics.resetBroadcasted();
3471
3754
  analytics.sendAnalytics();
3472
3755
  };
@@ -3484,7 +3767,7 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
3484
3767
  return;
3485
3768
  const laneStore = { claims: readLaneClaims() };
3486
3769
  const applyVariants = createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore);
3487
- await applyVariants(getCurrentPageUrl(), cdnProject, personalizationHeaders);
3770
+ await applyVariants(getCurrentPageUrl(), cdnProject, personalizationHeaders, undefined, (integrationStartTime || executionStartTime).getTime() + timeoutMs);
3488
3771
  if (!Coframe.variantsAndElements ||
3489
3772
  Coframe.variantsAndElements.length === 0) {
3490
3773
  revealPage();
@@ -1077,7 +1077,7 @@ function warnEventsNotSent() {
1077
1077
  }
1078
1078
  }
1079
1079
 
1080
- const SDK_VERSION = '2.7.0';
1080
+ const SDK_VERSION = '2.8.0';
1081
1081
  const queuedEvents = [];
1082
1082
  const queuedMonitoringEvents = [];
1083
1083
  const queuedSessions = [];
@@ -2565,7 +2565,7 @@ function checkAllAudiences(metadata, ctx) {
2565
2565
  }
2566
2566
  return result;
2567
2567
  }
2568
- async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}, silentFallback = false) {
2568
+ async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}, silentFallback = false, chooseCmabVariants) {
2569
2569
  var _a;
2570
2570
  const experimentsInfo = [];
2571
2571
  const cloakBot = !!((_a = project.settings) === null || _a === void 0 ? void 0 : _a.bot_cloaking_enabled) &&
@@ -2593,7 +2593,7 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2593
2593
  const validatedClaims = validateClaims(laneClaims, project.lanes);
2594
2594
  const previewVariantIds = splitPreviewVariantIds(previewVariantId);
2595
2595
  if (previewVariantIds.length > 0) {
2596
- const variantsAndElements = previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback);
2596
+ const variantsAndElements = await previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback);
2597
2597
  if (variantsAndElements) {
2598
2598
  return buildResponse({
2599
2599
  variantsAndElements,
@@ -2633,7 +2633,7 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
2633
2633
  final_state: false,
2634
2634
  level: GLOBAL_HOLDOUT,
2635
2635
  });
2636
- const assignment = assignFrames(activeFrames, validatedClaims, ctx);
2636
+ const assignment = await assignFrames(activeFrames, validatedClaims, ctx, chooseCmabVariants);
2637
2637
  return buildResponse({
2638
2638
  ...assignment,
2639
2639
  experimentsInfo: [...experimentsInfo, ...assignment.experimentsInfo],
@@ -2649,7 +2649,8 @@ function activateFrames(ctx) {
2649
2649
  return true;
2650
2650
  });
2651
2651
  }
2652
- function assignFrames(activeFrames, validatedClaims, ctx) {
2652
+ async function assignFrames(activeFrames, validatedClaims, ctx, chooseCmabVariants) {
2653
+ var _a;
2653
2654
  const { project, userToken, logger } = ctx;
2654
2655
  const experimentsInfo = [];
2655
2656
  const variantsAndElements = [];
@@ -2675,6 +2676,7 @@ function assignFrames(activeFrames, validatedClaims, ctx) {
2675
2676
  }
2676
2677
  const excludedExperimentIds = resolveLaneWinners(validatedClaims, reachableByLane, project.lanes, userToken);
2677
2678
  const updatedClaims = { ...validatedClaims };
2679
+ const assignmentEntries = [];
2678
2680
  let hasActivatedFrames = false;
2679
2681
  for (const frame of activeFrames) {
2680
2682
  let atLeastOneExperimentWasActivated = false;
@@ -2700,51 +2702,88 @@ function assignFrames(activeFrames, validatedClaims, ctx) {
2700
2702
  const experimentHoldoutInfo = metadata.holdout_info;
2701
2703
  const experimentUniverseVid = chooseVariant(userToken, experimentHoldoutInfo.variant_bins, experimentHoldoutInfo.seed, experimentHoldoutInfo.holdout_variant_id);
2702
2704
  if (experimentUniverseVid === experimentHoldoutInfo.holdout_variant_id) {
2703
- experimentsInfo.push({
2704
- variant_id: experimentHoldoutInfo.holdout_variant_id,
2705
- variant_slug: experimentHoldoutInfo.holdout_variant_slug,
2705
+ assignmentEntries.push({
2706
+ kind: 'info',
2707
+ info: {
2708
+ variant_id: experimentHoldoutInfo.holdout_variant_id,
2709
+ variant_slug: experimentHoldoutInfo.holdout_variant_slug,
2710
+ experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2711
+ experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2712
+ iteration_id: '',
2713
+ level: EXPERIMENT_HOLDOUT,
2714
+ final_state: true,
2715
+ holdout_experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2716
+ holdout_experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2717
+ },
2718
+ });
2719
+ continue;
2720
+ }
2721
+ assignmentEntries.push({
2722
+ kind: 'info',
2723
+ info: {
2724
+ variant_id: experimentHoldoutInfo.treatment_variant_id,
2725
+ variant_slug: experimentHoldoutInfo.treatment_variant_slug,
2706
2726
  experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2707
2727
  experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2708
2728
  iteration_id: '',
2709
2729
  level: EXPERIMENT_HOLDOUT,
2710
- final_state: true,
2730
+ final_state: false,
2711
2731
  holdout_experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2712
2732
  holdout_experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2713
- });
2714
- continue;
2715
- }
2716
- experimentsInfo.push({
2717
- variant_id: experimentHoldoutInfo.treatment_variant_id,
2718
- variant_slug: experimentHoldoutInfo.treatment_variant_slug,
2719
- experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2720
- experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2721
- iteration_id: '',
2722
- level: EXPERIMENT_HOLDOUT,
2723
- final_state: false,
2724
- holdout_experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2725
- holdout_experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2733
+ },
2726
2734
  });
2727
- const variantId = chooseVariant(userToken, metadata.allocation, metadata.seed, '');
2728
- const variant = variants.find((v) => v.id === variantId);
2729
- if (!variant) {
2730
- logger.error(`Variant not found for experiment ${metadata.experiment_id}`, 'variant_not_found', {
2731
- experiment_id: metadata.experiment_id,
2732
- });
2733
- break;
2734
- }
2735
- experimentsInfo.push({
2736
- variant_id: variant.id,
2737
- variant_slug: variant.slug,
2735
+ assignmentEntries.push({ kind: 'variant', metadata, variants, elements });
2736
+ }
2737
+ }
2738
+ const cmabMetadataById = new Map();
2739
+ for (const entry of assignmentEntries) {
2740
+ if (entry.kind === 'variant' && entry.metadata.type === 'CMAB') {
2741
+ cmabMetadataById.set(entry.metadata.experiment_id, entry.metadata);
2742
+ }
2743
+ }
2744
+ let mlVariantIds = new Map();
2745
+ if (chooseCmabVariants && cmabMetadataById.size > 0) {
2746
+ try {
2747
+ mlVariantIds = await chooseCmabVariants(Array.from(cmabMetadataById.values()), userToken);
2748
+ }
2749
+ catch (_b) {
2750
+ mlVariantIds = new Map();
2751
+ }
2752
+ }
2753
+ for (const entry of assignmentEntries) {
2754
+ if (entry.kind === 'info') {
2755
+ experimentsInfo.push(entry.info);
2756
+ continue;
2757
+ }
2758
+ const { metadata, variants, elements } = entry;
2759
+ let mlVariantId = metadata.type === 'CMAB'
2760
+ ? ((_a = mlVariantIds.get(metadata.experiment_id)) !== null && _a !== void 0 ? _a : null)
2761
+ : null;
2762
+ if (mlVariantId && !variants.some((v) => v.id === mlVariantId)) {
2763
+ logger.warn(`CMAB variant ${mlVariantId} not in experiment ${metadata.experiment_id}`, 'cmab_variant_not_found', { experiment_id: metadata.experiment_id });
2764
+ mlVariantId = null;
2765
+ }
2766
+ const variantId = mlVariantId ||
2767
+ chooseVariant(userToken, metadata.allocation, metadata.seed, '');
2768
+ const variant = variants.find((v) => v.id === variantId);
2769
+ if (!variant) {
2770
+ logger.error(`Variant not found for experiment ${metadata.experiment_id}`, 'variant_not_found', {
2738
2771
  experiment_id: metadata.experiment_id,
2739
- experiment_slug: metadata.slug,
2740
- iteration_id: metadata.iteration_id,
2741
- level: EXPERIMENT,
2742
- final_state: true,
2743
- holdout_experiment_id: experimentHoldoutInfo.holdout_experiment_id,
2744
- holdout_experiment_slug: experimentHoldoutInfo.holdout_experiment_slug,
2745
2772
  });
2746
- variantsAndElements.push({ variant, elements });
2773
+ continue;
2747
2774
  }
2775
+ experimentsInfo.push({
2776
+ variant_id: variant.id,
2777
+ variant_slug: variant.slug,
2778
+ experiment_id: metadata.experiment_id,
2779
+ experiment_slug: metadata.slug,
2780
+ iteration_id: metadata.iteration_id,
2781
+ level: EXPERIMENT,
2782
+ final_state: true,
2783
+ holdout_experiment_id: metadata.holdout_info.holdout_experiment_id,
2784
+ holdout_experiment_slug: metadata.holdout_info.holdout_experiment_slug,
2785
+ });
2786
+ variantsAndElements.push({ variant, elements });
2748
2787
  }
2749
2788
  return {
2750
2789
  variantsAndElements,
@@ -2753,7 +2792,7 @@ function assignFrames(activeFrames, validatedClaims, ctx) {
2753
2792
  laneClaims: updatedClaims,
2754
2793
  };
2755
2794
  }
2756
- function previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback) {
2795
+ async function previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback) {
2757
2796
  const previewRollouts = previewVariantIds.includes(ROLLOUTS_PREVIEW_ID);
2758
2797
  const requestedVariantIds = previewVariantIds.filter((id) => !isPreviewSentinel(id));
2759
2798
  const requestedVariants = findVariants(ctx.project.frames, requestedVariantIds);
@@ -2766,7 +2805,7 @@ function previewVariants(previewVariantIds, validatedClaims, ctx, silentFallback
2766
2805
  window.alert(`[Coframe] Variant preview unavailable: ${previewVariantIds.join(',')}.`);
2767
2806
  }
2768
2807
  const rolledOutVariants = previewRollouts && !previewVariantIds.includes(GLOBAL_HOLDOUT)
2769
- ? assignFrames(keepRolledOutExperiments(activateFrames(ctx)), validatedClaims, ctx).variantsAndElements
2808
+ ? (await assignFrames(keepRolledOutExperiments(activateFrames(ctx)), validatedClaims, ctx)).variantsAndElements
2770
2809
  : [];
2771
2810
  return uniqueByVariantId([...rolledOutVariants, ...requestedVariants]);
2772
2811
  }
@@ -2844,6 +2883,226 @@ function getNavigationType(isInitialApply) {
2844
2883
  return undefined;
2845
2884
  }
2846
2885
 
2886
+ const CMAB_ASSIGNMENT_TIMEOUT_MS = 500;
2887
+ const CMAB_ASSIGNMENT_MAX_EXPERIMENTS = 64;
2888
+ const ASSIGNMENT_REQUEST_SCHEMA = 'coframe.edge.assign-request/2';
2889
+ const ASSIGNMENT_RESPONSE_SCHEMA = 'coframe.edge.assign-response/2';
2890
+ async function requestAssignmentChunk(options, metadata, userToken) {
2891
+ var _a, _b, _c, _d, _e;
2892
+ const assignments = new Map();
2893
+ const decisions = new Map();
2894
+ let fallbackReason = 'request_failed';
2895
+ let timeoutId;
2896
+ let cancel;
2897
+ try {
2898
+ if (options.disableRemoteAssignment) {
2899
+ fallbackReason = 'network_policy';
2900
+ return { assignments, decisions };
2901
+ }
2902
+ const controller = new AbortController();
2903
+ const budget = Date.now() + CMAB_ASSIGNMENT_TIMEOUT_MS;
2904
+ const deadline = options.deadline !== undefined && options.deadline < budget
2905
+ ? options.deadline
2906
+ : budget;
2907
+ const remaining = deadline - Date.now();
2908
+ if (((_a = options.signal) === null || _a === void 0 ? void 0 : _a.aborted) || remaining <= 0) {
2909
+ fallbackReason = ((_b = options.signal) === null || _b === void 0 ? void 0 : _b.aborted)
2910
+ ? 'cancelled'
2911
+ : 'deadline_exceeded';
2912
+ return { assignments, decisions };
2913
+ }
2914
+ const cancelled = new Promise((resolve) => {
2915
+ var _a;
2916
+ cancel = () => {
2917
+ var _a;
2918
+ fallbackReason = ((_a = options.signal) === null || _a === void 0 ? void 0 : _a.aborted)
2919
+ ? 'cancelled'
2920
+ : 'deadline_exceeded';
2921
+ resolve(null);
2922
+ controller.abort();
2923
+ };
2924
+ timeoutId = setTimeout(cancel, remaining);
2925
+ (_a = options.signal) === null || _a === void 0 ? void 0 : _a.addEventListener('abort', cancel);
2926
+ });
2927
+ const userContext = sanitizeUserContext(options.userContext);
2928
+ const pending = fetch(`${"https://edge.cofra.me"}/api/v2/assign`, {
2929
+ method: 'POST',
2930
+ credentials: 'omit',
2931
+ headers: { 'Content-Type': 'application/json' },
2932
+ signal: controller.signal,
2933
+ body: JSON.stringify({
2934
+ schema: ASSIGNMENT_REQUEST_SCHEMA,
2935
+ project_id: options.projectId,
2936
+ user_token: userToken,
2937
+ current_url: options.currentUrl,
2938
+ referrer: options.referrer,
2939
+ experiment_ids: metadata.map((item) => item.experiment_id),
2940
+ ...(userContext ? { user_context: userContext } : {}),
2941
+ }),
2942
+ }).then((response) => {
2943
+ if (!response.ok)
2944
+ return null;
2945
+ fallbackReason = 'invalid_response';
2946
+ return response.json();
2947
+ });
2948
+ const payload = await Promise.race([pending, cancelled]);
2949
+ if (controller.signal.aborted ||
2950
+ ((_c = options.signal) === null || _c === void 0 ? void 0 : _c.aborted) ||
2951
+ Date.now() >= deadline) {
2952
+ fallbackReason = ((_d = options.signal) === null || _d === void 0 ? void 0 : _d.aborted)
2953
+ ? 'cancelled'
2954
+ : 'deadline_exceeded';
2955
+ return { assignments, decisions };
2956
+ }
2957
+ if ((payload === null || payload === void 0 ? void 0 : payload.schema) !== ASSIGNMENT_RESPONSE_SCHEMA ||
2958
+ !Array.isArray(payload.assignments)) {
2959
+ return { assignments, decisions };
2960
+ }
2961
+ const requested = new Map(metadata.map((item) => [item.experiment_id, item]));
2962
+ const seen = new Set();
2963
+ for (const row of payload.assignments) {
2964
+ if (!row || typeof row.experiment_id !== 'string')
2965
+ continue;
2966
+ const id = row.experiment_id;
2967
+ const expected = requested.get(id);
2968
+ if (!expected)
2969
+ continue;
2970
+ if (seen.has(id)) {
2971
+ assignments.delete(id);
2972
+ decisions.delete(id);
2973
+ continue;
2974
+ }
2975
+ seen.add(id);
2976
+ if (typeof row.iteration_id !== 'string' ||
2977
+ row.iteration_id !== expected.iteration_id ||
2978
+ (row.variant_id !== null && typeof row.variant_id !== 'string'))
2979
+ continue;
2980
+ assignments.set(id, row.variant_id);
2981
+ const decision = {
2982
+ experiment_id: id,
2983
+ iteration_id: row.iteration_id,
2984
+ variant_id: row.variant_id,
2985
+ };
2986
+ for (const key of [
2987
+ 'decision_source',
2988
+ 'fallback_reason',
2989
+ 'model_id',
2990
+ 'feature_spec_version',
2991
+ 'policy_id',
2992
+ 'policy_version',
2993
+ 'policy_route',
2994
+ 'active_model_failure',
2995
+ 'model_load_state',
2996
+ 'scored_by',
2997
+ ]) {
2998
+ if (typeof row[key] === 'string' && row[key].length <= 256)
2999
+ decision[key] = row[key];
3000
+ }
3001
+ if (typeof payload.assignment_request_id === 'string' &&
3002
+ payload.assignment_request_id.length <= 256) {
3003
+ decision.assignment_request_id = payload.assignment_request_id;
3004
+ }
3005
+ if (typeof row.assignment_probability === 'number' &&
3006
+ row.assignment_probability >= 0 &&
3007
+ row.assignment_probability <= 1) {
3008
+ decision.assignment_probability = row.assignment_probability;
3009
+ }
3010
+ if (typeof row.decision_latency_ms === 'number' &&
3011
+ row.decision_latency_ms >= 0 &&
3012
+ row.decision_latency_ms < Infinity) {
3013
+ decision.decision_latency_ms = row.decision_latency_ms;
3014
+ }
3015
+ if (Array.isArray(row.eligible_variant_ids) &&
3016
+ row.eligible_variant_ids.length <= 256 &&
3017
+ row.eligible_variant_ids.every((id) => typeof id === 'string' && id.length <= 256)) {
3018
+ decision.eligible_variant_ids = row.eligible_variant_ids;
3019
+ }
3020
+ if (row.action_probabilities &&
3021
+ typeof row.action_probabilities === 'object' &&
3022
+ !Array.isArray(row.action_probabilities)) {
3023
+ const keys = Object.keys(row.action_probabilities);
3024
+ if (keys.length <= 256 &&
3025
+ keys.every((key) => key.length <= 256 &&
3026
+ typeof row.action_probabilities[key] === 'number' &&
3027
+ row.action_probabilities[key] >= 0 &&
3028
+ row.action_probabilities[key] <= 1)) {
3029
+ decision.action_probabilities = row.action_probabilities;
3030
+ }
3031
+ }
3032
+ decisions.set(id, decision);
3033
+ }
3034
+ return { assignments, decisions };
3035
+ }
3036
+ catch (_f) {
3037
+ return { assignments, decisions };
3038
+ }
3039
+ finally {
3040
+ if (timeoutId !== undefined)
3041
+ clearTimeout(timeoutId);
3042
+ if (cancel)
3043
+ (_e = options.signal) === null || _e === void 0 ? void 0 : _e.removeEventListener('abort', cancel);
3044
+ for (const item of metadata) {
3045
+ if (!decisions.has(item.experiment_id))
3046
+ decisions.set(item.experiment_id, {
3047
+ experiment_id: item.experiment_id,
3048
+ iteration_id: item.iteration_id,
3049
+ variant_id: null,
3050
+ decision_source: 'client_fallback',
3051
+ fallback_reason: fallbackReason,
3052
+ });
3053
+ }
3054
+ }
3055
+ }
3056
+ function createCmabVariantBatchChooser(options) {
3057
+ const decisions = new Map();
3058
+ const choose = async (metadata, userToken) => {
3059
+ decisions.clear();
3060
+ const uniqueMetadata = Array.from(new Map(metadata.map((item) => [item.experiment_id, item])).values());
3061
+ const chunks = [];
3062
+ for (let index = 0; index < uniqueMetadata.length; index += CMAB_ASSIGNMENT_MAX_EXPERIMENTS) {
3063
+ chunks.push(uniqueMetadata.slice(index, index + CMAB_ASSIGNMENT_MAX_EXPERIMENTS));
3064
+ }
3065
+ const results = await Promise.all(chunks.map((chunk) => requestAssignmentChunk(options, chunk, userToken)));
3066
+ const assignments = new Map();
3067
+ for (const result of results) {
3068
+ result.assignments.forEach((value, id) => {
3069
+ assignments.set(id, value);
3070
+ });
3071
+ result.decisions.forEach((value, id) => {
3072
+ decisions.set(id, value);
3073
+ });
3074
+ }
3075
+ return assignments;
3076
+ };
3077
+ choose.getDecisions = (experiments) => {
3078
+ const selected = [];
3079
+ for (const info of experiments) {
3080
+ const decision = decisions.get(info.experiment_id);
3081
+ if (!decision ||
3082
+ decision.iteration_id !== info.iteration_id ||
3083
+ !info.final_state)
3084
+ continue;
3085
+ if (decision.variant_id === info.variant_id)
3086
+ selected.push(decision);
3087
+ else
3088
+ selected.push({
3089
+ experiment_id: info.experiment_id,
3090
+ iteration_id: info.iteration_id,
3091
+ variant_id: info.variant_id,
3092
+ decision_source: 'client_fallback',
3093
+ fallback_reason: decision.variant_id === null
3094
+ ? decision.fallback_reason || 'missing_choice'
3095
+ : 'unsupported_variant',
3096
+ ...(decision.assignment_request_id
3097
+ ? { assignment_request_id: decision.assignment_request_id }
3098
+ : {}),
3099
+ });
3100
+ }
3101
+ return selected;
3102
+ };
3103
+ return choose;
3104
+ }
3105
+
2847
3106
  const CACHE_TTL_MS = 60 * 1000;
2848
3107
  const cache = new Map();
2849
3108
  function getCachedFrames(url) {
@@ -2970,7 +3229,7 @@ function observeUrlChanges(baseUrlMatch, onUrlChange) {
2970
3229
  patchHistoryMethod('replaceState');
2971
3230
  }
2972
3231
 
2973
- function startSession(Coframe, cookiesEnabled, executionStartTime, personalizationHeaders, sessionContext) {
3232
+ function startSession(Coframe, cookiesEnabled, executionStartTime, personalizationHeaders, sessionContext, pageUrl = getCurrentPageUrl()) {
2974
3233
  const sessionPayload = [
2975
3234
  Coframe,
2976
3235
  Coframe.orgId,
@@ -2983,7 +3242,7 @@ function startSession(Coframe, cookiesEnabled, executionStartTime, personalizati
2983
3242
  getBrowserPlatform(),
2984
3243
  getBrowserLanguages(),
2985
3244
  getBrowserUserAgent(),
2986
- getCurrentPageUrl(),
3245
+ pageUrl,
2987
3246
  getBrowserFignerprintInfo(),
2988
3247
  ];
2989
3248
  {
@@ -3373,12 +3632,14 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
3373
3632
  let navigationAbortController = null;
3374
3633
  observeUrlChanges(false, async (newPageUrl, _oldPageUrl) => {
3375
3634
  var _a;
3376
- await new Promise((resolve) => requestAnimationFrame(resolve));
3377
- infiniteLoopProtection = 0;
3378
3635
  if (navigationAbortController)
3379
3636
  navigationAbortController.abort();
3380
3637
  navigationAbortController = new AbortController();
3381
3638
  const { signal } = navigationAbortController;
3639
+ await new Promise((resolve) => requestAnimationFrame(resolve));
3640
+ if (signal.aborted)
3641
+ return;
3642
+ infiniteLoopProtection = 0;
3382
3643
  let pageProject = project;
3383
3644
  let pagePersonalizationHeaders = Coframe.personalizationHeaders;
3384
3645
  const cached = getCachedFrames(newPageUrl);
@@ -3406,7 +3667,7 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
3406
3667
  }
3407
3668
  if (signal.aborted)
3408
3669
  return;
3409
- await applyVariants(newPageUrl, pageProject, pagePersonalizationHeaders);
3670
+ await applyVariants(newPageUrl, pageProject, pagePersonalizationHeaders, signal);
3410
3671
  });
3411
3672
  }
3412
3673
  function readLaneClaims() {
@@ -3439,15 +3700,33 @@ function persistLaneClaims(claims, logger) {
3439
3700
  }
3440
3701
  function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore) {
3441
3702
  let isFirstApply = true;
3442
- return async (pageUrl, pageProject, personalizationHeaders) => {
3703
+ let applicationId = 0;
3704
+ return async (pageUrl, pageProject, personalizationHeaders, signal, deadline) => {
3705
+ var _a;
3706
+ const currentApplication = ++applicationId;
3707
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted)
3708
+ return;
3443
3709
  const sessionId = isFirstApply ? Coframe.sessionId : generateUUID();
3444
3710
  const sessionContext = buildSessionContext(isFirstApply);
3445
3711
  isFirstApply = false;
3712
+ const preview = resolvePreview(pageUrl, previewVariantId, isSessionVariant);
3713
+ const chooseCmabVariants = createCmabVariantBatchChooser({
3714
+ projectId: Coframe.projectId,
3715
+ currentUrl: pageUrl,
3716
+ referrer: (_a = config.referrer) !== null && _a !== void 0 ? _a : getBrowserReferrer(),
3717
+ signal,
3718
+ deadline,
3719
+ disableRemoteAssignment: Boolean(config.projectUrl || config.ingestUrl),
3720
+ userContext: config.userContext,
3721
+ });
3722
+ const { variantsAndElements, experimentsInfo, hasActivatedFrames, laneClaims, } = await getVariantsAndElements(personalizationHeaders, pageProject, preview.previewVariantId, Coframe.userToken, pageUrl, getBrowserLanguages(), getBrowserPlatform(), Coframe.logger, config.nonce, laneStore.claims, preview.isSessionVariant, chooseCmabVariants);
3723
+ if ((signal === null || signal === void 0 ? void 0 : signal.aborted) ||
3724
+ currentApplication !== applicationId ||
3725
+ (signal && !urlsMatch(pageUrl, getCurrentPageUrl())))
3726
+ return;
3446
3727
  Coframe.sessionId = sessionId;
3447
3728
  Coframe.personalizationHeaders = personalizationHeaders;
3448
- const preview = resolvePreview(pageUrl, previewVariantId, isSessionVariant);
3449
3729
  setIsPreviewVariant(!!preview.previewVariantId);
3450
- const { variantsAndElements, experimentsInfo, hasActivatedFrames, laneClaims, } = await getVariantsAndElements(personalizationHeaders, pageProject, preview.previewVariantId, Coframe.userToken, pageUrl, getBrowserLanguages(), getBrowserPlatform(), Coframe.logger, config.nonce, laneStore.claims, preview.isSessionVariant);
3451
3730
  laneStore.claims = laneClaims;
3452
3731
  persistLaneClaims(laneClaims, Coframe.logger);
3453
3732
  Coframe.experimentInfo = experimentsInfo;
@@ -3464,7 +3743,11 @@ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant
3464
3743
  else {
3465
3744
  replaceVariants(variantsAndElements, Coframe.orgId, Coframe.logger, config.nonce);
3466
3745
  }
3467
- startSession(Coframe, isCookiesEnabled, executionStartTime, personalizationHeaders, sessionContext);
3746
+ const cmabDecisions = chooseCmabVariants.getDecisions(experimentsInfo);
3747
+ startSession(Coframe, isCookiesEnabled, executionStartTime, personalizationHeaders, {
3748
+ ...sessionContext,
3749
+ ...(cmabDecisions.length ? { cmab_decisions: cmabDecisions } : {}),
3750
+ }, pageUrl);
3468
3751
  analytics.resetBroadcasted();
3469
3752
  analytics.sendAnalytics();
3470
3753
  };
@@ -3482,7 +3765,7 @@ async function modifyPage(Coframe, configResult, projectResult, analytics) {
3482
3765
  return;
3483
3766
  const laneStore = { claims: readLaneClaims() };
3484
3767
  const applyVariants = createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore);
3485
- await applyVariants(getCurrentPageUrl(), cdnProject, personalizationHeaders);
3768
+ await applyVariants(getCurrentPageUrl(), cdnProject, personalizationHeaders, undefined, (integrationStartTime || executionStartTime).getTime() + timeoutMs);
3486
3769
  if (!Coframe.variantsAndElements ||
3487
3770
  Coframe.variantsAndElements.length === 0) {
3488
3771
  revealPage();
@@ -163,9 +163,11 @@ interface Experiment {
163
163
  }
164
164
  interface ExperimentMetadata {
165
165
  experiment_id: string;
166
+ source_experiment_id?: string;
166
167
  slug: string;
167
168
  iteration_id: string;
168
169
  seed: number;
170
+ type?: 'CMAB';
169
171
  holdout_info: HoldoutInfo;
170
172
  allocation: Allocation;
171
173
  activation_condition: {
@@ -166,9 +166,11 @@ interface Experiment {
166
166
  }
167
167
  interface ExperimentMetadata {
168
168
  experiment_id: string;
169
+ source_experiment_id?: string;
169
170
  slug: string;
170
171
  iteration_id: string;
171
172
  seed: number;
173
+ type?: 'CMAB';
172
174
  holdout_info: HoldoutInfo;
173
175
  allocation: Allocation;
174
176
  activation_condition: {
@@ -129,9 +129,11 @@ interface Experiment {
129
129
  }
130
130
  interface ExperimentMetadata {
131
131
  experiment_id: string;
132
+ source_experiment_id?: string;
132
133
  slug: string;
133
134
  iteration_id: string;
134
135
  seed: number;
136
+ type?: 'CMAB';
135
137
  holdout_info: HoldoutInfo;
136
138
  allocation: Allocation;
137
139
  activation_condition: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coframe/sdk",
3
- "version": "1.1.22",
3
+ "version": "1.1.23",
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",
@@ -131,4 +131,4 @@
131
131
  "ua-parser-js": "^2.0.10"
132
132
  },
133
133
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
134
- }
134
+ }