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

# Bug Report Sentiment → Product Focus Group

> Pull customer-reported bugs from Linear and run synthetic focus groups to assess severity by persona segment

## Scenario

Customer-reported bugs contain language revealing pain severity beyond a priority label. This job pulls issues labeled "bug" and "customer-reported", creates user personas for different customer segments, then runs a Mavera Focus Group asking "How critical is this bug to your workflow?" — producing a severity map driven by synthetic customer voices.

**Flow:** Linear GraphQL (issues with labels "bug" + "customer-reported") → Mavera `POST /personas` → `POST /focus-groups` → Poll for results

## Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time

  LN, MV = os.environ["LINEAR_API_KEY"], os.environ["MAVERA_API_KEY"]
  LB, MB = "https://api.linear.app/graphql", "https://app.mavera.io/api/v1"
  LH = {"Authorization": LN, "Content-Type": "application/json"}
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  query = """query {
    issues(filter: {
      labels: { every: { name: { in: ["bug", "customer-reported"] } } }
      state: { type: { nin: ["completed", "canceled"] } }
    }, first: 40, orderBy: createdAt) {
      nodes { identifier title description priority labels { nodes { name } } team { name } }
    }
  }"""

  r = requests.post(LB, headers=LH, json={"query": query})
  if r.status_code == 429:
      time.sleep(int(r.headers.get("Retry-After", 60)))
      r = requests.post(LB, headers=LH, json={"query": query})
  r.raise_for_status()
  bugs = r.json()["data"]["issues"]["nodes"]
  print(f"Fetched {len(bugs)} customer-reported bugs")

  bug_block = "\n\n".join(
      f"[{b['identifier']}] {b['title']} (priority: {b.get('priority','none')})\n"
      f"  Team: {(b.get('team') or {}).get('name','Unknown')}\n"
      f"  Description: {(b.get('description') or '')[:400]}"
      for b in bugs)

  SEGMENTS = [
      {"name": "Enterprise Admin", "desc": "Manages 200+ person org. Bugs block teams. Values stability and SLAs."},
      {"name": "Startup Founder", "desc": "5-person team. Tolerates quirks, not data loss or broken integrations."},
      {"name": "Product Manager", "desc": "Relies on tool for roadmap planning. Reporting bugs erode stakeholder trust."},
      {"name": "Developer (IC)", "desc": "Lives in tool 8+ hrs/day. API and Git integration matter most."},
  ]

  persona_ids = []
  for seg in SEGMENTS:
      time.sleep(0.3)
      p = requests.post(f"{MB}/personas", headers=MH, json={
          "name": f"Bug Triage: {seg['name']}", "description": seg["desc"]}).json()
      persona_ids.append(p["id"])

  fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
      "name": "Customer Bug Severity Assessment", "persona_ids": persona_ids,
      "questions": [
          f"Here are {len(bugs)} open bugs:\n\n{bug_block[:3000]}\n\nRate top 5 most critical for YOUR workflow (1-10). Explain.",
          "Which bugs would make you consider switching to a competitor if unfixed for 30 days?",
          "Any bugs that seem minor but compound into major frustration over time?",
      ], "responses_per_persona": 2,
  }).json()

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

  for resp in data.get("responses", []):
      idx = persona_ids.index(resp["persona_id"]) if resp.get("persona_id") in persona_ids else -1
      name = SEGMENTS[idx]["name"] if 0 <= idx < len(SEGMENTS) else "Unknown"
      print(f"\n[{name}] Q: {resp.get('question','')[:80]}\n  A: {resp.get('answer','')[:400]}")
  ```

  ```javascript JavaScript theme={"dark"}
  const LN = process.env.LINEAR_API_KEY, MV = process.env.MAVERA_API_KEY;
  const LB = "https://api.linear.app/graphql", MB = "https://app.mavera.io/api/v1";
  const LH = { Authorization: LN, "Content-Type": "application/json" };
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const query = `query {
    issues(filter: {
      labels: { every: { name: { in: ["bug", "customer-reported"] } } }
      state: { type: { nin: ["completed", "canceled"] } }
    }, first: 40, orderBy: createdAt) {
      nodes { identifier title description priority labels { nodes { name } } team { name } }
    }
  }`;

  let res = await fetch(LB, { method: "POST", headers: LH, body: JSON.stringify({ query }) });
  if (res.status === 429) {
    await new Promise(r => setTimeout(r, parseInt(res.headers.get("Retry-After") || "60") * 1000));
    res = await fetch(LB, { method: "POST", headers: LH, body: JSON.stringify({ query }) });
  }
  const bugs = (await res.json()).data?.issues?.nodes || [];
  console.log(`Fetched ${bugs.length} customer-reported bugs`);

  const bugBlock = bugs.map(b =>
    `[${b.identifier}] ${b.title} (priority: ${b.priority || "none"})\n` +
    `  Team: ${b.team?.name || "Unknown"}\n  Desc: ${(b.description || "").slice(0, 400)}`
  ).join("\n\n");

  const SEGMENTS = [
    { name: "Enterprise Admin", desc: "200+ person org. Bugs block teams. Values stability." },
    { name: "Startup Founder", desc: "5-person team. Tolerates quirks, not data loss." },
    { name: "Product Manager", desc: "Roadmap planning. Reporting bugs erode trust." },
    { name: "Developer (IC)", desc: "8+ hrs/day. API and Git integration matter most." },
  ];

  const personaIds = [];
  for (const seg of SEGMENTS) {
    await new Promise(r => setTimeout(r, 300));
    const p = await fetch(`${MB}/personas`, { method: "POST", headers: MH,
      body: JSON.stringify({ name: `Bug Triage: ${seg.name}`, description: seg.desc }),
    }).then(r => r.json());
    personaIds.push(p.id);
  }

  const fg = await fetch(`${MB}/focus-groups`, { method: "POST", headers: MH,
    body: JSON.stringify({
      name: "Customer Bug Severity Assessment", persona_ids: personaIds,
      questions: [
        `${bugs.length} open bugs:\n\n${bugBlock.slice(0,3000)}\n\nRate top 5 (1-10). Explain.`,
        "Which bugs would make you switch if unfixed 30 days?",
        "Any bugs that seem minor but compound into major frustration?",
      ], responses_per_persona: 2,
    }),
  }).then(r => r.json());

  let data;
  for (let i = 0; i < 30; 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 || []) {
    const idx = personaIds.indexOf(resp.persona_id);
    console.log(`\n[${idx >= 0 ? SEGMENTS[idx].name : "Unknown"}] Q: ${(resp.question||"").slice(0,80)}`);
    console.log(`  A: ${(resp.answer||"").slice(0, 400)}`);
  }
  ```
</CodeGroup>

## Example Output

```text theme={"dark"}
Fetched 17 customer-reported bugs

[Enterprise Admin] Q: Rate top 5 most critical for YOUR workflow (1-10)
  A: 1. ENG-289 "SSO login fails silently after token refresh" — 10/10.
     Locks out entire teams. 40 people unable to access for 2 hours.
  2. ENG-301 "Webhook deliveries drop during deploys" — 9/10.
     Our automation pipeline depends on webhooks.

[Startup Founder] Q: Which bugs would make you switch if unfixed 30 days?
  A: ENG-289 (SSO) is a dealbreaker — we just rolled out SSO and I
     can't tell my team "use password login as a workaround." Also
     ENG-308 (GitHub PR links not syncing) — the Git integration is
     the whole reason we chose Linear.

[Developer (IC)] Q: Any bugs that seem minor but compound?
  A: ENG-322 "Keyboard shortcut 'X' occasionally triggers on wrong
     issue" — I triage 50+ issues daily. One wrong archive per day
     × 5 days = 5 lost issues/week.
```

## Error Handling

<AccordionGroup>
  <Accordion title="Label filter matching">The `every` filter requires ALL listed labels. If your workspace uses "Bug" vs "bug" or "customer\_reported", adjust values. Query `issueLabels` to discover exact names.</Accordion>
  <Accordion title="Focus group polling timeout">Complex groups with 4 personas may take 60-90s. The loop allows 150s. Check the Mavera dashboard if `status` never reaches "completed".</Accordion>
  <Accordion title="Priority field values">Linear priorities are integers: 0 (none), 1 (urgent), 2 (high), 3 (medium), 4 (low). Map them to text in your prompt if personas need clearer severity context.</Accordion>
</AccordionGroup>
