Skip to content

feat(analytics): per-campaign fan-out + audit_creative + analyze_budget_efficiency (#120)#139

Merged
hyoshi merged 2 commits into
mainfrom
feat/analytics-fanout-creative-budget
May 23, 2026
Merged

feat(analytics): per-campaign fan-out + audit_creative + analyze_budget_efficiency (#120)#139
hyoshi merged 2 commits into
mainfrom
feat/analytics-fanout-creative-budget

Conversation

@hyoshi
Copy link
Copy Markdown
Collaborator

@hyoshi hyoshi commented May 23, 2026

Summary

Three additions on top of the live-wiring PR:

  1. Per-campaign fan-out for detect_anomalies — replaces the account-level aggregation that masked single-campaign anomalies when an offsetting campaign moved opposite. New fetch_*_per_campaign_metrics returns {campaign_id: (current, baseline)} and the adapter runs the pure detector once per campaign.
  2. audit_creative — Google flags RSAs with <3 headlines / <2 descriptions / below-recommended-15 headlines, RDAs missing copy / image. Meta flags ads without primary text / title / image / call_to_action.
  3. analyze_budget_efficiency — normalises conversions/cost across campaigns to [0,1] relative to the top performer, flags campaigns below 0.3, emits a reallocation suggestion when a clear split exists.

Both adapters now advertise all four AnalyticsCapability values; the MCP mureo_analytics_modules_list picks this up dynamically — no schema change.

Shared BYOD-tolerance helpers (google_row_metrics, meta_row_conversions) promoted from _live_clients to public _common.py so the three callers (aggregator, summariser, budget scorer) share one definition.

Test plan

  • +49 tests (115 → 130 cumulative on analytics package); 93.6% coverage
  • Code-review HIGH (_resolve_per_campaign_metrics return type tightened from object to CampaignMetrics; inline per-loop import removed) + MEDIUM (dead branch in score_budget_efficiency dropped; private helpers promoted) addressed
  • Live BYOD validation: analyze_budget_efficiency correctly identified 2 inefficient vs 2 efficient campaigns and produced a real reallocation suggestion
  • Zero regressions vs main

Stacked on #PR2 (feat/analytics-builtin-live-wiring).

Refs #120

@hyoshi hyoshi force-pushed the feat/analytics-builtin-live-wiring branch from 467f2df to 895bacb Compare May 23, 2026 07:09
@hyoshi hyoshi force-pushed the feat/analytics-fanout-creative-budget branch from e253f00 to a67ab0a Compare May 23, 2026 07:09
@hyoshi hyoshi force-pushed the feat/analytics-builtin-live-wiring branch from 895bacb to d128bb2 Compare May 23, 2026 07:14
@hyoshi hyoshi force-pushed the feat/analytics-fanout-creative-budget branch from a67ab0a to 6561635 Compare May 23, 2026 07:20
Base automatically changed from feat/analytics-builtin-live-wiring to main May 23, 2026 07:22
hyoshi added 2 commits May 23, 2026 16:23
Confirmed end-to-end during validation: the built-in google_ads /
meta_ads analytics adapters silently returned `cost=0` and
`conversions=0` in BYOD mode because the aggregators assumed the live
row shape (metrics nested under `row["metrics"]` for Google, conversion
counts inside `row["actions"]` for Meta), while
`ByodGoogleAdsClient.get_performance_report` returns metrics flat at
the top level and `ByodMetaAdsClient` returns conversions as a
top-level field with no `actions` list.

Both shapes are valid outputs of `mureo.mcp._client_factory.get_*_client`,
so the aggregator has to accept either. Introduce helpers
`_google_row_metrics` and `_meta_row_conversions` that try the live
shape first and fall back to the flat shape, and route both the
anomaly aggregators and the diagnose_performance summarisers through
them.

Validated post-fix against the local BYOD bundle:
- google_ads: 4 campaigns, spend = 4,256,000, CV 478.4, CPA 8,895
- meta_ads:   4 campaigns, spend =   854,000, CV 306.6, CPA 2,785

Regression tests added for both shapes; full analytics suite 96 → 98
passing, zero regressions in the full repo suite.

Refs #120
…et_efficiency (#120)

Three additions on top of the prior live-wiring PR, all on the same
adapter contract:

1. **Per-campaign fan-out for `detect_anomalies`**. The live fetcher
   now returns `{campaign_id: (current, baseline)}` and the adapter
   runs the pure detector once per campaign, surfacing anomalies the
   previous account-level aggregation masked when one campaign moved
   opposite to another. Day-grain rows summed, new campaigns get
   `baseline=None`, zero-spend baselines dropped. Legacy `MetricsFetcher`
   aggregate-path injection still works (back-compat with the existing
   suite).

2. **`audit_creative`** for both adapters. Google flags RSAs with
   <3 headlines or <2 descriptions (UI policy), RSAs with fewer than
   the recommended 15 headlines (Ad Strength), and RDAs missing
   long-headline / descriptions / marketing images. Meta flags ads
   without primary text, title, image, or call_to_action. Pure
   scorer (`_creative_audit.py`) consumes whatever shape `list_ads`
   returns — Live nested-creative and BYOD flat both accepted.

3. **`analyze_budget_efficiency`** for both adapters. Pure scorer
   (`_budget_efficiency.py`) normalises `conversions/cost` to [0,1]
   relative to the top performer, flags campaigns below the 0.3
   threshold, and emits a reallocation suggestion when at least one
   campaign clears 0.7. Zero-conversion accounts get a
   tracking-warning sentinel instead of a misleading score.

Both adapters now advertise all four `AnalyticsCapability` values.
The MCP tool `mureo_analytics_modules_list` picks this up dynamically
— no schema change.

Shared BYOD-tolerance helpers (`google_row_metrics`,
`meta_row_conversions`) promoted from `_live_clients` to `_common.py`
so the three callers — aggregator, summariser, budget scorer — share
one definition. `_resolve_per_campaign_metrics` return type tightened
to `dict[str, tuple[CampaignMetrics, CampaignMetrics | None]] | None`
so mypy sees `baseline.cost` is a real attribute.

Validated end-to-end against the local BYOD bundle:
- analyze_budget_efficiency: identified 2 inefficient vs 2 efficient
  campaigns with concrete reallocation suggestion text.
- detect_anomalies + per-campaign fan-out: empty (in-window data is
  steady) — fan-out behaviour pinned by adapter-level tests.

Coverage: 93.6% across `mureo/analytics/*` (49 new tests, 74 → 123).
Lint + mypy clean for the analytics package. Zero regressions vs the
prior branch.

Code-review findings addressed:
- HIGH: `_resolve_per_campaign_metrics` return-type tightening
- HIGH: removed inline per-loop import in `_summarise_performance`
- MEDIUM: dropped unreachable branch in `score_budget_efficiency`
- MEDIUM: promoted private BYOD-tolerance helpers to public `_common`

Refs #120
@hyoshi hyoshi force-pushed the feat/analytics-fanout-creative-budget branch from 6561635 to e6b4e45 Compare May 23, 2026 07:23
@hyoshi hyoshi merged commit 917969a into main May 23, 2026
9 checks passed
@hyoshi hyoshi deleted the feat/analytics-fanout-creative-budget branch May 23, 2026 07:27
hyoshi added a commit that referenced this pull request May 23, 2026
…atforms (#120) (#142)

Bump version across pyproject.toml / mureo/__init__.py / .claude-plugin/plugin.json
and add the 0.9.9 CHANGELOG entry summarising the five PRs that shipped
the analytics-module work (#137, #138, #139, #140, #141).

Headline:

- opt-in `AnalyticsModule` Protocol + registry + entry-point group
  `mureo.analytics`, so external-integration platforms (official MCPs
  and third-party plugins) can plug into mureo's deep analytics on
  the same terms as the built-in adapters
- built-in google_ads / meta_ads adapters wired against live + BYOD
  clients with sentinel handling for missing credentials
- per-campaign fan-out for `detect_anomalies`, full implementations
  of `audit_creative` and `analyze_budget_efficiency`, and DEEP scope
  drilldown for `diagnose_performance`
- 10 row-shape TypedDicts re-exported from `mureo.analytics` for
  plugin-side typing
- skill prompts (daily-check, rescue, _mureo-shared) consult the new
  `mureo_analytics_modules_list` MCP tool and report
  `analytics_not_available_for_<platform>` honestly when a module is
  absent

Refs #120
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