GA4 Standard

GA4 Standard

10 data marts78 fieldsView on GitHub

Devices6 fields1 relationshipEvent Traffic Sources6 fields2 relationshipsEvents7 fields2 relationshipsGeo4 fields1 relationshipPageviews7 fields2 relationshipsSessions25 fields8 relationshipsTraffic Sources6 fields1 relationshipTransactions6 fields1 relationshipUser Traffic Sources4 fields1 relationshipUsers7 fields1 relationship

The Google Analytics 4 export to BigQuery as ten Data Marts: sessions at the hub, with the events, page views and purchases that join them and the traffic sources, devices, locations and users behind them.

Overview

A ready-made reporting layer over the Google Analytics 4 export to BigQuery. It turns the raw events_* tables into ten analytics-ready tables: sessions, events, page views and purchases, plus the traffic sources, devices, locations and users they point at by hash. Every table is built from a staging table of sessions, and sessions are cut at the day boundary.

The SQL that builds the tables is part of the model, in references/sql-transformation, with a runbook for the first load and the daily run. It is written for the OWOX Transformations plugin in the BigQuery (GoogleSQL) dialect, and you can adapt it to your needs.

Sessions are the hub. Events, page views and purchases join them on date and the session key together, because the session number restarts every day. A session row is often a fragment of a GA4 session: the model breaks a visit after 30 idle minutes in the runbook's setup, at midnight, and on every event that carries a source, medium or gclid. So Sessions can hold several times more rows than GA4 counts sessions; on Google's sample, about six times. Count sessions with the sum of the sessions field, which tracks GA4's count closely, and do not compare a bounce rate built per row with GA4's. And the dimension tables carry no date: each run appends the same hashes again, so deduplicate them before joining in your own SQL.

Example Questions

  • Which sources and campaigns bring the sessions that end in a purchase?
  • How does revenue per session differ between mobile and desktop, by country?
  • On which visit do users make their first purchase?

Explore on canvas →

Devices Data Mart

Devices

The device a session ran on: category, operating system and its version, browser and its version, one row per combination. When a session's events disagree, the value from its latest event wins. Sessions point here through deviceHash, a FARM_FINGERPRINT of the five values. The table has no date, so a hash repeats once per run (once per day for daily runs). Count distinct hashes. In your own SQL over these tables, deduplicate it before joining, or rows multiply; OWOX Data Marts reports collapse joined fields per source row.

Fields

ColumnTypeAliasDescription
deviceHashSTRINGDevice HashPK. FARM_FINGERPRINT of the five device values, joined without a separator, with an empty value in place of NULL. Unique within one run; repeats across daily runs.
deviceCategorySTRINGDevice CategoryThe device category as GA4 reports it, such as desktop, mobile or tablet.
operatingSystemSTRINGOperating SystemThe operating system, such as Windows, iOS or Android.
operatingSystemInfoSTRINGOperating System VersionThe version of the operating system.
browserSTRINGBrowserThe browser, such as Chrome or Safari. Empty for app traffic.
browserVersionSTRINGBrowser VersionThe version of the browser. Empty for app traffic.

Event Traffic Sources Data Mart

Event Traffic Sources

The traffic source GA4 records on a single event or page view, from its source, medium, campaign, term and content parameters: UTM tags, or GA4's own attribution such as organic or referral. One row per combination. Events and Pageviews point here through eventTrafficSourceHash. Most hits carry none of these parameters: they share one hash, whose row has every value empty, meaning nothing was recorded on the hit, not a missing record. The table has no date, so a hash repeats once per run (once per day for daily runs). Count distinct hashes. In your own SQL over these tables, deduplicate it before joining, or rows multiply; OWOX Data Marts reports collapse joined fields per source row.

Fields

ColumnTypeAliasDescription
eventTrafficSourceHashSTRINGEvent Traffic Source HashPK. FARM_FINGERPRINT of source, medium, campaign, term and content, joined without a separator, with an empty value in place of NULL. Unique within one run; repeats across daily runs.
sourceSTRINGEvent SourceThe source parameter GA4 records on the hit: a UTM source, or GA4's own such as google. Empty when the hit carries none.
mediumSTRINGEvent MediumThe medium parameter GA4 records on the hit: a UTM medium, or GA4's own such as organic or referral. Empty when the hit carries none.
campaignSTRINGEvent CampaignThe campaign parameter on the hit. Empty when the hit carries none.
keywordSTRINGEvent KeywordThe term parameter on the hit, such as a paid keyword. Empty when the hit carries none.
adContentSTRINGEvent Ad ContentThe content parameter on the hit, such as an ad variant. Empty when the hit carries none.

Events Data Mart

Events

Every GA4 event other than a page view, one row each, tied to the session it fell in. Page views are in Pageviews. Join to Sessions on date and pseudo_session_id together, because the session number restarts every day. hitId identifies an event: a rerun of the same window gives the same value, a different window does not, and Pageviews numbers its hits separately, so one value can appear in both. time is a timestamp in microseconds. eventTrafficSourceHash points at the source GA4 records on this one event (UTM tags or GA4's own attribution); most events carry none. The table holds no event parameters beyond the event name.

Fields

ColumnTypeAliasDescription
dateDATEDatePK. The day of the event's session.
pseudo_session_idSTRINGSession KeyThe session the event belongs to: the GA4 pseudo user ID plus the session's number on that day. Join to Sessions on this and date together. FK to Sessions
hitIdSTRINGHit IDPK. MD5 of the pseudo user ID, the event timestamp and the event's order for that user. Stable for a rerun of the same window, not across different windows.
timeINTEGEREvent TimeTimestamp of the event, in microseconds since 1970-01-01 UTC.
eventsINTEGEREventsAlways 1. Sum it to count events.
event_nameSTRINGEvent NameThe GA4 event name, such as add_to_cart, scroll or purchase. Never page_view.
eventTrafficSourceHashSTRINGEvent Traffic Source HashFARM_FINGERPRINT of the source, medium, campaign, term and content GA4 records on this event. An event with none of them has the hash of empty values. FK to Event Traffic Sources

Relationships

Related data martOnCardinalityMeaning
Sessionsdate = date, pseudo_session_id = pseudo_sessionIdN:1The session the event fell in.
Event Traffic SourceseventTrafficSourceHash = eventTrafficSourceHashN:1The traffic source recorded on this one event; the hash repeats across runs.

Geo Data Mart

Geo

Where a session came from, as GA4 resolves it from the visitor's IP address: country, region and city, one row per combination. When a session's events disagree, the value from its latest event wins. Sessions point here through geoHash, a FARM_FINGERPRINT of the three values. The table has no date, so a hash repeats once per run (once per day for daily runs). Count distinct hashes. In your own SQL over these tables, deduplicate it before joining, or rows multiply; OWOX Data Marts reports collapse joined fields per source row.

Fields

ColumnTypeAliasDescription
geoHashSTRINGGeo HashPK. FARM_FINGERPRINT of country, region and city, joined without a separator, with an empty value in place of NULL. Unique within one run; repeats across daily runs.
countrySTRINGCountryThe country, by name, as GA4 reports it.
regionSTRINGRegionThe region or state within the country.
citySTRINGCityThe city.

Pageviews Data Mart

Pageviews

Every GA4 page_view event, one row each, tied to the session it fell in. Other events are in Events. The table carries no page path: the landing page is on Sessions. Join to Sessions on date and pseudo_session_id together, because the session number restarts every day. hitId identifies a page view: a rerun of the same window gives the same value, a different window does not, and Events numbers its hits separately, so one value can appear in both. time is a timestamp in microseconds. eventTrafficSourceHash points at the source GA4 records on this one page view (UTM tags or GA4's own attribution); most carry none.

Fields

ColumnTypeAliasDescription
dateDATEDatePK. The day of the page view's session.
pseudo_session_idSTRINGSession KeyThe session the page view belongs to: the GA4 pseudo user ID plus the session's number on that day. Join to Sessions on this and date together. FK to Sessions
hitIdSTRINGHit IDPK. MD5 of the pseudo user ID, the event timestamp and the page view's order for that user. Stable for a rerun of the same window, not across different windows.
timeINTEGERPage View TimeTimestamp of the page view, in microseconds since 1970-01-01 UTC.
pageviewsINTEGERPageviewsAlways 1. Sum it to count page views.
event_nameSTRINGEvent NameAlways page_view.
eventTrafficSourceHashSTRINGEvent Traffic Source HashFARM_FINGERPRINT of the source, medium, campaign, term and content GA4 records on this page view. A page view with none of them has the hash of empty values. FK to Event Traffic Sources

Relationships

Related data martOnCardinalityMeaning
Sessionsdate = date, pseudo_session_id = pseudo_sessionIdN:1The session the page view fell in.
Event Traffic SourceseventTrafficSourceHash = eventTrafficSourceHashN:1The traffic source recorded on this one page view; the hash repeats across runs.

Sessions Data Mart

Sessions

One row per session as this model cuts it, with its landing page, engagement, purchases and keys to its source, device, location and user. A new row starts at GA4's session_start, after 30 idle minutes in the runbook's setup, at midnight, and on every event that carries a source, medium or gclid. So rows can outnumber GA4's sessions several times; on Google's sample, about six times. Count sessions with the sum of sessions, which tracks GA4's count closely. A bounce rate built per row is not comparable to GA4's. A row is keyed by date and pseudo_sessionId. Times are in microseconds; cost, impressions and clicks are always 0. For deduplicated orders, use Transactions. Events without a user_pseudo_id are lost.

Fields

ColumnTypeAliasDescription
dateDATEDatePK. The day of the session, from the export table it was read from. A visit that crosses midnight is two sessions.
pseudo_sessionIdSTRINGSession KeyPK. The GA4 pseudo user ID plus the session's number for that user on that day. The number restarts every day, so pair it with date. Events, page views and purchases carry it as pseudo_session_id.
sessionIdSTRINGGA4 Session Start IDThe GA4 pseudo user ID plus the timestamp of the session_start event that opened the session. Empty for a session the model split off mid-visit, after a pause or on an event that carries a source, medium or gclid.
landingPageSTRINGLanding PagePath of the page the session started on, without host or query string. Empty when the session has no session_start event.
landingHostSTRINGLanding HostHost name of the page the session started on. Empty when the session has no session_start event.
touchpointSTRINGPlatformThe GA4 platform the session's events came from: WEB, IOS or ANDROID.
dataSourceSTRINGData SourceThe GA4 export the session was built from, as project.dataset.events_.
visitStartTimeINTEGERSession Start TimeTimestamp of the session's first event, in microseconds since 1970-01-01 UTC.
visitNumberINTEGERSession NumberThe user's visit count as GA4 reports it (ga_session_number): 1 for a first visit.
sessionsINTEGERSessions1 when the row opens with GA4's session_start event, 0 for a row the model split off mid-visit. Count sessions with its sum, which tracks GA4's session count closely; the row count can be several times higher.
hitsINTEGERHitsNumber of events in the session, page views included.
pageviewsINTEGERPageviewsNumber of page_view events in the session.
eventsINTEGEREventsNumber of events other than page_view in the session.
transactionsINTEGERPurchasesDistinct purchase IDs in the session. Not deduplicated across sessions, so the total can exceed the row count of Transactions.
transactionRevenueFLOATPurchase RevenueRevenue of the session's purchase events, in the currency the site reported. Not deduplicated across sessions.
bouncesINTEGERBounces1 when the row has no user_engagement event, otherwise 0. Set per row, and on Google's sample most rows hold a single event, so a bounce rate built on it is not comparable to GA4's.
timeOnSiteINTEGERTime on Site (microseconds)Time from the session's first event to its last, in microseconds; divide by 1,000,000 for seconds. 0 for a session with one event.
costFLOATCostAlways 0. A slot for ad cost blended in later.
impressionsINTEGERImpressionsAlways 0. A slot for ad impressions blended in later.
clicksINTEGERClicksAlways 0. A slot for ad clicks blended in later.
userHashSTRINGUser HashFARM_FINGERPRINT of the user's identifiers, today the GA4 pseudo user ID alone. FK to Users
userTrafficSourceHashSTRINGUser Traffic Source HashFARM_FINGERPRINT of the source, medium and campaign the user was first acquired by. Matches trafficSourceHash in User Traffic Sources. FK to User Traffic Sources
trafficSourceHashSTRINGTraffic Source HashFARM_FINGERPRINT of the session's source, medium, campaign, keyword and ad content. FK to Traffic Sources
deviceHashSTRINGDevice HashFARM_FINGERPRINT of the session's device category, operating system and version, browser and version. FK to Devices
geoHashSTRINGGeo HashFARM_FINGERPRINT of the session's country, region and city. FK to Geo

Relationships

Related data martOnCardinalityMeaning
Traffic SourcestrafficSourceHash = trafficSourceHashN:1The session's own source and campaign; the hash repeats across runs.
User Traffic SourcesuserTrafficSourceHash = trafficSourceHashN:1How the user was first acquired; the hash repeats across runs.
DevicesdeviceHash = deviceHashN:1The device the session ran on; the hash repeats across runs.
GeogeoHash = geoHashN:1Where the session came from; the hash repeats across runs.
UsersuserHash = userHashN:1The browser or app instance behind the session; the hash repeats across runs.

Traffic Sources Data Mart

Traffic Sources

The source, medium, campaign, keyword and ad content a session is attributed to, one row per combination. They come from parameters GA4 records on events: UTM tags, or GA4's own attribution such as organic or referral. A session opened by session_start takes the first values its GA4 session carries; one split off mid-visit takes those of the event that split it. With none it reads (direct) / (none). Sessions point here through trafficSourceHash. The table has no date, so a hash repeats once per run (once per day for daily runs). Count distinct hashes. In your own SQL over these tables, deduplicate it before joining, or rows multiply; OWOX Data Marts reports collapse joined fields per source row.

Fields

ColumnTypeAliasDescription
trafficSourceHashSTRINGTraffic Source HashPK. FARM_FINGERPRINT of source, medium, campaign, keyword and ad content, joined without a separator, with an empty value in place of NULL. Unique within one run; repeats across daily runs.
sourceSTRINGSourceWhere the session came from: a UTM source or GA4's own, such as google. (direct) when the session carries none; google for a session split off mid-visit by an event with a gclid.
mediumSTRINGMediumThe kind of traffic: a UTM medium or GA4's own, such as organic or referral. (none) when the session carries none; cpc for a session split off mid-visit by an event with a gclid.
campaignSTRINGCampaignThe campaign name, from the campaign parameter. Empty when the session carries none.
keywordSTRINGKeywordThe keyword or search term, from the term parameter. Empty when the session carries none.
adContentSTRINGAd ContentThe ad variant, from the content parameter. Empty when the session carries none.

Transactions Data Mart

Transactions

One row per purchase, deduplicated by its transaction ID: when GA4 records the same order more than once within the deduplication window (7 days in the runbook's setup), only the first is kept. Revenue is in the currency the site reported. Per deduplication window, at most one purchase with the ID (not set) and one with an empty ID are kept, so their revenue is understated. Purchases on events without a user_pseudo_id are missing. status is always empty and touchpoint is always WEB; both are slots. Join to Sessions on date and pseudo_session_id for the source and device behind each order. Totals here can differ from the purchase counts on Sessions, which are not deduplicated.

Fields

ColumnTypeAliasDescription
dateDATEDatePK. The day of the purchase's session.
pseudo_session_idSTRINGSession KeyPK. The session the purchase was made in: the GA4 pseudo user ID plus the session's number on that day. Join to Sessions on this and date together. FK to Sessions
transactionIdSTRINGTransaction IDPK. The order ID the site sent with the purchase event. (not set) and empty IDs each stand for many purchases collapsed into one.
transactionRevenueFLOATRevenueRevenue of the purchase, in the currency the site reported it in.
statusSTRINGStatusAlways empty. A slot for an order status from another source.
touchpointSTRINGTouchpointAlways WEB.

Relationships

Related data martOnCardinalityMeaning
Sessionsdate = date, pseudo_session_id = pseudo_sessionIdN:1The session the purchase was made in.

User Traffic Sources Data Mart

User Traffic Sources

How each user was first acquired: the source, medium and campaign GA4 records for the user's first visit, not for any later session. Missing values read (not set). Sessions point here through userTrafficSourceHash, which matches trafficSourceHash in this table although the names differ; both are a FARM_FINGERPRINT of the three values. The table has no date, so a hash repeats once per run (once per day for daily runs). Count distinct hashes. In your own SQL over these tables, deduplicate it before joining, or rows multiply; OWOX Data Marts reports collapse joined fields per source row.

Fields

ColumnTypeAliasDescription
trafficSourceHashSTRINGUser Traffic Source HashPK. FARM_FINGERPRINT of the user's first source, medium and campaign, joined without a separator. Sessions carry it as userTrafficSourceHash. Unique within one run; repeats across daily runs.
user_sourceSTRINGFirst User SourceThe source of the user's first visit, from GA4's traffic_source.source. (not set) when GA4 has none.
user_mediumSTRINGFirst User MediumThe medium of the user's first visit, from GA4's traffic_source.medium. (not set) when GA4 has none.
user_campaignSTRINGFirst User CampaignThe campaign of the user's first visit, from GA4's traffic_source.name. (not set) when GA4 has none.

Users Data Mart

Users

The visitors behind sessions, one row per GA4 pseudo user ID: the identifier GA4 gives a browser or app instance, not a person. The same person on two devices, or after clearing cookies, is two users here. clientId, userId, visitorId, phone and email are always empty: slots for identifiers from other sources. Sessions point here through userHash, a FARM_FINGERPRINT of all six identifiers, today the pseudo user ID alone. The table has no date, so a hash repeats once per run (once per day for daily runs). Count distinct hashes. In your own SQL over these tables, deduplicate it before joining, or rows multiply; OWOX Data Marts reports collapse joined fields per source row.

Fields

ColumnTypeAliasDescription
userHashSTRINGUser HashPK. FARM_FINGERPRINT of the six identifiers below, joined without a separator, with an empty value in place of NULL. Unique within one run; repeats across daily runs.
clientIdSTRINGClient IDAlways empty. A slot for a client ID from another source.
userIdSTRINGUser IDAlways empty. A slot for your own user ID; GA4's user_id is not loaded here.
visitorIdSTRINGVisitor IDAlways empty. A slot for a visitor ID from another source.
userPseudoIdSTRINGGA4 Pseudo User IDGA4's user_pseudo_id: one browser or app instance.
phoneSTRINGPhoneAlways empty. A slot for a phone number from your CRM.
emailSTRINGEmailAlways empty. A slot for an email address from your CRM.

Apply to your project

  1. 1

    Install the Import Model plugin

    One plugin, installed once, in your own OWOX workspace.

    Get the plugin →

  2. 2

    Import this model

    Point it at this bundle and it creates every data mart above, joins and all.

    Open the model →

  3. 3

    Plug in your data and destinations

    Connect your own sources and send the results where your team already works.

    Browse connectors →

4. Optional — customize as you wish. Rename a column, drop a mart, add your own: once it is imported it is yours, and nothing here syncs back.