---
title: "Google Analytics 4"
description: "Stream Google Analytics 4 reports, realtime data and property configuration into Streamkap with a service-account key from your own Google Cloud project."
---

The Google Analytics 4 source reads daily, weekly, monthly and realtime reports from one or more GA4 properties, plus the properties' configuration, into one Kafka topic per report, named `source_<id>.google_analytics.<report>`.

import Callout from 'blume/components/content/Callout.astro';

:::info
This connector is in alpha. See [API sources](/api-sources) for how API sources work and what alpha means.
:::

## Prerequisites

* A Google Cloud project where you can enable APIs and create a service account and a JSON key for it.
* **Administrator** access to each GA4 property, to add the service account as a user.

The service account needs no Google Cloud IAM role. It needs:

* The **Google Analytics Data API** enabled in its project.
* The **Viewer** role on every GA4 property the source reads.
* The **Google Analytics Admin API** enabled in the same project, for the `admin_*` [metadata resources](#metadata-resources) and for the **Property ID** picker. Without it, type the property IDs.

## Google Cloud Setup

### 1. Choose the Project

In the [Google Cloud console](https://console.cloud.google.com/), select the project the service account will live in, or create one.

### 2. Enable the APIs

* Go to **APIs & Services → Library**.
* Open **Google Analytics Data API** and click **Enable**.
* Do the same for **Google Analytics Admin API** if you want the `admin_*` resources or the property picker.

### 3. Create a Service Account and a JSON Key

* Go to **IAM & Admin → Service accounts** and click **Create service account**.
* Enter a name, for example `streamkap-ga4`. Skip the optional role steps and click **Done**.
* Open the service account, go to the **Keys** tab, and click **Add key → Create new key**.
* Choose **JSON** and click **Create**. The key file downloads.

:::warning
Google Cloud organizations created on or after 3 May 2024 block key creation by default through the organization policy constraint `iam.managed.disableServiceAccountKeyCreation` (legacy: `iam.disableServiceAccountKeyCreation`). If the console will not create the key, an organization policy administrator must allow an exception for this project.
:::

The key file is a credential for every property the service account can read. Paste it into Streamkap, then delete the downloaded copy or store it with your other secrets. The service account's email is the key's `client_email`, such as `streamkap-ga4@<project-id>.iam.gserviceaccount.com`.

## Google Analytics Setup

### 1. Add the Service Account to the Property

* In [Google Analytics](https://analytics.google.com/), open the property and click **Admin**.
* Under **Property**, open **Property access management**, click **+** and choose **Add users**.
* Enter the service account's `client_email` and choose the **Viewer** role.
* Leave **No Cost Metrics** and **No Revenue Metrics** unchecked. With either restriction GA4 serves the restricted metrics as zeros, so Streamkap refuses any report that asks for one.
* Click **Add**. Repeat for every property the source reads.

### 2. Find the Property ID

In **Admin → Property details**, copy the **Property ID**, a number such as `123456789`. It is not a web stream's **Measurement ID** (`G-…`), which Streamkap rejects.

## Streamkap Setup

### 1. Create the Source

* Navigate to [Add Connectors](https://app.streamkap.com/connectors/add?tab=Sources).
* Choose **Google Analytics 4**.

### 2. Connection Settings (Auth Tab)

![Google Analytics 4 source Auth step with the Service account key, Property ID and Additional property IDs fields](/blume-assets/content/docs/_assets/images/docs/sources-create-google-analytics-auth.png)

* **Source name**: A unique name for this source, for example `ga4-web`.
* **Service account key**: The whole JSON key file, from `{` to `}`.
* **Property ID**: The numeric ID of the GA4 property to sync. Once the key is in, the field lists the properties the key can read, by name; pick one or type the ID.
* **Additional property IDs** *(optional)*: More properties to sync with the same key; add the service account as a Viewer on each. All properties share one topic per report, and each row carries its `property_id`.

Click **Test connection** to check the key, the Data API and access to each property. See [Test connection and Test access](/api-sources#test-connection-and-test-access). Click **Next**.

### 3. Settings Tab

![Google Analytics 4 source Settings step with the report presets, the Reports list, Custom reports, Keep empty rows and API version](/blume-assets/content/docs/_assets/images/docs/sources-create-google-analytics-settings.png)

* **Reports**: The reports and metadata resources to sync; each becomes its own topic. The form starts with the eight defaults in [Preset Reports](#preset-reports). Select `custom_<name>` for each report in **Custom reports**. **Add a preset** adds one of these sets:
  * **Overview**: the eight defaults.
  * **Acquisition**: `traffic_acquisition_session_source_medium_report`, `traffic_acquisition_session_default_channel_group_report`, `traffic_acquisition_session_campaign_report`, `user_acquisition_first_user_source_medium_report`, `user_acquisition_first_user_campaign_report`.
  * **Engagement and content**: `pages`, `pages_path_report`, `pages_title_and_screen_name_report`, `events`, `key_events`, `content_group_report`.
  * **E-commerce**: `transactions`, `ecommerce_purchases_item_name_report`, `ecommerce_purchases_item_id_report`, `ecommerce_purchases_item_category_report`, `ecommerce_purchases_item_brand_report`.
  * **Audience and tech**: `demographic_country_report`, `demographic_city_report`, `demographic_language_report`, `tech_browser_report`, `tech_device_category_report`, `tech_operating_system_report`.
* **Custom reports** *(optional)*: Your own report definitions as JSON. See [Custom Reports](#custom-reports).
* **Keep empty rows** *(off by default)*: GA4 leaves out rows whose metrics are all zero. Turn this on to keep them.
* **API version**: `v1beta`, the only supported GA4 Data API version.
* **Backfill start date**: **Last 90 days** (the default), or **Specific start date**. GA4 has no data before 2015-08-14, and a date after yesterday reads from yesterday. See [Backfill start date](/api-sources#backfill-start-date).

**Test access** runs each selected report once per property with one row. A passing daily report shows its row count for yesterday, such as `412 rows for yesterday`; use it to judge [row volume](#row-volume-and-duplicates). A custom report that returned no rows for yesterday passes with a warning, because an item-scoped dimension with a session-scoped metric never returns rows.

### 4. Review and Create

Check the summary and click **Create**. Streamkap tests the key and every selected report again before it creates the source, and refuses the save if a report fails. Then send the topics to a destination: see [Send topics to a destination](/api-sources#send-topics-to-a-destination).

## Editing the Source

* **Rotating the key**: create a new JSON key on the service account, click **Replace** on the **Auth** tab, paste it, and save. Then delete the old key in Google Cloud.
* **Adding a property**: add the service account to it as a Viewer, then add its ID to **Additional property IDs**. Reports that have already synced read the new property from their current position, not from the **Backfill start date**. To read its history, reset the source.
* **Changing a custom report**: a synced definition is fixed, because its dimensions make up every row's `id` and columns. Add the changed definition under a new name and remove the old one.

For adding and removing reports, see [Editing an API source](/api-sources#editing-an-api-source).

## Preset Reports

There are 69 presets. Every daily report has `date` as its first dimension. Names are GA4 API names. **Default** marks the eight a new source starts with.

| Report | Dimensions (after `date`) | Metrics | Default |
| --- | --- | --- | --- |
| `daily_active_users` | none | `active1DayUsers` | Yes |
| `weekly_active_users` | none | `active7DayUsers` | Yes |
| `four_weekly_active_users` | none | `active28DayUsers` | Yes |
| `website_overview` | none | Overview, `engagedSessions`, `engagementRate` | Yes |
| `devices` | `deviceCategory`, `operatingSystem`, `browser` | Overview | Yes |
| `traffic_sources` | `sessionSource`, `sessionMedium` | Overview | Yes |
| `events` | `eventName` | `eventCount`, `totalUsers`, `eventCountPerUser`, `totalRevenue` | Yes |
| `key_events` | `eventName`, filtered to key events | `keyEvents`, `totalUsers`, `totalRevenue` | Yes |
| `locations` | `country`, `region`, `city` | Overview | |
| `pages` | `hostName`, `pagePath` | `screenPageViews`, `totalUsers`, `userEngagementDuration`, `bounceRate` | |
| `transactions` | none | `activeUsers`, `averageRevenuePerUser`, `newUsers`, `purchaseRevenue`, `sessions`, `totalRevenue`, `transactions` | |
| `traffic_sources_by_platform` | `source`, `medium`, `sourcePlatform` | `activeUsers`, `sessions`, `sessionsPerUser`, `bounceRate`, `engagementRate` | |
| `locations_with_ids` | `country`, `countryId`, `region`, `city`, `cityId` | Overview with `activeUsers` for `totalUsers`, plus `engagementRate` | |
| `devices_with_model` | `deviceCategory`, `deviceModel`, `operatingSystem`, `browser` | As `locations_with_ids` | |
| `pages_by_session_source` | `hostName`, `pagePath`, `sessionMedium`, `sessionSource` | `activeUsers`, `bounceRate`, `engagedSessions`, `engagementRate`, `eventCount`, `screenPageViews`, `screenPageViewsPerUser`, `screenPageViewsPerSession`, `userEngagementDuration` | |
| `audiences_report` | `audienceName` | `activeUsers`, `averageSessionDuration`, `newUsers`, `screenPageViewsPerSession`, `sessions`, `totalRevenue` | |

**Overview** is `totalUsers`, `newUsers`, `sessions`, `sessionsPerUser`, `averageSessionDuration`, `screenPageViews`, `screenPageViewsPerSession` and `bounceRate`. `pages` reads `pagePath` without the query string, which can carry tokens and multiplies the row count.

The report families share one metric set each, and each report adds the dimension named after it:

| Family | Reports and their dimension | Metrics |
| --- | --- | --- |
| User acquisition | `user_acquisition_first_user_medium_report` (`firstUserMedium`), `…_source_report` (`firstUserSource`), `…_source_platform_report` (`firstUserSourcePlatform`), `…_campaign_report` (`firstUserCampaignName`), `…_google_ads_ad_network_type_report` (`firstUserGoogleAdsAdNetworkType`), `…_google_ads_ad_group_name_report` (`firstUserGoogleAdsAdGroupName`), `…_source_medium_report` (`firstUserSource`, `firstUserMedium`) | `newUsers`, `engagedSessions`, `engagementRate`, `eventCount`, `keyEvents`, `totalRevenue`, `totalUsers`, `userEngagementDuration` |
| Traffic acquisition | `traffic_acquisition_session_source_medium_report` (`sessionSource`, `sessionMedium`), `…_medium_report` (`sessionMedium`), `…_source_report` (`sessionSource`), `…_campaign_report` (`sessionCampaignName`), `…_default_channel_group_report` (`sessionDefaultChannelGroup`), `…_source_platform_report` (`sessionSourcePlatform`) | `totalUsers`, `sessions`, `engagedSessions`, `eventsPerSession`, `engagementRate`, `eventCount`, `keyEvents`, `totalRevenue`, `userEngagementDuration` |
| Pages and screens | `pages_title_and_screen_class_report` (`unifiedScreenClass`), `pages_path_report` (`pagePath`), `pages_title_and_screen_name_report` (`unifiedScreenName`), `content_group_report` (`contentGroup`) | `screenPageViews`, `totalUsers`, `newUsers`, `eventCount`, `keyEvents`, `totalRevenue`, `userEngagementDuration` |
| Ecommerce purchases | `ecommerce_purchases_item_name_report` (`itemName`), `…_item_id_report` (`itemId`), `…_item_category_report` (`itemCategory`), `…_item_category_2_report` to `…_item_category_5_report` (`itemCategory2` to `itemCategory5`), `…_item_brand_report` (`itemBrand`), `…_item_category_report_combined` (all five categories) | `cartToViewRate`, `purchaseToViewRate`, `itemsPurchased`, `itemRevenue`, `itemsAddedToCart`, `itemsViewed` |
| Publisher ads | `publisher_ads_ad_unit_report` (`adUnitName`), `…_page_path_report` (`pagePath`), `…_ad_format_report` (`adFormat`), `…_ad_source_report` (`adSourceName`) | `publisherAdImpressions`, `adUnitExposure`, `publisherAdClicks`, `totalAdRevenue` |
| Demographics | `demographic_country_report`, `…_region_report`, `…_city_report`, `…_language_report` (`country`, `region`, `city`, `language`), `…_age_report` (`userAgeBracket`), `…_gender_report` (`userGender`), `…_interest_report` (`brandingInterest`) | `totalUsers`, `newUsers`, `engagedSessions`, `engagementRate`, `keyEvents`, `totalRevenue` |
| Tech | `tech_browser_report` (`browser`), `…_device_category_report` (`deviceCategory`), `…_device_model_report` (`deviceModel`), `…_screen_resolution_report` (`screenResolution`), `…_app_version_report` (`appVersion`), `…_platform_report` (`platform`), `…_platform_device_category_report` (`platform`, `deviceCategory`), `…_operating_system_report` (`operatingSystem`), `…_os_with_version_report` (`operatingSystemWithVersion`), `…_os_version_report` (`operatingSystemVersion`) | `totalUsers`, `newUsers`, `engagedSessions`, `engagementRate`, `eventCount`, `keyEvents`, `totalRevenue` |

Five presets read a whole week (`yearWeek`, Sunday to Saturday), month (`yearMonth`) or year (`year`) in place of `date`: `weekly_events` and `monthly_events` (the `events` report), and `weekly_website_overview`, `monthly_website_overview` and `yearly_website_overview` (the `website_overview` report). User counts are not additive, so each period is read whole: the current period every poll, and the one before while its last days can still change. `yearly_website_overview` re-reads two whole years every hour and costs the most quota of the daily-cadence reports.

`realtime_overview` holds the property's last 30 minutes, with no dimensions and the metrics `activeUsers`, `eventCount`, `keyEvents` and `screenPageViews`. For a realtime breakdown, define a [custom report](#custom-reports) with `"realtime": true`.

A preset never changes under its name; a changed definition ships under a new name.

## Metadata Resources

These describe the properties rather than their traffic. Each is re-read whole hourly, and a row that is gone is deleted.

| Resource | One row per |
| --- | --- |
| `admin_accounts` | Analytics account the service account can see |
| `admin_properties` | property of the source |
| `admin_data_streams` | web and app data stream |
| `admin_custom_dimensions` | custom dimension |
| `admin_custom_metrics` | custom metric |
| `admin_key_events` | key event |
| `admin_google_ads_links` | Google Ads link |
| `metadata_dimensions` | dimension a property offers, standard and custom |
| `metadata_metrics` | metric a property offers, with any `blocked_reasons` from a data restriction |

The `admin_*` resources read the Google Analytics Admin API. Each row carries every field Google documents for the object, in snake_case; a nested object or list is one JSON text column. `id` is the object's resource name, such as `properties/123456789/customDimensions/111`, or `<property>|<apiName>` for the `metadata_*` resources. Measurement Protocol secrets are not synced.

## Custom Reports

**Custom reports** is a JSON list of definitions. Each becomes the report `custom_<name>`, which you then select in **Reports**:

```json
[
  {
    "name": "landing_pages",
    "dimensions": ["landingPage", "deviceCategory"],
    "metrics": ["sessions", "engagedSessions", "keyEvents"]
  },
  {
    "name": "tutorial_progress",
    "dimensions": ["customEvent:level"],
    "metrics": ["eventCount"],
    "dimension_filter": {
      "filter": { "fieldName": "eventName", "stringFilter": { "value": "tutorial_begin" } }
    }
  }
]
```

Rules, checked when you save:

* **`name`**: lowercase letters, digits and underscores, starting with a letter, at most 41 characters, each defined once.
* **`dimensions`**: at most 9, including `date`, which is added first if you leave it out.
* **`metrics`**: 1 to 10.
* Names are GA4 API names, standard (`sessionSource`) or custom (`customEvent:level`, `customEvent:level[tutorial_start]`, `sessionCustomChannelGroup:1234`), none twice. The `currencyCode` dimension is refused because it clashes with the `currency_code` column.
* **`dimension_filter`**, **`metric_filter`** *(optional)*: GA4 `FilterExpression` objects, sent as written.
* **`grain`** *(optional)*: `dateHour`, `yearWeek`, `isoYearIsoWeek`, `yearMonth` or `year` in place of `date`; the report cannot also request `date` or another grain. `dateHour` gives one row per hour, with the hour in a `date_hour` column (`YYYYMMDDHH`, property time zone) that is part of the `id`, and is re-read by whole days.
* **`metric_aggregations`** *(optional)*: any of `TOTAL`, `MINIMUM`, `MAXIMUM`. See [Aggregate rows](#aggregate-rows). The report then makes one request per day of its window.
* **`comparisons`** *(optional)*: GA4 `Comparison` objects, each with exactly a `name` and a `dimensionFilter`; rows carry the name in a `comparison` column that is part of the `id`. A saved comparison (`"comparison": "comparisons/1234"`) is refused, because it can change in Google Analytics without the definition changing. Copy its filter instead.
* Every selected `custom_<name>` needs a definition, and every definition must be selected.
* The property must define every name, and GA4 must accept the combination; [GA4 Query Explorer](https://ga-dev-tools.google/ga4/query-explorer/) is a quick way to try one first.

### Re-listed Custom Reports

A definition with one of these keys has no date to resume from. It is read whole every poll, gets no `date` dimension, and a row that is gone is deleted (see [Re-listed reports](#re-listed-reports)):

* **`date_ranges`**: 1 to 4 GA4 `DateRange` objects, such as `{"startDate": "28daysAgo", "endDate": "yesterday", "name": "last_28"}`. With several, each row carries its range's name in a `date_range` column.
* **`pivots`**: GA4 `Pivot` objects, read with `runPivotReport`. Needs `date_ranges` or a `cohort_spec`; every dimension in exactly one pivot's `fieldNames`; a `limit` on each pivot, with the limits' product at most 10,000; no `offset`. A pivot's `metricAggregations` go inside the pivot.
* **`cohort_spec`**: a GA4 `CohortSpec`. The report must request the `cohort` dimension and takes no `date_ranges`.
* **`realtime`**: `true` reads the last 30 minutes with `runRealtimeReport` every poll interval. Realtime dimensions and metrics only, optional **`minute_ranges`** (up to 2), and no cohort, date ranges, pivots or comparisons.

```json
[
  { "name": "weekly_landing", "grain": "yearWeek", "dimensions": ["landingPage"], "metrics": ["sessions"] },
  {
    "name": "browser_by_country",
    "dimensions": ["country", "browser"],
    "metrics": ["sessions"],
    "date_ranges": [{ "startDate": "28daysAgo", "endDate": "yesterday" }],
    "pivots": [
      { "fieldNames": ["country"], "limit": 50, "metricAggregations": ["TOTAL"] },
      { "fieldNames": ["browser"], "limit": 5 }
    ]
  },
  {
    "name": "retention",
    "dimensions": ["cohort", "cohortNthWeek"],
    "metrics": ["cohortActiveUsers"],
    "cohort_spec": {
      "cohorts": [{ "name": "august", "dimension": "firstSessionDate", "dateRange": { "startDate": "2026-08-01", "endDate": "2026-08-07" } }],
      "cohortsRange": { "granularity": "WEEKLY", "endOffset": 5 }
    }
  }
]
```

## What Each Record Contains

One record is one GA4 row: a day, a combination of dimension values, and its metrics. A `devices` record (values illustrative):

```json
{
  "id": "123456789|devices|2026-09-24|desktop|Windows|Chrome",
  "property_id": "123456789",
  "date": "2026-09-24",
  "currency_code": "USD",
  "device_category": "desktop",
  "operating_system": "Windows",
  "browser": "Chrome",
  "total_users": 1520,
  "new_users": 310,
  "sessions": 2104,
  "sessions_per_user": 1.384,
  "average_session_duration": 142.7,
  "screen_page_views": 6120,
  "screen_page_views_per_session": 2.91,
  "bounce_rate": 0.42
}
```

* **`id`** is the property ID, report name, date and dimension values in order, joined with `|` (`|` and `%` inside a value are escaped as `%7C` and `%25`). It is the Kafka key and stays the same each time GA4 serves the row. A weekly, monthly or yearly row adds the period's value after the date (`…|weekly_events|2026-09-20|202639|page_view`); a re-listed report's row has no date (`…|custom_live_cities|France|Paris`).
* **`date`** is the report day as `YYYY-MM-DD`, or the first day of the period. A re-listed report carries **`streamkap_changed_at`**, the poll that last saw the row change, instead. **`currency_code`** is the property's currency; realtime reports have none.
* Dimensions and metrics are one column each, in snake_case: `sessionDefaultChannelGroup` becomes `session_default_channel_group`, `active1DayUsers` becomes `active1_day_users`. A custom definition keeps its case-sensitive parameter name after the scope and `__`: `customEvent:levels_unlocked` becomes `custom_event__levels_unlocked`.
* An empty value and GA4's `(not set)` are different values with different `id`s.
* Types: dimensions, `id`, `property_id`, `date` and `currency_code` are strings. 17 pure-count metrics are 64-bit integers: `active1DayUsers`, `active7DayUsers`, `active28DayUsers`, `activeUsers`, `totalUsers`, `newUsers`, `sessions`, `engagedSessions`, `screenPageViews`, `eventCount`, `itemsViewed`, `itemsAddedToCart`, `itemsPurchased`, `transactions`, `ecommercePurchases`, `publisherAdImpressions`, `publisherAdClicks`. Every other metric, including `keyEvents` and every custom metric, is a double. If GA4 starts serving an integer metric as a decimal, the report's poll fails naming the metric.

### Aggregate Rows

With `metric_aggregations`, each day's (or period's) totals, minimums and maximums arrive in the same topic. Every dimension holds GA4's `RESERVED_TOTAL`, `RESERVED_MINIMUM` or `RESERVED_MAXIMUM`, a **`row_type`** column says `row`, `total`, `minimum` or `maximum`, and the `id` ends with one extra `RESERVED_…` part so it never equals a data row's. Filter on `row_type = 'row'` to leave them out.

## Behavior & Limits

### Backfill

Each report's first poll reads from the **Backfill start date** through today, in the property's time zone. A long backfill arrives over several polls that run back to back.

### Restatement

GA4 revises recent days: data can change for 24 to 48 hours, late events are accepted for two days plus today, and key-event attribution can change for up to 12 days. Every poll re-reads a window before the latest day it has seen:

* **3 days** for most reports.
* **13 days** for `key_events`, `traffic_sources_by_platform`, and any custom report with a key-event-attributed dimension: `source`, `medium`, `sourceMedium`, `sourcePlatform`, `campaignId`, `campaignName`, `defaultChannelGroup`, `primaryChannelGroup`, and names starting with `manual`, `googleAds`, `cm360`, `dv360` or `sa360`.

An unchanged row is not sent again. A changed row is sent again **with the same `id`**, so **set the destination to upsert on `id` and never sum the topic**: an append-only destination keeps every version.

A row GA4 stops returning for a re-read day (all its metrics restated to zero, with **Keep empty rows** off) is deleted. Streamkap remembers up to 5,000 row ids per report for this, about 1,250 rows a day at a 3-day window; a vanished row beyond that memory stays in the destination. A re-read cut short, for example by a [quota](#quota) hold, deletes nothing.

### Re-listed Reports

Realtime, pivot, cohort and fixed-date-range reports, and the metadata resources, are read whole every poll. A new or changed row is sent, an unchanged one is not, and a row no longer served (a city with no active users in the last 30 minutes, an archived custom dimension) is deleted. Deletes are tracked for up to 10,000 rows per property; past that, most vanished rows are not deleted. A realtime read is one request of at most 250,000 rows; a property serving more is read cut, and nothing of that property is deleted on that poll.

### Row Volume and Duplicates

A report whose re-read window holds more than 5,000 rows sends some unchanged rows again every poll, as duplicate upserts: above about 1,250 rows a day for a 3-day report, or 357 for a 13-day one. They are harmless to an upserting destination but cost throughput, up to about 10,000 duplicates an hour per report. That is why `locations`, `pages` and `ecommerce_purchases_item_name_report` are off by default. Multiply the **Test access** row count by 4 (or 14) to see where a report stands.

### Cadence

Reports and metadata resources poll hourly. Realtime reports poll every poll interval, 5 minutes by default.

### Quota

GA4 meters the Data API in tokens per property: a Standard property has 200,000 a day, 40,000 an hour and 14,000 per Cloud project an hour; Analytics 360 has ten times as many. There are also limits of 10 concurrent requests, 10 server errors per project an hour and 120 potentially thresholded requests an hour. **The quota is shared** with everything else that reads the property, such as Looker Studio: once a bucket is empty, every request to the property fails.

Streamkap reads the remaining quota from every response. When a bucket falls to about 5% of its Standard size, the report keeps the rows it has read and holds:

* for an hourly bucket, one hour;
* for the daily bucket, until one hour after the next midnight Pacific time (`America/Los_Angeles`), when GA4 resets it.

The source shows **Throttled** with the time it resumes, and resumes on its own. If GA4 refuses a request because the daily quota is spent, the source is held until the same reset. An hourly-quota refusal fails that poll, and the next hourly poll retries. With several properties, a low bucket on any of them holds the whole report.

The connector paces its requests and retries a server error only a few times, because GA4 blocks a Cloud project from a property after 10 server errors in an hour.

### Access Problems

* **A rejected key, or a Data API that is not enabled**, stops all syncing. The source stays **Broken** until you fix the cause and save it.
* **Lost access to one of several properties**: that property is skipped with a warning in every report, and its rows already in the destination stay. It rejoins once access is restored; reset the source to read the days it missed.
* **Lost access to the only property, or to all of them**: every report pauses and is retried on its own, or at once when you save the source.
* **Admin API not enabled**: only the `admin_*` resources pause.

GA4 can withhold rows for privacy (thresholding), sample large results, and group excess values into an `(other)` row. Rows arrive as GA4 served them; the record does not say whether a row was thresholded or sampled.

Funnel reports are not supported. Connecting with **Sign in with Google** is not supported: paste a service-account key.

## Troubleshooting

**"The service account key is not valid JSON"**

The pasted value is not the whole key file. Open the downloaded `.json` file in a text editor and paste all of it, from `{` to `}`, or create a new JSON key.

**"This is not a service account key; OAuth client files and API keys do not work here"**

The JSON is another kind of Google credential. Create a key on a service account under **IAM & Admin → Service accounts → Keys → Add key → Create new key → JSON**, and paste that file.

**"… missing its client_email or private_key", or "… private_key is not a valid RSA private key"**

The key file is incomplete or was edited, including its line breaks. Paste it exactly as downloaded, or create a new JSON key.

**"Google rejected the service account key"**

The key was deleted or disabled in Google Cloud, or its service account no longer exists. Create a new JSON key on an existing service account and paste it on the **Auth** tab with **Replace**. For a new service account, add it to the property as a Viewer first.

**"The Google Analytics Data API is not enabled in the Google Cloud project …"**

In the key's project open **APIs & Services → Library**, enable **Google Analytics Data API**, wait a few minutes, and test or save again.

**"… does not have sufficient permissions on GA4 property …"**

The service account is not a user of the property, or **Property ID** names a different property. Add the `client_email` as a **Viewer** under **Admin → Property access management**, and check the ID. See [Access Problems](#access-problems).

**"Property ID must be the numeric ID …"**

A Measurement ID (`G-…`) identifies a web data stream, not a property. Copy the number from **Admin → Property details**.

**Couldn't list your properties: the Google Analytics Admin API is not enabled**

The **Property ID** picker reads the Admin API. Enable **Google Analytics Admin API** in the key's project, or type the property IDs.

**A report "would sync as zeros"**

The service account has the **No Revenue Metrics** or **No Cost Metrics** restriction on the property, and the report asks for a restricted metric. Remove the restriction under **Admin → Property access management**, or deselect the report.

**"GA4 property … does not define …"**

The report asks for a dimension or metric the property does not have, usually a custom definition from another property or a misspelled name. Check the name on the GA4 Data API schema page or under **Admin → Custom definitions**. A preset that fails this way can only be deselected.

**"GA4 refused …" a report**

GA4 will not report those dimensions and metrics together, or rejected a filter. Change the custom report's dimensions and metrics, try the combination in GA4 Query Explorer, and add it under a new name if the old one has synced.

**"Custom report custom_… changed …"**

A synced custom report's definition was edited. Restore the original, or add the new definition under a new name. See [Editing the Source](#editing-the-source).

**"The Google Analytics Admin API is not enabled", or an admin_* resource paused**

Enable **Google Analytics Admin API** in the key's project and save the source, or deselect the `admin_*` resources.

**The same day appears with different values, or totals look too high**

GA4 revised the day and the revision arrived with the same `id`. Set the destination to upsert on `id`, and do not sum rows from the topic. See [Restatement](#restatement).

**Numbers differ from the GA4 interface**

The interface and the Data API can apply thresholding, sampling or `(other)` rows differently, and recent days are still being revised. Compare a day older than the re-read window, with the same dimensions: user counts are not additive.

## Related Documentation

- [API sources](/api-sources) - how API sources work, Test access, sending topics to a destination, and statuses
- [Google Ads](/google-ads-source) - Google Ads objects and daily performance reports
- [Facebook Ads](/facebook-ads-source) - Meta ad objects and daily insights reports
