Appearance
Native Button Click Tracking
Status: Proposed Created: 2026-05-30 Context: analytics/visual-element-tracking-gap.md
Problem
Button blocks used as interactive UI elements (show/hide triggers, navigation, toggles) produce no identifiable analytics. Designers must create a parallel choice infrastructure using choice-click-target to get any tracking at all. This is:
- Tedious -- N buttons require N choices + a LibraryScript operator + circular flow wiring
- Fragile -- mapping breaks if block abbreviations change (reorder, delete, re-add)
- Architecturally hacky -- choices exist purely as analytics proxies, rendered in conversation but invisible to users
- Overkill for simple tracking -- sometimes you just want to know "how many users clicked this button"
The script itself is performant (see analysis in visual-element-tracking-gap.md), but the setup overhead is the real cost.
What We Want
When a user clicks a button block, the system should automatically track:
- Which button was clicked (identified by blockName or displayName)
- How many times per session
- Without requiring any flow setup, choices, or scripts
Proposed Approaches
Approach A: sendMetric per button click (recommended)
Add a sendMetric call to VisualElementMixin.handleClick() that fires on every click, regardless of clickthrough URL:
typescript
// VisualElementMixin.ts
handleClick(): void {
// Always track the click with block identification
const name = this.block.displayName || this.block.blockName
Analytics.sendMetric('button_click', name)
if (!this.clickthroughUrl) {
return
}
this.trackClick()
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
What this produces:
- Custom metric with composite key
button_click__Buy Now(orbutton_click__buttonProperties-3) - Value: 1 per click, aggregated with
sumper session - Stored in Bunny's
custom_metricsMAP column - Queryable via existing
/analytics/bunny/metrics/:creative_idendpoint
Scope considerations:
- Should this apply to ALL visual elements (text, graphic, HTML) or only buttons?
- Text/graphic/HTML blocks are rarely clicked intentionally without a clickthrough URL
- Recommendation: start with buttons only, extend if needed
Variant -- button-only implementation:
typescript
// CreativeButtonBlock.vue (instead of VisualElementMixin)
handleClick(): void {
const name = this.typedBlock.displayName || this.block.blockName
Analytics.sendMetric('button_click', name)
// Call parent mixin handler for clickthrough logic
VisualElementMixin.methods.handleClick.call(this)
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
| Pros | Minimal engine change (1-3 lines). Uses existing infrastructure. No backend changes. No impact on numUserActions or any existing metric. Automatically available in Bunny queries. |
| Cons | Custom metrics are not yet surfaced in the Reports UI. Requires either: (a) Reports UI work to show custom metrics, or (b) using the raw Bunny API/CSV export to see the data. |
| Effort | Engine: ~30 min. Reports UI: separate project. |
Approach B: Dedicated sendCount metric for button clicks
Add a new countable metric alongside headerClick (-1) and backgroundClick (-2):
typescript
// analytics.ts
export const COUNTABLE_METRICS = {
headerClick: -1,
backgroundClick: -2,
buttonClick: -3, // NEW
}
// CreativeButtonBlock.vue or VisualElementMixin
handleClick(): void {
Analytics.sendCount(COUNTABLE_METRICS.buttonClick)
// ...existing clickthrough logic
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
| Pros | Uses the established sendCount pattern. Appears in legacy pipeline (header_clicks equivalent). |
| Cons | sendCount doesn't carry block identification -- you'd know "a button was clicked" but not which one. Would need Aggregation API changes to add button_clicks column. Less flexible than sendMetric. |
| Effort | Engine: small. Aggregation API + Logs-Parser: medium. |
Approach C: Flow events from button blocks
Make button blocks behave like choices in the flow system -- emit user-action and trigger sendFlow():
typescript
// CreativeButtonBlock.vue
handleClick(): void {
this.$emit('user-action')
Analytics.sendFlow({
componentId: this.block.blockName,
type: 'Button',
position: 0,
})
// ...existing clickthrough logic
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
| Pros | Button clicks appear in flow analytics alongside choices. Full parity with choice tracking. |
| Cons | Buttons are NOT flow components -- this conflates two systems. sendFlow expects a flow component with componentId from the flow graph, not a block name. Would pollute flow analytics with non-flow events. Requires backend flow parser changes. Increases flowSequenceNumber for non-flow actions. |
| Effort | Engine: medium. Backend: significant. |
Not recommended. Mixing block-level events into flow analytics creates more confusion than it solves.
Approach D: Keep choice-click-target, improve the UX
Instead of engine changes, make the workaround easier to set up:
- Auto-generate choice mappings from a "Button Tracking" toggle in ButtonConfiguration
- Or create a new operator type that auto-wires button-to-choice mapping
- Or a one-click "Add tracking to all buttons" action in the flow editor
| Pros | No engine or backend changes. Works today. |
| Cons | Still architecturally hacky. Still requires choices in the flow. Doesn't solve the fundamental problem. |
| Effort | Frontend only: medium. |
Pipeline Reality Check
All approaches except D share a fundamental constraint: the analytics pipeline drops data it doesn't explicitly know about.
sendEvent()with new fields (e.g.clickedBlock): fields are sent to Bunny CDN, survive decoding, but dropped during Logs-Parser session/hourly aggregation (hardcoded SELECT with ~28 columns).sendMetric(): data reaches Bunny, but thecustom_metricsMAP column has reliability concerns (Backend silently falls back if query fails). Not yet used by any built-in engine feature.sendCount()with a new metric ID: requires new columns in both Logs-Parser aggregation and Aggregation APIALL_METRICS.
No engine-only change produces data visible in Reports. Every approach requires Logs-Parser and/or Backend changes to surface the data.
The one exception: flow analytics (sendFlow) has its own pipeline (v3/flow/hourly-buckets) with per-object_id hit counts, already aggregated and queryable. This is why choice-click-target works end-to-end -- it piggybacks on the flow pipeline, which already supports element-level identification.
See visual-element-tracking-gap.md for the full pipeline trace.
Recommendation
Keep using choice-click-target for now. It's the only approach that works end-to-end without pipeline changes, and it's performant.
When the analytics rework happens (McSneaky's "one good day"), native button click tracking should be part of that effort. The rework would need to address the pipeline constraint anyway -- either by adding element-level click tracking as a new aggregation path, or by making the existing pipeline flexible enough to handle arbitrary event fields.
In the meantime, Approach D (UX improvements to choice-click-target setup) could reduce the setup friction without requiring pipeline work.
sendMetric (Approach A) is a reasonable interim step if the custom_metrics pipeline is made reliable, but it blurs the line between built-in engine instrumentation and user-defined custom metrics. Discuss with McSneaky whether that's the right use of the API.
Open Questions
Should
numUserActionsincrement on button clicks? PR #743 discussion concluded "no" for now, since it was originally meant for flow interactions. But this means button clicks don't count towardstarted/continuedmetrics. Is that the right trade-off?What identifier to use?
displayNameis human-readable but can be changed/duplicated.blockName(buttonProperties-3) is stable but opaque. Both? The composite key approach (button_click__Buy Now) is nice for readability but breaks if the name changes mid-campaign.Should this be opt-in or automatic? Always tracking every button click adds noise for simple creatives where buttons are just link elements. A toggle in ButtonConfiguration ("Track clicks") would give control but adds setup friction -- which is what we're trying to reduce.
Interaction with
clickscounter: Once the@click.selfbug is fixed (see click-self-issues.md),clickswill count all container clicks including button clicks.button_clickcustom metric would be a more granular breakdown within that total. These should be documented as complementary, not overlapping.
Dependencies
- Logs-Parser aggregation changes -- required for ANY approach except D. New fields/columns must be explicitly added to session and hourly aggregation SELECTs in
Aggregator.js - Backend query changes -- required to surface new data in API responses
- Reports UI -- required to display new data. Custom metrics are not yet shown in Reports
clickscounter fix -- separate issue (click-self-issues.md), but fixing it would at least give a working raw click count- Analytics rework -- the broader effort McSneaky wants to do, which would be the natural home for this feature
Related
- analytics/visual-element-tracking-gap.md -- Full research and analysis
- analytics/engine-analytics.md -- Engine analytics reference
- analytics/click-self-issues.md --
clickscounter bug - script-library.md -- Script Library (choice-click-target)
- PR #743 --
secondsToActivefix,numUserActionsdiscussion