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

# Comment Analysis → Feature Messaging

> Extract voice-of-the-customer language from Jira issue comments, build a Brand Voice, and generate feature messaging

## Comment Analysis → Feature Messaging

### Scenario

Issue comments contain raw voice-of-the-customer language — how they describe problems, what they wish for. This job pulls comments from recent customer-reported issues, feeds the language into Mavera's Brand Voice engine, then generates feature messaging that mirrors how customers actually talk.

**Flow:** Jira `GET /issue/{id}/comment` → aggregate language → Mavera `POST /api/v1/brand-voices` → `POST /api/v1/generations`

### 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"}

  r = requests.post(f"{JB}/search", headers=JH, json={
      "jql": 'project = PROJ AND labels = "customer-reported" AND comment IS NOT EMPTY ORDER BY updated DESC',
      "startAt": 0, "maxResults": 30, "fields": ["summary"],
  })
  if r.status_code == 429:
      time.sleep(int(r.headers.get("Retry-After", 30)))
  r.raise_for_status()
  issues = r.json().get("issues", [])
  print(f"Found {len(issues)} issues with comments")

  all_comments = []
  for iss in issues:
      time.sleep(0.5)
      cr = requests.get(f"{JB}/issue/{iss['key']}/comment", headers=JH,
          params={"maxResults": 20, "orderBy": "-created"})
      if cr.status_code == 429:
          time.sleep(int(cr.headers.get("Retry-After", 30)))
          continue
      for c in cr.json().get("comments", []):
          text = ""
          if c.get("body") and c["body"].get("content"):
              for block in c["body"]["content"]:
                  for item in block.get("content", []):
                      if item.get("text"):
                          text += item["text"] + " "
          if len(text) > 20:
              all_comments.append(f"[{iss['key']}: {iss['fields']['summary']}]\n"
                                  f"By {c.get('author',{}).get('displayName','Unknown')}:\n{text.strip()[:500]}")

  print(f"Extracted {len(all_comments)} comments")

  bv = requests.post(f"{MB}/brand-voices", headers=MH, json={
      "name": "Voice of the Customer — Jira Comments",
      "extracted_content": "\n\n---\n\n".join(all_comments[:80])[:50000],
  }).json()
  print(f"Brand Voice: {bv['id']}")

  time.sleep(0.3)
  messaging = requests.post(f"{MB}/generations", headers=MH, json={
      "brand_voice_id": bv["id"],
      "prompt": "Using the customer voice from Jira comments, generate:\n"
                "1. Homepage hero — headline + subheadline in customer language\n"
                "2. 3 feature descriptions written how customers describe value\n"
                "3. 3 testimonial-style quotes echoing actual language patterns\n"
                "4. 5 email subject lines using phrases customers actually use\n"
                "5. 4 pain-to-solution pairs — customer quote → product answer",
  }).json()

  print(messaging.get("output", messaging.get("content", ""))[:3000])
  ```

  ```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" };

  let res = await fetch(`${JB}/search`, { method: "POST", headers: JH,
    body: JSON.stringify({
      jql: 'project = PROJ AND labels = "customer-reported" AND comment IS NOT EMPTY ORDER BY updated DESC',
      startAt: 0, maxResults: 30, fields: ["summary"],
    }),
  });
  if (res.status === 429) await new Promise(r => setTimeout(r, 30000));
  const issues = (await res.json()).issues || [];

  const allComments = [];
  for (const iss of issues) {
    await new Promise(r => setTimeout(r, 500));
    const cr = await fetch(`${JB}/issue/${iss.key}/comment?maxResults=20&orderBy=-created`, { headers: JH });
    if (cr.status === 429) continue;
    for (const c of (await cr.json()).comments || []) {
      let text = "";
      for (const b of (c.body?.content || []))
        for (const i of (b.content || [])) if (i.text) text += i.text + " ";
      if (text.length > 20)
        allComments.push(`[${iss.key}: ${iss.fields.summary}]\nBy ${c.author?.displayName || "?"}:\n${text.trim().slice(0,500)}`);
    }
  }

  const bv = await fetch(`${MB}/brand-voices`, { method: "POST", headers: MH,
    body: JSON.stringify({ name: "Voice of the Customer — Jira Comments",
      extracted_content: allComments.slice(0,80).join("\n\n---\n\n").slice(0,50000) }),
  }).then(r => r.json());

  await new Promise(r => setTimeout(r, 300));
  const msg = await fetch(`${MB}/generations`, { method: "POST", headers: MH,
    body: JSON.stringify({ brand_voice_id: bv.id,
      prompt: "Using customer voice from Jira comments:\n1) Homepage hero\n2) 3 feature descriptions\n3) 3 testimonial quotes\n4) 5 email subjects\n5) 4 pain-to-solution pairs" }),
  }).then(r => r.json());

  console.log((msg.output || msg.content || "").slice(0, 3000));
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Found 28 issues with comments
Extracted 142 comments
Brand Voice: bv_9pR3x

## Homepage Hero
Headline: "Stop fighting your tools. Start shipping."
Subheadline: "Teams switch to us when they're tired of workarounds."

## Feature Descriptions
### Real-Time Sync
"Everything just stays in sync" — changes propagate instantly
across boards, dashboards, and integrations. No stale data.
### Bulk Operations
"I shouldn't need 45 clicks to update 45 issues." Select, update,
done. Bulk edit fields and transitions in seconds.

## Email Subject Lines
1. "Your team deserves tools that don't fight back"
2. "What if bulk edits actually worked?"
3. "Stop context-switching. Start shipping."
4. "The sync problem your team complains about — solved"
5. "We built what Jira comments kept asking for"
```

### Error Handling

<AccordionGroup>
  <Accordion title="Comment body format (ADF)">Jira Cloud returns comment bodies in Atlassian Document Format. The code walks `body.content[].content[].text`. Inline code, mentions, and media nodes need type-specific handling.</Accordion>
  <Accordion title="Bot comment pollution">Automation bots post comments too. Filter by `author.accountType === "atlassian"` to include only humans, or maintain a bot exclusion list.</Accordion>
  <Accordion title="Pagination on comment threads">Issues with 100+ comments require pagination via `startAt`. The code fetches 20 most recent per issue. For thorough analysis, page through all comments.</Accordion>
</AccordionGroup>
