> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Responsive Search Ad Variation Testing

> Extract RSA asset performance from Google Ads and score headline/description combinations through a Mavera Focus Group by persona.

### Scenario

Google assembles RSA headlines and descriptions dynamically, but you only see aggregate performance — not which *combinations* resonate with which audiences. You pull individual RSA asset performance ratings, extract the top 5 headline/description combos, then run them through a Mavera Focus Group. The result tells you which combinations to pin and which to replace, broken down by persona.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Google Ads GAQL (RSA asset performance)"] --> B["Top 5 headline/description combos"] --> C["POST /api/v1/focus-groups"] --> D[Persona-scored rankings]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time
  from google.ads.googleads.client import GoogleAdsClient

  MV = os.environ["MAVERA_API_KEY"]
  CUSTOMER_ID = os.environ["GOOGLE_ADS_CUSTOMER_ID"]
  MB = "https://app.mavera.io/api/v1"
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  client = GoogleAdsClient.load_from_env()
  ga_service = client.get_service("GoogleAdsService")

  query = """
      SELECT
          ad_group_ad.ad.id,
          ad_group_ad.ad.responsive_search_ad.headlines,
          ad_group_ad.ad.responsive_search_ad.descriptions,
          metrics.impressions,
          metrics.conversions,
          metrics.ctr
      FROM ad_group_ad
      WHERE ad_group_ad.ad.type = RESPONSIVE_SEARCH_AD
          AND segments.date DURING LAST_30_DAYS
          AND metrics.impressions > 500
      ORDER BY metrics.impressions DESC
      LIMIT 5
  """

  response = ga_service.search(customer_id=CUSTOMER_ID, query=query)

  combos = []
  for row in response:
      rsa = row.ad_group_ad.ad.responsive_search_ad
      best_headlines = [
          a.text for a in rsa.headlines
          if a.performance_label and a.performance_label.name in ("BEST", "GOOD")
      ][:3] or [a.text for a in rsa.headlines[:3]]

      best_descs = [
          a.text for a in rsa.descriptions
          if a.performance_label and a.performance_label.name in ("BEST", "GOOD")
      ][:2] or [a.text for a in rsa.descriptions[:2]]

      combos.append({
          "ad_id": row.ad_group_ad.ad.id,
          "headlines": best_headlines,
          "descriptions": best_descs,
          "impressions": row.metrics.impressions,
          "ctr": row.metrics.ctr,
          "conversions": row.metrics.conversions,
          "all_headlines": [{"text": a.text, "perf": a.performance_label.name if a.performance_label else "UNKNOWN"} for a in rsa.headlines],
      })

  PERSONA_IDS = os.environ.get("RSA_PERSONA_IDS", "").split(",")
  if not PERSONA_IDS[0]:
      for name, desc in [
          ("Startup Founder", "CEO/Founder of a 10-50 person startup. Time-poor, skeptical of marketing claims."),
          ("Marketing Director", "Director of Marketing at mid-market B2B. Manages team of 5-10. Accountable for pipeline."),
          ("Procurement Manager", "Procurement lead at enterprise. Risk-averse, process-oriented, price-sensitive."),
      ]:
          p = requests.post(f"{MB}/personas", headers=MH, json={"name": name, "description": desc}).json()
          PERSONA_IDS.append(p["id"])
          time.sleep(0.3)

  combo_block = "\n\n".join(
      f"Combo {i+1} (CTR: {c['ctr']:.2%}, Conv: {c['conversions']:.0f}):\n"
      f"  Headlines: {' | '.join(c['headlines'])}\n"
      f"  Descriptions: {' | '.join(c['descriptions'])}"
      for i, c in enumerate(combos)
  )

  fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
      "name": "RSA Headline/Description Testing",
      "persona_ids": [pid for pid in PERSONA_IDS if pid],
      "questions": [
          f"Here are 5 ad combinations:\n\n{combo_block}\n\nRank them 1-5 and explain your top pick.",
          "Which headline would stop your scroll? What makes it stand out?",
          "Which description would convince you to click? Why?",
          "Rewrite the weakest headline to make it compelling for someone like you.",
      ],
      "context": "These are Google Search ads for a B2B marketing platform.",
      "responses_per_persona": 2,
  }).json()

  for _ in range(20):
      time.sleep(5)
      data = requests.get(f"{MB}/focus-groups/{fg['id']}", headers=MH).json()
      if data.get("status") == "completed":
          break

  print(f"Focus Group: {data.get('id')}")
  for resp in data.get("responses", []):
      print(f"\n[{resp.get('persona_id','?')}] {resp.get('question','')[:60]}")
      print(f"  → {resp.get('answer','')[:300]}")

  print("\n--- Asset Performance Summary ---")
  for c in combos:
      for h in c["all_headlines"]:
          print(f"  [{h['perf']:8s}] {h['text']}")
  ```

  ```javascript JavaScript theme={"dark"}
  const DEV_TOKEN = process.env.GOOGLE_ADS_DEVELOPER_TOKEN;
  const ACCESS_TOKEN = process.env.GOOGLE_ADS_ACCESS_TOKEN;
  const CUSTOMER_ID = process.env.GOOGLE_ADS_CUSTOMER_ID;
  const MV = process.env.MAVERA_API_KEY;
  const MB = "https://app.mavera.io/api/v1";
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const gaql = `
    SELECT ad_group_ad.ad.id, ad_group_ad.ad.responsive_search_ad.headlines,
      ad_group_ad.ad.responsive_search_ad.descriptions,
      metrics.impressions, metrics.conversions, metrics.ctr
    FROM ad_group_ad
    WHERE ad_group_ad.ad.type = RESPONSIVE_SEARCH_AD
      AND segments.date DURING LAST_30_DAYS AND metrics.impressions > 500
    ORDER BY metrics.impressions DESC LIMIT 5`;

  const gaRes = await fetch(
    `https://googleads.googleapis.com/v23/customers/${CUSTOMER_ID}/googleAds:searchStream`,
    {
      method: "POST",
      headers: { Authorization: `Bearer ${ACCESS_TOKEN}`, "developer-token": DEV_TOKEN, "Content-Type": "application/json" },
      body: JSON.stringify({ query: gaql }),
    }
  ).then((r) => r.json());

  const combos = (gaRes[0]?.results || []).map((row) => {
    const rsa = row.adGroupAd.ad.responsiveSearchAd;
    const bestH = (rsa.headlines || []).filter((h) => ["BEST", "GOOD"].includes(h.performanceLabel)).slice(0, 3);
    const bestD = (rsa.descriptions || []).filter((d) => ["BEST", "GOOD"].includes(d.performanceLabel)).slice(0, 2);
    return {
      headlines: (bestH.length ? bestH : (rsa.headlines || []).slice(0, 3)).map((h) => h.text),
      descriptions: (bestD.length ? bestD : (rsa.descriptions || []).slice(0, 2)).map((d) => d.text),
      impressions: parseInt(row.metrics.impressions),
      ctr: parseFloat(row.metrics.ctr),
      conversions: parseFloat(row.metrics.conversions),
      allHeadlines: (rsa.headlines || []).map((h) => ({ text: h.text, perf: h.performanceLabel || "UNKNOWN" })),
    };
  });

  const personaIds = [];
  for (const [name, desc] of [
    ["Startup Founder", "CEO at 10-50 person startup. Time-poor, skeptical."],
    ["Marketing Director", "Director at mid-market B2B. Manages team, pipeline-accountable."],
    ["Procurement Manager", "Enterprise procurement. Risk-averse, price-sensitive."],
  ]) {
    const p = await fetch(`${MB}/personas`, { method: "POST", headers: MH,
      body: JSON.stringify({ name, description: desc }) }).then((r) => r.json());
    personaIds.push(p.id);
    await new Promise((r) => setTimeout(r, 300));
  }

  const comboBlock = combos.map((c, i) =>
    `Combo ${i + 1} (CTR: ${(c.ctr * 100).toFixed(1)}%, Conv: ${c.conversions.toFixed(0)}):\n  Headlines: ${c.headlines.join(" | ")}\n  Descriptions: ${c.descriptions.join(" | ")}`
  ).join("\n\n");

  const fg = await fetch(`${MB}/focus-groups`, { method: "POST", headers: MH,
    body: JSON.stringify({
      name: "RSA Testing", persona_ids: personaIds,
      questions: [
        `Rank these ad combos 1-5:\n\n${comboBlock}`,
        "Which headline stops your scroll?",
        "Which description convinces you to click?",
        "Rewrite the weakest headline for someone like you.",
      ],
      context: "Google Search ads for a B2B marketing platform.",
      responses_per_persona: 2,
    }),
  }).then((r) => r.json());

  let data;
  for (let i = 0; i < 20; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    data = await fetch(`${MB}/focus-groups/${fg.id}`, { headers: MH }).then((r) => r.json());
    if (data.status === "completed") break;
  }

  for (const resp of data.responses || []) {
    console.log(`\n[${resp.persona_id}] ${(resp.question || "").slice(0, 60)}`);
    console.log(`  → ${(resp.answer || "").slice(0, 300)}`);
  }

  console.log("\n--- Asset Performance ---");
  combos.forEach((c) => c.allHeadlines.forEach((h) => console.log(`  [${h.perf.padEnd(8)}] ${h.text}`)));
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
[Startup Founder] Rank these ad combos 1-5
  → #1: "Launch in 15 Minutes — No Demo Required" — I don't have time for
    demos. Self-serve or I'm gone. #5: "Enterprise-Grade Marketing Platform"
    — I'm not enterprise. That language pushes me away.

[Procurement Manager] Which description convinces you to click?
  → "SOC 2 certified. No annual commitment." — Addresses my two biggest
    concerns in 6 words. The ones with percentage claims feel unverifiable.

--- Asset Performance ---
  [BEST    ] Launch in 15 Minutes
  [GOOD    ] Cut Reporting Time 60%
  [LOW     ] Best Marketing Platform
  [UNKNOWN ] Enterprise-Grade Solution
```

### Error Handling

<AccordionGroup>
  <Accordion title="Performance labels unavailable">Asset performance labels (`BEST`, `GOOD`, `LOW`) require \~5,000 impressions per ad. New ads show `UNKNOWN` or `LEARNING`. Filter for ads with sufficient volume.</Accordion>
  <Accordion title="Pinned assets skew results">Pinned headlines always appear in position 1/2/3, inflating their performance. Note pinning status (`pinned_field`) when interpreting results.</Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="All Google Ads jobs" icon="rectangle-history" href="/integrations/google-ads">
    View all 7 Google Ads integration jobs
  </Card>

  <Card title="Focus Groups API" icon="comments" href="/api-reference/focus-groups">
    Full reference for POST /api/v1/focus-groups
  </Card>
</CardGroup>
