Skip to main content

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

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

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']}")

Example Output

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

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