Warning
The Optimization SDK Suite is pre-release (alpha). Breaking changes may be published at any time.
This SDK implements functionality specific to the Web environment, based on the Optimization Core Library. This SDK is part of the Contentful Optimization SDK Suite.
Table of Contents
Install using an NPM-compatible package manager, pnpm for example:
pnpm install @contentful/optimization-webImport the Optimization class; both CJS and ESM module systems are supported, ESM preferred:
import ContentfulOptimization from '@contentful/optimization-web'Configure and initialize the Optimization Web SDK:
const optimization = new ContentfulOptimization({ clientId: 'abc123' })Important
Initialize the Web SDK once per page runtime. Reuse window.contentfulOptimization (or your own
singleton container binding) instead of creating additional instances.
Alternatively, the Web SDK can be used directly within an HTML file:
<script src="https://cdn.jsdelivr.net/npm/@contentful/optimization-web@latest/dist/contentful-optimization-web.umd.js"></script>
<script>
new ContentfulOptimization({ clientId: 'abc123' })
// is equal to:
// window.contentfulOptimization = new ContentfulOptimization({ clientId: 'abc123' })
</script>- Web Vanilla: Example static Web page that renders and emits Insights API events for optimized content using a vanilla JS drop-in build of the Web SDK
| Option | Required? | Default | Description |
|---|---|---|---|
allowedEventTypes |
No | ['identify', 'page'] |
Allow-listed event types permitted when consent is not set |
api |
No | See "API Options" | Unified configuration for the Experience API and Insights API endpoints |
app |
No | undefined |
The application definition used to attribute events to a specific consumer app |
autoTrackEntryInteraction |
No | { views: false, clicks: false, hovers: false } |
Opt-in automated tracking of entry interactions (views, clicks, hovers) |
clientId |
Yes | N/A | Shared API key for Experience API and Insights API requests |
cookie |
No | { domain: undefined, expires: 365 } |
Cookie configuration for anonymous ID persistence |
defaults |
No | undefined |
Set of default state values applied on initialization |
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 |
getAnonymousId |
No | undefined |
Function used to obtain an anonymous user identifier |
logLevel |
No | 'error' |
Minimum log level for the default console sink |
onEventBlocked |
No | undefined |
Callback invoked when an event call is blocked by guards |
queuePolicy |
No | See "Queue Policy Options" | Shared queue and retry configuration for stateful delivery |
Configuration method signatures:
-
cookie:{ domain?: string expires?: number }
-
getAnonymousId:() => string | undefined -
onEventBlocked:(event: BlockedEvent) => void
| Option | Required? | Default | Description |
|---|---|---|---|
experienceBaseUrl |
No | 'https://experience.ninetailed.co/' |
Base URL for the Experience API |
insightsBaseUrl |
No | 'https://ingest.insights.ninetailed.co/' |
Base URL for the Insights API |
beaconHandler |
No | Built-in beacon API integration | Handler used to enqueue Insights API events via the Beacon API or similar |
enabledFeatures |
No | ['ip-enrichment', 'location'] |
Enabled features the Experience API may use for each request |
ip |
No | undefined |
IP address override used by the Experience API for location analysis |
locale |
No | 'en-US' (in API) |
Locale used to translate location.city and location.country |
plainText |
No | false |
Sends performance-critical Experience API endpoints in plain text |
preflight |
No | false |
Instructs the Experience API to aggregate a new profile state but not store it |
Configuration method signatures:
beaconHandler:(url: string | URL, data: BatchInsightsEventArray) => boolean
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 |
No | 'web' |
The channel that identifies where events originate from (e.g. 'web', 'mobile') |
library |
No | { name: '@contentful/optimization-web', version: '0.0.0' } |
The client library metadata that is attached to all events |
getLocale |
No | Built-in locale resolution | Function used to resolve the locale for outgoing events |
getPageProperties |
No | Built-in page properties resolution | Function that returns the current page properties |
getUserAgent |
No | Built-in user agent resolution | Function used to obtain the current user agent string when applicable |
The channel option may contain one of the following values:
webmobileserver
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 SDK's bundled API clients. 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>onFailedAttemptandonRequestTimeout:(options: FetchMethodCallbackOptions) => void
Note
Web SDK fetch retry behavior intentionally matches the shared API Client contract: default retries
apply only to HTTP 503 responses (Service Unavailable). This is deliberate and aligned with
current Experience API and Insights API expectations.
queuePolicy is available in the stateful Web SDK runtime and combines shared flush retry settings
with Experience API offline buffering controls.
Configuration shape:
{
flush?: {
baseBackoffMs?: number,
maxBackoffMs?: number,
jitterRatio?: number,
maxConsecutiveFailures?: number,
circuitOpenMs?: number,
onFlushFailure?: (context: QueueFlushFailureContext) => void,
onCircuitOpen?: (context: QueueFlushFailureContext) => void,
onFlushRecovered?: (context: QueueFlushRecoveredContext) => void
},
offlineMaxEvents?: number,
onOfflineDrop?: (context: ExperienceQueueDropContext) => void
}Supporting callback payloads:
type ExperienceQueueDropContext = {
droppedCount: number
droppedEvents: ExperienceEventArray
maxEvents: number
queuedEvents: number
}
type QueueFlushFailureContext = {
consecutiveFailures: number
queuedBatches: number
queuedEvents: number
retryDelayMs: number
}
type QueueFlushRecoveredContext = {
consecutiveFailures: number
}Notes:
flushapplies the same retry/backoff/circuit policy to both Insights API flushing and Experience API offline replay.- Invalid numeric values fall back to defaults.
jitterRatiois clamped to[0, 1].maxBackoffMsis normalized to be at leastbaseBackoffMs.- Failed flush attempts include both
falseresponses and thrown send errors.
Web storage persistence is best-effort. If localStorage writes fail (for example due to quota or
access restrictions), the SDK continues operating with in-memory state and will retry persistence on
future writes.
tracking is a namespaced API for entry-interaction tracking controls.
Available methods:
enable(interaction, options?): Enable automatic tracking for an interactiondisable(interaction): Disable automatic tracking for an interactionenableElement(interaction, element, options?): Force-enable tracking for one elementdisableElement(interaction, element): Force-disable tracking for one elementclearElement(interaction, element): Remove a manual element override
Supported interaction values:
'views''clicks''hovers'
Precedence behavior:
enable/disablecontrols automatic tracking globally for the interaction.enableElementanddisableElementtake precedence for the specific element.clearElementremoves the element-level override and restores automatic behavior.
Example: global tracking controls:
optimization.tracking.enable('views', { dwellTimeMs: 1500, minVisibleRatio: 0.25 })
optimization.tracking.disable('clicks')
optimization.tracking.enable('hovers', { dwellTimeMs: 1000, hoverDurationUpdateIntervalMs: 5000 })Example: per-element override:
optimization.tracking.enableElement('views', element, { dwellTimeMs: 2000 })
// later
optimization.tracking.clearElement('views', element)See Entry View Tracking, Entry Hover Tracking, and Entry Click Tracking for interaction-specific behavior and payload options.
states exposes signal-backed observables from the shared stateful Core runtime.
Available state streams:
consent: Current consent state (boolean | undefined)blockedEventStream: Latest blocked-call metadata (BlockedEvent | undefined)eventStream: Latest emitted Insights API or Experience API event (InsightsEvent | ExperienceEvent | undefined)flag(name): Key-scoped flag observable (Observable<Json>)canOptimize: Whether optimization selections are available (boolean;selectedOptimizations !== undefined)profile: Current profile (Profile | undefined)selectedOptimizations: Current selected optimizations (SelectedOptimizationArray | 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 emitscurrent, then emits future updatessubscribeOnce(next): Emits the first non-nullish value, then auto-unsubscribes
current and callback payloads are deep-cloned snapshots, so local mutations do not affect SDK
internal state.
Update behavior:
blockedEventStreamupdates whenever a call is blocked by consent guards.eventStreamupdates when a valid event is accepted for send/queue.flag(name)updates when the resolved value for that key changes.canOptimizeupdates wheneverselectedOptimizationsbecomes defined orundefined.consentupdates from defaults andoptimization.consent(...).previewPanelAttachedandpreviewPanelOpenare controlled by preview tooling and are preserved acrossreset().
Example: read synchronously from the latest snapshot:
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 the first available profile:
optimization.states.profile.subscribeOnce((profile) => {
console.log(`Profile ${profile.id} loaded`)
})Arguments marked with an asterisk (*) are always required.
Updates the user consent state.
Arguments:
accept: A boolean value specifying whether the user has accepted (true) or denied (false)
Resets all internal state except consent. This method expects no arguments and returns no value.
Flushes queued Insights API and Experience API events. This method expects no arguments and returns
a Promise<void>.
Destroys the current SDK instance and releases runtime listeners/resources. This method is intended for explicit teardown paths, such as tests or hot-reload workflows. It expects no arguments and returns no value.
Starts automatic tracking for a specific entry interaction.
Arguments:
interaction: The interaction type to track (for example,'views','clicks', or'hovers')options: Interaction startup options to pass to the tracker
When interaction is 'views', supported options are:
dwellTimeMs: Required time before emitting the view event; default 1,000msviewDurationUpdateIntervalMs: Interval for periodic view-duration update events; default 5,000msminVisibleRatio: Minimum intersection ratio considered "visible"; default0.1(10%)root:IntersectionObserverroot; defaultnull(viewport)rootMargin:IntersectionObserverrootMargin; default0px
When interaction is 'clicks', no additional startup options are currently supported.
When interaction is 'hovers', supported options are:
dwellTimeMs: Required hover time before the first hover event; default 1,000mshoverDurationUpdateIntervalMs: Interval for periodic hover-duration update events; default 5,000ms
Warning
This method is called internally for auto-enabled interactions in autoTrackEntryInteraction when
consent is given.
Disables automatic tracking for a specific entry interaction.
Arguments:
interaction: The interaction type to stop tracking (for example,'views','clicks', or'hovers')
This method returns no value. If force-enabled element overrides exist for that interaction, those overrides can keep the detector active.
Warning
This method is called internally for auto-enabled interactions in autoTrackEntryInteraction when
consent has not been given.
Manually force-enables tracking for a specific element and interaction.
Arguments:
interaction: The interaction type to track (for example,'views','clicks', or'hovers')element: A DOM element that directly contains the entry content to be trackedoptions: Optional per-element options- when
interactionis'views':data: Entry-specific data used for payload extraction; see "Entry View Tracking"dwellTimeMs: Per-element override of the required time before emitting the view event
- when
interactionis'clicks':data: Entry-specific data used for click payload extraction
- when
interactionis'hovers':data: Entry-specific data used for hover payload extraction; see "Entry Hover Tracking"dwellTimeMs: Per-element override of the required time before emitting the first hover eventhoverDurationUpdateIntervalMs: Per-element override for periodic hover-duration updates
- when
enableElement can be used even when automatic tracking for the interaction is disabled.
Manually force-disables tracking for a specific element and interaction.
Arguments:
interaction: The interaction type to disable (for example,'views','clicks', or'hovers')element: The target DOM element
disableElement takes precedence over automatic tracking for that element.
Clears a manual element override for a specific interaction.
Arguments:
interaction: The interaction type to clear (for example,'views','clicks', or'hovers')element: The target DOM element
After clearElement, the element falls back to automatic behavior for that interaction.
Get the specified Custom Flag's value from the provided changes array, or from the current internal state.
Arguments:
name*: The name/key of the Custom Flagchanges: Changes array
Returns:
- The resolved value for the specified Custom Flag, or
undefinedif it cannot be found.
Behavior notes:
- Web SDK is stateful; calling
getFlag(...)automatically emits a flag view event viatrackFlagView. states.flag(name)also emits flag view events when read/subscribed.- If full map resolution is needed for advanced use cases, use
optimization.flagsResolver.resolve(changes).
Resolve a baseline Contentful entry to an optimized variant using the provided selected optimizations, or from the current internal state.
Type arguments:
S: Entry skeleton typeM: Chain modifiersL: Locale code
Arguments:
entry*: The baseline entry to resolveselectedOptimizations: Selected optimizations
Returns:
- The resolved optimized entry variant, or the supplied baseline entry if baseline is the selected variant or a variant cannot be found.
Resolve 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 profile
Only the following methods may return an OptimizationData object:
identifypagescreentracktrackView(whenpayload.stickyistrue)
trackClick, trackHover, and trackFlagView return no data. When returned, OptimizationData
contains:
changes: Currently used for Custom FlagsselectedOptimizations: Selected optimizations for the profileprofile: Profile associated with the evaluated events
Identify the current profile/visitor to associate traits with a profile.
Arguments:
payload*: Identify event builder arguments object, including an optionalprofileproperty with aPartialProfilevalue that requires only anid
Record an Experience API page view.
Arguments:
payload*: Page view event builder arguments object, including an optionalprofileproperty with aPartialProfilevalue that requires only anid
Record an Experience API screen view.
Arguments:
payload*: Screen view event builder arguments object, including an optionalprofileproperty with aPartialProfilevalue that requires only anid
Record an Experience API custom track event.
Arguments:
payload*: Track event builder arguments object, including an optionalprofileproperty with aPartialProfilevalue that requires only anid
Record an Insights API entry view event. When the payload marks the entry as "sticky", an additional
Experience API entry view is recorded. This method only returns OptimizationData when the entry is
marked as "sticky".
Arguments:
payload*: Entry view event builder arguments object, including an optionalprofileproperty with aPartialProfilevalue that requires only anid
Record an Insights API entry click event.
Returns:
void
Arguments:
payload*: Entry click event builder arguments object
Record an Insights API entry hover event.
Returns:
void
Arguments:
payload*: Entry hover event builder arguments object
Track a feature flag view via the Insights API. This is functionally the same as a non-sticky flag view event.
Returns:
void
Arguments:
payload*: Flag view event builder arguments object
Tracking of entry views is based on the element that contains that entry's content. The Optimization Web SDK can automatically track observed entry elements for entry view, hover, and click events, and it can also automatically observe elements that are marked as entry-related elements.
Interaction observers are passive with respect to host event flow:
- They do not call
event.preventDefault(). - They do not call
event.stopPropagation().
To manually track entry views using custom tracking code, simply call trackView with the necessary
arguments when appropriate.
Example:
optimization.trackView({ componentId: 'entry-123', ... })Entry views can be tracked automatically for observed entry-related elements by simply setting
autoTrackEntryInteraction.views to true, or by calling the tracking.enable('views', ...)
method if further setup is required depending on the consumer's SDK integration solution.
View detection is based on IntersectionObserver plus dwell-time timers. The SDK tracks visibility
cycles, pauses/resumes timers on page visibility changes, and periodically sweeps disconnected
elements.
Different integration patterns can show different relative performance:
- A smaller set of observed elements is typically the most efficient path.
- Larger sets of concurrently observed elements increase intersection-processing and state-management work.
- Elements that frequently hover around
minVisibleRatiocan reset visibility cycles often, which increases timer churn and can delay view firing. - Shorter
dwellTimeMscan increase callback frequency when many elements become visible in short bursts. - Frequent tab hide/show transitions add pause/resume work across active tracked elements.
In practice, callback and transport work (for example, trackView processing and event delivery)
often dominates overall cost once a view is detected.
For best runtime behavior, track only relevant elements, disable tracking for elements that are no
longer needed, and choose stable minVisibleRatio and dwellTimeMs values that match your UI
behavior.
Entry hovers can be tracked automatically for observed entry-related elements by setting
autoTrackEntryInteraction.hovers to true, or by calling tracking.enable('hovers', ...).
A hover emits component_hover events for the tracked entry after dwell-time threshold is reached,
and can continue to emit periodic duration updates while the same hover cycle remains active.
Hover detection is based on pointer/mouse enter and leave events with dwell-time timers.
Different integration patterns can show different relative performance:
- A smaller set of observed elements is typically the most efficient path.
- Shorter
dwellTimeMscan increase callback frequency in pointer-heavy UIs. - Smaller
hoverDurationUpdateIntervalMsvalues can increase periodic update event volume. - Rapid pointer movement across dense interactive surfaces can increase hover-cycle churn.
- Frequent tab hide/show transitions add pause/resume work across active tracked elements.
For best runtime behavior, track only relevant elements and choose stable dwellTimeMs /
hoverDurationUpdateIntervalMs values that match expected user interaction patterns.
Entry clicks can be tracked automatically for observed entry-related elements by setting
autoTrackEntryInteraction.clicks to true, or by calling tracking.enable('clicks').
A click emits a component_click event for the tracked entry only when one of the following is
true:
- The entry element itself is clickable
- The entry has a clickable ancestor
- The click originates from a clickable descendant inside the entry
Click detection uses two complementary checks:
- A browser-native selector traversal (
Element.closest) for semantic clickability (for example, links, buttons, form controls, role-based clickables,[onclick], and[data-ctfl-clickable="true"]) - A lightweight ancestor walk in SDK code to resolve the tracked entry element and to preserve
support for
onclickproperty handlers assigned in JavaScript
Different DOM shapes can show different relative performance:
- Clickable ancestor and non-clickable paths are typically the most improved, because native selector traversal can quickly determine whether a clickable selector exists in the ancestry.
- Entry-self-clickable and clickable-descendant paths are generally near parity, since both native clickability detection and tracked-entry resolution still run.
onclickproperty-only paths (for example,element.onclick = handlerwith no matching clickable selector) rely on the SDK fallback walk and are usually the least optimized path.
For best runtime behavior, prefer semantic clickable elements (such as <button> or <a href>) or
explicit clickable hints ([role="button"], [data-ctfl-clickable="true"]) over relying
exclusively on JavaScript-assigned onclick properties.
To override tracking behavior for a specific element, use:
tracking.enableElement('views', element, options)ortracking.enableElement('clicks', element, options)ortracking.enableElement('hovers', element, options)to force-enable tracking for that elementtracking.disableElement('views', element)ortracking.disableElement('clicks', element)ortracking.disableElement('hovers', element)to force-disable tracking for that elementtracking.clearElement('views', element)ortracking.clearElement('clicks', element)ortracking.clearElement('hovers', element)to remove a manual override and return to automatic behavior
Manual API overrides take precedence over data-attribute overrides (see below). After
clearElement, the element falls back to attribute overrides first, then normal automatic behavior.
The optional data option supports the following members:
entryId*: The ID of the content entry to be tracked; should be the selected variant if the entry is optimizedoptimizationId: The ID of the optimization/experience entry associated with the content entry; only required if the entry is optimizedsticky: A boolean value that marks that the current user should always see this variant; ignored if the entry is not optimizedvariantIndex: The index of the selected variant; only required if the entry is optimized
Example:
optimization.tracking.enableElement('views', element, {
data: { entryId: 'abc-123', ... },
})Elements that are associated to entries using the following data attributes will be automatically detected for observation and entry-interaction tracking:
data-ctfl-entry-id*: The ID of the content entry to be tracked; should be the selected variant if the entry is optimizeddata-ctfl-optimization-id: The ID of the optimization/experience entry associated with the content entry; only required if the entry is optimizeddata-ctfl-sticky: A boolean value that marks that the current user should always see this variant; ignored if the entry is not optimizeddata-ctfl-variant-index: The index of the selected variant; only required if the entry is optimizeddata-ctfl-track-views: Optional per-element override for view tracking (true= force-enable,false= force-disable)data-ctfl-view-duration-update-interval-ms: Optional per-element override for periodic view-duration updates (milliseconds)data-ctfl-track-clicks: Optional per-element override for click tracking (true= force-enable,false= force-disable)data-ctfl-track-hovers: Optional per-element override for hover tracking (true= force-enable,false= force-disable)data-ctfl-hover-duration-update-interval-ms: Optional per-element override for periodic hover-duration updates (milliseconds)
Example:
<div data-ctfl-entry-id="abc-123">Entry Content</div>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 state
Example interceptor usage:
optimization.interceptors.event((event) => {
event.properties.timestamp = new Date().toISOString()
})Warning
Interceptors are intended to enable low-level interoperability; to simply read and react to
Optimization SDK events, use the states observables.