Skip to main content

Overview

Reportr connects to three Google services to collect comprehensive SEO data for your client reports:
  • Google Search Console - Organic search performance (clicks, keywords, positions)
  • Google Analytics 4 - Website traffic and user behavior
  • PageSpeed Insights - Performance scores and Core Web Vitals
All integrations use read-only API access. Reportr never modifies your Google data.

Authentication Architecture

Reportr uses OAuth 2.0 to securely access Google APIs without storing passwords.

OAuth Flow

1

User Initiates Connection

Click “Connect Google Accounts” on a client card. This triggers the OAuth authorization flow.
2

Authorization Request

Reportr redirects to Google’s OAuth consent screen with:
  • Client ID from environment variables
  • Requested scopes (Search Console, Analytics)
  • Redirect URI for callback
3

User Grants Access

Select Google account and approve permissions. Google returns an authorization code.
4

Token Exchange

Reportr exchanges the authorization code for:
  • Access Token (short-lived, 1 hour)
  • Refresh Token (long-lived, no expiration)
5

Secure Storage

Tokens are encrypted and stored in the database:
model Client {
  googleAccessToken     String?  // Encrypted
  googleRefreshToken    String?  // Encrypted
  googleTokenExpiry     DateTime?
  googleConnectedAt     DateTime?
}

Token Refresh Mechanism

Access tokens expire after 1 hour. Reportr automatically refreshes them:
Token Refresh (from config.ts)
export async function refreshAccessToken(
  refreshToken: string
): Promise<{ accessToken: string; expiresAt: Date }> {
  const oauth2Client = createGoogleAuthClient();
  oauth2Client.setCredentials({ refresh_token: refreshToken });
  
  const { credentials } = await oauth2Client.refreshAccessToken();
  
  return {
    accessToken: credentials.access_token!,
    expiresAt: new Date(credentials.expiry_date!)
  };
}
Refresh tokens don’t expire unless the user revokes access. Reportr can maintain connection indefinitely with automatic token refresh.

Google Search Console

Required Permissions

OAuth Scope: https://www.googleapis.com/auth/webmasters.readonly What this allows:
  • Read Search Console performance data
  • List verified sites and properties
  • Access search analytics reports
What this does NOT allow:
  • Modify site settings
  • Add/remove users
  • Change verification status

Available Data

Reportr fetches comprehensive Search Console metrics:
Aggregated metrics for the selected date range:
{
  totalClicks: 1234,
  totalImpressions: 45678,
  averagePosition: 12.3,
  averageCTR: 2.7  // percentage
}

Property Types

Search Console supports two property types:

URL-Prefix Property

Format: https://example.com/
  • Specific protocol (http/https)
  • Includes subdomain
  • Requires trailing slash
Example: https://www.example.com/

Domain Property

Format: sc-domain:example.com
  • All protocols (http, https)
  • All subdomains
  • All paths
Example: sc-domain:example.com

API Implementation

Search Console data fetching with retry logic:
Search Console Client (from search-console.ts)
export class SearchConsoleClient {
  async getPerformanceData(
    siteUrl: string,
    accessToken: string,
    startDate: string,
    endDate: string
  ): Promise<SearchConsoleData> {
    return await retryWithBackoff(async () => {
      const auth = createGoogleAuthClient();
      auth.setCredentials({ access_token: accessToken });
      
      // Fetch overall metrics, keywords, pages, and daily data in parallel
      const [overallResponse, topKeywords, topPages, dailyData] = 
        await Promise.all([
          this.searchconsole.searchanalytics.query({
            siteUrl,
            auth,
            requestBody: {
              startDate,
              endDate,
              dimensions: [],  // No dimensions = overall totals
              aggregationType: 'auto'
            }
          }),
          this.getTopKeywords(siteUrl, accessToken, 10),
          this.getTopPages(siteUrl, accessToken, 10),
          this.getDailyPerformanceData(siteUrl, accessToken, startDate, endDate)
        ]);
      
      return {
        totalClicks: Math.round(overallResponse.data.rows?.[0]?.clicks || 0),
        totalImpressions: Math.round(overallResponse.data.rows?.[0]?.impressions || 0),
        averagePosition: Number((overallResponse.data.rows?.[0]?.position || 0).toFixed(1)),
        averageCTR: Number(((overallResponse.data.rows?.[0]?.ctr || 0) * 100).toFixed(2)),
        topKeywords,
        topPages,
        dailyData,
        dateRange: { startDate, endDate }
      };
    });
  }
}

Rate Limits

Google Search Console API limits:
Queries per day
limit
1,200 requests per day per projectReportr uses 3-5 queries per report generation.
Queries per minute
limit
300 requests per minuteReportr implements automatic retry with exponential backoff if rate limits are hit.
Data freshness
constraint
2-3 day delayLatest available data is typically 2-3 days old. Reports should use date ranges ending at least 2 days ago.

Google Analytics 4

Required Permissions

OAuth Scopes:
  • https://www.googleapis.com/auth/analytics.readonly
  • https://www.googleapis.com/auth/analytics.edit (for property listing only)
What this allows:
  • Read GA4 report data
  • List accessible properties
  • Access admin metadata
What this does NOT allow:
  • Modify property settings
  • Change user permissions
  • Delete data

Available Data

{
  organicSessions: 2345,
  sessionsDelta: 15.3,  // % change from previous period
  bounceRate: 42.5,
  averageSessionDuration: 145.2  // seconds
}

GA4 Data API

Reportr uses the GA4 Data API (v1beta):
Analytics Client (from analytics.ts)
export class AnalyticsClient {
  async getOrganicTrafficData(
    propertyId: string,
    accessToken: string,
    startDate: string,
    endDate: string
  ): Promise<AnalyticsData> {
    const auth = createGoogleAuthClient();
    auth.setCredentials({ access_token: accessToken });
    
    // Calculate previous period for comparison
    const daysDiff = getDaysBetween(startDate, endDate);
    const prevStartDate = subtractDays(startDate, daysDiff);
    const prevEndDate = subtractDays(endDate, 1);
    
    // Fetch current and previous period in parallel
    const [currentResponse, previousResponse, landingPages, trafficTrend] = 
      await Promise.all([
        this.getOrganicMetrics(propertyId, auth, startDate, endDate),
        this.getOrganicMetrics(propertyId, auth, prevStartDate, prevEndDate),
        this.getTopLandingPages(propertyId, accessToken, startDate, endDate),
        this.getTrafficTrend(propertyId, auth, startDate, endDate)
      ]);
    
    return {
      organicSessions: currentResponse.sessions,
      sessionsDelta: calculateChange(
        currentResponse.sessions, 
        previousResponse.sessions
      ),
      bounceRate: currentResponse.bounceRate,
      averageSessionDuration: currentResponse.averageSessionDuration,
      topLandingPages: landingPages,
      trafficTrend: trafficTrend,
      dateRange: { startDate, endDate }
    };
  }
}

Organic Search Filtering

Reportr filters GA4 data to only include organic search traffic:
Dimension Filter
{
  dimensionFilter: {
    filter: {
      fieldName: 'sessionDefaultChannelGroup',
      stringFilter: {
        matchType: 'EXACT',
        value: 'Organic Search'
      }
    }
  }
}

Rate Limits

Requests per day
limit
250,000 requests per day per project
Concurrent requests
limit
10 concurrent requestsReportr batches requests and uses queuing for high-volume operations.
Data freshness
constraint
24-48 hour delayGA4 data is typically 24-48 hours delayed. Use date ranges ending at least 1 day ago.

PageSpeed Insights

API Access

PageSpeed Insights uses a simple API key (no OAuth required):
PageSpeed Client (from pagespeed.ts)
export class PageSpeedClient {
  private readonly apiKey: string;
  private readonly baseUrl = 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed';
  
  constructor() {
    this.apiKey = process.env.PAGESPEED_API_KEY!;
  }
  
  async getPageSpeedData(url: string): Promise<PageSpeedData> {
    // Fetch mobile and desktop in parallel
    const [mobileData, desktopData] = await Promise.all([
      this.getPageSpeedForStrategy(url, 'mobile'),
      this.getPageSpeedForStrategy(url, 'desktop')
    ]);
    
    return {
      url,
      mobileScore: Math.round(mobileData.score * 100),
      desktopScore: Math.round(desktopData.score * 100),
      coreWebVitals: this.extractCoreWebVitals(mobileData),
      opportunities: this.extractOpportunities(mobileData, desktopData)
    };
  }
}

Core Web Vitals

PageSpeed Insights measures three key metrics:

LCP

Largest Contentful PaintTime until main content loads
  • Good: ≤ 2.5s
  • Needs Work: 2.5s - 4.0s
  • Poor: > 4.0s

FID

First Input DelayTime until page is interactive
  • Good: ≤ 100ms
  • Needs Work: 100ms - 300ms
  • Poor: > 300ms

CLS

Cumulative Layout ShiftVisual stability score
  • Good: ≤ 0.1
  • Needs Work: 0.1 - 0.25
  • Poor: > 0.25

Performance Opportunities

PageSpeed Insights provides actionable recommendations:
Opportunity Extraction
private extractOpportunities(
  mobileData: PageSpeedResponse,
  desktopData: PageSpeedResponse
): PageSpeedOpportunity[] {
  const opportunities: PageSpeedOpportunity[] = [];
  
  // Key audits that provide optimization opportunities
  const opportunityAudits = [
    'unused-css-rules',
    'unused-javascript',
    'render-blocking-resources',
    'unminified-css',
    'modern-image-formats',
    'uses-webp-images',
    'server-response-time',
    // ... 10+ more audits
  ];
  
  for (const auditKey of opportunityAudits) {
    const audit = mobileData.lighthouseResult.audits[auditKey];
    
    if (audit && audit.score < 1) {
      const savings = this.extractSavings(audit);
      
      opportunities.push({
        title: audit.title,
        description: audit.description,
        savings: savings,  // milliseconds
        impact: this.categorizeImpact(savings)
      });
    }
  }
  
  // Sort by impact and return top 10
  return opportunities
    .sort((a, b) => b.savings - a.savings)
    .slice(0, 10);
}

Rate Limits

Requests per day
limit
25,000 requests per day (default quota)Can be increased via Google Cloud Console.
Analysis time
constraint
20-60 seconds per URLPageSpeed runs Lighthouse analysis which takes time. Reportr shows progress indicators.
Performance Impact: PageSpeed Insights actually loads your client’s website in Chrome to measure real performance. This can take 30-60 seconds per analysis.

Error Handling

Reportr implements comprehensive error handling for Google APIs:

Retry Strategy

Exponential Backoff (from error-handling.ts)
export async function retryWithBackoff<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      if (attempt === maxRetries) throw error;
      
      // Check if error is retryable
      if (!isRetryableError(error)) throw error;
      
      // Exponential backoff: 1s, 2s, 4s, 8s...
      const delay = baseDelay * Math.pow(2, attempt);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

function isRetryableError(error: any): boolean {
  const retryableStatusCodes = [429, 500, 502, 503, 504];
  return retryableStatusCodes.includes(error.statusCode);
}

Error Types

Cause: Invalid or expired access tokenReportr Action:
  1. Attempt to refresh token using refresh_token
  2. If refresh fails, mark connection as “needs reauth”
  3. Notify user to reconnect Google account
Cause: User doesn’t have access to requested resourceReportr Action:
  1. Check if property still exists
  2. Verify user has proper permissions
  3. Suggest user check Google Console access
Cause: Too many requests to Google APIReportr Action:
  1. Automatic retry with exponential backoff
  2. Wait 1s → 2s → 4s → 8s between attempts
  3. Max 3 retries before failing
Cause: Google API temporary outageReportr Action:
  1. Automatic retry (these errors are transient)
  2. Exponential backoff between attempts
  3. Log error for monitoring

Error Handling Implementation

Google API Error Handler
export function handleGoogleAPIError(
  error: any,
  service: 'gsc' | 'ga4' | 'pagespeed'
): GoogleAPIError {
  const statusCode = error.response?.status || error.code || 500;
  
  switch (statusCode) {
    case 401:
      return {
        statusCode: 401,
        message: 'Google authentication expired. Please reconnect.',
        retryable: false,
        needsReauth: true
      };
      
    case 403:
      return {
        statusCode: 403,
        message: 'You do not have access to this Google resource.',
        retryable: false,
        needsReauth: false
      };
      
    case 429:
      return {
        statusCode: 429,
        message: 'Google API rate limit exceeded. Retrying...',
        retryable: true,
        needsReauth: false
      };
      
    default:
      return {
        statusCode,
        message: error.message || 'Failed to fetch Google data',
        retryable: statusCode >= 500,
        needsReauth: false
      };
  }
}

Disconnecting Integrations

Users can disconnect Google accounts:
1

Navigate to Client

Open the Manage Client modal for the client you want to disconnect.
2

Find Danger Zone

Scroll to the “Disconnect Google” section.
3

Confirm Disconnection

Click “Disconnect Google Account” and confirm the action.
4

Data Cleared

Reportr removes:
  • Access and refresh tokens
  • Selected property IDs
  • Connection timestamps
Historical reports are preserved but new reports cannot be generated until reconnected.
Disconnecting removes API access but does NOT revoke the OAuth grant. To fully revoke access, users should visit Google Account Permissions and remove Reportr.

Security Best Practices

Token Encryption

All OAuth tokens are encrypted in the database using AES-256 encryption before storage.

Minimal Scopes

Reportr requests only the minimum scopes needed:
  • Read-only Search Console
  • Read-only Analytics
  • No admin or write permissions

Automatic Token Refresh

Access tokens are refreshed automatically when expired, preventing authentication failures.

Error Logging

All API errors are logged for debugging while sanitizing sensitive data (tokens redacted).

Testing Integrations

After connecting Google, test the integration:
Test API Connectivity
# 1. Check Search Console connection
GET /api/clients/{clientId}/google/search-console?startDate=2025-01-01&endDate=2025-01-31

# 2. Check Analytics connection  
GET /api/clients/{clientId}/google/analytics?startDate=2025-01-01&endDate=2025-01-31

# 3. Check PageSpeed
GET /api/clients/{clientId}/pagespeed

# Successful responses return 200 with data
# Failed responses return error codes with details

Client Management

Set up clients and manage Google connections

Report Generation

Use Google data to generate reports

Google API Docs

Official Google API documentation

Build docs developers (and LLMs) love