@coframe/sdk 1.1.46 → 1.1.48
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 +174 -0
- package/dist/pkg/cf.cjs.js +94 -51
- package/dist/pkg/cf.esm.mjs +94 -51
- package/dist/pkg/feature-flags.cjs.js +4 -2
- package/dist/pkg/feature-flags.esm.mjs +4 -2
- package/dist/pkg/server.cjs.js +410 -40
- package/dist/pkg/server.esm.mjs +409 -41
- package/dist/pkg/types/index.d.ts +8 -1
- package/dist/pkg/types/react.d.ts +8 -1
- package/dist/pkg/types/server.d.ts +62 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -140,6 +140,178 @@ const projectData = serverData?.project
|
|
|
140
140
|
|
|
141
141
|
Pass `projectData` into `<CoframeProvider>` to avoid the extra client-side project fetch and personalize on first paint.
|
|
142
142
|
|
|
143
|
+
### Server-side assignment
|
|
144
|
+
|
|
145
|
+
Use `getExperimentAssignment()` when the server must know one experiment's
|
|
146
|
+
variant before rendering or calling an upstream service. Edge performs the full
|
|
147
|
+
assignment; the package never falls back to local bucketing.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import {
|
|
151
|
+
generateUserToken,
|
|
152
|
+
getExperimentAssignment,
|
|
153
|
+
} from '@coframe/sdk/server'
|
|
154
|
+
|
|
155
|
+
// Next.js `request.cookies.get()` returns `{ name, value }`; other runtimes
|
|
156
|
+
// return the string. Either way `userToken` must be the string value.
|
|
157
|
+
const existingToken = request.cookies.get('coframe.user.token')?.value
|
|
158
|
+
const userToken = existingToken ?? generateUserToken()
|
|
159
|
+
|
|
160
|
+
if (!existingToken) {
|
|
161
|
+
// Do not set `httpOnly`: the browser SDK reads this cookie to reuse the
|
|
162
|
+
// same identity. An HttpOnly token makes the browser generate a new one and
|
|
163
|
+
// bucket differently, with no error.
|
|
164
|
+
response.cookies.set('coframe.user.token', userToken, {
|
|
165
|
+
path: '/',
|
|
166
|
+
secure: true,
|
|
167
|
+
sameSite: 'lax',
|
|
168
|
+
maxAge: 400 * 24 * 60 * 60,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const assignment = await getExperimentAssignment({
|
|
173
|
+
projectId: 'your-project-id',
|
|
174
|
+
experimentId: 'your-experiment-id',
|
|
175
|
+
userToken,
|
|
176
|
+
currentUrl: request.url,
|
|
177
|
+
headers: request.headers,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const upstreamVersion =
|
|
181
|
+
assignment?.variant?.slug === 'variant-2' ? 'v2' : 'v1'
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The package derives visitor context from an allowlist of request headers.
|
|
185
|
+
Explicit `userAgent`, `languages`, `browserPlatform`, `country`, `region`,
|
|
186
|
+
`city`, `device`, `botScore`, `verifiedBotCategory`, `clientIp`,
|
|
187
|
+
`userVisitCount`, and `userContext` options override the derived values. Only
|
|
188
|
+
the lane cookie is forwarded to Edge; authorization and application cookies are
|
|
189
|
+
not forwarded. `clientIp` is used only for existing blocklist targeting.
|
|
190
|
+
|
|
191
|
+
Geography, device, bot score, and client IP are read from `cf-*` request
|
|
192
|
+
headers. Cloudflare adds `cf-ipcountry` and `cf-connecting-ip` to every
|
|
193
|
+
request that reaches your origin; `cf-region-code` and `cf-ipcity` need the
|
|
194
|
+
"Add visitor location headers" Managed Transform, `cf-device-type` needs
|
|
195
|
+
"cache by device type", and `cf-bot-score` needs Bot Management plus a
|
|
196
|
+
Transform Rule. If your server is not behind Cloudflare, or a header is not
|
|
197
|
+
enabled, that value is missing: the country becomes `unknown`, country
|
|
198
|
+
audiences never match, and the device falls back to the user agent. Pass
|
|
199
|
+
`country`, `region`, `city`, and `device` explicitly in that case, from
|
|
200
|
+
whatever geo source your stack already has. Values are forwarded verbatim and
|
|
201
|
+
audiences compare them exactly, so use Cloudflare's spelling: `country` is the
|
|
202
|
+
uppercase ISO 3166-1 alpha-2 code (`US`, not `us`), `region` the ISO 3166-2
|
|
203
|
+
subdivision code (`CA`), and `device` one of `desktop`, `mobile`, `tablet`.
|
|
204
|
+
|
|
205
|
+
When a result is available, pass its project snapshot to the provider. The
|
|
206
|
+
browser reads the same standard token cookie and runs its existing assignment
|
|
207
|
+
flow without fetching project data again.
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
<CoframeProvider
|
|
211
|
+
projectConfig={{ projectId: 'your-project-id' }}
|
|
212
|
+
projectData={assignment?.projectData}
|
|
213
|
+
>
|
|
214
|
+
{children}
|
|
215
|
+
</CoframeProvider>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`assignment` is `undefined` when the call failed (see below), so read
|
|
219
|
+
`projectData` with optional chaining and decide there whether the browser may
|
|
220
|
+
initialize on its own.
|
|
221
|
+
|
|
222
|
+
`generateUserToken()` uses Web Crypto when available and falls back to a
|
|
223
|
+
non-cryptographic UUID generator in server runtimes without Web Crypto. The
|
|
224
|
+
token is assignment identity, not an authentication credential. It remains
|
|
225
|
+
stable until its cookie is removed, expires, or is replaced.
|
|
226
|
+
The separate visit count uses the SDK's existing two-hour visit boundary. The
|
|
227
|
+
package only reads the visit cookies the browser SDK writes (`coframe.cvc`,
|
|
228
|
+
`coframe.lastVisitTime`) and never sets them, so without the browser SDK running
|
|
229
|
+
after consent the count stays at 1 and `returning` audiences never match.
|
|
230
|
+
|
|
231
|
+
The result contains either one `variant` or `variant: null` with a structured
|
|
232
|
+
reason (`experiment_not_found`; `not_assigned`, which also covers a visitor
|
|
233
|
+
held out of this experiment or of the whole project by a holdout and renders
|
|
234
|
+
the default experience in the browser too; `unsupported_experiment_type` for
|
|
235
|
+
CMAB targets; `preview_active` for URL previews; a newer Edge may add reasons,
|
|
236
|
+
so treat any reason you do not know as "no variant"). Both forms include the exact `projectData` snapshot used by Edge, so the
|
|
237
|
+
browser can initialize from that snapshot. The snapshot also carries the
|
|
238
|
+
normalized language, platform, and visit-count values used by Edge. The browser
|
|
239
|
+
uses those values only for its initial assignment and returns to its normal
|
|
240
|
+
browser-derived context for later SPA navigations. In every case without a
|
|
241
|
+
variant, use the default upstream experience.
|
|
242
|
+
|
|
243
|
+
The function returns `undefined` for transport, timeout (`timeoutMs`,
|
|
244
|
+
default 1500 ms), Edge, or response validation failures, and when
|
|
245
|
+
`currentUrl` carries `cf_disable`. Handle that state explicitly. Invalid options are a
|
|
246
|
+
programming error and throw instead: an empty `projectId`, `experimentId` or
|
|
247
|
+
`userToken`, a `currentUrl` that is not an absolute URL (Express and Fastify
|
|
248
|
+
`req.url` is a path, build the full URL first), out-of-range `languages`,
|
|
249
|
+
`device`, `botScore`, `timeoutMs` and `userVisitCount` values, any string
|
|
250
|
+
option longer than its limit (4096 characters for URLs, 2048 for the user
|
|
251
|
+
agent, 200 for `projectId`, 512 for `experimentId` and `userToken`), or a
|
|
252
|
+
`userContext` that serializes to more than 4096 bytes (`userContext is too large`). Values derived from
|
|
253
|
+
request headers are clamped instead. To preserve server/browser
|
|
254
|
+
consistency for the response, do not let the browser independently fetch project
|
|
255
|
+
data and assign an experiment after the server has fallen back to its default.
|
|
256
|
+
Applications that accept that possible mismatch may instead initialize the
|
|
257
|
+
browser normally as their fail-open policy.
|
|
258
|
+
|
|
259
|
+
#### What Edge cannot evaluate
|
|
260
|
+
|
|
261
|
+
Server assignment runs the same deterministic logic as the browser, with the
|
|
262
|
+
same token, the same project snapshot, and the same visitor context. The
|
|
263
|
+
browser then reaches the same bucket because every input is the same. Five
|
|
264
|
+
kinds of input only exist in the page, so Edge cannot evaluate them:
|
|
265
|
+
|
|
266
|
+
- **JavaScript targeting.** A `js_function` condition (custom JavaScript that
|
|
267
|
+
reads a page value, a cookie, `localStorage`, or the `dataLayer`) runs only
|
|
268
|
+
in the browser, whether it sits on an audience or on a frame's activation
|
|
269
|
+
condition. Edge treats it as not matched.
|
|
270
|
+
- If the target experiment itself uses a JavaScript audience, the result is
|
|
271
|
+
`variant: null` with reason `not_assigned`. Do not use JavaScript audiences
|
|
272
|
+
on experiments you resolve on the server.
|
|
273
|
+
- Experiments in the same frame are evaluated in order and the first match
|
|
274
|
+
wins. If a JavaScript-targeted experiment sits **ahead of the target in
|
|
275
|
+
the same frame**, Edge skips it and assigns the target, while the browser
|
|
276
|
+
can match it and skip the target. Keep JavaScript-targeted experiments out
|
|
277
|
+
of the target experiment's frame, or place them after the target.
|
|
278
|
+
- The same applies to a JavaScript-targeted experiment that shares a
|
|
279
|
+
**lane** with the target, wherever it sits: lane winners are drawn from
|
|
280
|
+
the experiments the visitor can reach, and Edge cannot tell whether that
|
|
281
|
+
one is reachable. Keep JavaScript-targeted experiments out of the target's
|
|
282
|
+
lane.
|
|
283
|
+
- **CMAB experiments.** A target CMAB experiment returns `variant: null` with
|
|
284
|
+
reason `unsupported_experiment_type`.
|
|
285
|
+
- **URL fragments.** A server never sees the `#fragment` of the page URL, and
|
|
286
|
+
the browser re-evaluates the snapshot with its full `location.href`. Frames
|
|
287
|
+
that use `base_url_matches` ignore query and fragment and are safe; an exact
|
|
288
|
+
`url` condition compares both, and a `url_regex` runs against the full
|
|
289
|
+
`location.href` in the browser, so pass the same URL the browser will have,
|
|
290
|
+
without a fragment, and avoid regexes anchored on the end of the URL.
|
|
291
|
+
- **Previews.** Previews are browser-only. A `coframe_variant_id` URL
|
|
292
|
+
parameter reaches Edge, but the published preview payloads do not name the
|
|
293
|
+
experiment a variant belongs to, so the server answers `variant: null` with
|
|
294
|
+
reason `preview_active` and the browser renders the preview from the
|
|
295
|
+
returned snapshot. A preview kept across navigation by the Preview Session
|
|
296
|
+
tool lives in the browser's `sessionStorage` and never reaches Edge; the
|
|
297
|
+
browser discards the snapshot and fetches its own project. Both affect
|
|
298
|
+
preview users only; render the default upstream experience.
|
|
299
|
+
- **Lane state.** Edge reads lane claims from the `coframe.lanes` cookie only.
|
|
300
|
+
Before cookie consent the browser keeps claims in `localStorage`, which the
|
|
301
|
+
server cannot see, and the server does not persist the claims Edge commits.
|
|
302
|
+
Laned experiments therefore agree once the browser has written the cookie,
|
|
303
|
+
not for a visitor without cookie consent.
|
|
304
|
+
|
|
305
|
+
Audience conditions based on URL (`base_url_matches`), country, device,
|
|
306
|
+
language, platform, visit type, search, time, allocation, or `userContext`
|
|
307
|
+
evaluate identically on both sides, as does bot cloaking. For platform, Edge derives the same
|
|
308
|
+
`navigator.platform` family the browser reports (a `Linux …` string on Android,
|
|
309
|
+
for example `Linux armv8l`), or takes an explicit `browserPlatform` option.
|
|
310
|
+
|
|
311
|
+
If the application uses a custom identity cookie, call `setUserToken()` before
|
|
312
|
+
browser initialization with `waitForUserToken: true`. That integration must also
|
|
313
|
+
keep visit and lane state aligned between the server and browser.
|
|
314
|
+
|
|
143
315
|
## Feature flags and remote config
|
|
144
316
|
|
|
145
317
|
Browser Feature Flags use the existing public `projectId` and receive only
|
|
@@ -453,6 +625,8 @@ Modern resolution modes handle package subpath exports more reliably.
|
|
|
453
625
|
### `@coframe/sdk/server`
|
|
454
626
|
|
|
455
627
|
- `getProjectData(options)`
|
|
628
|
+
- `getExperimentAssignment(options)`
|
|
629
|
+
- `generateUserToken()`
|
|
456
630
|
- `evaluateFlags(options)`
|
|
457
631
|
- `getFeatureFlag(key, options)` / `isFeatureEnabled(key, options)`
|
|
458
632
|
- `getFeatureFlagPayload(key, options)` / `getRemoteConfig(key, options)`
|
package/dist/pkg/cf.cjs.js
CHANGED
|
@@ -302,9 +302,11 @@ const generateUUID = () => {
|
|
|
302
302
|
const cryptoObject = (_a = getGlobalScope()) === null || _a === void 0 ? void 0 : _a.crypto;
|
|
303
303
|
if (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.randomUUID)
|
|
304
304
|
return cryptoObject.randomUUID();
|
|
305
|
+
const randomByte = (cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues)
|
|
306
|
+
? () => cryptoObject.getRandomValues(new Uint8Array(1))[0]
|
|
307
|
+
: () => Math.floor(Math.random() * 256);
|
|
305
308
|
return `${1e7}${ -1e3}${ -4e3}${ -8e3}${ -1e11}`.replace(/[018]/g, (c) => {
|
|
306
|
-
|
|
307
|
-
const randomValue = (_b = (_a = cryptoObject === null || cryptoObject === void 0 ? void 0 : cryptoObject.getRandomValues) === null || _a === void 0 ? void 0 : _a.call(cryptoObject, new Uint8Array(1))[0]) !== null && _b !== void 0 ? _b : 0;
|
|
309
|
+
const randomValue = randomByte();
|
|
308
310
|
return (c ^
|
|
309
311
|
(randomValue & (15 >> (c / 4)))).toString(16);
|
|
310
312
|
});
|
|
@@ -371,16 +373,30 @@ const getOrGenerateUserToken = () => {
|
|
|
371
373
|
const setUserTokenCookie = (uuid, logger) => {
|
|
372
374
|
setCookie(USER_TOKEN_COOKIE_KEY, uuid, logger);
|
|
373
375
|
};
|
|
376
|
+
const VISIT_BOUNDARY_MS = 2 * 60 * 60 * 1000;
|
|
377
|
+
function startsNewVisit(lastVisitTime, now) {
|
|
378
|
+
return !lastVisitTime || now - lastVisitTime > VISIT_BOUNDARY_MS;
|
|
379
|
+
}
|
|
380
|
+
function nextVisitCount(existing, lastVisitTime, now) {
|
|
381
|
+
return startsNewVisit(lastVisitTime, now) ? existing + 1 : existing;
|
|
382
|
+
}
|
|
383
|
+
function parseNonNegativeInt(raw) {
|
|
384
|
+
if (!raw || !/^\d+$/.test(raw))
|
|
385
|
+
return undefined;
|
|
386
|
+
const parsed = Number(raw);
|
|
387
|
+
return parsed <= 9007199254740991 ? parsed : undefined;
|
|
388
|
+
}
|
|
389
|
+
function parseVisitCount(raw) {
|
|
390
|
+
var _a;
|
|
391
|
+
return (_a = parseNonNegativeInt(raw)) !== null && _a !== void 0 ? _a : 0;
|
|
392
|
+
}
|
|
374
393
|
const trackVisitCookie = (logger) => {
|
|
375
|
-
const
|
|
376
|
-
const lastVisitTime =
|
|
394
|
+
const existing = parseVisitCount(getCookie(VISIT_COUNT_COOKIE_KEY));
|
|
395
|
+
const lastVisitTime = parseVisitCount(getCookie(LAST_VISIT_TIME_KEY));
|
|
377
396
|
const currentTime = Date.now();
|
|
378
|
-
const
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
? Number.parseInt(visitCountString, 10) + 1
|
|
382
|
-
: 1;
|
|
383
|
-
setCookie(VISIT_COUNT_COOKIE_KEY, visitCount.toString(), logger);
|
|
397
|
+
const next = nextVisitCount(existing, lastVisitTime, currentTime);
|
|
398
|
+
if (next !== existing) {
|
|
399
|
+
setCookie(VISIT_COUNT_COOKIE_KEY, next.toString(), logger);
|
|
384
400
|
setCookie(LAST_VISIT_TIME_KEY, currentTime.toString(), logger);
|
|
385
401
|
}
|
|
386
402
|
};
|
|
@@ -2069,6 +2085,7 @@ const applyVariantChanges = (variantElements, currentUrl, logger, nonce, lifecyc
|
|
|
2069
2085
|
? lifecycleContext.runtime.createVariantSession({
|
|
2070
2086
|
batchId: lifecycleContext.batchId,
|
|
2071
2087
|
variantId: variant.id,
|
|
2088
|
+
experimentId: variant.experiment_id,
|
|
2072
2089
|
})
|
|
2073
2090
|
: null;
|
|
2074
2091
|
if (!variant.modifications ||
|
|
@@ -2106,7 +2123,7 @@ const applyVariantChanges = (variantElements, currentUrl, logger, nonce, lifecyc
|
|
|
2106
2123
|
});
|
|
2107
2124
|
}
|
|
2108
2125
|
else if (selectedElement instanceof HTMLElement) {
|
|
2109
|
-
selectedElement.setAttribute('coframe-exp-id', variant.
|
|
2126
|
+
selectedElement.setAttribute('coframe-exp-id', variant.experiment_id);
|
|
2110
2127
|
}
|
|
2111
2128
|
}
|
|
2112
2129
|
return true;
|
|
@@ -2292,6 +2309,16 @@ function murmurHash3_x86_32(key, seed) {
|
|
|
2292
2309
|
return h1 >>> 0;
|
|
2293
2310
|
}
|
|
2294
2311
|
|
|
2312
|
+
function parseLaneCookie(raw) {
|
|
2313
|
+
if (!raw)
|
|
2314
|
+
return {};
|
|
2315
|
+
try {
|
|
2316
|
+
return parseClaims(decodeURIComponent(raw));
|
|
2317
|
+
}
|
|
2318
|
+
catch (_a) {
|
|
2319
|
+
return parseClaims(raw);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2295
2322
|
function parseClaims(raw) {
|
|
2296
2323
|
if (!raw)
|
|
2297
2324
|
return {};
|
|
@@ -2725,7 +2752,7 @@ function checkAllAudiences(metadata, ctx) {
|
|
|
2725
2752
|
const audience = ctx.project.audiences[audienceId];
|
|
2726
2753
|
if (!audience)
|
|
2727
2754
|
continue;
|
|
2728
|
-
const activated = checkActivationCondition(audience.selector, ctx.personalizationHeaders, ctx.currentPageUrl, ctx.languages, ctx.browserPlatform, ctx.userToken, ctx.userVisitCount,
|
|
2755
|
+
const activated = checkActivationCondition(audience.selector, ctx.personalizationHeaders, ctx.currentPageUrl, ctx.languages, ctx.browserPlatform, ctx.userToken, ctx.userVisitCount, ctx.runJsFunction, ctx.logger);
|
|
2729
2756
|
if (activated === false)
|
|
2730
2757
|
return false;
|
|
2731
2758
|
if (activated !== true)
|
|
@@ -2733,8 +2760,12 @@ function checkAllAudiences(metadata, ctx) {
|
|
|
2733
2760
|
}
|
|
2734
2761
|
return result;
|
|
2735
2762
|
}
|
|
2736
|
-
|
|
2737
|
-
|
|
2763
|
+
function audiencesPass(metadata, ctx) {
|
|
2764
|
+
const result = checkAllAudiences(metadata, ctx);
|
|
2765
|
+
return ctx.unknownIsInactive ? result === true : result !== false;
|
|
2766
|
+
}
|
|
2767
|
+
async function getVariantsAndElements(personalizationHeaders, project, previewVariantId, userToken, currentPageUrl, languages, browserPlatform, logger, nonce, laneClaims = {}, silentFallback = false, chooseCmabVariants, userVisitCountOverride, runtime = {}) {
|
|
2768
|
+
var _a, _b;
|
|
2738
2769
|
const experimentsInfo = [];
|
|
2739
2770
|
const cloakBot = !!((_a = project.settings) === null || _a === void 0 ? void 0 : _a.bot_cloaking_enabled) &&
|
|
2740
2771
|
personalizationHeaders['x-is-bot'] === 'true';
|
|
@@ -2746,7 +2777,11 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
|
|
|
2746
2777
|
laneClaims: meClaims,
|
|
2747
2778
|
};
|
|
2748
2779
|
};
|
|
2749
|
-
const userVisitCount =
|
|
2780
|
+
const userVisitCount = userVisitCountOverride !== null && userVisitCountOverride !== void 0 ? userVisitCountOverride : parseVisitCount(getCookie(VISIT_COUNT_COOKIE_KEY));
|
|
2781
|
+
const evaluateJsFunctions = (_b = runtime.evaluateJsFunctions) !== null && _b !== void 0 ? _b : true;
|
|
2782
|
+
const runJsFunction = evaluateJsFunctions
|
|
2783
|
+
? (js, args) => executeJs(js, args, { isExpression: true, nonce })
|
|
2784
|
+
: undefined;
|
|
2750
2785
|
const ctx = {
|
|
2751
2786
|
project,
|
|
2752
2787
|
personalizationHeaders,
|
|
@@ -2755,8 +2790,9 @@ async function getVariantsAndElements(personalizationHeaders, project, previewVa
|
|
|
2755
2790
|
languages,
|
|
2756
2791
|
browserPlatform,
|
|
2757
2792
|
userVisitCount,
|
|
2758
|
-
nonce,
|
|
2759
2793
|
logger,
|
|
2794
|
+
runJsFunction,
|
|
2795
|
+
unknownIsInactive: !evaluateJsFunctions,
|
|
2760
2796
|
};
|
|
2761
2797
|
const validatedClaims = validateClaims(laneClaims, project.lanes);
|
|
2762
2798
|
const previewVariantIds = splitPreviewVariantIds(previewVariantId);
|
|
@@ -2811,27 +2847,24 @@ function activateFrames(ctx) {
|
|
|
2811
2847
|
return ctx.project.frames.filter((frame) => {
|
|
2812
2848
|
if (!frame.experiments)
|
|
2813
2849
|
return false;
|
|
2814
|
-
const frameActivated = checkActivationCondition(frame.activation_condition, ctx.personalizationHeaders, ctx.currentPageUrl, ctx.languages, ctx.browserPlatform, ctx.userToken, ctx.userVisitCount,
|
|
2815
|
-
|
|
2816
|
-
return false;
|
|
2817
|
-
return true;
|
|
2850
|
+
const frameActivated = checkActivationCondition(frame.activation_condition, ctx.personalizationHeaders, ctx.currentPageUrl, ctx.languages, ctx.browserPlatform, ctx.userToken, ctx.userVisitCount, ctx.runJsFunction, ctx.logger);
|
|
2851
|
+
return ctx.unknownIsInactive ? frameActivated === true : !!frameActivated;
|
|
2818
2852
|
});
|
|
2819
2853
|
}
|
|
2820
2854
|
async function assignFrames(activeFrames, validatedClaims, ctx, chooseCmabVariants) {
|
|
2821
|
-
var _a;
|
|
2855
|
+
var _a, _b, _c;
|
|
2822
2856
|
const { project, userToken, logger } = ctx;
|
|
2823
2857
|
const experimentsInfo = [];
|
|
2824
2858
|
const variantsAndElements = [];
|
|
2825
|
-
const evalAudiences = (metadata) => checkAllAudiences(metadata, ctx) !== false;
|
|
2826
2859
|
const audiencePass = new Map();
|
|
2827
2860
|
const reachableByLane = {};
|
|
2828
2861
|
for (const frame of activeFrames) {
|
|
2829
|
-
for (const experiment of frame.experiments) {
|
|
2862
|
+
for (const experiment of (_a = frame.experiments) !== null && _a !== void 0 ? _a : []) {
|
|
2830
2863
|
const metadata = experiment.metadata;
|
|
2831
2864
|
const laneIds = experimentLaneIds(metadata);
|
|
2832
2865
|
if (laneIds.length === 0)
|
|
2833
2866
|
continue;
|
|
2834
|
-
const passes =
|
|
2867
|
+
const passes = audiencesPass(metadata, ctx);
|
|
2835
2868
|
audiencePass.set(metadata.experiment_id, passes);
|
|
2836
2869
|
if (passes) {
|
|
2837
2870
|
for (const laneId of laneIds) {
|
|
@@ -2848,7 +2881,7 @@ async function assignFrames(activeFrames, validatedClaims, ctx, chooseCmabVarian
|
|
|
2848
2881
|
let hasActivatedFrames = false;
|
|
2849
2882
|
for (const frame of activeFrames) {
|
|
2850
2883
|
let atLeastOneExperimentWasActivated = false;
|
|
2851
|
-
for (const experiment of frame.experiments) {
|
|
2884
|
+
for (const experiment of (_b = frame.experiments) !== null && _b !== void 0 ? _b : []) {
|
|
2852
2885
|
if (atLeastOneExperimentWasActivated)
|
|
2853
2886
|
break;
|
|
2854
2887
|
const metadata = experiment.metadata;
|
|
@@ -2858,7 +2891,7 @@ async function assignFrames(activeFrames, validatedClaims, ctx, chooseCmabVarian
|
|
|
2858
2891
|
continue;
|
|
2859
2892
|
let activated = audiencePass.get(metadata.experiment_id);
|
|
2860
2893
|
if (activated === undefined) {
|
|
2861
|
-
activated =
|
|
2894
|
+
activated = audiencesPass(metadata, ctx);
|
|
2862
2895
|
}
|
|
2863
2896
|
if (!activated)
|
|
2864
2897
|
continue;
|
|
@@ -2914,7 +2947,7 @@ async function assignFrames(activeFrames, validatedClaims, ctx, chooseCmabVarian
|
|
|
2914
2947
|
try {
|
|
2915
2948
|
mlVariantIds = await chooseCmabVariants(Array.from(cmabMetadataById.values()), userToken);
|
|
2916
2949
|
}
|
|
2917
|
-
catch (
|
|
2950
|
+
catch (_d) {
|
|
2918
2951
|
mlVariantIds = new Map();
|
|
2919
2952
|
}
|
|
2920
2953
|
}
|
|
@@ -2925,7 +2958,7 @@ async function assignFrames(activeFrames, validatedClaims, ctx, chooseCmabVarian
|
|
|
2925
2958
|
}
|
|
2926
2959
|
const { metadata, variants, elements } = entry;
|
|
2927
2960
|
let mlVariantId = metadata.type === 'CMAB'
|
|
2928
|
-
? ((
|
|
2961
|
+
? ((_c = mlVariantIds.get(metadata.experiment_id)) !== null && _c !== void 0 ? _c : null)
|
|
2929
2962
|
: null;
|
|
2930
2963
|
if (mlVariantId && !variants.some((v) => v.id === mlVariantId)) {
|
|
2931
2964
|
logger.warn(`CMAB variant ${mlVariantId} not in experiment ${metadata.experiment_id}`, 'cmab_variant_not_found', { experiment_id: metadata.experiment_id });
|
|
@@ -2978,9 +3011,12 @@ async function previewVariants(previewVariantIds, validatedClaims, ctx, silentFa
|
|
|
2978
3011
|
return uniqueByVariantId([...rolledOutVariants, ...requestedVariants]);
|
|
2979
3012
|
}
|
|
2980
3013
|
function findVariants(frames, variantIds) {
|
|
2981
|
-
return frames.flatMap((frame) =>
|
|
2982
|
-
|
|
2983
|
-
|
|
3014
|
+
return frames.flatMap((frame) => {
|
|
3015
|
+
var _a;
|
|
3016
|
+
return ((_a = frame.experiments) !== null && _a !== void 0 ? _a : []).flatMap((experiment) => experiment.variants
|
|
3017
|
+
.filter((variant) => variantIds.includes(variant.id))
|
|
3018
|
+
.map((variant) => ({ variant, elements: frame.elements })));
|
|
3019
|
+
});
|
|
2984
3020
|
}
|
|
2985
3021
|
function uniqueByVariantId(items) {
|
|
2986
3022
|
const seen = {};
|
|
@@ -2992,10 +3028,13 @@ function uniqueByVariantId(items) {
|
|
|
2992
3028
|
});
|
|
2993
3029
|
}
|
|
2994
3030
|
function keepRolledOutExperiments(frames) {
|
|
2995
|
-
return frames.map((frame) =>
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3031
|
+
return frames.map((frame) => {
|
|
3032
|
+
var _a;
|
|
3033
|
+
return ({
|
|
3034
|
+
...frame,
|
|
3035
|
+
experiments: ((_a = frame.experiments) !== null && _a !== void 0 ? _a : []).filter((experiment) => experiment.metadata.serving_mode === 'winner'),
|
|
3036
|
+
});
|
|
3037
|
+
});
|
|
2999
3038
|
}
|
|
3000
3039
|
|
|
3001
3040
|
function buildSessionContext(isInitialApply) {
|
|
@@ -3666,6 +3705,7 @@ async function resolveProject(Coframe, previewVariantId, isSessionVariant, confi
|
|
|
3666
3705
|
const queuedHeaders = getQueuedPersonalizationHeaders();
|
|
3667
3706
|
let personalizationHeaders = queuedHeaders;
|
|
3668
3707
|
let project = isSessionVariant ? undefined : projectData === null || projectData === void 0 ? void 0 : projectData.project;
|
|
3708
|
+
const assignmentContext = project ? projectData === null || projectData === void 0 ? void 0 : projectData.assignmentContext : undefined;
|
|
3669
3709
|
if (!project) {
|
|
3670
3710
|
const edgeResult = await fetchProject(config.projectUrl, Coframe.projectId, config.currentUrl || getCurrentPageUrl(), undefined, config.userContext, previewVariantId || undefined, undefined, (_b = config.referrer) !== null && _b !== void 0 ? _b : getBrowserReferrer());
|
|
3671
3711
|
personalizationHeaders = mergeClientHeaders(queuedHeaders, edgeResult === null || edgeResult === void 0 ? void 0 : edgeResult.personalizationHeaders);
|
|
@@ -3682,6 +3722,7 @@ async function resolveProject(Coframe, previewVariantId, isSessionVariant, confi
|
|
|
3682
3722
|
return {
|
|
3683
3723
|
project,
|
|
3684
3724
|
personalizationHeaders: personalizationHeaders,
|
|
3725
|
+
...(assignmentContext ? { assignmentContext } : {}),
|
|
3685
3726
|
};
|
|
3686
3727
|
}
|
|
3687
3728
|
|
|
@@ -4159,7 +4200,7 @@ function setupListeners(Coframe, executionStartTime) {
|
|
|
4159
4200
|
for (const variantElement of Coframe.variantsAndElements || []) {
|
|
4160
4201
|
const variant = variantElement.variant;
|
|
4161
4202
|
if (!experiencedCoframeIds.has(variant.id)) {
|
|
4162
|
-
const elements = document.querySelectorAll(`[coframe-exp-id="${variant.
|
|
4203
|
+
const elements = document.querySelectorAll(`[coframe-exp-id="${variant.experiment_id}"]`);
|
|
4163
4204
|
for (const el of Array.from(elements)) {
|
|
4164
4205
|
if (isElementVisible(el)) {
|
|
4165
4206
|
el.setAttribute('coframe-experienced', 'true');
|
|
@@ -4453,18 +4494,12 @@ function setupSPANavigation(Coframe, project, config, previewVariantId, isSessio
|
|
|
4453
4494
|
}
|
|
4454
4495
|
function readLaneClaims() {
|
|
4455
4496
|
const cookie = getCookie(LANE_COOKIE_KEY);
|
|
4456
|
-
if (cookie)
|
|
4457
|
-
|
|
4458
|
-
return parseClaims(decodeURIComponent(cookie));
|
|
4459
|
-
}
|
|
4460
|
-
catch (_a) {
|
|
4461
|
-
return parseClaims(cookie);
|
|
4462
|
-
}
|
|
4463
|
-
}
|
|
4497
|
+
if (cookie)
|
|
4498
|
+
return parseLaneCookie(cookie);
|
|
4464
4499
|
try {
|
|
4465
4500
|
return parseClaims(localStorage.getItem(LANE_COOKIE_KEY));
|
|
4466
4501
|
}
|
|
4467
|
-
catch (
|
|
4502
|
+
catch (_a) {
|
|
4468
4503
|
return {};
|
|
4469
4504
|
}
|
|
4470
4505
|
}
|
|
@@ -4479,14 +4514,17 @@ function persistLaneClaims(claims, logger) {
|
|
|
4479
4514
|
setCookie(LANE_COOKIE_KEY, encodeURIComponent(serialized), logger);
|
|
4480
4515
|
}
|
|
4481
4516
|
}
|
|
4482
|
-
function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore, runtime) {
|
|
4517
|
+
function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore, runtime, initialAssignmentContext) {
|
|
4483
4518
|
let isFirstApply = true;
|
|
4519
|
+
let snapshotContext = initialAssignmentContext;
|
|
4484
4520
|
let applicationId = 0;
|
|
4485
4521
|
const apply = async (pageUrl, pageProject, personalizationHeaders, signal, deadline) => {
|
|
4486
|
-
var _a, _b;
|
|
4522
|
+
var _a, _b, _c, _d;
|
|
4487
4523
|
const currentApplication = ++applicationId;
|
|
4488
4524
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
4489
4525
|
return 'skipped';
|
|
4526
|
+
const assignmentContext = snapshotContext;
|
|
4527
|
+
snapshotContext = undefined;
|
|
4490
4528
|
const preview = resolvePreview(pageUrl, previewVariantId, isSessionVariant);
|
|
4491
4529
|
const chooseCmabVariants = createCmabVariantBatchChooser({
|
|
4492
4530
|
projectId: Coframe.projectId,
|
|
@@ -4497,7 +4535,7 @@ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant
|
|
|
4497
4535
|
disableRemoteAssignment: Boolean(config.projectUrl || config.ingestUrl),
|
|
4498
4536
|
userContext: config.userContext,
|
|
4499
4537
|
});
|
|
4500
|
-
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);
|
|
4538
|
+
const { variantsAndElements, experimentsInfo, hasActivatedFrames, laneClaims, } = await getVariantsAndElements(personalizationHeaders, pageProject, preview.previewVariantId, Coframe.userToken, pageUrl, (_b = assignmentContext === null || assignmentContext === void 0 ? void 0 : assignmentContext.languages) !== null && _b !== void 0 ? _b : getBrowserLanguages(), (_c = assignmentContext === null || assignmentContext === void 0 ? void 0 : assignmentContext.browserPlatform) !== null && _c !== void 0 ? _c : getBrowserPlatform(), Coframe.logger, config.nonce, laneStore.claims, preview.isSessionVariant, chooseCmabVariants, assignmentContext === null || assignmentContext === void 0 ? void 0 : assignmentContext.userVisitCount);
|
|
4501
4539
|
if ((signal === null || signal === void 0 ? void 0 : signal.aborted) ||
|
|
4502
4540
|
currentApplication !== applicationId ||
|
|
4503
4541
|
(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()) ||
|
|
@@ -4505,7 +4543,7 @@ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant
|
|
|
4505
4543
|
return 'skipped';
|
|
4506
4544
|
if (!signal &&
|
|
4507
4545
|
runtime &&
|
|
4508
|
-
((
|
|
4546
|
+
((_d = pageProject === null || pageProject === void 0 ? void 0 : pageProject.settings) === null || _d === void 0 ? void 0 : _d.mutation_observer_active) &&
|
|
4509
4547
|
!urlsMatch(pageUrl, getCurrentPageUrl(), true))
|
|
4510
4548
|
return 'stale-route';
|
|
4511
4549
|
if (!signal && runtime) {
|
|
@@ -4551,7 +4589,7 @@ function createApplyVariants(Coframe, config, previewVariantId, isSessionVariant
|
|
|
4551
4589
|
async function modifyPage(Coframe, configResult, projectResult, analytics, lifecycle) {
|
|
4552
4590
|
var _a;
|
|
4553
4591
|
const { config, timeoutMs, previewVariantId, isSessionVariant, integrationStartTime, executionStartTime, } = configResult;
|
|
4554
|
-
const { project: cdnProject, personalizationHeaders } = projectResult;
|
|
4592
|
+
const { project: cdnProject, personalizationHeaders, assignmentContext, } = projectResult;
|
|
4555
4593
|
const revealCoordinator = (_a = lifecycle === null || lifecycle === void 0 ? void 0 : lifecycle.revealCoordinator) !== null && _a !== void 0 ? _a : createRevealCoordinator(revealPage, logRevealListenerError(() => Coframe.logger));
|
|
4556
4594
|
const runtime = lifecycle === null || lifecycle === void 0 ? void 0 : lifecycle.runtime;
|
|
4557
4595
|
if (runtime && config.waitForHydration) {
|
|
@@ -4577,7 +4615,7 @@ async function modifyPage(Coframe, configResult, projectResult, analytics, lifec
|
|
|
4577
4615
|
if (!(await ensureBody(Coframe, cdnProject, timeoutMs, config.nonce, revealCoordinator, () => !(runtime === null || runtime === void 0 ? void 0 : runtime.isDestroyed()))))
|
|
4578
4616
|
return;
|
|
4579
4617
|
const laneStore = { claims: readLaneClaims() };
|
|
4580
|
-
const applier = createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore, runtime);
|
|
4618
|
+
const applier = createApplyVariants(Coframe, config, previewVariantId, isSessionVariant, executionStartTime, analytics, laneStore, runtime, assignmentContext);
|
|
4581
4619
|
const initialUrl = getCurrentPageUrl();
|
|
4582
4620
|
if (runtime) {
|
|
4583
4621
|
const runtimeUrl = runtime.getState().currentUrl;
|
|
@@ -5180,6 +5218,9 @@ function injectOptions(options) {
|
|
|
5180
5218
|
projectData: {
|
|
5181
5219
|
project: options.projectData.project,
|
|
5182
5220
|
personalizationHeaders: (_a = options.projectData.personalizationHeaders) !== null && _a !== void 0 ? _a : {},
|
|
5221
|
+
...(options.projectData.assignmentContext
|
|
5222
|
+
? { assignmentContext: options.projectData.assignmentContext }
|
|
5223
|
+
: {}),
|
|
5183
5224
|
},
|
|
5184
5225
|
});
|
|
5185
5226
|
}
|
|
@@ -5389,6 +5430,7 @@ function createVariantSession(options) {
|
|
|
5389
5430
|
getPageState: () => ({ ...options.getPageState() }),
|
|
5390
5431
|
getState: () => ({ ...state }),
|
|
5391
5432
|
getVariantId: () => state.variantId,
|
|
5433
|
+
getExperimentId: () => options.experimentId,
|
|
5392
5434
|
onPage: (event, handler) => subscribe(event, handler, false),
|
|
5393
5435
|
oncePage: (event, handler) => subscribe(event, handler, true),
|
|
5394
5436
|
ready() {
|
|
@@ -5748,6 +5790,7 @@ function createPageRuntime(options) {
|
|
|
5748
5790
|
const session = createVariantSession({
|
|
5749
5791
|
sessionId: createId(),
|
|
5750
5792
|
variantId: input.variantId,
|
|
5793
|
+
experimentId: input.experimentId,
|
|
5751
5794
|
batchId: batch.batchId,
|
|
5752
5795
|
routeVersion: batch.routeVersion,
|
|
5753
5796
|
getPageState: () => ({ ...state }),
|