Connecting Gravity Forms to GA4
Gravity Forms stores the entry in your own WordPress database, which makes it one of the easier sources to join correctly. 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 Gravity Forms entry 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
Gravity Forms to GA4: read _ga in the hook, not a hidden field
Skip hidden fields for GA4. In gform_after_submission, read the _ga cookie for client_id and the measurement-specific cookie for session_id, then send generate_lead through Measurement Protocol with the form ID. Disable the Gravity Forms GA add-on or Tag Manager triggers to avoid doubles, mark only lead forms as key events, and skip spam.
For GA4 you do not need dynamic population at all, which sidesteps the caching problem. The submission is a POST from the visitor's browser, never served from cache, and it carries the visitor's cookies. Inside gform_after_submission, read the _ga cookie from the request and take the last two dot-separated parts as the client_id; read the _ga_ cookie for your measurement ID to get the session_id. Then send generate_lead through Measurement Protocol with both, plus the form ID as a parameter.
Check whether you also have Gravity Forms' own Google Analytics add-on enabled, or a Tag Manager trigger on the confirmation. Either of those plus your server event means two generate_lead events per entry, and GA4 does nothing to collapse them. Pick the server hook, because AJAX forms that show an inline confirmation and forms that redirect to another page both run the same PHP hook, while browser triggers have to be built separately for each confirmation type.
Mark generate_lead as a key event only for the forms that are leads. A site with a contact form, a newsletter signup and a job application all on Gravity Forms should pass a form name parameter, and the newsletter and careers forms should not count as key events. Skip entries Gravity Forms flags as spam before sending.
Gravity Forms → GA4 · what breaks
Why this join usually fails — Gravity Forms → GA4
- 01
The GA4 client_id never reaches Gravity Forms
The identifier must be written into a hidden field with dynamic population enabled, and that only works if the parameter is on the URL when the page renders. Page caching serves a cached render to the visitor, so the dynamic population never runs — the same caching problem that breaks WooCommerce.
- 02
Dynamic population fails silently under page caching
The field is configured correctly and still arrives empty, because the visitor was served a cached page rendered for someone else. This is the most common Gravity Forms failure and it looks like nothing is wrong.
- 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.
Gravity Forms → GA4 · the fix
What working looks like — Gravity Forms → GA4
The GA4 client_id on every entry
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
Gravity Forms → GA4 · architecture
How Gravity Forms and GA4 actually connect
- 1
Gravity Forms holds the truth
The entry lives in your database with every field value attached. You can query it, back it up and reconcile against it without depending on any third party, which is a real advantage over hosted form tools.
- 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 Gravity Forms entry.
- 4
The event source decides reliability
Fire server-side from the PHP hook. This runs regardless of caching, AJAX submission, or what the confirmation page does.
- 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.
Gravity Forms → GA4 · normal or broken
Working vs. broken, side by side — Gravity Forms → GA4
Normal, not a fault
The GA4 client_id present on the entry
Sent from the gform_after_submission hook
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
The identifier must be written into a hidden field with dynamic population enabled, and that only works if the parameter is on the URL when the page renders. Page caching serves a cached render to the visitor, so the dynamic population never runs — the same caching problem that breaks WooCommerce.
Sent from the browser
The field is configured correctly and still arrives empty, because the visitor was served a cached page rendered for someone else. This is the most common Gravity Forms failure and it looks like nothing is wrong.
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
Gravity Forms → GA4 · diagnose
Diagnostic order — Gravity Forms → GA4
- 01
Count entrys carrying the GA4 client_id
- 02
Follow one real conversion end to end
- 03
Submit a test entry through a cached page
Not through a logged-in admin session, which bypasses the cache. If the hidden field is empty on the cached path and populated on the admin path, caching is your cause.
- 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.
Gravity Forms → GA4 · prove
How it gets proven — Gravity Forms → GA4
Source of truth
The Gravity Forms entries list for the same window, or the wp_gf_entry table directly.
- The GA4 client_id confirmed present on a live entry.
- Server events confirmed in DebugView attaching to an existing session with the original traffic source intact.
- Entry meta confirmed populated on a submission made from a fully cached page as an anonymous visitor.
Gravity Forms → GA4 · free audit
What we check in the free audit
- Count entrys carrying the GA4 client_id
- Follow one real conversion end to end
- Submit a test entry through a cached page
- Check whether server events carry a session and a source
- Read the match rate, not the success message
Checked against
The Gravity Forms entries list for the same window, or the wp_gf_entry table directly.
Free30 minutesNo obligation
Gravity Forms → GA4 · when not to buy
When you don't need this — Gravity Forms → GA4
- Measure identifier coverage yourself first — pull your last hundred entrys and count how many carry the GA4 client_id.
- If your site has no caching layer and one form, a developer can wire this in an afternoon. Worth trying before hiring anyone.
- 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.
Gravity Forms → GA4 · what we deliver
What gets built — Gravity Forms → GA4
Identifier capture into Gravity Forms
Hidden field with 'allow field to be populated dynamically' enabled and a parameter name set, plus a cookie fallback via a gform filter so the identifier survives a visitor who browsed several pages before converting.
Event source: the gform_after_submission hook
Fire server-side from the PHP hook. This runs regardless of caching, AJAX submission, or what the confirmation page does.
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
Nothing to reverse, though entries marked spam should be excluded from the send.
Written guarantee
95%+ tracking accuracy
Measured against The Gravity Forms entries list for the same window, or the wp_gf_entry table directly.. Miss it and we keep working at no cost.
From $400 · See packages
FAQ
Gravity Forms → GA4 questions, answered
Read the _ga cookie from the request inside the gform_after_submission hook; the submission POST carries the visitor's cookies even when the page itself was cached. The client_id is the last two numeric parts of the cookie value.
Usually a browser event (the add-on or a Tag Manager trigger) and a server event are both sending generate_lead. GA4 does not deduplicate Measurement Protocol, so choose one sender.
Almost always because the GA4 client_id never reached the Gravity Forms entry. The identifier must be written into a hidden field with dynamic population enabled, and that only works if the parameter is on the URL when the page renders. Page caching serves a cached render to the visitor, so the dynamic population never runs — the same caching problem that breaks WooCommerce. 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 gform_after_submission hook. Fire server-side from the PHP hook. This runs regardless of caching, AJAX submission, or what the confirmation page does.
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 Gravity Forms → GA4
- Gravity Forms + GoogleGoogle Ads and GA4 tracking for Gravity Forms
- Gravity Forms → Google AdsConnecting Gravity Forms to Google Ads
- Gravity Forms → MetaConnecting Gravity Forms to Meta Conversions API
RELATED GUIDES
Where Gravity Forms → GA4 fits in the rest of your tracking
- Formidable Forms → GA4Connecting Formidable Forms to GA4
- Paperform → GA4Connecting Paperform to GA4
- Contact Form 7 → GA4Connecting Contact Form 7 to GA4
- Gravity Forms → MixpanelConnecting Gravity Forms to Mixpanel
- Gravity Forms → PostHogConnecting Gravity Forms to PostHog
- Local servicesConversion tracking for local businesses, where most conversions bypass the website
- Fluent Forms → GA4Connecting Fluent Forms to GA4
- Ninja Forms → GA4Connecting Ninja Forms to GA4
Free · 30 min
Get Gravity Forms and GA4 joined properly
Free30 minutesNo obligation
