Common Mistakes
These mistakes appear repeatedly across GA4 implementations. Some produce incorrect data silently; others produce obvious symptoms but unclear causes. Knowing them prevents hours of debugging.
1. Data retention left at 2 months
Section titled “1. Data retention left at 2 months”What happens: Events older than 2 months disappear from Explorations. You cannot compare this month to the same month last year in an Exploration. Custom segments, funnels, and cohort analysis are limited to 2 months of history.
Why it happens: The 2-month default seems intentional but is almost certainly wrong for any real analytics use case.
Fix: Admin → Data Settings → Data Retention → Set to 14 months. Do this the day you create the property. It cannot recover already-deleted data.
2. No BigQuery export
Section titled “2. No BigQuery export”What happens: You cannot do unsampled analysis, access raw event data, or query historical data beyond GA4’s retention window. Data that was never exported cannot be recovered.
Why it happens: BigQuery requires a GCP project setup that many GA4 users do not have initially.
Fix: Link BigQuery from day one. Even if you do not use it immediately, the export costs essentially nothing for most properties and the raw data cannot be reconstructed retroactively.
3. Double-firing purchase events
Section titled “3. Double-firing purchase events”What happens: Revenue appears doubled in GA4 reports. Key event count is 2x actual transactions.
Why it happens: GA4 deduplicates purchase events based on transaction_id — if two purchase events share the same transaction_id, GA4 counts the revenue only once. So the real double-counting happens when:
transaction_idis missing from one or both events (GA4 treats each as unique)- The
transaction_idvalues don’t match between client-side and server-side (e.g., the client sends#12345while the webhook sendsorder_12345) - One event has
transaction_idas a string and the other as a number, causing a type mismatch
Diagnose — find purchases without transaction_id (the real culprit):
SELECT event_name, (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') AS transaction_id, COUNT(*) AS countFROM `project.analytics_PROPERTY_ID.events_*`WHERE event_name = 'purchase' AND _TABLE_SUFFIX BETWEEN '20240101' AND '20240131'GROUP BY event_name, transaction_idHAVING COUNT(*) > 1 OR transaction_id IS NULLORDER BY count DESCFix: Ensure every purchase event includes a consistent transaction_id. If you fire from both client-side and server-side (which is fine for reliability), the transaction_id must match exactly — GA4’s built-in deduplication handles the rest. If you cannot guarantee matching IDs, choose one source. For most e-commerce setups, server-side (Measurement Protocol triggered by payment confirmation) is more reliable.
4. Missing cross-domain tracking
Section titled “4. Missing cross-domain tracking”What happens: Traffic from shop.example.com to checkout.example.com appears as direct. Attribution is broken — paid campaigns that lead to key events show as direct key events. Session counts are inflated.
Why it happens: GA4 treats each domain as a separate origin by default. The _ga cookie does not transfer across domains without configuration.
Fix: Admin → Data Streams → Configure tag settings → Configure your domains. Add all your domains to the list.
5. Enhanced Measurement scroll tracking at 90% only
Section titled “5. Enhanced Measurement scroll tracking at 90% only”What happens: You have scroll depth tracking showing only one value (90%). You cannot determine where users stop reading — only whether they reached the very bottom.
Why it happens: Enhanced Measurement scroll fires at 90% only. This looks like comprehensive scroll tracking but is actually a single threshold.
Fix: Implement a custom scroll depth trigger in GTM that fires at 25%, 50%, 75%, and 90%. GTM has a built-in Scroll Depth trigger that does this.
6. Custom dimensions not registered before data collection
Section titled “6. Custom dimensions not registered before data collection”What happens: Events fire with parameters, but those parameters are invisible in GA4 reports. Stakeholders cannot filter or segment by the values.
Why it happens: GA4 does not backfill custom dimension data. Parameters are collected in BigQuery but not available in the GA4 UI until you register the dimension, and only from the registration date forward.
Fix: Register all custom dimensions at the start of implementation, before going live. Review planned parameters against the 50 event-scoped dimension limit.
7. Leaving debug_mode active in production
Section titled “7. Leaving debug_mode active in production”What happens: All users’ events appear in your DebugView. Developer Traffic filters may exclude real user data.
Why it happens: The gtm_debug URL parameter is added automatically by Google Tag Assistant when you click Preview in GTM — it is not something you manually add to URLs. However, if a developer hardcodes debug_mode: true in the gtag config for testing and deploys it to production, or if a GTM Preview session URL (which contains a gtm_debug token) gets shared or bookmarked, debug mode can inadvertently activate for production users.
Fix: Never deploy debug_mode: true in your gtag config to production. Use GTM Preview mode for testing (it activates debug mode only for your session). If Developer Traffic filters are active, events with debug_mode set will be excluded from reports — so leaving debug mode on in production means losing real user data from filtered reports.
8. Wrong attribution model comparison
Section titled “8. Wrong attribution model comparison”What happens: “I set up data-driven attribution but my Traffic acquisition report looks the same.”
Why it happens: The attribution model setting (data-driven vs. last click) affects how key event credit is distributed — and this does show up in Traffic acquisition reports, not only in the Advertising section. What does NOT change is the dimensions (source, medium, campaign) in Traffic acquisition, because those are always session-scoped — they reflect what started the session.
So the report’s rows (sources) stay the same, but the key event and revenue columns should shift depending on the model. If they look identical, the likely cause is that the property doesn’t have enough key event volume for the data-driven model to diverge meaningfully from last click.
Fix: Compare the key event columns in Traffic acquisition between data-driven and last click (you can switch models in Admin → Attribution Settings). For small properties with limited key event volume, the two models often produce near-identical results. The Advertising section and Explore reports with event-scoped dimensions give the most visible difference.
9. Internal traffic not filtered before launch
Section titled “9. Internal traffic not filtered before launch”What happens: Team members browsing the site, QA testing, and developer debugging inflate session counts and skew engagement metrics during the testing phase. This data cannot be removed retroactively.
Why it happens: Data filters are set up after launch when the contamination has already occurred.
Fix: Create internal traffic definition and activate the data filter before publishing the tracking to production.
10. SPA not firing page_view on route changes
Section titled “10. SPA not firing page_view on route changes”What happens: GA4 shows only one page_view per session in a React/Vue/Angular app. All navigation within the SPA is invisible. Engagement time and bounce rate calculations are wrong.
Why it happens: SPAs use client-side routing — the URL changes but no HTTP request is made, so the GA4 tag does not re-fire.
Fix: Listen to your router’s navigation events and fire page_view on each route change, or use GTM’s History Change trigger.
11. Consent mode blocking without fallback
Section titled “11. Consent mode blocking without fallback”What happens: In EU markets, GA4 shows 30-60% fewer sessions than other analytics tools. Conversion rates appear much higher than reality.
Why it happens: Consent mode is implemented and users decline analytics cookies, but no GA4 modeling is active to estimate the missing data.
Fix: Ensure Consent Mode v2 is implemented correctly. GA4’s modeling for consent-denied users requires proper Consent Mode signals. Review your consent implementation against Google’s Consent Mode documentation.
12. Sending PII in event parameters
Section titled “12. Sending PII in event parameters”What happens: Personally identifiable information (email, name, phone) is stored in GA4 and BigQuery, violating GA4’s Terms of Service and creating GDPR/CCPA liability.
Why it happens: Parameters are set on login events or user properties without considering what data is included.
Common culprits:
// ❌ Never do thisgtag('event', 'login', { email: user.email, // PII name: user.displayName // PII});
// ✅ Use pseudonymous identifiers onlygtag('event', 'login', { user_id: user.hashedUserId, // pseudonymous auth_method: 'email'});Fix: Audit all event parameters and user properties for PII. Remove any identifiable data immediately and document a review process for future implementation.
13. Audiences created too late
Section titled “13. Audiences created too late”What happens: You need to analyze “purchasers in Q1” as an audience, but you created the audience in Q2. The audience only contains Q2 purchasers.
Why it happens: Audiences do not backfill. They start collecting users from the creation date.
Fix: Create audiences for every important segment at implementation time. Common audiences to create immediately: All purchasers, Trial users, Free tier users, Engaged readers, Cart abandoners.
14. Ecommerce items not in the items array
Section titled “14. Ecommerce items not in the items array”What happens: The Items report is empty. Product-level revenue attribution is missing. GA4 ecommerce reports show transactions but no item details.
Why it happens: Items are sent as flat event parameters (product_name, product_price) rather than in the items array.
// ❌ Items as flat parameters — not visible in item reportsgtag('event', 'purchase', { transaction_id: 'T-001', product_name: 'Widget Pro', // wrong product_price: 49.99 // wrong});
// ✅ Items in the items arraygtag('event', 'purchase', { transaction_id: 'T-001', value: 49.99, currency: 'USD', items: [{ item_id: 'SKU-001', item_name: 'Widget Pro', price: 49.99, quantity: 1 }]});15. Querying BigQuery without _TABLE_SUFFIX filter
Section titled “15. Querying BigQuery without _TABLE_SUFFIX filter”What happens: BigQuery queries are slow and expensive because they scan your entire event history.
Why it happens: The wildcard events_* without date constraints scans all tables.
Fix: Always include WHERE _TABLE_SUFFIX BETWEEN '20240101' AND '20240131' in every query.