GA4 is a fundamentally different tracking model than Universal Analytics, and most of the “GA4 implementation guides” online were written by marketers explaining the UI. This guide is about what actually matters from an implementation perspective: the data model, the constraints you will hit in production, and the decisions that determine whether your GA4 data is trustworthy.
gtag.js vs Google Tag Manager: When Each Is the Right Answer
The question is not which is better — it is which fits your architecture.
gtag.js directly is the right choice when:
- You are implementing complex custom events that require application state (data from your Redux store, authenticated user context, dynamic product data)
- Your frontend framework makes it difficult to surface data to the GTM data layer cleanly
- You are doing server-side event sending via the Measurement Protocol
- You need deterministic control over when events fire relative to your application lifecycle
Google Tag Manager is appropriate when:
- Your team wants marketers to be able to modify tracking without code deployments
- You are managing tracking across multiple tools and a tag manager reduces implementation overhead
- Your events are straightforward page-level or click-based interactions that do not require deep application state
The common mistake: choosing GTM for complex single-page applications with heavy application state, then spending weeks debugging race conditions where the data layer is not populated when the tag fires. GTM was designed for server-rendered pages where data is available at load time. In a React application that fetches data asynchronously, you are fighting the tool.
For complex implementations, a hybrid approach works: gtag.js for application events that require state access, GTM for simpler marketing tags like ad pixels and heatmaps that do not need application context.
Recommended Events vs Custom Events: Why the Distinction Matters
GA4 has a list of recommended events — predefined event names and parameters that Google has built reporting around. Using the recommended event name for a given action unlocks enhanced reporting automatically. Using a custom event name for the same action means building the analysis yourself.
The e-commerce recommended events (purchase, add_to_cart, view_item, begin_checkout) populate GA4’s monetization reports automatically. If you send a checkout_started event instead of begin_checkout, that data does not appear in the funnel report — it exists in your event stream but is not connected to the pre-built reporting.
The decision rule: if there is a recommended event that maps to what you are tracking, use it and follow the parameter spec exactly. If your use case does not map to any recommended event, create a custom event with a descriptive name using underscores. GA4 strips spaces and lowercases event names on ingestion anyway.
The naming constraints GA4 enforces:
- Event names: max 40 characters, letters, digits, underscores, must start with a letter
- Parameter names: max 40 characters, same character set
- Parameter values: max 100 characters for strings, or numeric
Working Within the 25-Parameter Limit
GA4 allows 25 parameters per event. That sounds like a lot until you are tracking a complex e-commerce event and realize you have hit the wall.
The purchase event’s recommended parameters alone — transaction_id, value, currency, tax, shipping, coupon, items — use 7 slots. Add affiliation, payment_method, user_type, and promo_code and you are at 11. Each item in the items array can have its own set of item-scoped parameters (item_id, item_name, item_category, item_brand, price, quantity), but custom item-scoped parameters require registering custom dimensions in GA4’s property configuration.
When you are approaching the parameter limit:
Register the highest-value parameters as custom dimensions in GA4 Admin. These count against a separate quota — 50 custom dimensions per property for standard properties, 125 for GA4 360. Prioritize parameters you will actually query. Sending a parameter to GA4 that you never segment by or report on wastes quota. For data that is too complex for GA4’s parameter model, use BigQuery export and analyze raw events in SQL — the export includes the full event payload without the parameter limits that apply to the reporting interface.
Implementing E-Commerce Events Correctly
The purchase event is the most scrutinized event in any implementation. Getting it wrong produces revenue discrepancies between GA4 and your actual transaction data — which creates the worst kind of meeting.
// Fire on order confirmation page, after successful payment processing
gtag('event', 'purchase', {
transaction_id: order.id,
value: order.revenueAmount,
currency: 'USD',
tax: order.taxAmount,
shipping: order.shippingAmount,
coupon: order.couponCode || '',
items: order.lineItems.map(function(item) {
return {
item_id: item.sku,
item_name: item.name,
item_category: item.category,
price: item.unitPrice,
quantity: item.quantity,
discount: item.discountAmount || 0
};
})
});
Common errors that produce incorrect revenue data:
Firing on page render instead of on confirmed payment. Users who reach the confirmation page via browser back-navigation after an incomplete purchase fire the event twice. GA4 deduplicates purchase events with identical transaction_id values within a 24-hour window, but that is a safety net, not a primary strategy.
Including tax and shipping in value. The value parameter should be revenue. Tax and shipping have their own parameters. Whether to include or exclude them is a business decision, but you must be consistent, and the definition must match how you report revenue elsewhere.
Non-unique transaction_id. Using timestamps or session IDs as transaction IDs creates collisions. Use your order database’s primary key — something that is guaranteed to be unique and stable.
Using DebugView for Validation
DebugView is GA4’s real-time event inspection tool. It shows events with full parameter details as they fire, and it is the correct way to validate tracking before it ships to production.
To enable debug mode without modifying production traffic:
gtag('config', 'G-XXXXXXXXXX', {
'debug_mode': true
});
In GTM, Preview mode appends a debug parameter to your browser session and activates DebugView automatically. Debug events do not appear in standard GA4 reports — they are quarantined in the DebugView interface so you can fire test events without inflating production metrics.
What to verify in DebugView for each event:
- Event name matches the spec exactly — no typos, correct format
- All required parameters are present and non-null
- Parameter values are the correct data type. Sending
"99.00"as a string instead of99.00as a number breaks revenue calculations silently - Events fire at the correct moment in the user flow, not early due to async data loading issues
Consent Mode Implementation
Consent Mode v2 is required for Google’s advertising measurement features to function in EEA regions. It is also the correct architectural pattern for handling analytics consent under GDPR.
// Must execute before gtag.js loads
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Set denied defaults before any consent is collected
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied'
});
// Update when user grants analytics consent
function onAnalyticsConsentGranted() {
gtag('consent', 'update', {
'analytics_storage': 'granted'
});
}
// Update when user grants advertising consent
function onAdvertisingConsentGranted() {
gtag('consent', 'update', {
'ad_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted'
});
}
With analytics_storage denied, GA4 uses cookieless pings — events are recorded without cookies or user identifiers, enabling aggregate modeling but not user-level analysis. This is significantly better than not collecting data at all for denied users.
The implementation failure to avoid: not setting consent defaults before loading gtag.js. Without explicit defaults, GA4 waits for a consent update before doing anything. Users who leave without interacting with a consent banner produce zero data instead of modeled data.
FAQ
What is the cardinality limit problem in GA4 custom dimensions?
GA4’s reporting interface is built on aggregated data, not raw event logs. High-cardinality dimensions — parameters with many unique values like order IDs, session IDs, or user IDs — cause GA4 to apply (other) bucketing, where low-frequency values get grouped together and become unqueryable. Register custom dimensions for parameters you plan to segment by in GA4’s reporting UI, and keep the value space bounded. Order IDs and user IDs belong in BigQuery exports where you can query them with SQL, not as registered dimensions for UI-based segmentation.
Should we implement GA4 server-side via the Measurement Protocol?
Yes, for critical transactional events where guaranteed delivery matters. The Measurement Protocol sends events from your server directly to GA4, bypassing browser-side ad blockers and consent restrictions. Use it for purchase confirmation events — fire both the client-side event and the server-side event, and rely on transaction_id deduplication to prevent double-counting. Server-side Measurement Protocol events require you to explicitly pass the client_id from your session data, since the server has no access to the GA4 cookie.
How does GA4 handle cross-domain tracking?
GA4 handles cross-domain tracking through automatic link decoration. In the GA4 property configuration under Data Streams, add all domains that are part of the same user journey. GA4 appends a _gl parameter to links between configured domains, which carries the session and client ID across. Without this configuration, users crossing domains appear as new sessions with direct traffic attribution, breaking funnel analysis. The most common scenario is a main marketing site and a separate checkout or subdomain — both need to be configured as part of the same measurement domain group.
Why do GA4 conversion numbers never match our CRM data?
Several reasons compound each other. Ad blockers suppress client-side events — typically 15-30% of users depending on your audience demographics. Consent denials in European regions prevent cookie-based tracking entirely. Cached pages from previous sessions may not re-fire events. Bot traffic inflates page views without generating conversions. And GA4’s session-based attribution model may assign the conversion to a different channel than your CRM’s lead source field. The correct response is to stop trying to reconcile GA4 and the CRM exactly — use GA4 for directional channel analysis and relative performance trends, and your CRM as the authoritative record for actual revenue.
What is the correct way to track single-page applications where the URL does not change on navigation?
Disable automatic page view collection in your gtag configuration with send_page_view: false, then fire explicit page_view events on application state transitions. Pass a logical page_path and page_title that represents the current screen — not the URL, which is not changing. In a multi-step wizard or tabbed interface, each step gets its own logical path like /checkout/step-1, /checkout/step-2. This gives you meaningful navigation data in GA4’s reports instead of a single page view per session. The alternative, relying on GA4’s automatic detection in a true SPA, produces useless session data with one page view regardless of user engagement depth.
