Reviews Management Runbook
This runbook covers operational procedures for the unified review management system, including multi-platform review aggregation, review response handling, and review analytics.
Owner: Platform Team Version: 1.0.0 Last Updated: 2025-12-27 Related Docs: GOOGLE_BUSINESS.md, YELP_INTEGRATION.md, FACEBOOK_INSTAGRAM.md
Table of Contents
- Prerequisites
- Health Check Commands
- Common Failure Modes
- Verification Commands
- Rollback Steps
- Escalation
Prerequisites
Required Services
| Service | Purpose | Health Check |
|---|---|---|
| PostgreSQL (Supabase) | Review storage | pnpm db:health |
| Google My Business API | Google reviews | Google Cloud Console |
| Yelp Fusion API | Yelp reviews | Yelp Developer Portal |
| Facebook Graph API | Facebook reviews | Meta Developer Portal |
| Redis (Upstash) | Sync caching | Upstash Dashboard |
Required Environment Variables
# Database
DATABASE_URL="postgresql://..."
# Google Business (for Google reviews)
NEXT_PUBLIC_GOOGLE_CLIENT_ID="..."
GOOGLE_CLIENT_SECRET="..."
# Yelp (for Yelp reviews)
YELP_API_KEY="..."
# Facebook (for Facebook reviews)
NEXT_PUBLIC_FACEBOOK_APP_ID="..."
FACEBOOK_CLIENT_SECRET="..."
Key Files
| Component | File Path |
|---|---|
| Unified Review Service | lib/services/reviews/unifiedReviewService.ts |
| Platform Adapter | lib/services/reviews/platformAdapter.ts |
| Google Review Adapter | lib/services/reviews/adapters/googleReviewAdapter.ts |
| Yelp Review Adapter | lib/services/reviews/adapters/yelpReviewAdapter.ts |
| Facebook Review Adapter | lib/services/reviews/adapters/facebookReviewAdapter.ts |
| BBB Review Adapter | lib/services/reviews/adapters/bbbReviewAdapter.ts |
| Apple Maps Adapter | lib/services/reviews/adapters/appleMapsReviewAdapter.ts |
| Nextdoor Review Adapter | lib/services/reviews/adapters/nextdoorReviewAdapter.ts |
| Review Hooks | lib/hooks/reviews/ |
Supported Platforms
| Platform | Adapter | Features |
|---|---|---|
| Google Business | googleReviewAdapter | Fetch, respond, metrics |
| Yelp | yelpReviewAdapter | Fetch, draft responses |
facebookReviewAdapter | Fetch, respond, ratings | |
| BBB | bbbReviewAdapter | Fetch, complaints |
| Apple Maps | appleMapsReviewAdapter | Fetch only |
| Nextdoor | nextdoorReviewAdapter | Fetch, neighborhood |
Health Check Commands
Quick Health Check
# Check review API endpoint
curl https://your-domain.com/api/reviews/health
# Expected: {"status":"ok","platforms":["google","yelp","facebook"]}
Platform-Specific Health Checks
# Google Reviews - check connection
curl "https://mybusiness.googleapis.com/v4/accounts" \
-H "Authorization: Bearer $GOOGLE_ACCESS_TOKEN"
# Yelp Reviews - check API
curl "https://api.yelp.com/v3/businesses/$YELP_BUSINESS_ID/reviews" \
-H "Authorization: Bearer $YELP_API_KEY"
# Facebook Reviews - check page rating
curl "https://graph.facebook.com/v18.0/$PAGE_ID/ratings?access_token=$PAGE_ACCESS_TOKEN"
Database Health
# Check review counts by platform
psql $DATABASE_URL -c "
SELECT
source as platform,
COUNT(*) as review_count,
ROUND(AVG(rating), 2) as avg_rating,
MAX(created_at) as newest_review,
MAX(synced_at) as last_sync
FROM \"Review\"
GROUP BY source
ORDER BY review_count DESC;
"
# Check review response rates
psql $DATABASE_URL -c "
SELECT
source as platform,
COUNT(*) as total_reviews,
COUNT(response_text) as responded,
ROUND(
COUNT(response_text)::numeric / NULLIF(COUNT(*), 0) * 100, 2
) as response_rate_pct
FROM \"Review\"
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY source;
"
# Check rating distribution
psql $DATABASE_URL -c "
SELECT
rating,
COUNT(*) as count,
ROUND(COUNT(*)::numeric / SUM(COUNT(*)) OVER() * 100, 2) as percentage
FROM \"Review\"
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY rating
ORDER BY rating DESC;
"
Sync Status Check
# Check last sync times per client
psql $DATABASE_URL -c "
SELECT
c.id as client_id,
c.business_name,
MAX(r.synced_at) as last_review_sync,
COUNT(r.id) as review_count
FROM \"Client\" c
LEFT JOIN \"Review\" r ON c.id = r.client_id
GROUP BY c.id, c.business_name
ORDER BY last_review_sync DESC NULLS LAST
LIMIT 20;
"
Common Failure Modes
1. Review Sync Failures
| Symptom | Likely Cause | Resolution |
|---|---|---|
| Reviews not syncing | Platform token expired | Re-authenticate platform |
| Missing old reviews | Pagination not handled | Implement cursor pagination |
| Duplicate reviews | Deduplication failed | Check external_id handling |
| Sync job timing out | Too many reviews | Batch processing |
Diagnostic Commands:
# Check for sync errors
psql $DATABASE_URL -c "
SELECT
source,
COUNT(*) as error_count,
MAX(error_message) as last_error,
MAX(attempted_at) as last_attempt
FROM \"ReviewSyncLog\"
WHERE status = 'failed'
AND attempted_at > NOW() - INTERVAL '24 hours'
GROUP BY source;
"
# Check for duplicate reviews
psql $DATABASE_URL -c "
SELECT external_id, source, COUNT(*)
FROM \"Review\"
GROUP BY external_id, source
HAVING COUNT(*) > 1
LIMIT 20;
"
# Check sync logs
grep "review.*sync\|ReviewSync" /var/log/petunia/app.log | tail -30
2. Review Response Issues
| Symptom | Likely Cause | Resolution |
|---|---|---|
| Response not posting | API permission denied | Check OAuth scopes |
| Response rejected | Content policy violation | Review response content |
| Response delayed | Rate limiting | Implement backoff |
| Wrong review responded | ID mismatch | Verify review mapping |
Diagnostic Commands:
# Check failed response attempts
psql $DATABASE_URL -c "
SELECT
r.id,
r.source,
r.external_id,
r.response_status,
r.response_error,
r.response_attempted_at
FROM \"Review\" r
WHERE r.response_status = 'failed'
AND r.response_attempted_at > NOW() - INTERVAL '24 hours'
ORDER BY r.response_attempted_at DESC
LIMIT 20;
"
# Check response logs
grep "review.*response\|ReviewResponse" /var/log/petunia/app.log | tail -30
3. Rating Calculation Issues
| Symptom | Likely Cause | Resolution |
|---|---|---|
| Wrong average rating | Stale calculation | Recalculate aggregates |
| Missing platform ratings | Adapter not returning | Check adapter implementation |
| Rating mismatch | Sync out of date | Force full sync |
Diagnostic Commands:
# Recalculate average rating
psql $DATABASE_URL -c "
SELECT
client_id,
source,
COUNT(*) as review_count,
ROUND(AVG(rating), 2) as calculated_avg,
MIN(rating) as min_rating,
MAX(rating) as max_rating
FROM \"Review\"
WHERE client_id = 'your-client-id'
GROUP BY client_id, source;
"
4. Platform-Specific Issues
| Platform | Common Issue | Resolution |
|---|---|---|
| OAuth token expired | Re-authenticate via OAuth | |
| Yelp | API rate limit | Implement rate limiting |
| Page permissions | Check page token scopes | |
| BBB | Scraping blocked | Update user agent/headers |
Verification Commands
Test Unified Review Service
import { unifiedReviewService } from '@/lib/services/reviews';
// Fetch reviews for a client
const result = await unifiedReviewService.getReviewsForClient('client-id', {
filter: { minRating: 3, platforms: ['google', 'yelp'] },
sort: { field: 'createdAt', direction: 'desc' },
limit: 50,
});
console.log('Reviews:', result.reviews.length);
console.log('Total:', result.total);
// Get review metrics
const metrics = await unifiedReviewService.getMetricsForClient('client-id');
console.log('Average rating:', metrics.averageRating);
console.log('Response rate:', metrics.responseRate);
Test Platform Adapters
import { getPlatformAdapter } from '@/lib/services/reviews/platformAdapter';
// Get Google adapter
const googleAdapter = getPlatformAdapter('google');
const googleReviews = await googleAdapter.fetchReviews('connection-id');
console.log('Google reviews:', googleReviews.length);
// Get Yelp adapter
const yelpAdapter = getPlatformAdapter('yelp');
const yelpReviews = await yelpAdapter.fetchReviews('connection-id');
console.log('Yelp reviews:', yelpReviews.length);
Test Review Response
# Test Google review response
curl -X PUT "https://mybusiness.googleapis.com/v4/$REVIEW_NAME/reply" \
-H "Authorization: Bearer $GOOGLE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"comment": "Thank you for your feedback!"}'
Run Review Tests
# Run review service tests
pnpm test -- --testPathPattern="reviews"
# Run adapter tests
pnpm test -- --testPathPattern="reviewAdapter"
Rollback Steps
Disable Review Sync
# Disable review sync globally
curl -X POST https://your-domain.com/api/admin/features \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"feature": "reviews.sync.enabled", "enabled": false}'
Disable Review Responses
# Disable auto-responses
curl -X POST https://your-domain.com/api/admin/features \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"feature": "reviews.autoResponse.enabled", "enabled": false}'
Reset Sync State
# Reset last sync timestamp for a client
psql $DATABASE_URL -c "
UPDATE \"ReviewSyncState\"
SET last_sync_at = NULL, sync_cursor = NULL
WHERE client_id = 'your-client-id';
"
# Clear sync cache
curl -X POST "$UPSTASH_REDIS_REST_URL/del/reviews:sync:$CLIENT_ID" \
-H "Authorization: Bearer $UPSTASH_REDIS_REST_TOKEN"
Rollback Deployment
# If review changes caused issues
vercel list --app petunia
# Rollback
vercel rollback <previous-deployment-url>
# Verify review endpoints
curl https://your-domain.com/api/reviews/health
Escalation
When to Escalate
| Severity | Criteria | Action |
|---|---|---|
| P1 - Critical | All review sync failing | Immediate on-call page |
| P2 - High | Major platform sync broken | Escalate within 30 minutes |
| P3 - Medium | Response posting issues | Escalate within 2 hours |
| P4 - Low | Minor display issues | Normal ticket queue |
Escalation Contacts
| Role | Contact | Availability |
|---|---|---|
| On-Call Engineer | PagerDuty | 24/7 |
| Platform Team | Slack #platform | Business hours |
| Google Support | cloud.google.com/support | Business hours |
| Yelp Partner Support | partner-support@yelp.com | Business hours |
Information to Include
When escalating, include:
- Platforms Affected: Which review platforms are failing
- Client Impact: Number of affected clients
- Error Messages: Specific API errors
- Last Successful Sync: When sync last worked
- Review Volume: Reviews pending sync
Monitoring & Alerts
Key Metrics to Watch
| Metric | Healthy Range | Alert Threshold |
|---|---|---|
| Sync success rate | > 99% | < 95% |
| Average rating | Varies | Sudden drop > 0.5 |
| Response rate | > 80% | < 60% |
| Review volume | Stable | Drop > 50% |
| Sync latency | < 5m | > 30m |
Alert Configuration
- Sentry: Review API errors
- Slack
#reviews-alerts: Sync failures, rating drops - PagerDuty: P1/P2 incidents
Review-Specific Dashboards
- Google Business Profile: https://business.google.com
- Yelp for Business: https://biz.yelp.com
- Facebook Business Suite: https://business.facebook.com
Debug Findings (Cursor debug.log)
These notes capture root causes and fixes discovered via Cursor debug.log instrumentation (NDJSON) during MVP hardening.
2026-01-03 — Reviews error flood (“Unauthorized” / “[object Object]”)
-
Runtime evidence
- Repeated
REVIEWS_FETCH_HTTPfor/api/reviews/unified?...with status401/500 - Repeated
AUTH_ROUTE_NO_SESSIONfor/api/reviews/unified
- Repeated
-
Root causes
- Refetch loop from unstable
filter/sortidentities (inline object literals causing hook churn) - Non-string error payloads producing
[object Object] - Auth/setup failures treated like “real errors” → console spam
- Refetch loop from unstable
-
Fixes applied
lib/hooks/reviews/useReviews.ts- Stabilized dependency behavior to prevent refetch loops
- Improved error message extraction
- Classified auth/setup failures as expected (no
console.errorspam)
lib/hooks/reviews/useReviewMetrics.ts- Added
needsSetupstate to treat 401/403/404/500 as “not configured yet”
- Added
app/(client)/reviews/page.tsx+components/dashboard/ReviewsTabContent.tsx- Show “Connect review platforms” empty state with 0s and clear CTA when
needsSetup
- Show “Connect review platforms” empty state with 0s and clear CTA when
Review Response Best Practices
Response Templates
// Positive review (4-5 stars)
const positiveTemplate = `Thank you so much for the wonderful review, {{reviewer_name}}! We're thrilled to hear about your experience. We look forward to serving you again!`;
// Neutral review (3 stars)
const neutralTemplate = `Thank you for your feedback, {{reviewer_name}}. We appreciate you taking the time to share your experience. If there's anything we can do to improve, please let us know at {{support_email}}.`;
// Negative review (1-2 stars)
const negativeTemplate = `We're sorry to hear about your experience, {{reviewer_name}}. This is not the standard we strive for. Please contact us at {{support_email}} so we can make this right.`;
Response Guidelines
- Respond quickly: Within 24-48 hours
- Personalize: Use reviewer's name when available
- Stay professional: Even for negative reviews
- Address specifics: Acknowledge specific concerns
- Provide next steps: Offer resolution path
Monitoring Review Trends
-- Weekly review trends
SELECT
DATE_TRUNC('week', created_at) as week,
COUNT(*) as review_count,
ROUND(AVG(rating), 2) as avg_rating,
COUNT(CASE WHEN rating >= 4 THEN 1 END) as positive,
COUNT(CASE WHEN rating <= 2 THEN 1 END) as negative
FROM "Review"
WHERE created_at > NOW() - INTERVAL '12 weeks'
GROUP BY week
ORDER BY week DESC;
Last verified: 2025-12-27