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

# Brand Tracker Data → Competitive Analysis

> Enrich Qualtrics brand tracking data with Mavera web search for competitive intelligence and market dynamics research

### Scenario

Your quarterly brand tracking survey measures awareness, consideration, preference, and NPS against competitors. The numbers tell you *where* you stand, but not *why*. This job exports brand tracker data, sends it to Mave Agent with web search enabled, and asks: "Research the underlying market dynamics driving these brand perception shifts." The result is AI research that connects your tracking data to real market events — competitor launches, industry shifts, PR events — giving your brand team actionable context.

**Flow:** Qualtrics brand tracker export → Extract brand metrics over time → Mave `POST /api/v1/mave/chat` (with web search): "Research underlying market dynamics" → Cited competitive intelligence report

### Architecture

```mermaid theme={"dark"}
flowchart LR
A["Brand Tracker export"] --> B["Extract brand metrics"] --> C["Compare vs competitors"] --> D["Mave Agent with web search"] --> E["Cited market dynamics report"]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, csv, io, zipfile, requests, time
  from collections import defaultdict

  QT = os.environ["QUALTRICS_TOKEN"]
  DC = os.environ["QUALTRICS_DC"]
  MV = os.environ["MAVERA_API_KEY"]
  Q_BASE = f"https://{DC}.qualtrics.com/API/v3"
  MB = "https://app.mavera.io/api/v1"
  Q_H = {"X-API-TOKEN": QT, "Content-Type": "application/json"}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  SURVEY_ID = os.environ.get("BRAND_TRACKER_ID", "SV_xxxxx")

  # 1. Export responses (async)
  export = requests.post(f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses",
      headers=Q_H, json={"format": "csv"}).json()
  progress_id = export["result"]["progressId"]

  file_id = None
  for _ in range(60):
      time.sleep(5)
      status = requests.get(
          f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses/{progress_id}",
          headers=Q_H).json()
      if status["result"].get("percentComplete") == 100:
          file_id = status["result"]["fileId"]
          break

  zip_data = requests.get(
      f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses/{file_id}/file",
      headers=Q_H).content

  with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
      csv_name = [n for n in zf.namelist() if n.endswith(".csv")][0]
      with zf.open(csv_name) as f:
          reader = csv.DictReader(io.TextIOWrapper(f, encoding="utf-8-sig"))
          rows = [r for r in reader if r.get("Finished") == "1"]

  print(f"Brand tracker: {len(rows)} responses")

  # 2. Extract brand metrics
  BRANDS = os.environ.get("TRACKED_BRANDS", "OurBrand,CompetitorA,CompetitorB,CompetitorC").split(",")
  METRICS = ["Awareness", "Consideration", "Preference", "NPS"]

  brand_data = defaultdict(lambda: defaultdict(list))
  for row in rows:
      quarter = row.get("RecordedDate", "")[:7]
      for brand in BRANDS:
          for metric in METRICS:
              col_variants = [
                  f"{brand}_{metric}", f"{metric}_{brand}",
                  f"Q_{brand}_{metric}", f"{brand} - {metric}",
              ]
              for col in col_variants:
                  val = row.get(col, "").strip()
                  if val:
                      try:
                          brand_data[brand][f"{metric}_{quarter}"].append(float(val))
                      except ValueError:
                          brand_data[brand][f"{metric}_{quarter}"].append(val)
                      break

  # 3. Build brand comparison summary
  summary_parts = []
  for brand in BRANDS:
      brand_summary = f"### {brand}\n"
      metrics_by_type = defaultdict(dict)
      for key, values in sorted(brand_data[brand].items()):
          metric, quarter = key.rsplit("_", 1)
          nums = [v for v in values if isinstance(v, (int, float))]
          if nums:
              metrics_by_type[metric][quarter] = sum(nums) / len(nums)

      for metric, quarters in metrics_by_type.items():
          trend = " → ".join(f"{q}: {v:.1f}" for q, v in sorted(quarters.items()))
          brand_summary += f"  {metric}: {trend}\n"
      summary_parts.append(brand_summary)

  brand_summary = "\n".join(summary_parts)
  if not brand_summary.strip():
      brand_summary = f"Raw column sample: {', '.join(list(rows[0].keys())[:20])}"
      print("Warning: Could not auto-detect brand metric columns. Check column naming.")

  # 4. Mave competitive analysis with web search
  analysis = requests.post(f"{MB}/mave/chat", headers=MV_H, json={
      "message": f"""Analyze this brand tracking data and research the underlying market dynamics.

  BRAND TRACKING DATA ({len(rows)} respondents):
  {brand_summary[:4000]}

  Tasks:
  1) Identify significant shifts in brand perception — which brands gained/lost and by how much
  2) Research recent market events that explain these shifts (product launches, funding, PR, partnerships)
  3) Competitive positioning analysis — where does each brand sit in the market map?
  4) Identify emerging threats not yet reflected in tracking data
  5) Recommend strategic responses to competitive movements
  6) Predict next quarter's brand perception based on current trajectories

  Use web search to ground your analysis in real market events."""
  }).json()

  print(f"\n{'='*60}")
  print(f"COMPETITIVE INTELLIGENCE REPORT")
  print(f"{'='*60}")
  print(analysis.get("content", "")[:3000])
  print(f"\nSources: {len(analysis.get('sources', []))}")
  for src in analysis.get("sources", [])[:5]:
      print(f"  - {src.get('title', src.get('url', ''))}")
  ```

  ```javascript JavaScript theme={"dark"}
  const QT = process.env.QUALTRICS_TOKEN;
  const DC = process.env.QUALTRICS_DC;
  const MV = process.env.MAVERA_API_KEY;
  const Q_BASE = `https://${DC}.qualtrics.com/API/v3`;
  const MB = "https://app.mavera.io/api/v1";
  const qH = { "X-API-TOKEN": QT, "Content-Type": "application/json" };
  const mvH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
  const SURVEY_ID = process.env.BRAND_TRACKER_ID || "SV_xxxxx";
  const BRANDS = (process.env.TRACKED_BRANDS || "OurBrand,CompetitorA,CompetitorB").split(",");

  // 1. Export (async)
  const exp = await fetch(`${Q_BASE}/surveys/${SURVEY_ID}/export-responses`,
    { method: "POST", headers: qH, body: JSON.stringify({ format: "csv" }) }).then(r => r.json());

  let fileId = null;
  for (let i = 0; i < 60; i++) {
    await new Promise(r => setTimeout(r, 5000));
    const s = await fetch(`${Q_BASE}/surveys/${SURVEY_ID}/export-responses/${exp.result.progressId}`,
      { headers: qH }).then(r => r.json());
    if (s.result?.percentComplete === 100) { fileId = s.result.fileId; break; }
  }

  // 2. Download + parse
  const JSZip = (await import("jszip")).default;
  const zipBuf = await fetch(`${Q_BASE}/surveys/${SURVEY_ID}/export-responses/${fileId}/file`,
    { headers: qH }).then(r => r.arrayBuffer());
  const zip = await JSZip.loadAsync(zipBuf);
  const csvFile = Object.keys(zip.files).find(n => n.endsWith(".csv"));
  const csvText = await zip.files[csvFile].async("string");
  const lines = csvText.split("\n");
  const headers = lines[0].split(",").map(h => h.replace(/"/g, "").trim());
  const rows = lines.slice(3).filter(l => l.trim()).map(line => {
    const vals = line.match(/(".*?"|[^,]*)/g) || [];
    const obj = {};
    headers.forEach((h, i) => { obj[h] = (vals[i] || "").replace(/"/g, "").trim(); });
    return obj;
  }).filter(r => r.Finished === "1");
  console.log(`Brand tracker: ${rows.length} responses`);

  // 3. Extract metrics + build summary
  const brandData = {};
  for (const brand of BRANDS) {
    brandData[brand] = {};
    for (const row of rows) {
      const quarter = (row.RecordedDate || "").slice(0, 7);
      for (const metric of ["Awareness", "Consideration", "Preference", "NPS"]) {
        for (const col of [`${brand}_${metric}`, `${metric}_${brand}`, `Q_${brand}_${metric}`]) {
          const val = (row[col] || "").trim();
          if (val && !isNaN(Number(val))) {
            const key = `${metric}_${quarter}`;
            (brandData[brand][key] ??= []).push(Number(val));
            break;
          }
        }
      }
    }
  }

  const summaryParts = BRANDS.map(brand => {
    let s = `### ${brand}\n`;
    const byMetric = {};
    for (const [key, vals] of Object.entries(brandData[brand])) {
      const [metric, quarter] = key.split(/_(?=[^_]+$)/);
      (byMetric[metric] ??= {})[quarter] = vals.reduce((a, b) => a + b, 0) / vals.length;
    }
    for (const [metric, quarters] of Object.entries(byMetric)) {
      s += `  ${metric}: ${Object.entries(quarters).sort().map(([q, v]) => `${q}: ${v.toFixed(1)}`).join(" → ")}\n`;
    }
    return s;
  }).join("\n");

  // 4. Mave analysis
  const analysis = await fetch(`${MB}/mave/chat`, { method: "POST", headers: mvH,
    body: JSON.stringify({
      message: `Analyze brand tracking data. Research market dynamics.

  DATA (${rows.length} respondents):
  ${summaryParts.slice(0, 4000)}

  1) Brand perception shifts 2) Market events explaining shifts
  3) Competitive positioning 4) Emerging threats 5) Strategic responses 6) Next-quarter prediction
  Use web search for real market events.`,
    }),
  }).then(r => r.json());

  console.log(`\n${"=".repeat(60)}\nCOMPETITIVE INTELLIGENCE REPORT\n${"=".repeat(60)}`);
  console.log((analysis.content || "").slice(0, 3000));
  console.log(`\nSources: ${(analysis.sources || []).length}`);
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
============================================================
COMPETITIVE INTELLIGENCE REPORT
============================================================

## Brand Perception Shifts (Q3 2025 → Q1 2026)

### OurBrand
- Awareness: 42% → 48% (+6pp) — Driven by podcast sponsorship campaign
- Consideration: 31% → 35% (+4pp) — Product Hunt re-launch helped
- NPS: 38 → 41 (+3) — Onboarding improvements showing results

### CompetitorA
- Awareness: 67% → 71% (+4pp) — $50M Series C press coverage
- Consideration: 45% → 39% (-6pp) — Pricing backlash on social media
- NPS: 52 → 44 (-8) — API deprecation angered developer community

### CompetitorB
- Awareness: 23% → 31% (+8pp) — Aggressive content marketing
- Consideration: 12% → 19% (+7pp) — Free tier launch in January

## Market Dynamics
1. CompetitorA's pricing change created a switching window — 3 Reddit threads
   with 200+ upvotes on "CompetitorA alternatives"
2. Category analyst report (Forrester, Jan 2026) elevated 2 new entrants
3. EU AI Act compliance requirements creating new evaluation criteria

## Strategic Recommendations
1. Target CompetitorA's disaffected developers with migration guides
2. Pursue the Forrester evaluation for inclusion in next wave
3. Launch competitive pricing page addressing CompetitorA's backlash

Sources: 7
  - TechCrunch: "CompetitorA raises $50M Series C"
  - Reddit: "r/SaaS - Best CompetitorA alternatives 2026"
  - Forrester: "The AI Platform Landscape, Q1 2026"
```

### Error Handling

<AccordionGroup>
  <Accordion title="Column naming varies">Brand tracker surveys use custom column names. The code tries multiple patterns (`Brand_Metric`, `Metric_Brand`, `Q_Brand_Metric`). If no matches found, it prints the raw column names for manual mapping.</Accordion>
  <Accordion title="Web search availability">Mave's web search requires specific permissions. If web search is unavailable, the analysis will rely on the survey data alone without external market context.</Accordion>
  <Accordion title="Quarterly data alignment">Responses are grouped by `RecordedDate` month. Ensure your brand tracker runs on a consistent schedule for meaningful trend analysis.</Accordion>
</AccordionGroup>
