# MGID Performance Diagnostics

Explains a metric move. Retrospective and read-only: it answers **what changed
and why**, then hands off. It does not decide what to do — that is the
simulator's and the signal skills' job.

Tools: [../_shared/ads-mcp-tools.md](../_shared/ads-mcp-tools.md)
Assumptions: [../_shared/assumptions.md](../_shared/assumptions.md)

---

## Required inputs

| Input | Required | Default |
|---|---|---|
| Symptom metric | Yes | ask: CPA, spend, conversions, CTR, or "something feels off" |
| Scope | No | whole account |
| Break window | No | last 3 days vs the 7 days before |

The user rarely knows the exact day. Find it rather than asking twice.

---

## Order of elimination

Work top-down and **stop at the first layer that explains the move**. Each layer
is cheaper and more common than the one below it.

| # | Layer | Question | Evidence |
|---|---|---|---|
| 0 | Tracking | Did conversions stop being *recorded*? | `mgid-tracking-health-monitor` |
| 1 | Own changes | Did we change something? | ask the user; changes are not readable through the tools |
| 2 | Composition | Did the mix shift between campaigns? | `get_portfolio_statistics`, two windows |
| 3 | Delivery | Did one campaign's volume or price move? | `get_campaign_statistics` daily rows |
| 4 | Placement | Did the traffic mix inside a campaign move? | `get_selective_bidding_statistics` |
| 5 | Creative | Did a teaser die or a new one take over? | `run_custom_report` / `get_teaser_statistics` |
| 6 | Unexplained | None of the above | say so plainly |

**Layer 0 first, always.** A broken postback looks exactly like a CPA
catastrophe, and every action taken on it makes things worse.

**Layer 1 costs a question, not a call.** No tool reads back what was changed,
so ask the user what moved recently — bids, filters, budgets, creatives — and
correlate the answer with when the metric broke. Changes made in the MGID
dashboard are equally invisible, so never rule a change out: if the break is
real and nobody remembers touching anything, say a dashboard change cannot be
excluded.

---

## Workflow

### Phase 0 — Gate

Run `mgid-tracking-health-monitor` on the scope. On fail: report the tracking
break, its start, the spend at risk, and **stop**. Do not diagnose CPA on
untrustworthy conversion data.

### Phase 1 — Locate the break

```
get_portfolio_statistics(dateInterval: interval, startDate, endDate)  // symptom window
get_portfolio_statistics(dateInterval: interval, startDate, endDate)  // baseline window
```

Two calls for the whole account. Compute per campaign: Δspend, Δclicks, Δavcpc,
Δconversions, ΔCPA, Δprofit — absolute **and** as a share of the account delta.

Rank campaigns by **contribution to the account-level move**, not by their own
percentage change: a 300% CPA rise on $4 of spend is noise, a 20% rise on $4,000
is the story.

### Phase 2 — Attribute

For the top contributors (cap at 5), pull daily rows:

```
get_campaign_statistics(campaignId, dateFrom, dateTo)   // symptom + baseline in one range
```

Decompose the CPA move — this is the part a dashboard cannot do:

```
CPA = spend / conversions = (clicks × avgCPC) / (clicks × CVR) = avgCPC / CVR
```

so

- **avgCPC up, CVR flat** → an auction/price story → Layer 4
- **CVR down, avgCPC flat** → a traffic-quality or landing-page story → Layer 4/5
- **both moved** → report both contributions, largest first
- **conversions flat, spend up** → a pacing or limit change → Layer 1/3
- **clicks collapsed, rates flat** → delivery: budget exhausted, campaign paused,
  moderation rejection, or lost auction share

State the split numerically ("CPA +38%: +31pp from CPC, +7pp from CVR"), never as
an adjective.

### Phase 3 — Localise inside the campaign

```
get_selective_bidding_statistics(campaignId, entity, symptom window)
get_selective_bidding_statistics(campaignId, entity, baseline window)
```

Entity per the campaign-type table. Look for **new** entities carrying material
spend in the symptom window and absent from the baseline — a newly opened source
is the single most common cause of an overnight CPA jump.

Creative layer only if placement does not explain it: a teaser that stopped
serving (moderation rejection, exhausted, paused) or a new teaser that took the
share. `list_teasers` carries the rejection reason.

### Phase 4 — Report

---

## Output

**Chat**, in this order:

1. **One-sentence verdict.** "CPA rose 38% because source X took 22% of spend at
   4× the account CPA, starting Tuesday."
2. **The break date**, with the numbers on either side of it.
3. **Contribution table** — campaign, Δ, share of the account move.
4. **The decomposition** — CPC vs CVR, in points.
5. **What was ruled out**, explicitly: tracking, our own changes, composition.
6. **Confidence**, plus what would raise it (a longer window, the customReports
   grant, a dashboard change history the API cannot see).

Then hand off: "`mgid-source-auditor` would price or block the sources involved"
— do not do it here.

---

## Guardrails

- Read-only skill. It recommends **no** writes and calls **no** write tool.
- Never report a cause without the numbers that support it.
- Never attribute to a change in the audit log without matching the timestamp to
  the break; correlation must at least be in the right order.
- Fewer than **50 conversions** in either window (**ASSUMPTION**) → label the CPA
  move as possibly statistical noise and say so before anything else.
- Same-day and yesterday-only windows are unreliable — stats settle. Prefer
  complete days, and name the window in the output.
- If nothing explains it, say "unexplained" and list what was checked. An
  invented cause is worse than an open question.
