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

# Customer-Reported Issues → Persona Pain Points

> Pull customer-reported issues from Jira via JQL, analyze pain points with Mave Agent, and create pain-point personas

## Customer-Reported Issues → Persona Pain Points

### Scenario

Your support and engineering teams label customer-reported issues in Jira, but that pain signal never reaches marketing. This job uses JQL to find issues labeled "customer-reported", extracts summaries and descriptions, sends the aggregate to Mave Agent for pain-point analysis, then creates enriched personas representing common customer pain patterns.

**Flow:** Jira `POST /search` (JQL) → aggregate issues → Mavera `POST /api/v1/mave/chat` (pain point analysis) → `POST /api/v1/personas` (pain-point personas)

### Code

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

  DOMAIN, EMAIL = os.environ["JIRA_DOMAIN"], os.environ["JIRA_EMAIL"]
  TOKEN, MV = os.environ["JIRA_API_TOKEN"], os.environ["MAVERA_API_KEY"]
  JB, MB = f"https://{DOMAIN}.atlassian.net/rest/api/3", "https://app.mavera.io/api/v1"
  cred = base64.b64encode(f"{EMAIL}:{TOKEN}".encode()).decode()
  JH = {"Authorization": f"Basic {cred}", "Content-Type": "application/json"}
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  jql = 'labels = "customer-reported" ORDER BY created DESC'
  issues, start = [], 0
  while True:
      r = requests.post(f"{JB}/search", headers=JH, json={
          "jql": jql, "startAt": start, "maxResults": 100,
          "fields": ["summary", "description", "issuetype", "priority", "status"],
      })
      if r.status_code == 429:
          time.sleep(int(r.headers.get("Retry-After", 30)))
          continue
      r.raise_for_status()
      data = r.json()
      issues.extend(data.get("issues", []))
      if start + data["maxResults"] >= data["total"]:
          break
      start += data["maxResults"]
      time.sleep(0.5)

  print(f"Fetched {len(issues)} customer-reported issues")

  issue_lines = []
  for iss in issues[:60]:
      f = iss["fields"]
      desc = ""
      if f.get("description") and f["description"].get("content"):
          for block in f["description"]["content"]:
              for item in block.get("content", []):
                  if item.get("text"):
                      desc += item["text"] + " "
      issue_lines.append(f"- [{iss['key']}] {f['summary']} | {f['issuetype']['name']} | "
                         f"{f['priority']['name']}\n  {desc[:250]}")

  analysis = requests.post(f"{MB}/mave/chat", headers=MH, json={
      "message": (
          f"Pain-point analyst. Analyze {len(issues)} customer-reported Jira issues.\n\n"
          f"ISSUES:\n" + "\n".join(issue_lines)[:8000] + "\n\n"
          "Identify: 1) Top 5 pain clusters 2) Severity ranking 3) Persona archetypes "
          "4) Language patterns 5) Persona definitions (name, role, pain points)"
      ),
  }).json()

  PERSONAS = [
      {"name": "Frustrated Power User", "pain": "Hits edge cases and performance limits daily"},
      {"name": "Blocked Team Lead", "pain": "Integration failures prevent team workflows"},
      {"name": "Confused New User", "pain": "Onboarding gaps and unclear error messages"},
  ]
  for p in PERSONAS:
      time.sleep(0.3)
      r = requests.post(f"{MB}/personas", headers=MH, json={
          "name": f"Pain Point: {p['name']}",
          "description": f"From {len(issues)} Jira issues. Core pain: {p['pain']}. "
                         f"Context:\n{analysis.get('content', '')[:800]}",
      })
      r.raise_for_status()
      print(f"  {r.json()['id']}: Pain Point: {p['name']}")
  ```

  ```javascript JavaScript theme={"dark"}
  const DOMAIN = process.env.JIRA_DOMAIN, EMAIL = process.env.JIRA_EMAIL;
  const TOKEN = process.env.JIRA_API_TOKEN, MV = process.env.MAVERA_API_KEY;
  const JB = `https://${DOMAIN}.atlassian.net/rest/api/3`, MB = "https://app.mavera.io/api/v1";
  const cred = btoa(`${EMAIL}:${TOKEN}`);
  const JH = { Authorization: `Basic ${cred}`, "Content-Type": "application/json" };
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const issues = [];
  let start = 0;
  while (true) {
    const res = await fetch(`${JB}/search`, { method: "POST", headers: JH,
      body: JSON.stringify({ jql: 'labels = "customer-reported" ORDER BY created DESC',
        startAt: start, maxResults: 100,
        fields: ["summary", "description", "issuetype", "priority", "status"] }),
    });
    if (res.status === 429) { await new Promise(r => setTimeout(r, 30000)); continue; }
    const data = await res.json();
    issues.push(...(data.issues || []));
    if (start + data.maxResults >= data.total) break;
    start += data.maxResults;
    await new Promise(r => setTimeout(r, 500));
  }

  const lines = issues.slice(0, 60).map(iss => {
    let desc = "";
    for (const b of (iss.fields.description?.content || []))
      for (const i of (b.content || [])) if (i.text) desc += i.text + " ";
    return `- [${iss.key}] ${iss.fields.summary} | ${iss.fields.issuetype.name}\n  ${desc.slice(0,250)}`;
  }).join("\n");

  const analysis = await fetch(`${MB}/mave/chat`, { method: "POST", headers: MH,
    body: JSON.stringify({ message: `Pain-point analyst. ${issues.length} issues.\n\n${lines.slice(0,8000)}\n\n1) Pain clusters 2) Severity 3) Archetypes 4) Language patterns 5) Persona definitions` }),
  }).then(r => r.json());

  for (const p of [
    { name: "Frustrated Power User", pain: "Edge cases and performance limits" },
    { name: "Blocked Team Lead", pain: "Integration failures block workflows" },
    { name: "Confused New User", pain: "Onboarding gaps, unclear errors" },
  ]) {
    await new Promise(r => setTimeout(r, 300));
    const per = await fetch(`${MB}/personas`, { method: "POST", headers: MH,
      body: JSON.stringify({ name: `Pain Point: ${p.name}`,
        description: `From ${issues.length} issues. Pain: ${p.pain}. ${(analysis.content||"").slice(0,800)}` }),
    }).then(r => r.json());
    console.log(`  ${per.id}: ${per.name}`);
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Fetched 83 customer-reported issues
  per_4xM2a: Pain Point: Frustrated Power User
  per_7kN9b: Pain Point: Blocked Team Lead
  per_2jQ5c: Pain Point: Confused New User

Top 5 Pain Clusters:
1. Data Export Failures (18 issues) — Exports hang on large datasets
2. Webhook Delivery Gaps (14 issues) — Webhooks drop during deploys
3. Onboarding Confusion (12 issues) — Unclear errors, hidden settings
4. Search Performance (9 issues) — JQL queries timeout on large projects
5. Permission Errors (7 issues) — "Access Denied" on visible issues
```

### Error Handling

<AccordionGroup>
  <Accordion title="JQL syntax errors (400)">Jira returns `400` with `errorMessages` if JQL is malformed. Common mistakes: unquoted strings with spaces, invalid field names, missing escape for reserved characters. Test JQL in Jira's issue navigator first.</Accordion>
  <Accordion title="ADF description parsing">Jira Cloud uses Atlassian Document Format (ADF) for descriptions — nested JSON, not plain text. The code walks `content[].content[].text`. Rich content (tables, code blocks) needs deeper traversal.</Accordion>
  <Accordion title="Rate limit (429)">Jira uses a points-based system — search requests cost more than single-issue reads. The code reads `Retry-After` and waits. Add 500ms between pages for large result sets.</Accordion>
</AccordionGroup>
