Skip to content

feat(marketing-analytics): add a customer acquisition cost column#73264

Draft
jabahamondes wants to merge 5 commits into
javier/ma-roas-columnfrom
javier/ma-cac-column
Draft

feat(marketing-analytics): add a customer acquisition cost column#73264
jabahamondes wants to merge 5 commits into
javier/ma-roas-columnfrom
javier/ma-cac-column

Conversation

@jabahamondes

Copy link
Copy Markdown
Contributor

Note

Top of the chain, on top of ROAS (#73052). This is now the branch that has everything: flags, write endpoints, MCP tools, currency conversion, ROAS, and CAC. Check out javier/ma-cac-column to try the full read-side end to end.

Problem

The companion to ROAS. Once you can see revenue over spend, the next question is what a customer costs to acquire. Spend per channel is there, and now goals can be marked counts_as_customer, so the ratio is computable.

Changes

A Cost per customer column: the channel's spend divided by the customers produced by the conversion goals marked counts_as_customer.

It's the mirror image of the ROAS column from #73052, and shares the same machinery (_sum_conversion_values, the include_cost_per guard, the same "hidden until a goal is flagged" behavior). Two differences:

  • Inverse formula. ROAS is revenue over spend; CAC is spend over customers.
  • Naming for free formatting. The alias is built from the existing Cost per prefix, so it falls through to_marketing_analytics_data to the currency / higher-is-worse formatting the other cost-per columns already get. No new metadata.

The assumption, stated plainly

Each customer goal contributes its conversion count as the customer count. That is correct when the goal marks a once-per-person conversion, a sign-up or a first purchase, where counting conversions and counting people are the same thing.

It is wrong if someone points a customer goal at a repeatable event, say a generic payment that fires on every monthly renewal. Then a returning customer's second payment would be counted as a new customer, understating CAC. Getting that right needs each person's first-ever conversion, a historical first-time-per-user scan the marketing precompute can't do today (it only materializes the queried window). That's a real piece of work, and it's deliberately not here.

So this ships the version that's correct for the natural case (a goal marking the moment someone becomes a customer) and is honest in code and comment about where it breaks. The column name says "customer", not "new customer", so it doesn't over-promise acquisition semantics it isn't enforcing.

How did you test this code?

3 new unit tests in test_conversion_goals_aggregator.py, mirroring the ROAS ones, run by me (Claude):

  • Denominator counts only the customer goals. Four goals, two flagged customer at indices 1 and 3. Asserts the CAC expression references the value columns of 1 and 3 and not 0 or 2, and that spend is the numerator (the inverse of ROAS). This is the core regression.
  • Absent when no goal is a customer goal.
  • Absent without campaign_costs.

Full test_conversion_goals_aggregator.py, test_marketing_analytics_table_query_runner.py and _business.py pass (112), snapshots unchanged. ruff clean; mypy clean on the file (the one printed error is the pre-existing unrelated personhog_client one).

No end-to-end ClickHouse test, same reasoning as ROAS: the construction is unit-covered and the propagation is the shared path the cost-per columns already exercise.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

Holding with the rest of the read-side until the dashboard redesign decides how these columns surface.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

I directed this, Claude wrote it. Invoked /writing-tests.

Worth recording the design turn, since it's the interesting part. My first read was that CAC needed a historical first-time-per-user scan and was therefore a large, precompute-touching project, so I nearly deferred it. Javier pushed back: a person can't sign up twice, so why the history? That's right, and it reframed the whole thing. The history only matters for repeatable events; for a once-per-person conversion, counting conversions already counts customers. So CAC collapsed to the same shape as ROAS. This PR takes that simpler, correct-for-the-common-case path and documents exactly where the harder version would be needed, rather than building the heavy scan speculatively.

CAC per channel is the channel's spend over the customers produced by the
conversion goals marked counts_as_customer. It mirrors ROAS: same guards (a
customer goal must exist and spend must be available), inverse formula. Each
customer goal contributes its conversion count as customers, which holds when the
goal marks a once-per-person conversion. A repeatable event would overcount and
needs a first-time-per-person scan we don't do here.
@jabahamondes jabahamondes self-assigned this Jul 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🤖 CI report

ℹ️ MCP UI apps size — 31 app(s), 16602.7 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 599.5 KB 187.4 KB
action 457.8 KB 187.4 KB
action-list 564.3 KB 187.4 KB
cohort 456.8 KB 187.4 KB
cohort-list 563.3 KB 187.4 KB
email-template 456.6 KB 187.4 KB
error-details 472.4 KB 187.4 KB
error-issue 457.5 KB 187.4 KB
error-issue-list 564.2 KB 187.4 KB
experiment 561.5 KB 187.4 KB
experiment-list 565.1 KB 187.4 KB
experiment-results 563.2 KB 187.4 KB
feature-flag 567.1 KB 187.4 KB
feature-flag-list 570.9 KB 187.4 KB
feature-flag-testing 461.0 KB 187.4 KB
insight-actors 562.1 KB 187.4 KB
invite-email-preview 456.0 KB 187.4 KB
llm-costs 559.5 KB 187.4 KB
session-recording 458.6 KB 187.4 KB
session-summary 463.9 KB 187.4 KB
survey 458.4 KB 187.4 KB
survey-global-stats 562.2 KB 187.4 KB
survey-list 565.0 KB 187.4 KB
survey-stats 562.2 KB 187.4 KB
trace-span 457.2 KB 187.4 KB
trace-span-list 564.2 KB 187.4 KB
workflow 457.1 KB 187.4 KB
workflow-list 563.7 KB 187.4 KB
query-results 744.8 KB 187.4 KB
render-ui 825.3 KB 187.4 KB
visual-review-snapshots 461.6 KB 187.4 KB

@trunk-io

trunk-io Bot commented Jul 23, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant