Connecting Concrete CMS to GA4
Concrete CMS is a self-hosted PHP CMS used by small organisations and agencies, typically with commerce added through an add-on rather than built in. GA4 is where the business goes to understand what happened, which makes an unattributed conversion there expensive in decisions rather than in ad spend. The join between them succeeds or fails on one thing: whether the GA4 client_id reaches the Concrete CMS order before the conversion happens, because GA4 can only match a conversion it can tie back to a click it recognizes.
Free30 minutesNo obligation
Specific to this pair
Concrete CMS to GA4: purchase events that survive full page cache
Concrete CMS full page caching breaks template-based GA4 purchase tags. Store the _ga client_id and session_id as order attributes when the commerce add-on creates the order, then send purchase through Measurement Protocol from the order-completed event, with the order number as transaction_id, and remove the template call. Brochure sites send generate_lead from Express entries.
On a Concrete CMS store, GA4 purchase tracking usually lives in the order confirmation template, and Concrete's full page cache is what breaks it. If that page is served from cache, the template never runs and the gtag purchase call never fires, or a cached copy fires with a previous order's values. Exclude checkout and confirmation pages from full page caching first, then decide which side owns purchase.
The better owner is the server. Read the client_id from the _ga cookie, and the session_id from the _ga_<ID> cookie, in PHP at the moment the commerce add-on creates the order, and store both as order attributes. When the add-on's order-completed event fires, send purchase through Measurement Protocol with transaction_id set to the order number, value and currency from the add-on's totals, and items built from the order lines. Then remove the template's purchase call, or GA4 counts every order twice and does nothing to stop it.
Which add-on you run decides the hook. Community Store is the common one and dispatches its own order events, but confirm the event names in your installed version, since nothing transfers between two Concrete sites unchecked. On brochure-only Concrete sites the GA4 conversion is generate_lead sent when an Express form entry is saved, not purchase. Refunds or cancellations the add-on records can go to GA4 as a refund event with the same transaction_id.
Concrete CMS → GA4 · what breaks
Why this join usually fails — Concrete CMS → GA4
- 01
The GA4 client_id never reaches Concrete CMS
Full page caching and the add-on ecosystem both interfere. Because commerce is an add-on rather than core, where the order is created differs by which add-on is installed.
- 02
Commerce as an add-on, not a core feature
Which add-on is installed determines the entire integration surface, so nothing transfers between two Concrete sites without checking first.
- 03
Everything lands under (direct)/(none)
The classic Measurement Protocol symptom, and it always means the same thing: the client_id was missing or wrong, so GA4 had no session to attach the event to and invented a new user.
- 04
Nothing reconciles afterwards
GA4 DebugView shows events arriving with their parameters, and Realtime confirms the user and session they attached to. Neither tells you whether you are double-counting — for that, compare GA4's conversion count against the source system's record for the same window.
Concrete CMS → GA4 · the fix
What working looks like — Concrete CMS → GA4
The GA4 client_id on every order
Conversions arriving in GA4
Conversions appear in GA4 attributed to the original session's source and medium, so acquisition reporting reflects what actually drove revenue.
Reconciled against the source
Concrete CMS → GA4 · architecture
How Concrete CMS and GA4 actually connect
- 1
Concrete CMS holds the truth
Self-hosted means you own and can query the data directly, which makes reconciliation straightforward once the identifier is present.
- 2
GA4 needs a click to match
GA4 needs the client_id to attach a server event to the user and session that produced the click. Without it there is no attribution to inherit and the event is orphaned.
- 3
The identifier is the bridge
The GA4 client_id, captured at the visit and carried into the Concrete CMS order.
- 4
The event source decides reliability
Fire server-side. Page caching means the confirmation template may not execute.
- 5
Deduplication at the destination
GA4 has no built-in deduplication for Measurement Protocol. If the browser also sends the event, it counts twice. The only reliable approach is to decide per event which side sends it, and enforce that decision — GA4 will not protect you from your own duplicates.
Concrete CMS → GA4 · normal or broken
Working vs. broken, side by side — Concrete CMS → GA4
Normal, not a fault
The GA4 client_id present on the order
Sent from the order-completed event from your commerce add-on
Deduplicated correctly
GA4 has no built-in deduplication for Measurement Protocol. If the browser also sends the event, it counts twice. The only reliable approach is to decide per event which side sends it, and enforce that decision — GA4 will not protect you from your own duplicates.
Reconciles with the source
Actually broken
The GA4 client_id missing on most records
Full page caching and the add-on ecosystem both interfere. Because commerce is an add-on rather than core, where the order is created differs by which add-on is installed.
Sent from the browser
Which add-on is installed determines the entire integration surface, so nothing transfers between two Concrete sites without checking first.
Counted twice, or not at all
The classic Measurement Protocol symptom, and it always means the same thing: the client_id was missing or wrong, so GA4 had no session to attach the event to and invented a new user.
Nobody has reconciled it
Concrete CMS → GA4 · diagnose
Diagnostic order — Concrete CMS → GA4
- 01
Count orders carrying the GA4 client_id
- 02
Follow one real conversion end to end
- 03
Identify which commerce add-on is in use
That decides where the order is created and what events exist to hook.
- 04
Check whether server events carry a session and a source
In GA4 Realtime or DebugView, find the event and look at its session and traffic source. A brand-new user with no source on every server event is the missing-client_id signature.
- 05
Read the match rate, not the success message
GA4 DebugView shows events arriving with their parameters, and Realtime confirms the user and session they attached to. Neither tells you whether you are double-counting — for that, compare GA4's conversion count against the source system's record for the same window.
Concrete CMS → GA4 · prove
How it gets proven — Concrete CMS → GA4
Source of truth
Your own database's order records, at agreed states.
- The GA4 client_id confirmed present on a live order.
- Server events confirmed in DebugView attaching to an existing session with the original traffic source intact.
- Order attribute confirmed populated on a purchase through a cached page.
Concrete CMS → GA4 · free audit
What we check in the free audit
- Count orders carrying the GA4 client_id
- Follow one real conversion end to end
- Identify which commerce add-on is in use
- Check whether server events carry a session and a source
- Read the match rate, not the success message
Checked against
Your own database's order records, at agreed states.
Free30 minutesNo obligation
Concrete CMS → GA4 · when not to buy
When you don't need this — Concrete CMS → GA4
- Measure identifier coverage yourself first — pull your last hundred orders and count how many carry the GA4 client_id.
- If your Concrete site is brochure-only with enquiries by form, treat it as a lead-gen site rather than a commerce one.
- GA4 is reporting, not bidding. If your problem is ad platform optimization, fix the ad platform destination first — GA4 will not change what any campaign does.
Concrete CMS → GA4 · what we deliver
What gets built — Concrete CMS → GA4
Identifier capture into Concrete CMS
Capture the identifier and persist it as order attribute data through an event listener, server-side.
Event source: the order-completed event from your commerce add-on
Fire server-side. Page caching means the confirmation template may not execute.
Dispatch to GA4
Measurement Protocol events with the client_id, session_id and the correct measurement ID and API secret, sent server-side.
Reversals where they exist
Order state changes recorded by the add-on.
Written guarantee
95%+ tracking accuracy
Measured against Your own database's order records, at agreed states.. Miss it and we keep working at no cost.
From $400 · See packages
FAQ
Concrete CMS → GA4 questions, answered
Usually the order confirmation page is served from Concrete's full page cache, so the purchase tag never runs. Exclude checkout and confirmation pages from caching, or send purchase server-side from the commerce add-on's order event.
Yes, through Measurement Protocol, as long as you read the _ga client_id when the order is created and store it on the order. Without it, the purchase lands under (direct)/(none).
Almost always because the GA4 client_id never reached the Concrete CMS order. Full page caching and the add-on ecosystem both interfere. Because commerce is an add-on rather than core, where the order is created differs by which add-on is installed. Measure coverage before debugging the dispatch.
The client_id, from the _ga cookie. Measurement Protocol events without it are rejected or create a brand-new user with no session and no source, which is how server events end up filed under (direct)/(none) and make paid channels look worse than they are. Send session_id alongside it so the event joins the existing session, and include engagement_time_msec or the event may not appear as expected in reports.
The order-completed event from your commerce add-on. Fire server-side. Page caching means the confirmation template may not execute.
GA4 DebugView shows events arriving with their parameters, and Realtime confirms the user and session they attached to. Neither tells you whether you are double-counting — for that, compare GA4's conversion count against the source system's record for the same window.
More on Concrete CMS → GA4
- Craft → GA4Connecting Craft CMS to GA4
- Concrete CMS → Google AdsConnecting Concrete CMS to Google Ads
- Concrete CMS → MetaConnecting Concrete CMS to Meta Conversions API
RELATED GUIDES
Where Concrete CMS → GA4 fits in the rest of your tracking
- AEM → GA4Connecting Adobe Experience Manager to GA4
- Craft → GA4Connecting Craft CMS to GA4
- Sanity → GA4Connecting Sanity to GA4
- Concrete CMS → PostHogConnecting Concrete CMS to PostHog
- Concrete CMS → MatomoConnecting Concrete CMS to Matomo
- Local servicesConversion tracking for local businesses, where most conversions bypass the website
- Drupal Commerce → GA4Connecting Drupal Commerce to GA4
- Ghost → GA4Connecting Ghost memberships to GA4
Free · 30 min
Get Concrete CMS and GA4 joined properly
Free30 minutesNo obligation
