Appearance
Viewability Tracking in Expandable/Bubble Creatives
How Viewability Works
Analytics.init()callscheckForTCF()then schedulesVisibility.setup()viasetTimeoutVisibility.setup()creates an IntersectionObserver on#creative-containerwiththreshold: 0.5- When >= 50% of the element is visible,
startViewableTimer()begins counting - After 1 continuous second visible, the first
viewable: trueevent is sent - Subsequent events at 5s, 10s, 15s, 30s, 45s, 60s, 120s, 180s, 240s, 300s buckets
TCF/GDPR Gating
Visibility tracking is gated behind TCF consent:
typescript
// analytics.ts
if (DataStore.tcfTrackingEnabled.value) {
Visibility.setup(mainDiv)
}1
2
3
4
2
3
4
tcfTrackingEnabled defaults to true and is set to false by TCF.ts when:
purpose.legitimateInterests[7] === false(purpose 7: measurement)vendor.legitimateInterests[729] === false(Cavai's TCF vendor ID)
The check uses strict equality (=== false), so undefined (vendor not in CMP list) passes. TCF callback is async, so there's a race between the callback and Visibility.setup().
All analytics events include analytics_allowed: tcfTrackingEnabled.value. If TCF callback fires after init, early events may have analytics_allowed: true while later events have analytics_allowed: false.
Known Issues with Cross-Origin Iframes
The Expandable stub (Expandable.ts) tries to inject into the top document:
typescript
// getTopmostDocument.ts
if (!sfDetected && embedTag) {
try { topmostDocument = window.top!.document } catch { }
}1
2
3
4
2
3
4
In cross-origin ad iframes (GAM, etc.), window.top.document throws SecurityError and the creative stays inside the ad iframe. Consequences:
position: fixedis relative to the iframe, not the page- IntersectionObserver checks visibility against the iframe viewport
- The 50% threshold may not behave as expected
Possible Improvements
- SafeFrame viewability - When
sfDetectedis true, use$sf.ext.inViewPercentage()instead of IntersectionObserver - postMessage-based viewability - For cross-origin iframes, use postMessage to ask the parent frame about viewport intersection
- Separate bubble icon tracking - Track viewability on the bubble icon element (always visible) rather than the creative container (initially hidden)
- TCF race condition - Defer
Visibility.setup()until after TCF callback resolves, or re-check TCF status before sending events