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%
{
"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:| 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 for real-time monitoring
- Add competitive analysis to your reports
- Export data to your BI tools

