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
Code
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")
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`);
}
Example Output
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
Funnel report requires GA4 360 or exploration API
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.Custom events vs page paths
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.Focus Group polling timeout
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.
What’s Next
GA4 Integration
Back to GA4 integration overview
Interest Category → Content Strategy
Build a content plan from audience interests
Landing Page Performance → Content Refresh
Rewrite underperforming landing page copy
Focus Groups API
Full reference for POST /api/v1/focus-groups