How OpenSEO Handles Google Analytics 4 Data Fetching: A Deep Dive into the Architecture

OpenSEO uses a layered TypeScript architecture with separate services for OAuth connection management, property discovery via the GA4 Admin API, and report execution via the GA4 Data API.

The open-source SEO platform OpenSEO integrates Google Analytics 4 through a clean separation of concerns. This article examines how the codebase in every-app/open-seo authenticates users, manages connections, and fetches analytics data—complete with actual file paths and implementation details from the source.

GA4 Authentication: OAuth Grants and Connection State

OpenSEO stores GA4 authentication in a shared OAuth table. In src/shared/ga4.ts, Google Analytics is identified by providerId = "google-analytics". The system supports two deployment modes:

  • Self-hosted deployments: Use a custom OAuth app configured via startSelfHostedGa4Link
  • Hosted deployments: Currently rely on GA4_OAUTH_APP_PENDING while awaiting Google approval

The server-function endpoints in src/serverFunctions/ga4.ts expose five key operations:

  • getGa4Connection — retrieve current project connection
  • listGa4Properties — discover available GA4 properties
  • setGa4Property — link a specific property to the project
  • disconnect — remove the connection and clean up grants
  • startSelfHostedGa4Link — initiate OAuth flow for self-hosted instances

These functions wrap Ga4Service methods and include telemetry via captureServerEvent for PostHog logging.

Connection Management with Ga4Service

The Ga4Service class in src/server/features/ga4/services/Ga4Service.ts handles all connection-level business logic:

getConnection — Fetches the project-level GA4 connection from Ga4ConnectionRepository

userHasGrant — Validates whether the authenticated user has a stored Google OAuth grant

listPropertiesForUserWithGrantStatus — Iterates through all stored Google accounts, creates a GA4 Admin client via createGa4AdminClient, and retrieves GA4 properties for each grant. Properties requiring reconnection are flagged with requiresReconnect

setProperty — Validates the selected property against user grants, fetches property metadata via the Admin API, and upserts a Ga4Connection row

disconnect — Removes the project-level connection and deletes the user's grant if no other projects reference that GA4 account

// List available GA4 properties for the current user
import { listGa4Properties } from "@/serverFunctions/ga4";

const { accounts } = await listGa4Properties({ 
  projectId: "proj_123" 
});

// Result includes account hierarchies with properties
// accounts: [{ accountId, properties: [{ propertyId, isSelected, ... }] }]
// Connect a specific property to your project
import { setGa4Property } from "@/serverFunctions/ga4";

await setGa4Property({
  projectId: "proj_123",
  accountId: "1234567890",
  propertyId: "properties/9876543210"
});

Report Execution: Ga4ReportingService

Data fetching happens in src/server/features/ga4/services/Ga4ReportingService.ts. The runReport method implements the core GA4 Data API interaction:

  1. Creates a GA4 Data client using createGa4DataClient with the user's OAuth token
  2. Builds a request with property ID, date range, metrics, and dimensions
  3. Calls client.runReport from Google's Analytics Data API
  4. Normalizes the response and maps errors to the Ga4ReportError union

Error handling distinguishes between ga4_quota_exhausted, ga4_upstream_unavailable, and other failure modes defined in src/server/lib/ga4Errors.ts.

Specialized services consume Ga4ReportingService for specific analytics needs:

  • Ga4MeasurementHealthService — validates GA4 implementation health
  • Ga4OrganicOverviewService — retrieves organic traffic summaries
  • SearchOpportunityService — analyzes keyword-level opportunities
// Fetch organic overview metrics
import { Ga4OrganicOverviewService } from "@/server/features/ga4/services/Ga4OrganicOverviewService";

const report = await Ga4OrganicOverviewService.run({
  propertyId: "properties/9876543210",
  dateRange: { start: "2024-01-01", end: "2024-01-31" },
  metrics: ["sessions", "users"]
});

Client Library Abstraction

src/server/lib/ga4Client.ts encapsulates Google API client creation:

  • createGa4AdminClient — Admin API client for property discovery and metadata
  • createGa4DataClient — Data API client for report execution

Both factories wrap API errors in custom error classes (Ga4AdminApiError, Ga4TokenError) for consistent handling upstream.

Error Handling and Telemetry

All GA4 operations integrate with OpenSEO's observability layer. Server functions log events to PostHog via captureServerEvent, tracking actions like ga4:property_select and ga4:disconnect.

The Ga4ReportError union in src/server/lib/ga4Errors.ts provides structured error types that UI components translate into user-facing messages: ga4_not_connected, ga4_quota_exhausted, ga4_reconnect_required, and others.

Complete Data Flow


UI Component
    ↓
startSelfHostedGa4Link / listGa4Properties / setGa4Property (server functions)
    ↓
Ga4Service (connection logic)
    ↓
ga4Client.ts (Admin/Data client factories) ←→ Google OAuth / GA4 APIs
    ↓
Ga4ConnectionRepository (persistence)
    ↓
Ga4ReportingService.runReport (data fetching)
    ↓
Ga4OrganicOverviewService / Ga4MeasurementHealthService / SearchOpportunityService
    ↓
UI with normalized analytics data

Summary

  • OAuth layer: Shared grant storage with providerId = "google-analytics" for authentication state
  • Connection service: Ga4Service in src/server/features/ga4/services/Ga4Service.ts manages property discovery and linking
  • Client abstraction: ga4Client.ts provides typed Admin and Data API clients
  • Reporting core: Ga4ReportingService.runReport executes all GA4 Data API calls with normalized error handling
  • Domain services: Specialized services consume the reporting layer for measurement health, organic overviews, and search opportunities
  • Observability: Full PostHog telemetry and structured error unions for reliable operations

Frequently Asked Questions

How does OpenSEO authenticate with Google Analytics 4?

OpenSEO uses OAuth 2.0 with grants stored in a shared account table identified by providerId = "google-analytics". Self-hosted deployments configure custom OAuth apps via startSelfHostedGa4Link in src/serverFunctions/ga4.ts, while hosted deployments use a pending Google-approved application.

What is the difference between Ga4Service and Ga4ReportingService?

Ga4Service handles connection lifecycle: discovering properties, validating grants, and persisting selected properties. Ga4ReportingService focuses exclusively on executing GA4 Data API reports and normalizing responses. This separation allows connection logic to evolve independently from data fetching implementations.

Where does OpenSEO handle GA4 API rate limits and errors?

Error normalization occurs in two places. src/server/lib/ga4Errors.ts defines the Ga4ReportError union with variants like ga4_quota_exhausted. The Ga4ReportingService.runReport method catches Google API errors and maps them to these structured types, enabling consistent UI messaging and retry logic across the application.

Can OpenSEO fetch any GA4 metric or dimension?

Yes, through direct use of Ga4ReportingService.runReport. However, the specialized services (Ga4OrganicOverviewService, Ga4MeasurementHealthService) provide pre-configured metric sets for common SEO workflows. These services build upon the same underlying report execution layer with validated, SEO-focused metric selections.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →