Skip to content

Viewability Tracking in Expandable/Bubble Creatives

How Viewability Works

  1. Analytics.init() calls checkForTCF() then schedules Visibility.setup() via setTimeout
  2. Visibility.setup() creates an IntersectionObserver on #creative-container with threshold: 0.5
  3. When >= 50% of the element is visible, startViewableTimer() begins counting
  4. After 1 continuous second visible, the first viewable: true event is sent
  5. 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)
}

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 { }
}

In cross-origin ad iframes (GAM, etc.), window.top.document throws SecurityError and the creative stays inside the ad iframe. Consequences:

  • position: fixed is relative to the iframe, not the page
  • IntersectionObserver checks visibility against the iframe viewport
  • The 50% threshold may not behave as expected

Possible Improvements

  1. SafeFrame viewability - When sfDetected is true, use $sf.ext.inViewPercentage() instead of IntersectionObserver
  2. postMessage-based viewability - For cross-origin iframes, use postMessage to ask the parent frame about viewport intersection
  3. Separate bubble icon tracking - Track viewability on the bubble icon element (always visible) rather than the creative container (initially hidden)
  4. TCF race condition - Defer Visibility.setup() until after TCF callback resolves, or re-check TCF status before sending events

Internal documentation