Skip to main content
Automate weekly GEO performance reports with trend analysis and week-over-week comparisons.
This guide uses the API client from the Guides index. Copy it to your project first.

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

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,
    },
  };
}
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'],
        },
    }

Usage

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

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:
{
  "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.
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}"`));
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']}\"")

Send Report

Deliver the report via Slack or email.
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);
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']);
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'])
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'])

Scheduling

Run this report automatically:
PlatformMethod
Cron0 9 * * MON (every Monday at 9am)
GitHub Actionsschedule: cron: '0 9 * * 1'
AWS LambdaEventBridge rule with cron expression
Google CloudCloud Scheduler + Cloud Functions

Next Steps