Skip to content

Creative Engine Analytics System

Source: /Creative-Engine/src/analytics/

Architecture

Three files under src/analytics/:

  • analytics.ts -- Core Analytics singleton with all event sending, metric tracking, lead capture
  • visibility.ts -- Visibility singleton with IntersectionObserver-based viewability tracking
  • TCF.ts -- GDPR/TCF consent framework that can disable visibility tracking

Dual Pipeline

Every sendAnalytics() call fires to two endpoints:

  1. Legacy Cloudflare Worker (URL from tag stub) using old field names (id, chatbotId, interactionId, chatbotActions)
  2. Bunny CDN (analytics-3.cavai.com/analytics/v1.gif) via sendBunnyAnalytics() using modern names (sessionId, creativeId, creativeGroupId, creativeActions) plus the clicks counter. Skipped for dev/test environments.

Field Name Mapping (Legacy to Bunny)

Legacy (Cloudflare)BunnySource
idsessionIdUUID v4 per session
chatbotIdcreativeIdCreative database ID
interactionIdcreativeGroupIdCreative group ID
chatbotActionscreativeActionsnumUserActions counter
N/Aclicksclicks counter (Bunny-only)

Message Types

TypeMethodPurpose
initpostInit()Sent once at creative load
eventsendEvent()Primary event type for all interactions, visibility, lifecycle
flowsendFlow()Flow component transitions (with sequence number, object ID/type)
countsendCount()Countable element interactions: headerClick (-1), backgroundClick (-2)
metricsendMetric()Custom metrics from creative SDK

Counters

numUserActions (sent as chatbotActions / creativeActions)

Semantics: Count of meaningful user interactions within the creative. NOT a raw click counter.

Incremented at 6 locations:

FileTrigger
MessageHolder.vue:614Choice/consent button click or text input submit in conversation flow
FormClickoutLinkInput.vue:33Form clickout link click
FormSubmitButton.vue:137Form submit button click
CloseButton.vue:161Close/collapse button click
CreativeSliderBlock.vue:956Slider slide click-through (old Slider)
ToggleExpand.ts:20Expanding a bubble creative (only expand, not collapse)

The @user-action event reaching MessageHolder is emitted by:

  • Choice.vue -- choice selection and link clicks
  • TextInput.vue -- text submission
  • SliderSlide.vue -- slide link click (SliderV2)

Sent as:

  • Every sendEvent() includes it as chatbotActions: this.numUserActions
  • sendBunnyAnalytics() maps it to creativeActions

clicks

Semantics: Raw click count on the entire creative container -- every click anywhere, regardless of what was clicked.

Incremented at 1 location:

  • Creative.vue -- creativeContainerClicked() handler on #creative-container background clicks

Known issue: @click.self almost never fires because CreativeBody covers the entire container, so event.target is never the container itself. clicks is effectively always 0. See click-self-issues.md for details and proposed fixes.

Sent: Only in Bunny pipeline (not legacy). NOT sent to Cloudflare.

Key Difference

MetricWhat it countsExample
numUserActionsIntentional interactions (choices, submits, expand, link clicks)User selects a flow answer
clicksAll clicks anywhere on creative containerUser clicks the background

numUserActions should NEVER be incremented by general container clicks. They are fundamentally different metrics.

DataStore State: creativeClickedOnce

Location: dataStore.ts:62 -- ref(false)

Set at: Creative.vue via handleFirstInteraction() -- a @click.capture handler on #creative-container that fires on the first trusted user click anywhere in the creative. Uses capture phase so it fires before any child handlers.

History (PR #735): Previously used @click.self on creativeContainerClicked(), which never fired because CreativeBody covers the entire container and .self requires the click target to be the container element itself. The metric was broken across all creative formats.

First click triggers:

  1. Start goal timer (Analytics.startGoalTimer())
  2. Send secondsToActive event (time from page load to first click)
  3. PostMessage creativeClickedOnce: true to parent window (for VPAID callbacks)

Once true, never reverts within a session.

Event Data Fields

FieldSent fromMeaning
secondsToActiveCreative.vue (first click)Seconds from load to first interaction
secondsTotalActiveToggleExpand.ts (collapse)Total seconds creative was expanded
clickedLink6 componentsUser clicked a link
linkUrlSameThe clicked URL
timeGoalMetanalytics.ts goal timerGoal time elapsed
reachedGoalMessageHolder, MiddlewareReached a goal flow component
reachedEndconversationFlow.tsReached end of flow
reachedInputMessageHolderText input component presented
formSubmitClickedFormSubmitButtonForm submit clicked
viewableVisibilityCreative viewable (>= 1s continuous)
secondsInViewVisibilityCumulative seconds in view

Visibility System

  • Uses IntersectionObserver with threshold: 0.5 (>= 50% pixels visible)
  • viewable becomes true after 1 continuous second of visibility
  • secondsInView accumulates total view time (pauses on scroll-out or tab background)
  • Events fire at time buckets: 5s, 10s, 15s, 30s, 45s, 60s, 120s, 180s, 240s, 300s
  • Uses performance.now() to avoid clock drift

Visual Element Click Tracking

Visual elements (Button, Text, Graphic, HTML) use VisualElementMixin for click handling. The mixin's handleClick() only fires analytics when a clickthrough URL is configured. Without a clickthrough URL, it returns immediately -- no metrics, no identification, nothing.

This means interactive buttons (used for show/hide, navigation, toggles) are invisible to the analytics system. See visual-element-tracking-gap.md for full analysis.

PR #743 decision: numUserActions should NOT be incremented by visual element clicks. It was originally meant for flow interactions only. Visual element clicks should be tracked separately (proposed: via sendMetric).

Countable Metrics (sendCount)

Separate from both numUserActions and clicks:

objectIdMetricSource
-1headerClickVisualElementMixin.ts, Tagline.vue
-2backgroundClickCreativeBody.vue

These do NOT increment numUserActions. Note that headerClick is a misleading name -- it tracks visual element clickthrough clicks, not header clicks. Renaming to element_clicks has been discussed (PR #743) but not yet done.

Known issue: backgroundClick relies on @click.self on CreativeBody, which doesn't fire when visual elements cover the background. Clickable background is effectively broken for creatives with graphic/text/HTML blocks. See click-self-issues.md.

Flow System

Flow tracking is completely separate from click/action counters:

  1. User makes a choice -> @progress-flow -> eventEmitter.emitProgression(nextStepId)
  2. ConversationFlow.addComponentsToFlow() resolves next step, adds components to DataStore.liveFlow
  3. Component renders -> emits @block-rendered -> MessageHolder.postAnalyticsData()
  4. Analytics.sendFlow(block) sends messageType: 'flow' with objectId, objectType, sequenceNumber, position

flowSequenceNumber auto-increments per sendFlow() call.

Swipe Analytics (Temporary)

Separate system for slider block swipes. Gated by DataStore.enableSwipeAnalytics. Pings delivery-3.cavai.com/assets/general/stub.js.gif via blind <img> request. 300ms debounce. Not connected to main analytics pipeline.

Internal documentation