For end users
ga4
Pull web analytics from Google Analytics 4 into the analytics_metrics table.
Type: inbound. The runner calls fetch.ts directly; the model is not invoked at fetch time. Per the inbound/outbound split, GA4’s only user-facing surface is its Settings page — no dashboards, no lists. Data lands in shared tables and consumer apps (today: Content Insights’s Insights surface) read it.
What it fetches
Three reports per refresh, all 30 days of data by default:
- Overall daily — sessions, totalUsers, newUsers, screenPageViews, engagedSessions, averageEngagementTime, conversions.
- Per landing page — sessions, screenPageViews, conversions, grouped by
page_path. - Per source/medium — sessions, conversions, grouped by
source+medium.
All three land as rows in analytics_metrics with dimensions stored in jsonb. Same shape future Google Ads / Meta / LinkedIn / Microsoft Ads writers will use, so consumer apps read across all sources from one table.
What you need
- A GA4 property the project is reporting to.
- A Google service account with Viewer access to that property.
- The GA4 Data API enabled in your Google Cloud project.
The Google service account section of Set up your accounts covers all three.
Configuration
The CMS surface is one Settings page at /brands/<slug>/projects/<code>/apps/ga4/settings. It embeds the service account JSON paste, the GA4 property ID field, a Test connection button, and a Refresh now button that runs the fetch pipeline against the last 30 days. The shared /brands/<slug>/settings/integrations/google page is the canonical credential store; the GA4 Settings page reads from it and offers a write-through form when missing.
project_data_freshness.surface = 'ga4' records the last-refreshed timestamp. Clicking Analyze now in Insights auto-triggers a refresh if that timestamp is older than 24 hours.
What reads the data
cms/src/lib/insights/gather-ga4.ts reads analytics_metrics filtered to source = 'ga4' for the last 30 days and produces three groupings consumed by Stage 1 of the Insights analysis: overall daily totals, top 50 pages by sessions (with conversion rate), and top 20 traffic-source rows. Stage 1 turns those into the readable interpretation panel (traffic source mix, top-performing pages, declining pages).
The cross-source reader at cms/src/lib/analytics.ts exposes readAnalyticsMetrics({projectId, sources?, metricNames?, since, until}), aggregateByDate, and readBlendedCpa for any future consumer (the planned growth-dashboard outbound app reads from here).
Where it lives
pectusai/apps/ga4/APP.md