← Guides·BigQuery Data Agents

BigQuery Data Agents

Inside the GA4 BigQuery export: schema and traps

How to turn on the GA4 BigQuery export, how its event tables are built, and the traps that make an agent’s answer differ from what GA4 reports.

David PombarSwiss army knife at Trackingplan
7 min read · 1581 words

Your data agent will read the GA4 export, so its answers will be exactly as good as that export and your understanding of it. The export is not a copy of the GA4 reports. It is the raw list of events, one row each, with the parameters nested inside. Most of the wrong answers an agent gives on GA4 data come from treating that table as if it were the report.

This guide covers how to turn the export on, how its tables are built, and the six traps that make an agent’s number differ from what GA4 shows. You will need all of it to build the views in guide 4.

Long shelves of identical archive boxes in a storage room
The export keeps everything, one box per day. Finding the answer is your job, or your agent’s. Image: Samuel Zeller, Wikimedia Commons, CC0

Goal of this guide: know what is in the GA4 export, and what is not. You need: the SQL from guide 2. To link your own property, Editor access to it and Owner on a Google Cloud project.

Turn on the export

In GA4, go to Admin > Product links > BigQuery links > Link. Choose the Google Cloud project, the location of the data, which data streams and events to include, and the export type. Google’s setup page walks through each screen.

There are three export types:

The export starts from the day you link. It does not backfill history, so link it as early as you can, even before you need it.

How the tables are built

The export creates a dataset named analytics_<property_id>. Inside, you get one table per day:

There is also an optional user-data export, with tables keyed by pseudonymous ID and by user ID. The rest of this series works on the event tables.

Diagram of one GA4 export row: flat columns such as event_date, event_timestamp, event_name, user_pseudo_id, device, and geo, plus nested records event_params with keys and typed values, ecommerce, and a repeated items list.
One row is one event. The flat columns are easy; the nested ones are where agents go wrong. Illustrative values.

The columns an agent uses most, from Google’s export schema:

ColumnWhat it holds
event_dateThe date, as a YYYYMMDD string, in the property’s time zone
event_timestampWhen the event happened, in microseconds, in UTC
event_namepage_view, purchase, and the rest
event_paramsA list of key and value pairs; each value sits in string_value, int_value, or double_value
user_pseudo_idThe pseudonymous ID of the browser or app instance
user_idYour own user ID, if you send one
device, geoDevice category, browser, country, city
traffic_sourceThe source that first acquired the user, which never changes afterwards
collected_traffic_sourceThe UTMs and click IDs collected with this event
session_traffic_source_last_clickLast-click source of the session
ecommerce, itemsTransaction totals, and the list of products in the event

Notice that event_date and event_timestamp use different time zones. An agent that filters on one and groups by the other can move events across midnight.

The six traps

1. There are no sessions, only events

The export has no session table and no session column. A session is a unique pair of user_pseudo_id and the ga_session_id parameter, which GA4 sets on every event of the session. Sessions are not reset at midnight, so counting them day by day and adding the days counts the same session twice. Google’s bridge-the-gap post explains the method.

2. traffic_source is the user’s first source, not the session’s

This is the trap that catches agents most often. Asked for “revenue by channel”, an agent reaches for traffic_source.medium, which describes how the user was first acquired, possibly months ago. For the source of the session, use session_traffic_source_last_click or the parameters in collected_traffic_source. traffic_source is also empty in the intraday tables.

3. Values live in different columns

A parameter value is in string_value, int_value, or double_value depending on what was sent. ga_session_id is an integer, page_location a string, value usually a double. If the tracking sends a number as text one week and as a number the next, the same parameter moves column, and a query that reads only one of them loses half the data.

4. (not set), empty, and NULL

Missing values arrive as NULL, empty strings, or, in the GA4 interface, as (not set). In the public sample, Google also replaced some values with <Other>. An agent that groups by a column with many missing values returns a large unlabeled bucket and presents it as a category. Our post on (not set) in GA4 covers where those values come from.

5. The export will not match the interface

Expect differences, and know why. The GA4 interface approximates users and sessions (HyperLogLog++), hides rows under thresholds, adds modeled data for visitors who declined consent, and uses Google signals. The export counts exactly and includes none of those: modeled data and Google signals are not exported. Daily tables can also be updated for up to 72 hours after the day ends. An agent’s number that is a few percent off the report is often correct.

6. Attribution can’t be rebuilt exactly

GA4’s attribution models run on Google’s side and can’t be reproduced from the export. You can compute last-click by session, first-click by user, or your own rules. You can’t get “the same as the Traffic acquisition report” to the decimal. Tell your agent which one you mean; guide 7 shows how.

The export inherits every tracking bug

There is one more trap, and no query fixes it. The export holds what GA4 received. If a release dropped transaction_id from the purchase event on Tuesday, or the consent banner started blocking GA4 on one market, the export has the gap too, silently. Every agent you build on it will compute a conversion rate from that gap with full confidence.

That is the part of the data quality problem that has to be solved upstream, on the hits the site sends, before they reach any table. It is what Trackingplan does: it monitors the events, properties, and destinations your site and app send, on real traffic, and alerts when one breaks. Guide 12 covers how to fold that into an agent in production.

Practice on the public sample

The public dataset bigquery-public-data.ga4_obfuscated_sample_ecommerce holds the export of the Google Merchandise Store from November 1, 2020, to January 31, 2021. It is obfuscated, with placeholder values, and Google warns that its internal consistency is limited, so use it to learn the structure, not to draw conclusions.

Run this to count sessions properly over December:

SELECT
  COUNT(DISTINCT CONCAT(user_pseudo_id, '-',
    CAST((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS STRING)
  )) AS sessions
FROM `bigquery-public-data.ga4_obfuscated_sample_ecommerce.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20201201' AND '20201231';

Then compare it with the sum of the same count run day by day. The second number is higher, and trap 1 is the reason. Google’s own basic queries and advanced queries pages are a good next step.

How to know you got it right

Resources

FAQ

How do I export GA4 data to BigQuery? In GA4, go to Admin, Product links, BigQuery links, and click Link. Choose the Google Cloud project, the data location, the streams and events to include, and the export type: daily, streaming, or both. You need Editor access to the property and Owner on the project.

Why don’t my BigQuery numbers match the GA4 interface? Because they are not computed the same way. The interface approximates users and sessions, applies thresholds, and includes modeled data and Google signals; the export holds the raw events, counted exactly, with none of that. Daily tables can also change for up to 72 hours. Small differences are normal.

How do I count sessions in the GA4 BigQuery export? There is no session table. A session is a unique pair of user_pseudo_id and the ga_session_id event parameter. Count distinct pairs over the whole period, not day by day, because a session that crosses midnight appears in two daily tables.

Is there a limit on the GA4 BigQuery export? Standard GA4 properties can export up to 1 million events a day with the daily export. The streaming export has no event limit. Analytics 360 properties have higher limits and an extra Fresh Daily option.

Next: Build the layer an agent can read.

David PombarSwiss army knife at Trackingplan

Read more from David, a Senior Product Strategist with 18+ years in digital product development and an atypical error detection knack.

Trackingplan

See everything. Miss nothing.

Your implementations audited around the clock with real-time, real-user data. Real-time alerts about errors or changes in your data, campaigns, pixels, privacy and consent. Let AI flag issues before they cost you.