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_PENDINGwhile awaiting Google approval
The server-function endpoints in src/serverFunctions/ga4.ts expose five key operations:
getGa4Connection— retrieve current project connectionlistGa4Properties— discover available GA4 propertiessetGa4Property— link a specific property to the projectdisconnect— remove the connection and clean up grantsstartSelfHostedGa4Link— 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:
- Creates a GA4 Data client using
createGa4DataClientwith the user's OAuth token - Builds a request with property ID, date range, metrics, and dimensions
- Calls
client.runReportfrom Google's Analytics Data API - Normalizes the response and maps errors to the
Ga4ReportErrorunion
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 healthGa4OrganicOverviewService— retrieves organic traffic summariesSearchOpportunityService— 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 metadatacreateGa4DataClient— 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:
Ga4Serviceinsrc/server/features/ga4/services/Ga4Service.tsmanages property discovery and linking - Client abstraction:
ga4Client.tsprovides typed Admin and Data API clients - Reporting core:
Ga4ReportingService.runReportexecutes 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →