> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwairy.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Weekly Reports

> Generate automated week-over-week performance comparison reports

Automate weekly GEO performance reports with trend analysis and week-over-week comparisons.

<Note>
  This guide uses the API client from the [Guides index](/developers/guides/index#api-client). Copy it to your project first.
</Note>

## What You'll Build

A weekly report showing:

* **Current vs previous week** metrics comparison
* **Trend indicators** (up/down/stable)
* **Percentage change** for each KPI

***

## Generate Weekly Report

<CodeGroup>
  ```javascript JavaScript theme={null}
  async function generateWeeklyReport(client, brandId) {
    const now = new Date();

    // Calculate date ranges
    const thisWeekEnd = now.toISOString().split('T')[0];
    const thisWeekStart = new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
    const lastWeekEnd = thisWeekStart;
    const lastWeekStart = new Date(now - 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];

    // Fetch both periods in parallel
    const [thisWeek, lastWeek] = await Promise.all([
      client.getPerformance(brandId, { startDate: thisWeekStart, endDate: thisWeekEnd }),
      client.getPerformance(brandId, { startDate: lastWeekStart, endDate: lastWeekEnd }),
    ]);

    const calculateChange = (current, previous) => {
      if (!previous || previous === 0) return null;
      return ((current - previous) / previous * 100).toFixed(1);
    };

    const getTrend = (current, previous) => {
      if (current > previous) return 'up';
      if (current < previous) return 'down';
      return 'stable';
    };

    const metrics = [
      { name: 'Mention Rate', key: 'mentionRate' },
      { name: 'Source Rate', key: 'sourceRate' },
      { name: 'Coverage', key: 'coverage' },
      { name: 'Share of Voice', key: 'shareOfVoice' },
      { name: 'Sentiment', key: 'sentiment' },
    ];

    return {
      generatedAt: new Date().toISOString(),
      period: {
        thisWeek: { start: thisWeekStart, end: thisWeekEnd },
        lastWeek: { start: lastWeekStart, end: lastWeekEnd },
      },
      metrics: metrics.map(m => ({
        name: m.name,
        current: thisWeek.scores[m.key],
        previous: lastWeek.scores[m.key],
        change: calculateChange(thisWeek.scores[m.key], lastWeek.scores[m.key]),
        trend: getTrend(thisWeek.scores[m.key], lastWeek.scores[m.key]),
      })),
      methodology: {
        thisWeek: thisWeek.methodology.responsesTotal,
        lastWeek: lastWeek.methodology.responsesTotal,
      },
    };
  }
  ```

  ```python Python theme={null}
  from datetime import datetime, timedelta
  from typing import Optional


  def generate_weekly_report(client, brand_id: str) -> dict:
      """Generate a week-over-week comparison report."""
      now = datetime.now()

      # Calculate date ranges
      this_week_end = now.strftime('%Y-%m-%d')
      this_week_start = (now - timedelta(days=7)).strftime('%Y-%m-%d')
      last_week_end = this_week_start
      last_week_start = (now - timedelta(days=14)).strftime('%Y-%m-%d')

      # Fetch both periods
      this_week = client.get_performance(brand_id, start_date=this_week_start, end_date=this_week_end)
      last_week = client.get_performance(brand_id, start_date=last_week_start, end_date=last_week_end)

      def calculate_change(current: float, previous: float) -> Optional[float]:
          if not previous or previous == 0:
              return None
          return round((current - previous) / previous * 100, 1)

      def get_trend(current: float, previous: float) -> str:
          if current > previous:
              return 'up'
          if current < previous:
              return 'down'
          return 'stable'

      metrics = [
          ('Mention Rate', 'mentionRate'),
          ('Source Rate', 'sourceRate'),
          ('Coverage', 'coverage'),
          ('Share of Voice', 'shareOfVoice'),
          ('Sentiment', 'sentiment'),
      ]

      return {
          'generated_at': datetime.now().isoformat(),
          'period': {
              'this_week': {'start': this_week_start, 'end': this_week_end},
              'last_week': {'start': last_week_start, 'end': last_week_end},
          },
          'metrics': [
              {
                  'name': name,
                  'current': this_week['scores'][key],
                  'previous': last_week['scores'][key],
                  'change': calculate_change(this_week['scores'][key], last_week['scores'][key]),
                  'trend': get_trend(this_week['scores'][key], last_week['scores'][key]),
              }
              for name, key in metrics
          ],
          'methodology': {
              'this_week': this_week['methodology']['responsesTotal'],
              'last_week': last_week['methodology']['responsesTotal'],
          },
      }
  ```
</CodeGroup>

***

## Usage

<CodeGroup>
  ```javascript JavaScript theme={null}
  const client = new QwairyClient(process.env.QWAIRY_API_TOKEN);
  const report = await generateWeeklyReport(client, 'your-brand-id');

  // Console output
  console.log(`\n Weekly GEO Report: ${report.period.thisWeek.start} to ${report.period.thisWeek.end}\n`);
  console.log('Metric               Current    Previous    Change');
  console.log('─'.repeat(55));

  for (const m of report.metrics) {
    const trend = m.trend === 'up' ? '↑' : m.trend === 'down' ? '↓' : '→';
    const changeStr = m.change !== null ? `${m.change > 0 ? '+' : ''}${m.change}%` : 'N/A';
    console.log(`${m.name.padEnd(20)} ${String(m.current).padEnd(10)} ${String(m.previous).padEnd(11)} ${trend} ${changeStr}`);
  }
  ```

  ```python Python theme={null}
  client = QwairyClient()
  report = generate_weekly_report(client, 'your-brand-id')

  print(f"\n Weekly GEO Report: {report['period']['this_week']['start']} to {report['period']['this_week']['end']}\n")
  print(f"{'Metric':<20} {'Current':<10} {'Previous':<11} {'Change':<10}")
  print('─' * 55)

  for m in report['metrics']:
      trend = '↑' if m['trend'] == 'up' else '↓' if m['trend'] == 'down' else '→'
      change_str = f"{'+' if m['change'] and m['change'] > 0 else ''}{m['change']}%" if m['change'] is not None else 'N/A'
      print(f"{m['name']:<20} {m['current']:<10} {m['previous']:<11} {trend} {change_str}")
  ```
</CodeGroup>

***

## Example Output

**Console:**

```
Weekly GEO Report: 2024-12-12 to 2024-12-19

Metric               Current    Previous    Change
───────────────────────────────────────────────────────
Mention Rate         45.2       42.1        ↑ +7.4%
Source Rate          23.9       25.0        ↓ -4.4%
Coverage             33.33      31.0        ↑ +7.5%
Share of Voice       8.13       7.5         ↑ +8.4%
Sentiment            78.1       76.2        ↑ +2.5%
```

**JSON:**

```json theme={null}
{
  "generatedAt": "2024-12-19T10:30:00.000Z",
  "period": {
    "thisWeek": { "start": "2024-12-12", "end": "2024-12-19" },
    "lastWeek": { "start": "2024-12-05", "end": "2024-12-12" }
  },
  "metrics": [
    { "name": "Mention Rate", "current": 45.2, "previous": 42.1, "change": "7.4", "trend": "up" },
    { "name": "Source Rate", "current": 23.9, "previous": 25.0, "change": "-4.4", "trend": "down" },
    { "name": "Coverage", "current": 33.33, "previous": 31.0, "change": "7.5", "trend": "up" },
    { "name": "Share of Voice", "current": 8.13, "previous": 7.5, "change": "8.4", "trend": "up" },
    { "name": "Sentiment", "current": 78.1, "previous": 76.2, "change": "2.5", "trend": "up" }
  ]
}
```

***

## Add Diagnostic Drivers (Optional)

Enrich your report with the "why" behind metric changes: which providers moved, which prompts gained or lost citations.

<CodeGroup>
  ```javascript JavaScript theme={null}
  async function getReportDrivers(client, brandId, startDate, endDate) {
    const [answers, searchQueries] = await Promise.all([
      client.getAnswers(brandId, {
        startDate,
        endDate,
        hasSelfMention: true,
        hasSelfSource: false,
        limit: 5,
        sort: 'createdAt',
        order: 'desc',
      }),
      client.getSearch(brandId, {
        startDate,
        endDate,
        limit: 5,
        sort: 'createdAt',
        order: 'desc',
      }),
    ]);

    return {
      missedCitations: {
        count: answers.pagination.total,
        top: answers.answers.map(a => ({
          prompt: a.promptText,
          provider: a.provider,
          position: a.selfMentionPosition,
        })),
      },
      searchQueries: {
        count: searchQueries.pagination.total,
        top: searchQueries.searches.map(s => ({
          query: s.query,
          prompt: s.prompt,
        })),
      },
    };
  }

  // Usage: add to your report
  const drivers = await getReportDrivers(client, brandId, thisWeekStart, thisWeekEnd);
  console.log(`\nMissed citations this week: ${drivers.missedCitations.count}`);
  console.log('Top prompts where you are mentioned but not cited:');
  drivers.missedCitations.top.forEach(d => console.log(`  - [${d.provider}] "${d.prompt}"`));
  ```

  ```python Python theme={null}
  def get_report_drivers(client, brand_id: str, start_date: str, end_date: str) -> dict:
      """Get diagnostic drivers for the weekly report."""
      from concurrent.futures import ThreadPoolExecutor

      with ThreadPoolExecutor(max_workers=2) as executor:
          ans_future = executor.submit(
              client.get_answers, brand_id,
              has_self_mention=True, has_self_source=False, limit=5,
          )
          search_future = executor.submit(
              client.get_search, brand_id, limit=5,
          )

          answers = ans_future.result()
          search_queries = search_future.result()

      return {
          'missed_citations': {
              'count': answers['pagination']['total'],
              'top': [
                  {'prompt': a['promptText'], 'provider': a['provider'], 'position': a.get('selfMentionPosition')}
                  for a in answers['answers']
              ],
          },
          'search_queries': {
              'count': search_queries['pagination']['total'],
              'top': [
                  {'query': s['query'], 'prompt': s['prompt']}
                  for s in search_queries['searches']
              ],
          },
      }

  # Usage
  drivers = get_report_drivers(client, 'your-brand-id', this_week_start, this_week_end)
  print(f"\nMissed citations this week: {drivers['missed_citations']['count']}")
  for d in drivers['missed_citations']['top']:
      print(f"  - [{d['provider']}] \"{d['prompt']}\"")
  ```
</CodeGroup>

***

## Send Report

Deliver the report via Slack or email.

<CodeGroup>
  ```javascript Slack (JS) theme={null}
  async function sendToSlack(report, webhookUrl) {
    const formatMetric = (m) => {
      const icon = m.trend === 'up' ? ':chart_with_upwards_trend:' : m.trend === 'down' ? ':chart_with_downwards_trend:' : ':arrow_right:';
      const sign = m.change > 0 ? '+' : '';
      return `${icon} *${m.name}*: ${m.current} (${sign}${m.change}%)`;
    };

    const blocks = [
      {
        type: 'header',
        text: { type: 'plain_text', text: `Weekly GEO Report` },
      },
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Period:* ${report.period.thisWeek.start} to ${report.period.thisWeek.end}`,
        },
      },
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: report.metrics.map(formatMetric).join('\n'),
        },
      },
    ];

    await fetch(webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ blocks }),
    });
  }

  // Usage
  await sendToSlack(report, process.env.SLACK_WEBHOOK_URL);
  ```

  ```javascript Email (JS) theme={null}
  const { Resend } = require('resend');

  async function sendByEmail(report, to) {
    const resend = new Resend(process.env.RESEND_API_KEY);

    const formatRow = (m) => {
      const arrow = m.trend === 'up' ? '↑' : m.trend === 'down' ? '↓' : '→';
      const sign = m.change > 0 ? '+' : '';
      return `<tr><td>${m.name}</td><td>${m.current}</td><td>${m.previous}</td><td>${arrow} ${sign}${m.change}%</td></tr>`;
    };

    const html = `
      <h2>Weekly GEO Report</h2>
      <p><strong>Period:</strong> ${report.period.thisWeek.start} to ${report.period.thisWeek.end}</p>
      <table border="1" cellpadding="8" cellspacing="0">
        <tr><th>Metric</th><th>Current</th><th>Previous</th><th>Change</th></tr>
        ${report.metrics.map(formatRow).join('')}
      </table>
    `;

    await resend.emails.send({
      from: 'reports@yourdomain.com',
      to,
      subject: `Weekly GEO Report - ${report.period.thisWeek.end}`,
      html,
    });
  }

  // Usage
  await sendByEmail(report, ['team@company.com']);
  ```

  ```python Slack (Python) theme={null}
  import requests


  def send_to_slack(report: dict, webhook_url: str):
      """Send report to Slack channel."""
      def format_metric(m):
          icon = ':chart_with_upwards_trend:' if m['trend'] == 'up' else ':chart_with_downwards_trend:' if m['trend'] == 'down' else ':arrow_right:'
          sign = '+' if m['change'] and float(m['change']) > 0 else ''
          return f"{icon} *{m['name']}*: {m['current']} ({sign}{m['change']}%)"

      blocks = [
          {'type': 'header', 'text': {'type': 'plain_text', 'text': 'Weekly GEO Report'}},
          {'type': 'section', 'text': {'type': 'mrkdwn', 'text': f"*Period:* {report['period']['this_week']['start']} to {report['period']['this_week']['end']}"}},
          {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '\n'.join(format_metric(m) for m in report['metrics'])}},
      ]

      requests.post(webhook_url, json={'blocks': blocks})

  # Usage
  send_to_slack(report, os.environ['SLACK_WEBHOOK_URL'])
  ```

  ```python Email (Python) theme={null}
  import resend


  def send_by_email(report: dict, to: list):
      """Send report via email using Resend."""
      resend.api_key = os.environ['RESEND_API_KEY']

      def format_row(m):
          arrow = '↑' if m['trend'] == 'up' else '↓' if m['trend'] == 'down' else '→'
          sign = '+' if m['change'] and float(m['change']) > 0 else ''
          return f"<tr><td>{m['name']}</td><td>{m['current']}</td><td>{m['previous']}</td><td>{arrow} {sign}{m['change']}%</td></tr>"

      html = f"""
      <h2>Weekly GEO Report</h2>
      <p><strong>Period:</strong> {report['period']['this_week']['start']} to {report['period']['this_week']['end']}</p>
      <table border="1" cellpadding="8" cellspacing="0">
        <tr><th>Metric</th><th>Current</th><th>Previous</th><th>Change</th></tr>
        {''.join(format_row(m) for m in report['metrics'])}
      </table>
      """

      resend.Emails.send({
          'from': 'reports@yourdomain.com',
          'to': to,
          'subject': f"Weekly GEO Report - {report['period']['this_week']['end']}",
          'html': html,
      })

  # Usage
  send_by_email(report, ['team@company.com'])
  ```
</CodeGroup>

***

## Scheduling

Run this report automatically:

| Platform           | Method                                |
| ------------------ | ------------------------------------- |
| **Cron**           | `0 9 * * MON` (every Monday at 9am)   |
| **GitHub Actions** | `schedule: cron: '0 9 * * 1'`         |
| **AWS Lambda**     | EventBridge rule with cron expression |
| **Google Cloud**   | Cloud Scheduler + Cloud Functions     |

***

## Next Steps

* Build a [custom dashboard](/developers/guides/custom-dashboard) for real-time monitoring
* Add [competitive analysis](/developers/guides/competitive-analysis) to your reports
* [Export data](/developers/guides/data-export) to your BI tools
