Build a GA4 landing page performance report in Google Sheets with organic-traffic filtering, engaged sessions, and conversion data per page. Covers the GA4 Add-on, Apps Script Data API, and third-party connectors.
GA4 made landing page reporting harder. Universal Analytics had a simple Landing Pages report with sessions, bounce rate, and goal completions per URL. GA4 distributes this data across dimensions and metrics that do not combine the way UA did, and the default GA4 interface buries page-level performance.
This guide shows SEO and content teams how to pull the right GA4 data into Google Sheets: organic traffic only, per landing page, with the metrics that actually matter for content performance decisions.
What GA4 Broke (and What SEO Teams Actually Need)
In Universal Analytics, the Landing Pages report was straightforward: sessions, bounce rate, goal completions, per landing page URL, filterable by source/medium. You could export it, paste it into Sheets, and have a working content performance view in ten minutes.
GA4 removed the concept of a "session-scoped bounce rate" in the traditional sense. Bounce rate in GA4 is 100% minus engagement rate, which means a user who bounces is one who had a session with no engaged session. The metric exists but measures something slightly different.
More importantly, GA4 does not have a pre-built "landing page performance" report that combines page path, organic sessions, engagement rate, and conversions in one place. You have to build it.
What SEO and content teams need per landing page:
- Organic sessions (not all sessions)
- Engaged sessions (as a proxy for quality)
- Bounce rate (in the GA4 sense: 1 - engagement rate)
- Average engagement time
- Conversions (key events)
- New users vs. returning users
The Right Dimensions and Metrics to Pull
GA4's Data API uses specific dimension and metric names that differ from what you see in the GA4 interface. Here are the correct API names for a landing page report:
Dimensions:
| GA4 Interface Label | API Dimension Name | Notes |
|---|---|---|
| Landing page + query string | landingPage | Full path including query string |
| Landing page (path only) | landingPagePlusQueryString | Confusingly, this is path + query |
| Session source | sessionSource | e.g., google, bing |
| Session medium | sessionMedium | e.g., organic, cpc |
| Session source / medium | sessionSourceMedium | Combined: google / organic |
For a clean report, use landingPage as your page dimension and filter by sessionMedium = organic.
Metrics:
| GA4 Interface Label | API Metric Name | Notes |
|---|---|---|
| Sessions | sessions | Total sessions, apply source filter |
| Engaged sessions | engagedSessions | Sessions >= 10 sec or 2+ pages |
| Engagement rate | engagementRate | engagedSessions / sessions |
| Bounce rate | bounceRate | 1 - engagementRate |
| Average engagement time | averageSessionDuration | Seconds per session |
| Conversions | conversions | Sum of all key events |
| New users | newUsers | First-time visitors |
Recommended report structure: Pull landingPage, sessions, engagedSessions, bounceRate, averageSessionDuration, conversions, newUsers, filtered to sessionMedium = organic.
The Cardinality Problem and How to Work Around It
GA4 uses a row limit of 250,000 rows in the Data API (and far fewer in the interface). Beyond that, GA4 applies "other" bucketing: low-traffic pages get collapsed into an "(other)" row.
For large sites with thousands of landing pages, this means low-traffic pages disappear from your report. The GA4 interface's "cardinality limit" message is GA4 telling you it has stopped tracking individual rows for low-traffic combinations.
Practical workarounds:
Use a longer date range. Pulling 90 days instead of 30 days improves more pages above the threshold where they get collapsed into "(other)."
Filter to high-intent pages. If you only need to track pages in a specific subdirectory (e.g., /blog/ or /resources/), add a dimension filter for landingPage contains /blog/. This reduces total rows and pulls lower-traffic pages out of the "(other)" bucket.
Use BigQuery export. For large sites where cardinality is a real problem, enable the GA4 BigQuery export. BigQuery contains the raw event data without sampling or cardinality limits. You can query it directly and pull results into Sheets via BigQuery connected sheets or a data connector.
Pull sampled vs. unsampled. The GA4 Data API can run sampled queries for large date ranges. For accuracy, use smaller date ranges or request unsampled data (available on GA4 360).
Method 1: GA4 Add-On for Google Sheets
Google provides an official GA4 reporting add-on for Sheets. It is free, maintained by Google, and requires no coding.
Install:
- In Google Sheets: Extensions > Add-ons > Get add-ons
- Search for "Google Analytics", install the official add-on by Google
- After install: Extensions > Google Analytics > Create new report
Configure a landing page report:
- Click "Create new report"
- Name: "Organic Landing Pages"
- Select your GA4 property
- Dimensions: add
landingPage,sessionMedium - Metrics: add
sessions,engagedSessions,bounceRate,averageSessionDuration,conversions - Filters:
sessionMedium == organic - Set date range: last 90 days
- Click "Create Report"
The add-on creates a "Report Configuration" sheet and a results sheet. Click "Run reports" to execute and populate the results.
Schedule it: Extensions > Google Analytics > Schedule reports. Set to run daily at 6 AM. The sheet will auto-refresh with new data each morning.
Limitation: The add-on is limited to 10 reports per spreadsheet and the configuration syntax for filters can be non-obvious. The official documentation has the filter expression syntax.
Method 2: Apps Script + GA4 Data API
For more control (custom date ranges, multiple properties, automated transformations) use the GA4 Data API directly from Apps Script.
Enable the API:
- In Google Cloud Console, enable the "Google Analytics Data API" for your project
- Create a service account and download the JSON key file
- Share your GA4 property with the service account email (Viewer permission in GA4 Admin)
Apps Script function:
function fetchGA4LandingPages() {
const propertyId = 'YOUR_GA4_PROPERTY_ID';
const serviceAccountEmail = '[email protected]';
const payload = {
dateRanges: [{ startDate: '90daysAgo', endDate: 'yesterday' }],
dimensions: [
{ name: 'landingPage' },
{ name: 'sessionMedium' }
],
metrics: [
{ name: 'sessions' },
{ name: 'engagedSessions' },
{ name: 'bounceRate' },
{ name: 'averageSessionDuration' },
{ name: 'conversions' },
{ name: 'newUsers' }
],
dimensionFilter: {
filter: {
fieldName: 'sessionMedium',
stringFilter: { value: 'organic', matchType: 'EXACT' }
}
},
limit: 10000,
orderBys: [{ metric: { metricName: 'sessions' }, desc: true }]
};
const response = UrlFetchApp.fetch(
`https://analyticsdata.googleapis.com/v1beta/properties/${propertyId}:runReport`,
{
method: 'post',
headers: {
'Authorization': 'Bearer ' + getServiceAccountToken(),
'Content-Type': 'application/json'
},
payload: JSON.stringify(payload)
}
);
const data = JSON.parse(response.getContentText());
writeToSheet(data);
}Advantages over the add-on: You can add custom calculated columns (e.g., conversion rate), join with other data sources, handle pagination for large properties, and run the script on a trigger without opening Sheets.
Method 3: Third-Party Connector
Connectors like brooked.io, Supermetrics, or Porter Metrics can pull GA4 data into Sheets without code, with scheduling, and with better error handling than the GA4 add-on.
When to use a third-party connector:
- You need data from multiple GA4 properties in one sheet
- You want to combine GA4 data with data from other sources (Google Ads, Search Console)
- You need the sync to be reliable enough for a client-facing dashboard
- Your team does not have the technical capacity to maintain an Apps Script solution
brooked.io approach: Connect your GA4 property, configure the dimensions and metrics in the visual interface (no need to know API field names), set a schedule, and the data lands in your Sheets template automatically.
Filtering to Organic Traffic Only
Every method above requires a filter to isolate organic sessions. Here is the correct filter configuration for each:
GA4 Add-on filter syntax:
ga:sessionMedium==organic
In the new add-on format:
sessionMedium == organic
Data API dimension filter (JSON):
{
"dimensionFilter": {
"filter": {
"fieldName": "sessionMedium",
"stringFilter": {
"value": "organic",
"matchType": "EXACT"
}
}
}
}Additional filter: exclude (not provided): If you use Google Search Console data alongside GA4, you may want to filter to sessionSource == google in addition to sessionMedium == organic to exclude Bing, DuckDuckGo, and other organic sources for a cleaner keyword analysis.
Paid organic caveat: Google's sessionMedium dimension classifies Google Discover traffic as "organic." If Discover is a meaningful traffic source for your site, segment it separately using sessionSource and sessionMedium together.
Comparison Table
| Method | Setup effort | Scheduling | Custom filters | Multi-property | Cost |
|---|---|---|---|---|---|
| GA4 Add-on | Low | Built-in daily | Limited | No (one per sheet) | Free |
| Apps Script + Data API | High | Flexible | Full control | Yes | Dev time |
| brooked.io | Low | Flexible | Visual | Yes | Subscription |
| Supermetrics | Low | Flexible | Extensive | Yes | $99–199+/mo |
Building the Content Performance Dashboard
Once the data is in Sheets, structure it to answer the questions your SEO and content team actually asks.
Recommended tabs:
- Raw Data. The GA4 pull lands here, refreshed on schedule
- Top Pages. QUERY or SORT formula pulling top 50 pages by organic sessions
- Declining Pages. Compare current 30 days vs. prior 30 days; flag pages with >20% session decline
- Conversion Leaders, pages sorted by conversion count, with conversion rate calculated column
- Engagement Outliers, pages with above-average engaged sessions but below-average conversions (content opportunity)
Calculated columns to add:
| Column | Formula |
|---|---|
| Conversion Rate | =conversions/sessions |
| Engagement Gap | =engagementRate - AVERAGE(engagementRate column) |
| MoM Change | =(current_sessions - prior_sessions) / prior_sessions |
| Content Type | =IF(ISNUMBER(SEARCH("/blog/",landingPage)),"Blog","Other") |
Template Column Layout
Use this column order for the raw data sheet. It matches the GA4 Data API field order and makes formula references consistent.
| Column | Field | Format |
|---|---|---|
| A | Date range (label) | Text |
| B | landingPage | Text |
| C | sessions | Number |
| D | newUsers | Number |
| E | engagedSessions | Number |
| F | engagementRate | Percentage |
| G | bounceRate | Percentage |
| H | averageSessionDuration | Number (seconds) |
| I | conversions | Number |
| J | conversionRate (calculated) | Percentage |
| K | Data pulled timestamp | Datetime |
Troubleshooting
Report shows "(other)" as a top landing page. Cardinality limit has been hit. Use a shorter date range, add a page path filter, or switch to BigQuery export for raw data.
Bounce rate shows 0% for most pages. Confirm you are using bounceRate (the GA4 metric) not attempting to calculate it from event data. Also check if enhanced measurement is configured correctly: if pageview events are not firing, engagement calculations will be wrong.
Sessions in GA4 are lower than in Universal Analytics. This is expected. GA4 counts sessions differently. GA4 sessions reset at midnight (local time) rather than after 30 minutes of inactivity. The numbers are not directly comparable between UA and GA4.
Add-on shows "Access denied" when running reports. The Google account running the add-on must have at least Viewer access to the GA4 property. Check GA4 Admin > Account Access Management.
Apps Script returns a 403 on the Data API. The service account has not been granted access to the GA4 property. In GA4 Admin > Account Access Management (or Property Access Management), add the service account email as Viewer.
Bottom Line
GA4 landing page reporting is harder than UA but fully buildable in Google Sheets. The GA4 add-on is the fastest path for small teams. Apps Script with the Data API gives full control for developers. Third-party connectors like brooked.io remove the maintenance burden for teams that need reliability without code.
The key decisions are: use landingPage not pagePath, filter by sessionMedium = organic, and plan for cardinality limits on large sites. With those in place, the resulting Sheets dashboard is more flexible and customizable than anything in the GA4 interface.
Get Started With brooked.io
brooked.io pulls GA4 landing page data into Google Sheets on a schedule: with organic-traffic filtering, multi-property support, and no code required.
Build your GA4 content dashboard with brooked.io →
Related guides:
Troubleshooting quick reference
Frequently asked questions
Why does GA4 not have a simple "Landing Pages" report like Universal Analytics?
GA4 was redesigned around events rather than sessions, which changed how many reports are structured. The "Landing Page" dimension exists in GA4 Explore, but it is not surfaced as a pre-built report in the standard interface. Building it in Sheets gives you the same functionality, plus the ability to add calculated columns and combine with other data.
How do I get keyword-level data per landing page?
GA4 does not pass keyword data from Google Search. For keyword-level data, connect Google Search Console to Sheets alongside your GA4 pull. Search Console has query, page, clicks, impressions, and position data. Combining GSC's query + page data with GA4's engagement metrics per page gives a more complete picture.
Can I include data from multiple date ranges in the same report?
Yes. The GA4 Data API supports up to two date ranges in a single request, returning comparative data. Configure dateRanges with two objects (e.g., "last 30 days" and "prior 30 days") and the API returns both sets in one response.
Does the GA4 add-on support sampling?
The GA4 add-on can hit GA4's data thresholds and return sampled data for large properties or long date ranges. The add-on does not have an "unsampled" option. For unsampled data, use the Data API directly or upgrade to GA4 360.
How do I track content performance over time, not just current period?
Set up an append-only log: after each daily sync, add a row to a separate "Historical" sheet with the date and that day's metrics for your key pages. Over time, this builds a time series you can chart. Do not overwrite: append.
Ready to connect your data to Google Sheets?
Brooked's free tier covers 100 imports per month with AI Analyst included, no credit card required.
Install Brooked free →

