The Optimization SDK Suite is pre-release (alpha). Breaking changes may be published at any time.
The Optimization Core SDK encapsulates all platform-agnostic functionality and business logic. All other SDKs descend from the Core SDK.
Install using an NPM-compatible package manager, pnpm for example:
pnpm install @contentful/optimization-core
Import either the stateful or stateless Core class, depending on the target environment; both CJS and ESM module systems are supported, ESM preferred:
import { CoreStateful } from '@contentful/optimization-core'
// or
import { CoreStateless } from '@contentful/optimization-core'
Configure and initialize the Core SDK:
const optimization = new CoreStateful({ clientId: 'abc123' })
// or
const optimization = new CoreStateless({ clientId: 'abc123' })
The CoreStateless class is intended to be used as the basis for SDKs that would run in stateless
environments such as Node-based servers and server-side functions in meta-frameworks such as
Next.js.
In stateless environments, Core will not maintain any internal state, which includes user consent. These concerns should be handled by consumers to fit their specific architectural and design specifications.
The CoreStateful class is intended to be used as the basis for SDKs that would run in stateful
environments such as Web front-ends and mobile applications (via JavaScript runtime containers).
In stateful environments, Core maintains state internally for consent, an event stream, and profile-related data that is commonly obtained from requests to the Experience API. These states are exposed externally as read-only observables.
CoreStateful uses module-global state by design. Initialize exactly one stateful instance per
JavaScript runtime and reuse it.
| Option | Required? | Default | Description |
|---|---|---|---|
analytics |
No | See "Analytics Options" | Configuration specific to the Analytics/Insights API |
clientId |
Yes | N/A | The Optimization API key |
environment |
No | 'main' |
The environment identifier |
eventBuilder |
No | See "Event Builder Options" | Event builder configuration (channel/library metadata, etc.) |
fetchOptions |
No | See "Fetch Options" | Configuration for Fetch timeout and retry functionality |
logLevel |
No | 'error' |
Minimum log level for the default console sink |
personalization |
No | See "Personalization Options" | Configuration specific to the Personalization/Experience API |
The following configuration options apply only in stateful environments:
| Option | Required? | Default | Description |
|---|---|---|---|
allowedEventTypes |
No | ['identify', 'page', 'screen'] |
Allow-listed event types permitted when consent is not set |
defaults |
No | undefined |
Set of default state values applied on initialization |
getAnonymousId |
No | undefined |
Function used to obtain an anonymous user identifier |
onEventBlocked |
No | undefined |
Callback invoked when an event call is blocked by guards |
Configuration method signatures:
getAnonymousId: () => string | undefinedonEventBlocked: (event: BlockedEvent) => void| Option | Required? | Default | Description |
|---|---|---|---|
baseUrl |
No | 'https://ingest.insights.ninetailed.co/' |
Base URL for the Insights API |
The following configuration options apply only in stateful environments:
| Option | Required? | Default | Description |
|---|---|---|---|
beaconHandler |
No | undefined |
Handler used to enqueue events via the Beacon API or a similar mechanism |
queuePolicy |
No | See method signatures | Queue flush retry/backoff/circuit policy for stateful analytics |
Configuration method signatures:
beaconHandler: (url: string | URL, data: BatchInsightsEventArray) => boolean
queuePolicy:
{
baseBackoffMs?: number,
maxBackoffMs?: number,
jitterRatio?: number,
maxConsecutiveFailures?: number,
circuitOpenMs?: number,
onFlushFailure?: (context: QueueFlushFailureContext) => void,
onCircuitOpen?: (context: QueueFlushFailureContext) => void,
onFlushRecovered?: (context: QueueFlushRecoveredContext) => void
}
Supporting callback payloads:
type QueueFlushFailureContext = {
consecutiveFailures: number
queuedBatches: number
queuedEvents: number
retryDelayMs: number
}
type QueueFlushRecoveredContext = {
consecutiveFailures: number
}
Notes:
jitterRatio is clamped to [0, 1].maxBackoffMs is normalized to be at least baseBackoffMs.false responses and thrown send errors.Event builder options should only be supplied when building an SDK on top of Core or any of its descendent SDKs.
| Option | Required? | Default | Description |
|---|---|---|---|
app |
No | undefined |
The application definition used to attribute events to a specific consumer app |
channel |
Yes | N/A | The channel that identifies where events originate from (e.g. 'web', 'mobile') |
library |
Yes | N/A | The client library metadata that is attached to all events |
The channel option may contain one of the following values:
webmobileserverThe following configuration options apply only in stateful environments:
| Option | Required? | Default | Description |
|---|---|---|---|
getLocale |
No | () => 'en-US' |
Function used to resolve the locale for outgoing events |
getPageProperties |
No | () => DEFAULT_PAGE_PROPERTIES |
Function that returns the current page properties |
getUserAgent |
No | () => undefined |
Function used to obtain the current user agent string when applicable |
Configuration method signatures:
getLocale: () => string | undefined
getPageProperties:
() => {
path: string,
query: Record<string, string>,
referrer: string,
search: string,
title?: string,
url: string
}
getUserAgent: () => string | undefined
Fetch options allow for configuration of a Fetch API-compatible fetch method and the retry/timeout
logic integrated into the Optimization API Client. Specify the fetchMethod when the host
application environment does not offer a fetch method that is compatible with the standard Fetch
API in its global scope.
| Option | Required? | Default | Description |
|---|---|---|---|
fetchMethod |
No | undefined |
Signature of a fetch method used by the API clients |
intervalTimeout |
No | 0 |
Delay (in milliseconds) between retry attempts |
onFailedAttempt |
No | undefined |
Callback invoked whenever a retry attempt fails |
onRequestTimeout |
No | undefined |
Callback invoked when a request exceeds the configured timeout |
requestTimeout |
No | 3000 |
Maximum time (in milliseconds) to wait for a response before aborting |
retries |
No | 1 |
Maximum number of retry attempts |
Configuration method signatures:
fetchMethod: (url: string | URL, init: RequestInit) => Promise<Response>onFailedAttempt and onRequestTimeout: (options: FetchMethodCallbackOptions) => voidCore inherits the API Client retry contract: default retries intentionally apply only to HTTP
503 responses (Service Unavailable). This is deliberate and aligned with current Experience
and Insights API expectations; do not broaden retry status handling without an explicit API
contract change.
| Option | Required? | Default | Description |
|---|---|---|---|
baseUrl |
No | 'https://experience.ninetailed.co/' |
Base URL for the Experience API |
enabledFeatures |
No | ['ip-enrichment', 'location'] |
Enabled features which the API may use for each request |
ip |
No | undefined |
IP address to override the API behavior for IP analysis |
locale |
No | 'en-US' (in API) |
Locale used to translate location.city and location.country |
plainText |
No | false |
Sends performance-critical endpoints in plain text |
preflight |
No | false |
Instructs the API to aggregate a new profile state but not store it |
The following configuration options apply only in stateful environments:
| Option | Required? | Default | Description |
|---|---|---|---|
queuePolicy |
No | See method signatures | Queue and flush-retry policy for stateful personalization offline buffering |
Configuration method signatures:
queuePolicy:
{
maxEvents?: number,
onDrop?: (context: PersonalizationOfflineQueueDropContext) => void,
flushPolicy?: {
baseBackoffMs?: number,
maxBackoffMs?: number,
jitterRatio?: number,
maxConsecutiveFailures?: number,
circuitOpenMs?: number,
onFlushFailure?: (context: QueueFlushFailureContext) => void,
onCircuitOpen?: (context: QueueFlushFailureContext) => void,
onFlushRecovered?: (context: QueueFlushRecoveredContext) => void
}
}
Supporting callback payloads:
type PersonalizationOfflineQueueDropContext = {
droppedCount: number
droppedEvents: ExperienceEventArray
maxEvents: number
queuedEvents: number
}
type QueueFlushFailureContext = {
consecutiveFailures: number
queuedBatches: number
queuedEvents: number
retryDelayMs: number
}
type QueueFlushRecoveredContext = {
consecutiveFailures: number
}
Notes:
maxEvents is 100.onDrop is best-effort; callback errors are swallowed.flushPolicy uses the same normalization semantics as analytics.queuePolicy.The methods in this section are available in both stateful and stateless Core classes. However, be aware that there are some minor differences in argument usage between stateful and stateless Core implementations.
Arguments marked with an asterisk (*) are always required.
getCustomFlagGet the specified Custom Flag's value from the provided changes array, or from the current internal state in stateful implementations.
Arguments:
name*: The name/key of the Custom Flagchanges: Changes arrayReturns:
undefined if it cannot be found.If the changes argument is omitted in stateless implementations, the method will return
undefined.
personalizeEntryResolve a baseline Contentful entry to a personalized variant using the provided selected personalizations, or from the current internal state in stateful implementations.
Type arguments:
S: Entry skeleton typeM: Chain modifiersL: Locale codeArguments:
entry*: The entry to personalizepersonalizations: Selected personalizationsReturns:
If the personalizations argument is omitted in stateless implementations, the method will return
the baseline entry.
getMergeTagValueResolve a "Merge Tag" to a value based on the current (or provided) profile. A "Merge Tag" is a special Rich Text fragment supported by Contentful that specifies a profile data member to be injected into the Rich Text when rendered.
Arguments:
embeddedNodeEntryTarget*: The merge tag entry node to resolveprofile: The user profileIf the profile argument is omitted in stateless implementations, the method will return the
merge tag's fallback value.
Only the following methods may return an OptimizationData object:
identifypagescreentracktrackComponentView (when payload.sticky is true)trackComponentClick and trackFlagView return no data. When returned, OptimizationData
contains:
changes: Currently used for Custom Flagspersonalizations: Selected personalizations for the profileprofile: Profile associated with the evaluated eventsidentifyIdentify the current profile/visitor to associate traits with a profile.
Arguments:
payload*: Identify event builder arguments object, including an optional profile property
with a PartialProfile value that requires only an idpageRecord a personalization page view.
Arguments:
payload*: Page view event builder arguments object, including an optional profile property
with a PartialProfile value that requires only an idscreenRecord a personalization screen view.
Arguments:
payload*: Screen view event builder arguments object, including an optional profile property
with a PartialProfile value that requires only an idtrackRecord a personalization custom track event.
Arguments:
payload*: Track event builder arguments object, including an optional profile property with a
PartialProfile value that requires only an idtrackComponentViewRecord an analytics component view event. When the payload marks the component as "sticky", an
additional personalization component view is recorded. This method only returns OptimizationData
when the component is marked as "sticky".
Arguments:
payload*: Component view event builder arguments object, including an optional profile
property with a PartialProfile value that requires only an idtrackComponentClickRecord an analytics component click event.
Returns:
voidArguments:
payload*: Component click event builder arguments objecttrackFlagViewTrack a feature flag view via analytics. This is functionally the same as a non-sticky component view event.
Returns:
voidArguments:
payload*: Component view event builder arguments objectconsentUpdates the user consent state.
Arguments:
accept: A boolean value specifying whether the user has accepted (true) or denied (false)resetResets all internal state except consent. This method expects no arguments and returns no value.
flushFlushes queued analytics and personalization events. This method expects no arguments and returns a
Promise<void>.
destroyReleases singleton ownership for stateful runtime usage. This is intended for explicit teardown paths, such as tests or hot-reload workflows. This method expects no arguments and returns no value.
registerPreviewPanel (preview tooling only)Registers a preview consumer object and exposes internal signal references used by first-party preview tooling.
Arguments:
previewPanel: Required object that receives symbol-keyed signal bridge valuesReturns:
voidBridge symbols:
PREVIEW_PANEL_SIGNALS_SYMBOL: key used to expose internal signalsPREVIEW_PANEL_SIGNAL_FNS_SYMBOL: key used to expose internal signalFnsExample:
import {
PREVIEW_PANEL_SIGNAL_FNS_SYMBOL,
PREVIEW_PANEL_SIGNALS_SYMBOL,
type PreviewPanelSignalObject,
} from '@contentful/optimization-core'
const previewBridge: PreviewPanelSignalObject = {}
optimization.registerPreviewPanel(previewBridge)
const signals = previewBridge[PREVIEW_PANEL_SIGNALS_SYMBOL]
const signalFns = previewBridge[PREVIEW_PANEL_SIGNAL_FNS_SYMBOL]
This method intentionally exposes mutable internal signals for preview tooling. The Web and React Native preview panels are tightly coupled by design and rely on this bridge (plus state interceptors) to apply immediate local overrides without network round-trips. This coupling is deliberate and necessary for preview functionality.
CoreStateful only)states is available on CoreStateful and exposes signal-backed observables for runtime state.
Available state streams:
consent: Current consent state (boolean | undefined)blockedEventStream: Latest blocked-call metadata (BlockedEvent | undefined)eventStream: Latest emitted analytics/personalization event
(AnalyticsEvent | PersonalizationEvent | undefined)flags: Resolved Custom Flags (Flags | undefined)canPersonalize: Whether personalization selections are available (boolean;
personalizations !== undefined)profile: Current profile (Profile | undefined)personalizations: Current selected personalizations (SelectedPersonalizationArray | undefined)previewPanelAttached: Preview panel attachment state (boolean)previewPanelOpen: Preview panel open state (boolean)Each observable provides:
current: Deep-cloned snapshot of the latest valuesubscribe(next): Immediately emits current, then emits future updatessubscribeOnce(next): Emits the first non-nullish value, then auto-unsubscribescurrent and callback payloads are deep-cloned snapshots, so local mutations do not affect Core's
internal signal state.
Update behavior:
blockedEventStream updates whenever a call is blocked by consent guards.eventStream updates when a valid event is accepted for send/queue.flags, profile, and personalizations update from Experience API responses.canPersonalize updates whenever personalizations becomes defined or undefined.consent updates from defaults and optimization.consent(...).previewPanelAttached and previewPanelOpen are controlled by preview tooling and are preserved
across reset().Example: read the latest snapshot synchronously:
const profile = optimization.states.profile.current
if (profile) console.log(`Current profile: ${profile.id}`)
Example: subscribe and clean up:
const sub = optimization.states.profile.subscribe((profile) => {
if (!profile) return
console.log(`Profile ${profile.id} updated`)
})
// later (component unmount / teardown)
sub.unsubscribe()
Example: wait for first available profile:
optimization.states.profile.subscribeOnce((profile) => {
console.log(`Profile ${profile.id} loaded`)
})
Interceptors may be used to read and/or modify data flowing through the Core SDK.
event: Intercepts an event's data before it is queued and/or emittedstate: Intercepts state data retrieved from an Experience API call before updating the SDK's
internal stateExample interceptor usage:
optimization.interceptors.event((event) => {
event.properties.timestamp = new Date().toISOString()
})
Interceptors are intended to enable low-level interoperability; to simply read and react to
Optimization SDK events, use the states observables.
Optimization Core SDK — platform-agnostic personalization and analytics.