Developer Demo
Experimentation
Two Ways to Experiment #
Optimizely Experimentation offers two mechanisms for testing content. They differ in where the decision runs: Feature Experimentation decides on the server and integrates with CMS content variations through Graph; Web Experimentation & Personalization is a standalone product with its own visual editor that decides in the browser, separate from the CMS and Graph. This page covers the server-side path end to end.
Feature Experimentation - server-side
this demoEdge middleware evaluates the flag, rewrites the URL with the variation key, and Graph returns the matching CMS content variant - so every variation is its own ISR cache entry. A small client component fires the bucketing event after the visitor sees it. The rest of this page details this path.
Web Experimentation & Personalization - client-side
not covered hereBest for fast in-browser changes; the wrong tool when you need CMS-authored, server-rendered content. A separate, standalone Optimizely product (standalone Personalization is the same engine sold on its own) with its own visual editor and its own client-side delivery - it buckets visitors and mutates the page in the browser, independent of the CMS and Graph. Its upside: it can change anything on the rendered page, even edits never modeled in the CMS, without a developer exposing a field or component first.
Routing a WX bucket to a real CMS variation needs a bridge (a cookie on the next request, or a client-side refetch with a flicker). Not covered in this demo - see the Web Experimentation bridge.
How It All Fits Together: Configure, Then Serve #
The full lifecycle has two phases. Configure is one-time setup done entirely in the FX and CMS UIs - no code. Serve is what runs automatically on every page request. Once the integration code is in place, editors and experimenters add variations to any page without a developer ever touching the codebase.
Create the CMS variation
In Visual Builder, add a variation and name it to match the FX variation key.
Create the flag + rule
In FX, create the flag and a delivery rule. Set variation variables cms_flag: true and cms_route.
Create + attach audience
Define an audience from SDK attributes or ODP segments and attach it to the delivery rule.
Set traffic + enable
Allocate traffic and turn the rule on. FX now buckets visitors.
Pick the variation, put it in the URL
FX chooses which variation this visitor should see (that choice is the “decision”), and the middleware writes it into the request path so the page can be cached per variation.
decideAll([DISABLE_DECISION_EVENT])
// → { homepage: { variationKey: 'business' } }
// rewrite: /savings → /savings/__v_homepage--businesssrc/middleware.ts→ codeFetch the matching content from Graph
The page asks Optimizely Graph for the content variant whose name matches the chosen variation. The response is cached, so repeat visits stay fast.
variation: {
include: 'SOME',
value: ['business'],
includeOriginal: true,
}src/app/[[...slug]]/page.tsx→ codeRecord that the visitor saw it
In the browser, FX logs an “impression” - the event that tells the experiment this visitor was shown this variation, so results can be measured.
decide('homepage')src/components/FxBucketingEvent.tsx→ codeCMS Variations - Connecting FX to Content #
FX variation keys and CMS content variations share a single string contract. When Graph receives an active variation key, it looks for a CMS content variant with the same name and returns it. Editors create the variant in Visual Builder; the SDK wires it at runtime - no code change required after initial setup.
The contract: one string, three places
this demo's homepage personalization
FX variation key
flag homepage - the SDK buckets the user into
Graph filter value
variation: { include: SOME, value: [ … ] }
businessCMS variation name
Visual Builder serves the variant named
businessor the original if no match
The same business string is the only link between FX and the CMS - match it exactly (case-sensitive) and Graph serves the right variant.
# The variation key becomes a Graph filter. For a visitor bucketed into "business":
query {
_Content(
where: { _metadata: { url: { default: { eq: "/en/" } } } }
variation: { include: SOME, value: ["business"], includeOriginal: true }
) {
items {
_metadata { variation } # "business", or null for the base page
}
}
}Your Variation Keys → Graph Filter
This is what the live page route is passing to getContentByPath for your session right now.
variationOption = {
variation: {
include: "SOME",
value: ["semantic"],
includeOriginal: true,
},
}Your live session right now
Live Demo: hero_copy #
This card is driven entirely by the hero_copy flag and its headline + subheadline variables.
hero_copy
Flag is off - enable it in the FX dashboard to see the live hero copy variation.
Configure - Step by Step #
Once the integration code is in place (see below), editors can set up any number of content experiments without touching code. The variation key string is the only contract between Feature Experimentation and the CMS.
Create a flag and targeting rules in Feature Experimentation
homepage with five targeted-delivery rules, each serving 100% of its audience: personal (audience: persona == "personal"), business (audience: persona == "business"), mortgages (audience: persona == "mortgages"), and investments (audience: persona == "investments"). Visitors matching none fall through to the default off variation and see the base homepage. The persona attribute is set from the demo_persona cookie by the Audience Switcher. On each variation, set two variation variables: cms_flag: true (marks this as a CMS-content experiment the edge should route) and cms_route (the path the variation targets, e.g. "/" for the homepage, or "/products/*" for a section). The middleware only applies a variation whose cms_route matches the current page, so you can run many CMS experiments across the site without them colliding or fragmenting each other's cache. Leave cms_route unset to apply everywhere.Create variations in Visual Builder, then set their compositions
personal and business (case-sensitive - must match the FX variation keys exactly). Each becomes a new draft version in the CMS. Edit each variation's composition in Visual Builder and publish it - or discover the version numbers via GET /content/{key}/versions and PATCH them programmatically (see the note below).variation field exists in Graph's schema but is silently ignored by the Management API on write. However, once created in Visual Builder each variation becomes a new draft version - you can discover the version number via GET /content/{key}/versions and PATCH it with the correct composition + status: "published".Validate with the Audience Switcher
Enable the flag and start the experiment
Validate the experiment in your own app
__v_ segment on the rewritten request, the served content, and the participant count in your FX results. FX owns analytics, winner declaration, and rollout from here.Audience Targeting #
An audience attached to a delivery rule (step 3) can be backed two ways: SDK attributes passed at decision time, or ODP segments the visitor already qualifies for. Both are matched on the server - the browser never knows which audience it was matched to. Either way the rule resolves to a variation key, and everything downstream (Graph filter, CMS variant, impression) is identical. For the full breakdown of these two sources - native attributes vs the ODP behavioral layer - see Personalization.
Attribute-based audiences
this demoAttributes like device, persona, and logged_in are collected by getVisitorContext() (cookies + headers) and passed into the decision. FX audience conditions match against them at bucketing time.
ODP segment audiences
An FX audience can also be defined as membership in an ODP segment (built from behavioural + profile data in the Optimizely Data Platform). Server-side, queryOdpSegments(userId) in src/lib/optimizely/odp.ts resolves the visitor's qualified segments; those feed the FX decision the same way attributes do. See Personalization → ODP for the full walkthrough.
Built-in Attributes
deviceread from User-Agent header server-side (no cookie - GDPR safe)
desktoplogged_infrom demo_logged_in cookie (Audience Switcher)
falseAdding Custom Attributes
All attributes are passed when creating the user context. Merge your extras alongside the base visitor attributes - the SDK evaluates all of them against your FX audience conditions.
// All attributes go into createUserContext - no per-decide overrides.
const userCtx = client.createUserContext(userId, {
...attributes, // base: device, logged_in, persona
plan: "premium", // from your database
country: "GB", // from geo header
});
const decision = userCtx.decide("my_flag", [DISABLE_DECISION_EVENT]);Organizing CMS Flags: cms_flag vs a Separate Project #
The middleware has to know which flags drive CMS content (and should rewrite the URL) versus component or client-side experiments that should not. There are two ways to draw that line - this demo uses the first, but the second is the cleaner setup at scale.
A marker variable in one project
this demoEvery flag lives in a single FX project. Each CMS experiment sets a cms_flag: true variation variable, and the middleware filters decideAll() down to those. Quickest to start.
A dedicated FX project
recommendedCMS-routing experiments live in their own FX project with their own datafile. Project membership is the marker - cms_flag disappears and the middleware evaluates only CMS flags. cms_route still scopes each experiment to a page.
FxBucketingEvent. Point that client at the CMS project's public SDK key, or the impression lands in the wrong project and the experiment shows no participants. Everything else - the optimizelyEndUserId cookie, cms_route scoping, includeOriginal, and ISR-per-variation - is unchanged.Code: Middleware - decide + route-scoped URL rewrite #
Runs at the edge on every request. Sets the stable visitor ID, fetches the FX datafile (60s edge cache), evaluates all flags, keeps only the ones whose cms_flag and cms_route target this page, and rewrites the URL with variation path segments. The user's browser always sees the original URL - the rewrite is transparent.
// src/middleware.ts
import { createInstance, createStaticProjectConfigManager, OptimizelyDecideOption }
from "@optimizely/optimizely-sdk/universal";
export const VARIATION_MARKER = "__v_";
const MAX_CMS_VARIATIONS = 3; // safety backstop; cms_route is the real control
// An experiment declares its target route via the cms_route variation variable.
// "" / undefined = all routes, "/*" = all, "/products/*" = prefix, "/x" = exact.
function routeMatches(pathname, cmsRoute) {
if (!cmsRoute?.trim()) return true;
const path = pathname.replace(/\/$/, "") || "/";
return cmsRoute.split(",").some((raw) => {
const entry = raw.trim();
if (entry === "/*") return true;
if (entry.endsWith("/*")) {
const prefix = entry.slice(0, -2).replace(/\/$/, "") || "/";
return path === prefix || path.startsWith(prefix + "/");
}
return path === (entry.replace(/\/$/, "") || "/");
});
}
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
// Set a stable visitor ID (not httpOnly - browser SDK reads it for bucketing events).
const userId = request.cookies.get("optimizelyEndUserId")?.value ?? crypto.randomUUID();
if (!request.cookies.get("optimizelyEndUserId")) {
response.cookies.set("optimizelyEndUserId", userId, {
maxAge: 60 * 60 * 24 * 365, sameSite: "lax", path: "/",
});
}
// Skip API routes and already-rewritten paths.
if (request.nextUrl.pathname.startsWith("/api/")) return response;
if (request.nextUrl.pathname.includes(VARIATION_MARKER)) return response;
// Fetch datafile with 60s edge cache (Vercel CDN caches this automatically).
const datafile = await fetch(DATAFILE_URL, { next: { revalidate: 60 } }).then((r) => r.text());
const client = createInstance({
projectConfigManager: createStaticProjectConfigManager({ datafile }),
requestHandler: noOpRequestHandler, // no HTTP needed - static config only
});
const ctx = client.createUserContext(userId, { device, hostname, logged_in, persona });
const decisions = ctx.decideAll([OptimizelyDecideOption.DISABLE_DECISION_EVENT]);
// Keep only CMS-content experiments (cms_flag) whose cms_route targets THIS path.
// A visitor may match many experiments across the site; only the one authored on
// the current page should be applied here - this is what keeps the cache clean.
// Sorted by variationKey for a stable ISR cache key, then capped as a backstop.
const activeDecisions = Object.values(decisions)
.filter((d) => d.enabled && d.variationKey && d.variationKey !== "off")
.filter((d) => d.variables?.cms_flag === true)
.filter((d) => routeMatches(request.nextUrl.pathname, d.variables?.cms_route))
.sort((a, b) => (a.variationKey as string).localeCompare(b.variationKey as string))
.slice(0, MAX_CMS_VARIATIONS);
if (activeDecisions.length === 0) return response;
// Rewrite URL: /savings → /savings/__v_homepage--business
// Each segment encodes flagKey--variationKey so the page knows which flag fired.
// The user sees /savings in the browser - the rewrite is transparent.
// Next.js catches each rewritten path as a separate ISR cache entry.
const url = request.nextUrl.clone();
const variationSuffix = activeDecisions
.map((d) => `__v_${d.flagKey}--${d.variationKey}`)
.join("/");
url.pathname = url.pathname.replace(/\/$/, "") + `/${variationSuffix}`;
return NextResponse.rewrite(url, { headers: response.headers });
}Code: CMS page route - variation filter #
The catch-all route reads the variation keys out of the URL, collects them, and passes them to Graph. Every CMS page automatically serves the right content variant.
// src/app/[[...slug]]/page.tsx
// Middleware rewrites: /savings → /savings/__v_homepage--business
// VARIATION_MARKER = "__v_" FLAG_VAR_SEP = "--"
// Both flagKey and variationKey are encoded in the URL segment so the page
// knows which flag to fire the bucketing event for - no extra SDK call needed.
import { VARIATION_MARKER, FLAG_VAR_SEP } from "@/middleware";
function extractVariations(slug) {
const cleanSlug = slug?.filter((s) => !s.startsWith(VARIATION_MARKER));
const flagVariations = slug
?.filter((s) => s.startsWith(VARIATION_MARKER))
.map((s) => {
const [flagKey, variationKey] = s.slice(VARIATION_MARKER.length).split(FLAG_VAR_SEP);
return { flagKey, variationKey };
}) ?? [];
return {
cleanSlug,
activeVariations: flagVariations.map((fv) => fv.variationKey), // for Graph filter
flagVariations, // for bucketing event
};
}
async function CmsPage({ params }) {
const { slug } = await params;
const { cleanSlug, activeVariations, flagVariations } = extractVariations(slug);
// Pass variation keys to Graph - same filter as always, now sourced from URL not cookies
const variationOption = activeVariations.length > 0
? { variation: { include: "SOME" as const, value: activeVariations, includeOriginal: true } }
: undefined;
const client = getClient();
const items = await client.getContentByPath(`/${cleanSlug.join("/")}/`, variationOption);
const page = items.find((i) => activeVariations.includes(i._metadata?.variation)) ?? items[0];
// Look up flagKey by the variationKey Graph actually served
const servedVariation = page._metadata?.variation ?? null;
const servedFlagKey = flagVariations.find((fv) => fv.variationKey === servedVariation)?.flagKey ?? null;
return (
<>
<OptimizelyComponent content={page} />
{servedFlagKey && <FxBucketingEvent flagKey={servedFlagKey} />}
</>
);
}Code: Generated Graph query #
What getContentByPath sends to Graph under the hood when a variation is active - the base and the named variation come back together, and includeOriginal: true guarantees a safe fallback.
# Theoretical GraphQL generated by getContentByPath("/en/", variationFilter)
# when the user is bucketed into the "personal" variation:
query {
_Content(
where: {
_metadata: { url: { default: { eq: "/en/" } } }
}
variation: {
include: SOME
value: ["personal"]
includeOriginal: true # always include the base - safe fallback for non-experiment users
}
limit: 10
) {
items {
_metadata {
key
version
variation # "personal" | null (base version)
url { default }
}
... on HomePage {
composition { ... }
}
}
}
}
# Graph returns TWO items:
# items[0] → base homepage (variation: null)
# items[1] → "personal" variation (variation: "personal")
#
# The code picks items[1] because it matches activeVariations.
# If no matching CMS variation exists, includeOriginal: true ensures
# items[0] (the base) is returned - experiment is safe to deploy before
# editors create any CMS variations.Code: Bucketing event (client) #
Middleware encoded flagKey--variationKey into the URL, so extractVariations(slug) already knows the flagKey - no extra SDK call. When Graph confirms a variation was served, the page mounts <FxBucketingEvent />, which calls decide(flagKey, []) client-side for that flag only. Its attributes must mirror the middleware context that produced the variation.
// src/components/FxBucketingEvent.tsx
// flagKey is passed in from the page - it was encoded in the URL by middleware:
// /savings → /savings/__v_homepage--business
// extractVariations() in page.tsx parses it back out.
// No decideAll() here - the flagKey is already known from the route.
"use client";
export function FxBucketingEvent({ flagKey }: { flagKey: string }) {
useEffect(() => {
const userId = getCookie("optimizelyEndUserId");
if (!userId) return;
void getOptimizelyBrowserClient().then((client) => {
if (!client) return;
const ua = navigator.userAgent;
const device = /mobile|android|iphone|ipad/i.test(ua) ? "mobile" : "desktop";
const persona = getCookie("demo_persona");
// Attributes MUST mirror src/middleware.ts (which produced the served
// variation) - including logged_in - or the impression can land on a
// different variation than the one that was rendered.
const ctx = client.createUserContext(userId, {
device,
hostname: window.location.hostname,
logged_in: !!getCookie("demo_bucketing_id"),
...(persona ? { persona } : {}),
});
ctx?.decide(flagKey, []); // fire bucketing event for this flag only
});
}, [flagKey]);
return null;
}
// src/app/[[...slug]]/page.tsx
// flagKey comes from the URL segment, not from a client-side SDK call.
// flagVariations = [{ flagKey: "homepage", variationKey: "business" }]
const { cleanSlug, activeVariations, flagVariations } = extractVariations(slug);
const servedVariation = page._metadata?.variation ?? null;
const servedFlagKey = flagVariations.find((fv) => fv.variationKey === servedVariation)?.flagKey ?? null;
return (
<>
<OptimizelyComponent content={page} />
{servedFlagKey && <FxBucketingEvent flagKey={servedFlagKey} />}
</>
);Choosing an Approach #
There are three ways to integrate Feature Experimentation with a Next.js CMS page route. The right choice depends on whether CDN caching matters for your traffic profile.
Edge Middleware + URL rewrite
Middleware evaluates FX and rewrites the URL with __v_flagKey--variationKey. The page reads variation from params (no cookies()) so ISR works. Each variation URL is a separate CDN cache entry. A small client component fires the bucketing event after render.
force-dynamic SSR
The SDK user context is created in the page render, reading cookies for userId and attributes. decideAll() runs server-side, variation filter goes to Graph, and the bucketing event fires server-side too. Simpler code - no middleware changes or client component needed.
Client-side only
Page renders base content from the server. After hydration, the browser SDK evaluates flags and fetches the variation. Content swaps in after the initial render - the user sees a flash of the base content before the variation appears.
Code: Single flag decision in a component #
For feature-gating or variable-driven UI outside the CMS page route. The same impression rule applies - call userCtx.decide(flagKey) (no options, or empty array) when the variation is actually rendered to fire the impression.
// Server component - client and userId are resolved upstream
// (same pattern as middleware: createInstance with a 60s-cached datafile).
const userCtx = client.createUserContext(userId, attributes);
// Evaluate without firing an impression:
const decision = userCtx.decide(
"hero_copy",
[OptimizelyDecideOption.DISABLE_DECISION_EVENT],
);
if (!decision.enabled) return null;
// Variation will be rendered - fire the impression:
void userCtx.decide("hero_copy");
// Variables come back typed - cast to the type you expect:
const headline = decision.variables.headline as string;
const subheadline = decision.variables.subheadline as string;
return <Hero headline={headline} subheadline={subheadline} variation={decision.variationKey} />;Bucketing ID Override #
By default the FX SDK buckets users by their userId. Setting the reserved $opt_bucketing_id attribute overrides which ID drives bucketing - while keeping the original userId for analytics. This means all users sharing the same bucketing ID land in the same variation, regardless of their individual user IDs.
B2B / account-level experiments
Every seat on the same company account sees the same variation. Avoids the awkward situation where user A sees Variation 1 and user B on the same account sees Variation 2 in the same meeting.
Cross-device consistency
A logged-in user ID works as the bucketing ID - the same variation follows the user across their phone, tablet, and desktop, regardless of which device generated their anonymous visitor ID.
Gradual rollouts to accounts
Roll out a new feature to 10% of accounts (not 10% of users). Avoids fragmenting the experience inside the same company during a staged rollout.
// Normal decision - bucketed by the visitor's anonymous userId
const userCtx = client.createUserContext(userId, attributes);
const decision = userCtx.decide("hero_copy", [DISABLE_DECISION_EVENT]);
// Account-level decision - when logged in, bucket by account ID instead.
// All seats on the same account see the same variation.
// userId is still used for analytics; only bucketing is overridden.
const accountCtx = accountId
? client.createUserContext(userId, {
...attributes,
$opt_bucketing_id: accountId,
})
: null;
const accountDecision = accountCtx?.decide("hero_copy", [DISABLE_DECISION_EVENT]);Live comparison - hero_copy
Sign in via the Audience Switcher (bottom-right) to see the account-level decision alongside your normal decision.
Normal - bucketed by userId
hero_copy
Flag is off - enable it in the FX dashboard to see the live hero copy variation.
variation: off
Account - bucketed by not set
Sign in via the Audience Switcher to activate
All Flag Decisions (diagnostics) #
A stable optimizelyEndUserId cookie is set by Next.js middleware on first visit, and every flag is evaluated for it via userContext.decideAll() - reload and you always land in the same variation. Changes in the FX dashboard take effect within 60 seconds (datafile cache TTL). Each decision carries a variables map of typed values (strings, booleans, numbers, JSON) that drive copy or configuration per variation without code changes.
banneroff{
"title": "test",
"description": "",
"image": "",
"imageText": "",
"linkText": "",
"cache": true,
"banner_position": 0
}search_algorithmenabled{
"search_algorithm": {
"_ranking": "SEMANTIC",
"_semanticWeight": 0.9
}
}homepageoff{
"cms_flag": true
}hero_layoutoff{
"layout": "split"
}nav_search_styleoff{
"style": "icon"
}product_card_orderoff{
"order": "checking_first"
}trust_section_styleoff{
"style": "stats"
}footer_ctaoff{
"style": "app_download"
}hero_dual_ctaoff{
"secondaryLabel": "",
"secondaryUrl": ""
}hero_social_proofoffmobile_navoffrates_baroff{
"apy": "4.75%",
"product": "savings"
}sticky_offer_baroff{
"message": "Limited offer: 0% APR for 12 months on balance transfers.",
"linkText": "Learn more",
"linkUrl": "/personal/credit-cards",
"expiryLabel": "Ends Friday"
}hero_copyoff{
"headline": "",
"subheadline": ""
}example_turnoff_onoffKey Things to Know#
- →The variation key is the only contract between FX and the CMS. The string must match exactly (case-sensitive) between the FX variation key and the CMS variation name.
- →includeOriginal: true means users outside the experiment always get the original content. Safe to add the filter before any CMS variations exist.
- →Datafile is cached for 60 seconds via Next.js fetch revalidation. Changes in the FX dashboard propagate within one minute with no server restart.
- →React cache() is scoped to a single HTTP request. Wrapping the user context factory in React
cache()means any number of server components can call it and share one context per request. Concurrent visitors each get their own completely isolated context; nothing is shared across users. - →DISABLE_DECISION_EVENT suppresses bucketing events during the middleware routing pass. Once the variation is rendered,
<FxBucketingEvent flagKey={...} />mounts client-side and fires the impression for that flag only - its attributes must mirror the middleware context, or the impression can bucket differently than what was served. - →cms_route scopes an experiment to a page. The middleware only applies a variation whose
cms_routematches the current path, so many CMS experiments can run across the site without colliding or fragmenting each other's ISR cache. There is no server-side re-decide -page._metadata.variationconfirms what Graph served. - →Variations work on any content type - pages, shared blocks, navigation. Wherever Graph accepts a variation filter, the SDK wires in seamlessly.
- →CMS variations must be created in Visual Builder, but can then be updated via the Management API. The REST API silently ignores the
variationfield onPOST- you cannot create a named variation programmatically. But creating one in the UI generates a new draft version that you can PATCH and publish.
Source files1 file
import { cache } from "react";
import { OptimizelyDecideOption } from "@optimizely/optimizely-sdk";
import { getOptimizelyClient } from "./experimentation";
import type { FxDecision, FxAttributes } from "./experimentation";
import { getVisitorContext } from "./visitor";
type DecideOpts =
| OptimizelyDecideOption[]
| { options?: OptimizelyDecideOption[]; bucketingId?: string; attributes?: FxAttributes };
function resolveOpts(opts: DecideOpts | undefined): {
sdkOptions: OptimizelyDecideOption[];
bucketingId?: string;
attributes?: FxAttributes;
} {
if (!opts || Array.isArray(opts)) {
return { sdkOptions: opts ?? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] };
}
return {
sdkOptions: opts.options ?? [OptimizelyDecideOption.DISABLE_DECISION_EVENT],
bucketingId: opts.bucketingId,
attributes: opts.attributes,
};
}
const noDecision = (flagKey: string): FxDecision => ({
flagKey, enabled: false, variationKey: null, variables: {}, reasons: [],
});
const noOpUser = {
userId: "anonymous" as string,
bucketingId: undefined as string | undefined,
decide: (flagKey: string, _opts?: DecideOpts): FxDecision => noDecision(flagKey),
decideAll: (): Record<string, FxDecision> => ({}),
};
export const getOptimizelyUser = cache(async () => {
const [client, { userId, attributes, bucketingId }] = await Promise.all([
getOptimizelyClient(),
getVisitorContext(),
]);
if (!client) return { ...noOpUser, userId, bucketingId };
const ctx = client.createUserContext(userId, attributes);
if (!ctx) return { ...noOpUser, userId, bucketingId };
return {
userId,
bucketingId,
decide(flagKey: string, opts?: DecideOpts): FxDecision {
const { sdkOptions, bucketingId: bId, attributes: attrOverrides } = resolveOpts(opts);
const activeCtx = bId || attrOverrides
? client.createUserContext(userId, {
...attributes,
...attrOverrides,
...(bId ? { $opt_bucketing_id: bId } : {}),
}) ?? ctx
: ctx;
const d = activeCtx.decide(flagKey, sdkOptions);
return {
flagKey,
enabled: d.enabled,
variationKey: d.variationKey,
variables: d.variables as Record<string, unknown>,
reasons: d.reasons,
};
},
decideAll(): Record<string, FxDecision> {
const raw = ctx.decideAll([OptimizelyDecideOption.DISABLE_DECISION_EVENT]);
const out: Record<string, FxDecision> = {};
for (const [key, d] of Object.entries(raw)) {
out[key] = {
flagKey: key,
enabled: d.enabled,
variationKey: d.variationKey,
variables: d.variables as Record<string, unknown>,
reasons: d.reasons,
};
}
return out;
},
};
});