Interaction tracking in Web SDKs
Overview
Use this document to understand how browser interaction tracking works in @contentful/optimization-web and the React layer provided by @contentful/optimization-react-web. It explains how entry views, clicks, hovers, Custom Flag views, page events, and custom events move from browser behavior to Core event delivery.
- For setup steps, see Integrate the Optimization Web SDK in a web app and Integrate the Optimization React Web SDK in a React app.
- For server-owned rendering, see Interaction tracking in Node and stateless environments.
Tracking boundary
Interaction tracking has two separate jobs:
- Detection - Observing browser state or application calls and deciding that an interaction happened.
- Delivery - Building a valid Optimization event, applying consent and profile gates, and sending the event to the correct API.
The Web SDK owns browser detection for Contentful entry views, clicks, and hovers. Core owns event construction, consent checks, queues, profile state, and API delivery. React Web does not implement a separate tracking engine; it renders Web SDK tracking metadata and exposes the underlying Web SDK tracking API through React providers and hooks.
This split keeps the browser-specific code small. A view observer can focus on
IntersectionObserver, timers, and DOM lifecycle. Core can focus on whether a trackView() call is
allowed, which event type it becomes, and which queue receives it.
Layer responsibilities
The application owns rendering policy, consent UX, identity policy, route ownership, and any
business event taxonomy passed to track(). Contentful entry fetching is application-owned unless
the SDK is explicitly configured with a consumer-owned contentful.js client for managed entry
fetching.
Runtime prerequisites and defaults
This document applies to the browser Web SDK and the React Web layer. The Web SDK surface is the
ContentfulOptimization instance and its tracking.* APIs. React Web passes tracking-related props
through OptimizationRoot, renders metadata with OptimizedEntry, and exposes the same underlying
SDK instance through hooks.
Before relying on automatic entry interaction tracking, account for these runtime conditions:
Event paths
Tracking events use two API paths:
The Web SDK normally starts a browser journey with page() or identify(). Those calls use the
Experience API and populate the state that later Insights events need for attribution. If an
Insights event is emitted before the stateful SDK has a profile, Core logs a warning and drops that
Insights event because there is no profile to attach to the batch.
The entry interaction methods map to these wire event types:
When configuring allowedEventTypes, entry views require component, clicks require
component_click, and hovers require component_hover. Custom Flag views can use flag or
component; use flag when the pre-consent allow-list must admit Custom Flag views without also
admitting entry views.
Sticky entry views touch both paths. When trackView({ sticky: true, ... }) is called, Core sends
the view through Experience first, then sends an Insights view event. Non-sticky views only use
Insights.
Third-party analytics integrations that need one exposure for a sticky view must dedupe by semantic
fields such as viewId, componentId, experienceId, and variantIndex, not by messageId.
Consent and profile gates
The Web SDK defaults allowedEventTypes to ['identify', 'page']. Until consent is granted or an
event type is allow-listed, Core blocks non-allowed event types such as track, component,
component_click, and component_hover.
Consent affects two parts of automatic entry tracking:
- Observer startup - Entry interaction detectors are enabled by default, but they start only
when consent or
allowedEventTypespermits the matching event type. A manualtracking.enableElement(...)override can force observation while global automatic tracking is off, but only after that interaction type is allowed. Before consent or allow-list admission, the detector stays stopped and automatic tracking does not call Core, so no automatic blocked-event payload is produced for that element. Callingconsent(false)stops detectors that are not otherwise allow-listed. - Event delivery - Every event call still passes through Core’s consent guard. Manual
application calls, such as
trackView()ortrackClick(), are still blocked unless consent orallowedEventTypespermits the event type.
This means a typical browser integration needs both:
- A profile-producing event such as
page()oridentify(). - Consent or an explicit event allow-list for the interaction event types the application wants to send.
Blocked events are observable through onEventBlocked and states.blockedEventStream. Individual
payloads blocked by consent are not replayed later when consent changes. SDK-owned current-state
surfaces, such as active page or Custom Flag state, can emit fresh events after consent opens when
the underlying state is still current and the event has not already been accepted.
Tracked entry metadata
Automatic tracking starts from DOM metadata. The Web SDK looks for HTML or SVG elements with a
non-empty data-ctfl-entry-id attribute.
The tracking payload uses the resolved entry ID, not the baseline entry ID. When an application
needs the baseline ID for rerendering, store it separately, for example in data-ctfl-baseline-id.
The Web SDK does not use data-ctfl-baseline-id in event payloads.
optimizationContextId is SDK-owned runtime context. Core uses it to enrich follow-up event stream
emissions and diagnostics with the resolved entry and selected optimization context before building
API events. It is not an Experience API or Insights API event field.
Manual element observation can provide the same metadata without DOM attributes:
When manual data is valid, it takes precedence over data-ctfl-* values on the element. If manual
data is missing or invalid, the detector falls back to the element attributes.
Runtime control and precedence
Automatic tracking defaults to enabled for entry views, clicks, and hovers. Pass false for any
interaction type that the application does not observe:
The same runtime can be controlled after initialization:
For each element, the runtime resolves tracking in this order:
- Manual API override from
enableElement(...)ordisableElement(...). - Per-element
data-ctfl-track-*override. - Global automatic tracking state and the element registry.
reset() stops interaction trackers and clears manual element overrides. destroy() also
disconnects DOM observers and browser lifecycle listeners.
DOM discovery
The Web SDK has one shared entry element registry. When an interaction detector starts, the registry does two things:
- Seeds the initial set from
document.querySelectorAll('[data-ctfl-entry-id]'). - Subscribes to a
MutationObserverthat watches child additions, child removals, anddata-ctfl-entry-idattribute mutations in the document subtree.
When a tracked element is added, the registry notifies the running view, click, and hover detectors. When a tracked element is removed, the registry notifies those detectors so they can unobserve the element or stop treating it as tracked.
The registry observes element existence and data-ctfl-entry-id mutations, not arbitrary attribute
changes. Payload attributes such as data-ctfl-optimization-id,
data-ctfl-optimization-context-id, and data-ctfl-variant-index are read when an event fires, so
updated values can affect later payloads. Per-element tracking overrides such as
data-ctfl-track-views and interval attributes are resolved when the element is added to the
detector. If an existing mounted element needs dynamic non-entry-ID override changes, use the
tracking.*Element(...) API or remount the tracked element.
View tracking mechanics
View tracking uses IntersectionObserver and dwell-time timers.
Default view settings:
A view cycle works like this:
- The element crosses the configured visibility threshold.
- The observer starts a fresh cycle, assigns a
viewId, resets accumulated duration, and starts the dwell timer. - After the dwell time passes, the observer calls
trackView()withviewIdandviewDurationMs. - While the element remains visible, the observer sends periodic duration updates using the same
viewId. - When the element leaves view after at least one view event fired, the observer sends a final duration update.
- If the element leaves view before dwell time completes, the cycle resets without sending an event.
The observer pauses dwell accumulation when the page is hidden and resumes when the page becomes visible again. It coalesces in-flight callbacks so a slow or failing event send does not create duplicate concurrent sends for the same element. If visibility ends while an event send is in flight, the final duration update is sent after that in-flight attempt settles.
Sticky view handling is per DOM element. If the payload has sticky: true, the first view attempt
for that element sends sticky: true to Core. After Core accepts that sticky view, later view
events for the same element omit sticky. If the sticky attempt is blocked, the detector retries
sticky on the next visibility cycle for that element. Separately rendered elements with the same
entry ID are treated as separate sticky targets.
display: contents wrappers
React Web renders OptimizedEntry with display: contents by default so the wrapper can carry
tracking metadata without adding a layout box. Because the wrapper has no box of its own, view
tracking resolves what rendered area to measure.
When the wrapper has one rendered element child and no visible text, the view detector observes that
child element. When the wrapper contains multiple rendered boxes or visible text, the detector uses
virtual measurement of the wrapper contents against the viewport and clipping ancestors. The
detector remeasures when the rendered subtree, class, hidden, or style changes, and when
resize or scroll can change virtual visibility.
Click tracking mechanics
Click tracking uses one document-level capture listener. It does not call preventDefault() or
stopPropagation().
When a click occurs, the detector resolves two facts:
- Which tracked entry element owns the click.
- Whether the event path is semantically clickable.
The click detector treats these paths as clickable:
a[href]buttoninputexcepttype="hidden"selecttextareasummary[role="button"][role="link"][onclick]- elements with an
onclickproperty handler [data-ctfl-clickable="true"]
The tracked entry can be the clicked element, an ancestor of the clicked element, or a descendant of a clickable ancestor. The detector also handles click events whose original target is a text node by walking to the parent element.
If a tracked entry and a clickable path are found, the detector resolves entry metadata and calls
trackClick(). Click events do not include a click duration or click ID; the payload carries the
entry and optimization metadata.
Hover tracking mechanics
Hover tracking uses element-level listeners. When PointerEvent is available, the observer listens
for pointerenter, pointerleave, and pointercancel, and ignores touch pointer events. When
pointer events are unavailable, it falls back to mouseenter and mouseleave.
Default hover settings:
A hover cycle mirrors the view cycle:
- The pointer enters a tracked element.
- The observer starts a fresh cycle, assigns a
hoverId, resets accumulated duration, and starts the dwell timer. - After dwell time passes, the observer calls
trackHover()withhoverIdandhoverDurationMs. - While the pointer remains hovered, the observer sends periodic duration updates using the same
hoverId. - When the pointer leaves after at least one hover event fired, the observer sends a final duration update.
- If hover ends before dwell time completes, the cycle resets without sending an event.
Like view tracking, hover tracking pauses while the page is hidden, coalesces in-flight callbacks, and sweeps disconnected element state.
React Web mechanics
React Web wraps the Web SDK; it does not replace the Web SDK tracking runtime.
OptimizationRoot creates one ContentfulOptimization instance from its props and destroys that
instance on unmount. trackEntryInteraction maps to the lower-level Web SDK’s
autoTrackEntryInteraction option. allowedEventTypes, defaults, queuePolicy, and other Web
SDK configuration values pass through to the underlying instance.
OptimizedEntry does three tracking-related things:
- Resolves a provided baseline entry, or fetches the baseline first when
OptimizedEntryoruseOptimizedEntry()receives anentryIdand the SDK is configured with acontentful.jsclient. - Renders a wrapper element with
display: contents. - Adds tracking attributes for the resolved entry when resolved content is ready.
The wrapper receives:
data-ctfl-duplication-scope is emitted by React Web for optimization metadata, but the Web SDK
entry interaction payload does not use it.
Managed entry fetching expects the same single-locale CDA entry shape as manual baselineEntry
resolution. Do not use withAllLocales or locale=* for Web or React Web optimization surfaces.
During loading, OptimizedEntry does not emit resolved entry tracking attributes. Loading UI is
therefore not tracked as the resolved Contentful entry. If children is a direct ReactNode
instead of a render prop, the wrapper still receives tracking attributes, but the child content does
not change based on the resolved entry.
useOptimizedEntry() only resolves data. It does not add DOM attributes or register an element. If
a component uses useOptimizedEntry() directly, it must either render the data-ctfl-* attributes
itself or use sdk.tracking.enableElement(...).
[!IMPORTANT]
OptimizedEntrySourceControllerdoes not emit Web tracking attributes. Custom Web adapters that use it must renderdata-ctfl-*attributes after resolution or calloptimization.tracking.enableElement(...)with equivalent data.
React Web router adapters emit page() calls when supported routers change route. They are page
event helpers, not entry interaction detectors. Entry views, clicks, and hovers still come from the
Web SDK runtime.
Delivery and flushing
Insights events are queued by current profile ID and sent in batches. The queue flushes
periodically, flushes when the queued event count reaches the batch threshold, and can be flushed
explicitly with optimization.flush().
Experience events are sent immediately when the browser is online. When the browser is offline,
Experience events are queued up to the configured offline maximum and replayed when the online
signal becomes true.
The Web SDK wires browser lifecycle events into this queue model:
onlineandofflineupdate Core’s online signal. Going online forces a flush.visibilitychange,pagehide, andbeforeunloadforce an Insights flush withnavigator.sendBeacon()once per hide cycle.- Normal periodic, threshold, online, and explicit flushes use fetch so retry and failure policy can observe the response.
Browser storage writes are best-effort. If a localStorage write fails, live SDK state continues in
memory for the current runtime while durable continuity is limited. At startup, the SDK reads
consent from localStorage and resolves configured defaults with any profile-continuity values it
is allowed to load. Profile-continuity values such as profile, selected optimizations, Custom Flag
changes, and anonymous ID are read only when persistence consent permits it. The anonymous ID is
also persisted in the ctfl-opt-aid cookie and migrated from the legacy anonymous ID cookie when
profile continuity is enabled.
Debugging model
When an expected interaction does not appear, check the gates in this order:
- Profile - Call
page()oridentify()before relying on Insights events. Insights delivery needs a current profile in state. - Consent - Confirm
states.consent.current === trueor configureallowedEventTypesfor the event types that must emit before consent. - Detector startup - Confirm React Web
trackEntryInteraction, lower-level Web SDKautoTrackEntryInteraction,tracking.disable(...), ordata-ctfl-track-*="false"has not opted the relevant interaction out. - Element metadata - Confirm the tracked element is an HTML or SVG element with non-empty
data-ctfl-entry-id, or thatenableElement(...)supplies validdata.entryId. - View threshold - Confirm the element stays above
minVisibleRatiofordwellTimeMs. - Clickability - Confirm clicks happen on a semantic clickable path or an element marked with
data-ctfl-clickable="true". - Hover source - Confirm the event is not a touch pointer event and that the pointer remains over the element long enough to satisfy dwell time.
- Manual override precedence - Confirm
disableElement(...)ordata-ctfl-track-*="false"is not suppressing the element.
For local diagnostics, subscribe to states.eventStream and states.blockedEventStream, and use
onEventBlocked for consent-gating visibility. In React Web, use onStatesReady on
OptimizationRoot when those subscribers must be attached as soon as SDK state exists and before
provider children can emit router page() events or entry interactions.
Design boundaries
The Web SDKs do not own every part of tracking:
- They do not infer Contentful entries from tracking metadata. Managed entry fetching requires an
explicitly configured
contentful.jsclient. - They do not decide whether a user has granted consent.
- They do not infer a browser view from server rendering alone.
- They do not make non-clickable markup clickable.
- They do not replay individual event payloads blocked before consent.
- They do not guarantee persistence in browsers that deny storage access.
Keep these boundaries explicit when integrating or changing tracking behavior. Detection belongs to the browser runtime, event semantics belong to Core, and application-specific policy stays in the application.
Related documentation
- Core state management - Core state, consent, queues, observables, and event streams.
- Entry optimization and variant resolution - How resolved entries and selected optimization metadata are produced before tracking metadata is rendered.
- Interaction tracking in Node and stateless environments - How browser tracking fits when server runtimes own personalization.
- Optimization Web SDK README - Package-level orientation and Web SDK setup options.
- Optimization React Web SDK README - React provider, hook, router, and entry-rendering orientation.
- Integrate the Optimization Web SDK in a web app - Step-by-step browser integration flow.
- Integrate the Optimization React Web SDK in a React app - Step-by-step React integration flow.
- Building a custom JavaScript Optimization adapter - Low-level entry-source lifecycle guidance for custom adapter authors.
- Forwarding Optimization SDK context to analytics and tag-management tools - Consent-aware forwarding, sticky-view dedupe, and Custom Flag analytics handoff.