> ## 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.

# Conversion Path → Focus Group Validation

> Pull GA4 funnel drop-off data and run a Mavera Focus Group to get qualitative persona feedback on why visitors abandon each step

### Scenario

Your GA4 funnel shows where visitors drop off — maybe 40% leave after the pricing page, or 60% abandon the signup form. You know *where* but not *why*. You pull the funnel report, identify the worst drop-off steps, and run a Mavera Focus Group asking personas open-ended and ranking questions about each abandonment point. The result is qualitative insight layered on top of quantitative funnel data.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["GA4 RunFunnelReport (conversion path)"] --> B[Identify drop-off steps + rates] --> C["POST /api/v1/focus-groups"] --> D[Open-ended + ranking responses per persona]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time
  from google.analytics.data_v1beta import BetaAnalyticsDataClient
  from google.analytics.data_v1beta.types import (
      RunFunnelReportRequest, Funnel, FunnelStep, FunnelFilterExpression,
      FunnelFieldFilter, StringFilter, DateRange,
  )

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

  client = BetaAnalyticsDataClient()

  funnel_report = client.run_funnel_report(RunFunnelReportRequest(
      property=f"properties/{PROPERTY_ID}",
      date_ranges=[DateRange(start_date="30daysAgo", end_date="today")],
      funnel=Funnel(steps=[
          FunnelStep(name="Homepage visit", filter_expression=FunnelFilterExpression(
              funnel_field_filter=FunnelFieldFilter(
                  field_name="pagePath", string_filter=StringFilter(value="/", match_type=StringFilter.MatchType.EXACT)))),
          FunnelStep(name="Product page", filter_expression=FunnelFilterExpression(
              funnel_field_filter=FunnelFieldFilter(
                  field_name="pagePath", string_filter=StringFilter(value="/product", match_type=StringFilter.MatchType.BEGINS_WITH)))),
          FunnelStep(name="Pricing page", filter_expression=FunnelFilterExpression(
              funnel_field_filter=FunnelFieldFilter(
                  field_name="pagePath", string_filter=StringFilter(value="/pricing", match_type=StringFilter.MatchType.EXACT)))),
          FunnelStep(name="Signup start", filter_expression=FunnelFilterExpression(
              funnel_field_filter=FunnelFieldFilter(
                  field_name="pagePath", string_filter=StringFilter(value="/signup", match_type=StringFilter.MatchType.BEGINS_WITH)))),
          FunnelStep(name="Signup complete", filter_expression=FunnelFilterExpression(
              funnel_field_filter=FunnelFieldFilter(
                  field_name="eventName", string_filter=StringFilter(value="sign_up", match_type=StringFilter.MatchType.EXACT)))),
      ]),
  ))

  steps = []
  for row in funnel_report.funnel_table.rows:
      step_name = row.dimension_values[0].value
      users = int(row.metric_values[0].value)
      steps.append({"step": step_name, "users": users})

  drop_offs = []
  for i in range(1, len(steps)):
      prev = steps[i - 1]["users"]
      curr = steps[i]["users"]
      rate = (prev - curr) / max(prev, 1)
      drop_offs.append({
          "from": steps[i - 1]["step"], "to": steps[i]["step"],
          "users_lost": prev - curr, "drop_rate": rate,
      })

  drop_offs.sort(key=lambda d: d["drop_rate"], reverse=True)

  funnel_summary = "\n".join(f"- {s['step']}: {s['users']} users" for s in steps)
  dropoff_summary = "\n".join(
      f"- {d['from']} → {d['to']}: {d['users_lost']} users lost ({d['drop_rate']:.0%} drop-off)"
      for d in drop_offs
  )

  PERSONA_IDS = os.environ.get("FUNNEL_PERSONA_IDS", "").split(",")
  if not PERSONA_IDS[0]:
      for name, desc in [
          ("First-Time Visitor", "New to the product. Browsing casually, easily distracted, skeptical of claims."),
          ("Comparison Shopper", "Evaluating 3-4 tools. Focused on pricing, features, and proof points."),
          ("Technical Decision-Maker", "Engineer or IT lead. Needs API docs, security info, and integration details."),
      ]:
          p = requests.post(f"{MB}/personas", headers=MH, json={"name": name, "description": desc}).json()
          PERSONA_IDS.append(p["id"])
          time.sleep(0.3)

  worst = drop_offs[0] if drop_offs else {"from": "Unknown", "to": "Unknown", "drop_rate": 0}

  fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
      "name": "GA4 Funnel Drop-Off Investigation",
      "persona_ids": [pid for pid in PERSONA_IDS if pid],
      "questions": [
          f"You're browsing our website and reach the '{worst['from']}' step. What would make you leave before reaching '{worst['to']}'? Be specific.",
          f"Rank these reasons for abandoning a signup flow: (1) Too many form fields, (2) No pricing transparency, (3) Required credit card, (4) Unclear value proposition, (5) Slow page load. Explain your #1.",
          "What's the minimum information you need to see before signing up for a new software tool?",
          "If you saw a pricing page with three tiers, what would make you click away instead of choosing one?",
      ],
      "context": f"""This is a funnel analysis for a B2B marketing platform. Here's the conversion funnel:

  {funnel_summary}

  Drop-off analysis:
  {dropoff_summary}

  The biggest drop-off is {worst['from']} → {worst['to']} at {worst['drop_rate']:.0%}.""",
      "responses_per_persona": 2,
  }).json()

  for _ in range(24):
      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')} — {data.get('status')}\n")
  for resp in data.get("responses", []):
      print(f"[{resp.get('persona_id','?')}] {resp.get('question','')[:70]}")
      print(f"  → {resp.get('answer','')[:300]}\n")
  ```

  ```javascript JavaScript theme={"dark"}
  const MV = process.env.MAVERA_API_KEY;
  const PROPERTY_ID = process.env.GA4_PROPERTY_ID;
  const KEY_FILE = JSON.parse(require("fs").readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, "utf8"));
  const MB = "https://app.mavera.io/api/v1";
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const { GoogleAuth } = require("google-auth-library");
  const auth = new GoogleAuth({
    credentials: KEY_FILE,
    scopes: ["https://www.googleapis.com/auth/analytics.readonly"],
  });
  const accessToken = await auth.getAccessToken();

  const funnelRes = await fetch(
    `https://analyticsdata.googleapis.com/v1beta/properties/${PROPERTY_ID}:runFunnelReport`,
    {
      method: "POST",
      headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        dateRanges: [{ startDate: "30daysAgo", endDate: "today" }],
        funnel: {
          steps: [
            { name: "Homepage visit", filterExpression: { funnelFieldFilter: { fieldName: "pagePath", stringFilter: { value: "/", matchType: "EXACT" } } } },
            { name: "Product page", filterExpression: { funnelFieldFilter: { fieldName: "pagePath", stringFilter: { value: "/product", matchType: "BEGINS_WITH" } } } },
            { name: "Pricing page", filterExpression: { funnelFieldFilter: { fieldName: "pagePath", stringFilter: { value: "/pricing", matchType: "EXACT" } } } },
            { name: "Signup start", filterExpression: { funnelFieldFilter: { fieldName: "pagePath", stringFilter: { value: "/signup", matchType: "BEGINS_WITH" } } } },
            { name: "Signup complete", filterExpression: { funnelFieldFilter: { fieldName: "eventName", stringFilter: { value: "sign_up", matchType: "EXACT" } } } },
          ],
        },
      }),
    }
  ).then((r) => r.json());

  const steps = (funnelRes.funnelTable?.rows || []).map((row) => ({
    step: row.dimensionValues[0].value,
    users: parseInt(row.metricValues[0].value),
  }));

  const dropOffs = [];
  for (let i = 1; i < steps.length; i++) {
    const prev = steps[i - 1].users;
    const curr = steps[i].users;
    dropOffs.push({
      from: steps[i - 1].step, to: steps[i].step,
      usersLost: prev - curr, dropRate: (prev - curr) / (prev || 1),
    });
  }
  dropOffs.sort((a, b) => b.dropRate - a.dropRate);
  const worst = dropOffs[0] || { from: "Unknown", to: "Unknown", dropRate: 0 };

  const personaIds = [];
  for (const [name, desc] of [
    ["First-Time Visitor", "New to the product. Browsing casually, easily distracted."],
    ["Comparison Shopper", "Evaluating 3-4 tools. Focused on pricing and proof."],
    ["Technical Decision-Maker", "Engineer/IT lead. Needs API docs, security, integrations."],
  ]) {
    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 funnelSummary = steps.map((s) => `- ${s.step}: ${s.users} users`).join("\n");
  const dropSummary = dropOffs.map((d) => `- ${d.from} → ${d.to}: ${d.usersLost} lost (${(d.dropRate * 100).toFixed(0)}%)`).join("\n");

  const fg = await fetch(`${MB}/focus-groups`, { method: "POST", headers: MH,
    body: JSON.stringify({
      name: "GA4 Funnel Drop-Off",
      persona_ids: personaIds,
      questions: [
        `You reach '${worst.from}'. What would make you leave before '${worst.to}'?`,
        "Rank abandonment reasons: (1) Too many fields, (2) No pricing, (3) Credit card required, (4) Unclear value, (5) Slow load. Explain #1.",
        "What's the minimum info you need before signing up?",
        "What makes you click away from a pricing page?",
      ],
      context: `Funnel:\n${funnelSummary}\n\nDrop-offs:\n${dropSummary}\n\nWorst: ${worst.from} → ${worst.to} at ${(worst.dropRate * 100).toFixed(0)}%.`,
      responses_per_persona: 2,
    }),
  }).then((r) => r.json());

  let data;
  for (let i = 0; i < 24; 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(`[${resp.persona_id}] ${(resp.question || "").slice(0, 70)}`);
    console.log(`  → ${(resp.answer || "").slice(0, 300)}\n`);
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Focus Group: fg_funnel_8k2m — completed

[First-Time Visitor] You reach 'Pricing page'. What would make you leave?
  → If I can't tell in 5 seconds which plan fits me, I'm gone. Too many
    feature checkmarks with no guidance on who each tier is for. Also, if
    there's no monthly option — annual-only feels like a trap.

[Comparison Shopper] Rank abandonment reasons
  → #1: No pricing transparency. If I've clicked through to pricing and
    still see "Contact Sales," I immediately assume it's overpriced. I need
    a number — even a range — or I'll go to the competitor who shows one.

[Technical Decision-Maker] What's the minimum info before signing up?
  → API documentation, integration list, SOC 2 status, and a sandbox
    environment. If I can't evaluate the API without talking to sales,
    you've lost me to the tool with a public API playground.
```

### Error Handling

<AccordionGroup>
  <Accordion title="Funnel report requires GA4 360 or exploration API">The `runFunnelReport` method is part of the GA4 Data API but may have limitations on standard properties. If unavailable, build a step-by-step funnel using sequential `runReport` calls with page path filters.</Accordion>
  <Accordion title="Custom events vs page paths">The example uses page paths for steps 1–4 and an event for step 5. Adjust `filterExpression` to match your actual funnel. Check available events in GA4 → Reports → Events.</Accordion>
  <Accordion title="Focus Group polling timeout">Funnels with 3 personas × 4 questions × 2 responses each can take 60–120s. The loop allows \~120s. Increase iterations for larger configurations.</Accordion>
</AccordionGroup>

***

## What's Next

<CardGroup cols={2}>
  <Card title="GA4 Integration" icon="chart-line" href="/integrations/ga4">
    Back to GA4 integration overview
  </Card>

  <Card title="Interest Category → Content Strategy" icon="lightbulb" href="/integrations/ga4/interest-content-strategy">
    Build a content plan from audience interests
  </Card>

  <Card title="Landing Page Performance → Content Refresh" icon="wand-magic-sparkles" href="/integrations/ga4/landing-page-refresh">
    Rewrite underperforming landing page copy
  </Card>

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