> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://contentful.com/developers/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful.com/_mcp/server.
# React Native SDK interaction tracking mechanics
> Understand what the `@contentful/optimization-react-native` SDK is tracking, when each event fires, and how it leaves the device.
## Overview
Use this concept document to understand what the `@contentful/optimization-react-native` SDK tracks,
when each event fires, and how events leave the device. The goal is to make tracking behavior
predictable before you debug an entry view, tap, screen, or custom event in a running app.
For step-by-step setup, see
[Integrate the Optimization React Native SDK in a React Native app](/personalization/optimization-sdk/integrate-the-react-native-sdk-in-a-react-native-app/).
For the entry resolution model that supplies tracking metadata, see
[Entry optimization and variant resolution](/personalization/optimization-sdk/entry-personalization-and-variant-resolution/).
## Runtime boundary
This document applies to React Native applications that use `@contentful/optimization-react-native`.
The package is a stateful mobile runtime built on the shared Core SDK. It adds React providers,
hooks, `OptimizedEntry`, React Navigation helpers, AsyncStorage persistence, NetInfo connectivity
handling, and app lifecycle flushing.
The SDK does not own application routing, consent UI, identity policy, Contentful entry fetching, or
final rendering. Applications fetch entries, choose locales, render UI, and call `consent(...)` from
their own policy layer. The SDK observes mounted React Native components and sends events through
the shared Core event pipeline.
## Prerequisites and runtime constraints
Decide these policies before initialization because they control which events can leave the device
and which state can survive a process restart:
| Constraint | React Native behavior |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Consent | Consent starts as unset unless `defaults.consent` or persisted SDK consent provides a value. Until event consent is `true`, the React Native default allow-list permits `identify` and `screen`; entry views, taps, `page`, and custom `track` events do not emit unless `allowedEventTypes` permits them. |
| Persistence consent | Boolean `consent(true)` and `consent(false)` update event consent and durable profile-continuity persistence consent together. Use object-form consent when event emission and durable profile continuity follow separate policy decisions. |
| Allowed event types | `allowedEventTypes` replaces the React Native default pre-consent allow-list. Keep it narrow and align it with the application's privacy review. For interaction tracking, use `component` for entry views, `component_click` for taps, and `flag` or `component` for Custom Flag views. |
| Active profile | Insights events need a current Optimization profile. Hydrate a persisted or default profile, or bootstrap one through an Experience path such as `identify`, `screen`, `page`, `track`, or a sticky entry view before relying on entry views, taps, or flag views. |
| Scroll context | View tracking needs both entry layout and viewport position. Wrap scrollable screens in `OptimizationScrollProvider`; otherwise the hook assumes `scrollY = 0` and only screen-height visibility is considered. |
| Storage availability | AsyncStorage persists SDK consent and, when persistence consent is `true`, profile-continuity values such as profile, anonymous ID, selected optimizations, and pending changes. Live state reads use in-memory SDK state after startup. |
| Preview mode | The preview panel is an application opt-in surface. Mount it only in authoring or development flows; opening it forces live entry updates so audience and variant overrides are visible immediately. |
| Offline behavior | Insights queueing and offline Experience buffering are in memory. NetInfo lets the SDK pause flushing while offline and resume on reconnect. Background or inactive app transitions trigger a `flush()` and AsyncStorage drain, but the SDK does not provide a durable outbox across process death. |
| Configured defaults | Startup defaults apply before provider children mount. Use `defaults={{ consent: true }}` only for default-on application policies; do not set default consent later from a child effect because child tracking effects can run before that policy is applied. |
## Mental model
If you mount `OptimizationRoot`, wrap React Navigation with `OptimizationNavigationContainer`, and
wrap Contentful entries in ``, the SDK gives you these tracking behaviors:
* **Entry view tracking** - One session-start event after 1 s at 10% or greater visibility and one
session-end event with the same `viewId` and final duration when visibility ends. A sub-dwell
session emits nothing.
* **Screen tracking** - A `screen` event on every active route change.
* **Default pre-consent `identify` and `screen` events** - The React Native default allows these
events before event consent. A custom `allowedEventTypes` list can allow fewer, more, or different
event types.
* **Profile-gated Insights events** - Entry views, taps, and flag views are Insights events. They
need a current profile from an Experience path before the Insights queue accepts them.
* **Offline queueing and background flushing** - When `@react-native-community/netinfo` is
installed, Insights events and offline Experience events flush after connectivity returns and when
the app moves toward the background.
* **Persistence across launches** - AsyncStorage restores consent state and, when persistence
consent permits it, profile-continuity values such as profile, anonymous ID, and selected
optimizations.
Applications still own these choices:
* **Tap tracking opt-out** - Taps are tracked by default. Disable them with
`trackEntryInteraction={{ taps: false }}` or `trackTaps={false}` when an entry must not emit tap
events.
* **Scroll-aware view tracking** - Wrap scrollable content in ``.
* **Consent UI** - The SDK exposes `consent(true | false | { events, persistence })`; the banner or
CMP integration belongs to the application.
* **Manual tracking for non-Contentful surfaces** - Use `track({ event, properties })`,
`trackView({ componentId, viewId, viewDurationMs, ... })`, or `trackClick(...)` from the SDK
instance.
## Events the SDK emits
"Tracking" in the React Native SDK is a small, fixed set of event types. Some are fired by the SDK
as a side effect of component rendering and user behavior; others are explicit method calls you make
from application code.
### Automatic events
These are emitted by the SDK without an application-level call when consent or `allowedEventTypes`
permits the event type and the relevant provider or component is mounted. Insights-backed automatic
events also need an active profile.
| Event | When it fires | Required wiring |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| **Screen view** | Each time the active navigation route changes. | `` wrapping `NavigationContainer` (or `useScreenTracking` on each screen). |
| **Entry view (session start)** | Once a wrapped entry has accumulated 1000 ms at ≥ 10% visibility. | `` with view tracking enabled (the default). |
| **Entry view (session end)** | Once when a qualified session ends because the entry leaves visibility, unmounts, or the app backgrounds. | Same as above. |
| **Entry tap** | On touch end, when the touch moved less than 10 points from touch start, on a wrapped entry. | `` with tap tracking enabled (the default; opt out with `trackTaps={false}`). |
| **Flag view** | Attempted when a flag value is read or a subscribed value is delivered; accepted emissions are deduplicated. | Any `getFlag(...)` call or `states.flag(...)` subscription. |
### Manual events
Use these SDK instance methods from `useOptimization()` when a screen, component, or business event
doesn't fit the `OptimizedEntry` pattern. The generated reference owns full argument shapes.
| SDK method | Delivery path and consequence |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `identify` | Experience path. Associates a known user ID and traits with the profile and can bootstrap the active profile. |
| `screen` | Experience path. Adds route context to the profile; React Native allows it before consent by default unless configured otherwise. |
| `page` and `track` | Experience path. Use for non-navigation context or business events. |
| `trackView` | Insights path as wire type `component`; when `sticky: true`, also sends a sticky view through Experience. |
| `trackClick` | Insights path as wire type `component_click`. |
React hook helpers can call those SDK methods for you. `useScreenTrackingCallback` is a hook, not an
SDK instance method; it returns an imperative callback for dynamic screen names, calls `screen`
directly, and does not apply current-route deduplication.
## Event delivery from the device
### API paths
The SDK talks to two HTTP endpoints, both defaulting to Ninetailed hosts:
| API | Default base URL | Purpose |
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Experience API** | `https://experience.ninetailed.co/` | Profile evaluation and updates for identify, page, screen, track, sticky entry views, and variant resolution. |
| **Insights API** | `https://ingest.insights.ninetailed.co/` | Fire-and-forget Analytics interaction events: entry views, taps or clicks, and flag views. |
Both are configurable through SDK `api` configuration. The
[React Native SDK README](https://github.com/contentful/optimization/blob/optimization-swift-v2.0.1/packages/react-native-sdk/README.md#common-configuration) and
[generated reference](https://contentful.github.io/optimization/modules/_contentful_optimization-react-native.html)
own the full configuration surface.
A single user action can touch either or both APIs. `trackView({ sticky: true })` delivers through
Experience first because sticky views become part of the profile, then through Insights. Plain
`trackView` only hits Insights; `identify` only touches Experience.
Insights delivery has one extra gate after consent: the SDK must have a current profile. If an entry
view, tap, or flag view reaches `InsightsQueue` before `sdk.states.profile.current` exists, the
queue logs a warning and drops the event before it can be batched. In practice, hydrate a persisted
profile, call `identify`, emit `screen`, or send another Experience event before depending on
Insights-backed interaction data.
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`.
### Queueing, flushing, and offline
Insights and Experience use different delivery shapes:
* **Insights events** are queued by profile in memory and batched for the Insights API. Consent,
active profile state, online state, and flush policy all affect whether they leave the device.
* **Experience events** call `upsertProfile` immediately while online. When offline, the SDK buffers
them in memory and replays them through `upsertProfile` after reconnect. Retry and backoff are
configurable via `queuePolicy.flush`.
The React Native SDK layers React Native-specific behavior on top:
1. **Online/offline detection** via `@react-native-community/netinfo`. When offline, the SDK buffers
eligible in-memory events; when `isInternetReachable` (preferred) or `isConnected` flips back to
`true`, the SDK resumes flushing. If NetInfo is not installed, the SDK logs a warning and stays
always online. Tracking continues, but offline durability is reduced.
2. **Background flushing.** On `AppState` transition to `background` or `inactive`, the SDK calls
`flush()` and drains pending AsyncStorage persistence before the OS might suspend the process.
3. **View session end on background.** If an entry has a qualified visibility session when the app
backgrounds, `useViewportTracking` emits the session-end view event and resets. A sub-dwell
session resets without an event. Foregrounding re-evaluates visibility and starts a new session
when the entry still meets the visibility threshold.
`queuePolicy.offlineMaxEvents` caps offline Experience events, and `queuePolicy.onOfflineDrop` is
called when older offline Experience events are dropped to honor that cap. Insights events share the
flush policy, but these two offline drop controls belong to the Experience queue. See the
[React Native SDK README](https://github.com/contentful/optimization/blob/optimization-swift-v2.0.1/packages/react-native-sdk/README.md#common-configuration) for the
common queue configuration entry point.
### Persistence via AsyncStorage
`AsyncStorageStore` persists consent-related state independently from profile-continuity values:
| Key | Contents |
| ------------------------- | ---------------------------------------- |
| `CONSENT_KEY` | `'accepted'`, `'denied'`, or absent. |
| `PERSISTENCE_CONSENT_KEY` | `'accepted'`, `'denied'`, or absent. |
| `DEBUG_FLAG_KEY` | Forces `logLevel` to `'debug'` when set. |
Profile-continuity values persist and reload only when persistence consent allows durable storage:
| Key | Contents |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PROFILE_CACHE_KEY` | The aggregated profile returned from the Experience API. |
| `SELECTED_OPTIMIZATIONS_CACHE_KEY` | Current audience/variant assignments. Drives which variant renders on next launch. |
| `ANONYMOUS_ID_KEY` | Profile-continuity identifier used for future Experience requests; updated from Experience response `profile.id`, including `identify` responses. |
| `CHANGES_CACHE_KEY` | Pending profile changes. |
Persistence is best-effort; write failures keep the SDK running on in-memory state. Structured
values are schema-validated on load; malformed JSON is evicted.
AsyncStorage is not the source for live state reads after SDK initialization. The SDK hydrates
allowed continuity values into memory during startup, then `sdk.states`, entry rendering, tracking
metadata, and later Experience requests read from in-memory SDK state.
AsyncStorage writes are serialized. When persistence consent permits durable profile continuity,
Experience responses from `identify`, `page`, `screen`, `track`, or sticky `trackView` are mirrored
to storage before the SDK publishes the resulting profile, selected optimizations, or changes
through `sdk.states`. If AsyncStorage rejects a write, the SDK logs the failure and publishes the
in-memory state only after that failure has been handled.
Why this matters for tracking: when persistence consent permits durable profile continuity, selected
optimizations persist, so a user placed in Variant B continues to see it on the next launch. View
and tap events carry the correct `experienceId` and `variantIndex` without re-round-tripping
Experience first.
## Consent gating
The SDK gates event emission behind a three-valued consent state: `true`, `false`, or `undefined`
(unset). This is the most common cause of "tracking isn't working" during integration. Without
`defaults.consent: true` or a banner that calls `optimization.consent(true)`, the SDK emits only
event types permitted by `allowedEventTypes`. React Native permits `identify` and `screen` by
default.
| Consent | Behavior |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `undefined` | Only `allowedEventTypes` emit. React Native default: `['identify', 'screen']`. Entry views, taps, track, and page events do not emit unless explicitly allowed. |
| `true` | All event types can emit, subject to other runtime gates such as active profile state for Insights events. |
| `false` | Same as `undefined`: only `allowedEventTypes` emit. Persists until `consent(true)` is called again. |
To change the default pre-consent allow-list, pass `allowedEventTypes` to `OptimizationRoot`:
```tsx
```
For default-on application policies without an end-user consent prompt, set
`defaults={{ consent: true }}` on `OptimizationRoot` during initialization. Avoid setting default
consent later from a component effect; that delays persistence policy until after child effects can
start emitting events.
When consent flips:
* **`consent(true)`** - New events flow normally. Blocked events are *not* retroactively replayed;
they were dropped at the guard. SDK method calls that reach Core report consent blocks through
`onEventBlocked` and `states.blockedEventStream`; some automatic React Native paths skip before
calling Core and do not produce blocked-event diagnostics. Event consent and durable
profile-continuity persistence consent persist to AsyncStorage.
* **`consent({ events: true, persistence: false })`** - New events flow normally, but profile,
selected optimizations, changes, and the anonymous ID stay session-only until persistence consent
is granted.
* **`consent(false)`** - The allow-list gate re-engages. Queued events that already cleared the
guard are purged from SDK queues. SDK-managed durable profile-continuity storage is cleared.
### "Why is nothing tracking?"
Five checks, in order of likelihood:
1. **Consent.** Without `defaults.consent: true`, user acceptance, or a matching `allowedEventTypes`
entry, the SDK does not emit the event. Set `logLevel: 'info'` to see Core-blocked SDK method
calls in the console, but also check automatic React Native guards because entry views, taps, and
current-screen tracking can skip before Core is called.
2. **Active profile.** Insights-backed views, taps, and flag views drop before batching when no
current profile exists.
3. **Tap tracking opt-out.** Views and taps default to `true`; check for root or per-entry
`taps: false` or `trackTaps={false}` overrides.
4. **Visibility requirement.** The fixed requirement is 10% for 1 s. Brief scroll-by content
never fires.
5. **No scroll context.** An entry below the fold without `` will never
pass the visibility requirement because `scrollY` is assumed `0`.
## Entry view tracking mechanics
This section describes the internals of `useViewportTracking`, the hook `` uses
under the hood.
### Fixed visibility and timing
Entry view tracking uses these fixed values:
| Constant | Value | Meaning |
| --------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
| `DEFAULT_MIN_VISIBLE_RATIO` | `0.1` | Minimum visibility ratio (0.0 to 1.0). An entry is "visible" when at least 10% of its height is within the viewport. |
| `DEFAULT_DWELL_TIME_MS` | `1000` | Minimum continuous visible time (ms) before the session-start view event fires. |
Tap tracking has one additional requirement:
| Constant | Value | Meaning |
| ------------------------ | ----- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `TAP_DISTANCE_THRESHOLD` | `10` | Maximum pixel distance between `touchStart` and `touchEnd`. Beyond this, the gesture is classified as a scroll or drag, not a tap. |
### The visibility state machine
Each mounted `` runs a small state machine keyed on a visibility session. A session
starts when the entry goes from not visible to visible, and ends when it leaves visibility,
unmounts, or the app backgrounds. The hook keeps this tracking state in refs to avoid re-rendering
on every scroll tick.
On every scroll tick or layout change, `checkVisibility()` computes the overlap between the entry's
measured `{y, height}` and the current viewport `{scrollY, viewportHeight}` to derive a
`visibilityRatio`, and compares it to the fixed 10% threshold:
* **Not visible to visible** - `onVisibilityStart` resets session state, mints a fresh `viewId`, and
starts the qualification dwell timer.
* **Visible to not visible** - `onVisibilityEnd` clears an unfinished dwell timer, emits one
session-end event if the session qualified, and resets session state.
### Session-start and session-end events
Within a session, automatic view tracking emits at most two interactions:
| Interaction | When it fires |
| ----------- | ----------------------------------------------------------------------------------------- |
| Start | Once, after 1000 ms of continuous visibility at or above the 10% threshold. |
| End | Once, when the qualified session ends, with the same `viewId` and final `viewDurationMs`. |
After the start interaction, the hook measures duration without scheduling periodic interaction
timers or emissions. Leaving visibility resets the session after any eligible end interaction. If
the user scrolls away at 0.5 s and returns later, the return starts a fresh dwell timer from 0 ms
with a new `viewId`.
A few consequences:
* **An entry briefly scrolled into view (\< 1 s total) fires no events.** The session never
qualifies, so neither a start nor an end interaction is emitted.
* **An entry scrolled into view for 1 s and then immediately unmounted** fires one start interaction,
then one end interaction from the unmount cleanup effect.
* **Each event carries `viewDurationMs`**, computed from the session's accumulated time at the moment
of emission. The sequence for a 12 s continuous view is a start at about 1000 ms and an end at
about 12 000 ms.
* **Each event also carries `viewId`**, the UUID for the session. Both events in one session share a
`viewId`; a new session gets a fresh one. Use `viewId` downstream to correlate.
### App backgrounding and cleanup
Two additional transitions matter:
1. **AppState background or inactive transition.** The hook listens to `AppState` changes. On
transition to `background` or `inactive`, it clears an unfinished dwell timer, emits the end
interaction for a qualified session, and resets the session. When the app becomes `active`
again, it re-checks visibility from scratch, which starts a new session if the entry is still on
screen.
2. **Component unmount.** The unmount cleanup clears an unfinished dwell timer and, for a qualified
session, schedules the end interaction through the same async `trackView` path before resetting
the session.
Combined, these transitions mean that a qualified session gets one end interaction with the same
`viewId` and final duration when visibility ends naturally, the user backgrounds the app, or the
component unmounts. A sub-dwell session resets without emitting either interaction.
## Scroll context and viewport resolution
`useViewportTracking` needs the entry's position (`{y, height}` from `onLayout`) and the viewport
(`{scrollY, viewportHeight}`). Where the viewport comes from depends on whether the entry sits
inside ``.
### Inside OptimizationScrollProvider
`OptimizationScrollProvider` wraps React Native's `ScrollView` and publishes the current `scrollY`
and layout height through context. The hook reads scroll on every event (`scrollEventThrottle={16}`,
\~60 FPS) and recomputes visibility.
Use this for any scrollable screen. Without it, entries below the fold never transition to visible
no matter how far the user scrolls.
```tsx
```
The [React Native reference implementation](https://github.com/contentful/optimization/blob/optimization-swift-v2.0.1/implementations/react-native-sdk/README.md)
demonstrates this scroll-provider pattern in its entry list.
### Outside OptimizationScrollProvider
With no scroll context, the hook falls back to screen dimensions: `scrollY = 0` and viewport =
`Dimensions.get('window').height` with an orientation listener.
This is correct for full-screen non-scrollable layouts, hero or banner content always on screen, and
modal content. It is *wrong* for anything below the fold in a `ScrollView`; wrap those.
## Tap tracking semantics
Tap tracking is implemented by `useTapTracking`. Behavior:
1. The wrapping `View` gets `onTouchStart` and `onTouchEnd`, not `onPress`. Raw touch events mean
**taps are captured even when a child `Pressable` also handles the press**. A `Pressable` wrapper
gives the child's `onPress` precedence.
2. `onTouchStart` records `{ pageX, pageY }`.
3. `onTouchEnd` computes Euclidean distance from start to end. Under `TAP_DISTANCE_THRESHOLD` (10
points) is a tap; over 10 points is treated as a scroll or drag and ignored.
4. On a valid tap, the hook checks `hasConsent('trackClick')`. When allowed, it calls
`optimization.trackClick({ componentId, experienceId, variantIndex })` (wire type
`component_click`) for the Analytics event. If `onTap` was passed on ``, the hook
also invokes that application callback synchronously with the resolved entry, even when the
Analytics tap event is not emitted.
Tap tracking is **on by default**. Disable it with
`` or
``. Passing `onTap` keeps tap tracking enabled unless
`trackTaps={false}` is set on the entry. `onTap` is application behavior, not Analytics emission, so
a consent guard for `trackClick` can skip the event while the app callback still runs.
## Screen tracking paths
Screen tracking emits Experience `screen` events. React Native allows these events before consent by
default, and the SDK uses them for route-based profile attribution. A custom `allowedEventTypes`
list can make screen tracking stricter.
### OptimizationNavigationContainer
The highest-automation path wraps React Navigation's `NavigationContainer` and emits a `screen`
event when the active route identity changes, including the initial ready event when allowed by
consent or `allowedEventTypes`.
Internally, the container builds a route key from the current route name. When `includeParams: true`
is set, it JSON-validates route params, attaches them to event `properties`, and includes them in
the route key. `onStateChange` compares the previous route key with the current route key, so two
routes with the same name can still emit separate screen events when params are included. The
container calls `trackCurrentScreen`, which deduplicates accepted current-route emissions. When
`screen` is not allowed, the underlying current-state tracker treats the emission as
`attempted: false` before Core is called, so that skip does not produce an `onEventBlocked` or
blocked-stream diagnostic.
### useScreenTracking
Per-screen hook for apps not using React Navigation, or when you want control over when the event
fires, such as after data loads.
```tsx
function DetailsScreen() {
const { trackScreen } = useScreenTracking({
name: 'Details',
trackOnMount: false,
})
useEffect(() => {
if (dataLoaded) void trackScreen()
}, [dataLoaded, trackScreen])
}
```
With `trackOnMount: true` (the default), the hook calls `trackCurrentScreen` with the screen name as
the route key when the descriptor is ready. Changing the name changes the key and can emit again. If
automatic tracking is not allowed, the underlying current-state tracker treats the emission as
`attempted: false` without a Core blocked-event diagnostic. The returned `trackScreen` function
calls `screen` directly for manual retracking and is not current-route deduplicated.
### useScreenTrackingCallback
Returns a stable `(name, properties?) => void` callback for imperative screen tracking with names
that aren't known at render time (deep links, dynamic titles, navigation state transforms). It calls
`screen` directly and does not apply route-key deduplication.
```tsx
const trackScreen = useScreenTrackingCallback()
trackScreen('Deep Linked Article', { slug, source: 'email' })
```
## Configuration guidance
Tracking configuration is a set of gates and precedence rules, not a separate tracking system. Use
the [React Native SDK README](https://github.com/contentful/optimization/blob/optimization-swift-v2.0.1/packages/react-native-sdk/README.md#common-configuration) and
[generated reference](https://contentful.github.io/optimization/modules/_contentful_optimization-react-native.html)
for exhaustive prop and type details.
* `allowedEventTypes` replaces the React Native default pre-consent list. Use `[]` for strict
opt-in, or a narrow custom list when legal and privacy review permits specific pre-consent events.
Use `component` for entry views, `component_click` for taps, and `flag` or `component` for Custom
Flag views.
* `` view and tap props override `OptimizationRoot` `trackEntryInteraction`
defaults. `onTap` keeps tap tracking enabled unless the entry sets `trackTaps={false}` and can run
even when `trackClick` is not allowed.
* The preview panel forces live entry updates while it is open so authoring overrides can render
immediately.
* `queuePolicy.flush` controls shared retry, backoff, and circuit behavior. `offlineMaxEvents` and
`onOfflineDrop` apply to the offline Experience buffer.
Use `onStatesReady` when diagnostics or app-level observers must attach as soon as SDK state exists
and before provider children can emit `screen`, `eventStream`, or `blockedEventStream` updates.
## Manual tracking guidance
Manual tracking uses the same consent gates, profile requirements, and delivery paths as automatic
tracking. Reach for it when the event is meaningful but no `` lifecycle matches the
surface:
* Use `trackView` for a screen-wide or manually timed entry view. Provide a stable `viewId` and
measured `viewDurationMs`; automatic entry tracking generates those values for each visibility
session.
* Use `trackClick` when a non-Contentful wrapper still represents a component click or tap that
Analytics must count.
* Use `track` for business events unrelated to a Contentful entry. This follows the Experience path
and can update the profile while online.
For anything backed by a Contentful entry and visible in the viewport, prefer ``. It
owns session qualification, start-and-end sequencing, end-on-unmount behavior, end-on-background
behavior, and `viewId` correlation.
## Lifecycle summary
For a scrollable list screen with navigation and entry cards, tracking flows in this order:
* **Startup** - The SDK hydrates consent and, when persistence consent allows it, profile-continuity
state from AsyncStorage before provider children mount.
* **Navigation** - `OptimizationNavigationContainer` or `useScreenTracking` sends a `screen`
Experience event. While online, that event calls `upsertProfile` immediately and can establish the
active profile needed by Insights.
* **Entry rendering** - `OptimizedEntry` resolves variants from current selected optimizations and
attaches view and tap tracking metadata.
* **View session** - A card that stays at least 10% visible for 1 s emits the session-start Insights
view,
continues accumulating duration without periodic interaction emissions, then emits the end view
with the same `viewId` and final duration when visibility ends.
* **Tap** - A short touch movement on the wrapped entry emits a `component_click` Insights event
when `trackClick` is allowed and calls the application `onTap` handler when provided, even when
the Analytics event is skipped.
* **Background or offline** - Backgrounding triggers a final view for active sessions and asks queues
to flush. Offline Insights events and offline Experience events remain in memory and replay on
reconnect if the process survives.
## Related documentation
* [React Native SDK README](https://github.com/contentful/optimization/blob/optimization-swift-v2.0.1/packages/react-native-sdk/README.md) - Package-level orientation
and common configuration.
* [Core state management](/personalization/optimization-sdk/core-state-management/) - Shared state, consent, persistence, event
queues, and observable mechanics used by the React Native SDK.
* [Consent management in the Optimization SDK Suite](/personalization/optimization-sdk/consent-management-in-the-optimization-sdk-suite/) -
Cross-SDK consent, persistence consent, allow-list, and withdrawal policy guidance.
* [Entry optimization and variant resolution](/personalization/optimization-sdk/entry-personalization-and-variant-resolution/) -
Contentful entry contract, variant fallback behavior, and local resolution mechanics.
* [React Native reference implementation](https://github.com/contentful/optimization/blob/optimization-swift-v2.0.1/implementations/react-native-sdk/README.md) -
Working app that exercises the React Native SDK API surface in this monorepo.
* [Integrate the Optimization React Native SDK in a React Native app](/personalization/optimization-sdk/integrate-the-react-native-sdk-in-a-react-native-app/) -
Step-by-step React Native integration flow.
> Understand what the `@contentful/optimization-react-native` SDK is tracking, when each event fires, and how it leaves the device.