Published by Terminus, the marketing taxonomy governance platform.

Why Is My UTM Data Wrong in GA4? A Complete 2026 Diagnostic Flowchart

PC

Puru Choudhary

Last updated

UTM data shows up wrong in GA4 when the values reaching session_start do not match what you set in the ad platform. The cause is almost always upstream of GA4: a parameter miscased on the ad, a redirect that rewrote the query string, an SPA router that stripped it, an ESP that overwrote it, consent that denied the hit, or a channel grouping rule that re-bucketed an honest source. This is the broader companion to the paid-as-direct diagnostic, which covers paid landing in (direct) / (none) specifically. This piece covers every other way UTM data can be wrong: wrong source, wrong medium, wrong campaign, (not set), duplicate sessions, or a session that should have been Email but got classed as Referral.

TL;DR

  • GA4 reads UTM parameters once, at session start. Whatever it reads becomes canonical for the rest of the session.
  • Five-minute triage: open the live link in incognito, confirm DebugView shows the parameters you expect on session_start, then cross-check Default Channel Group rules.
  • Across published GA4 diagnostics, the top two causes are casing drift (Facebook vs facebook vs FB) and redirect chains that drop query strings.
  • Channel grouping is rule-based, top-down. GA4’s built-in Default Channel Group rules match case-insensitively, so capitalised mediums still resolve, but a custom rule with a case-sensitive operator, or a non-matching value like social where a rule expects paid_social, drops the session into Unassigned.
  • (not set) is not Direct. It means GA4 received the event but could not resolve the dimension. Causes include cardinality limits, late-arriving cost data, dimensions firing before value-set, and consent mode v2 modelling.
  • Server-side fixes hold up because they normalise data before GA4 ever sees it. The only durable place to enforce a taxonomy is upstream of the URL.

The five-minute triage

Run these three checks before reading the full causes list. They tell you whether the problem lives in the link, in the page, or in GA4 configuration.

  1. Open the live link in incognito with network recording on. Copy the destination URL from the ad platform or ESP. Open in fresh incognito with DevTools network tab recording. Trace every redirect hop, then settle on the final URL.
    • UTM parameters missing on the final URL: a redirect dropped them. See causes 2, 7, 8.
    • UTM parameters present but with wrong values (Facebook instead of facebook, CPC instead of cpc): a casing or naming bug at the link source. See causes 1, 4, 11.
    • UTM parameters present but the page strips them within 200ms: an SPA router is rewriting the URL after first paint. See cause 5.
  2. Open GA4 DebugView and click the same link. Admin > Data display > DebugView. Look at session_start parameters: source, medium, campaign, term, content.
    • DebugView shows expected parameters, reports show different values or Unassigned: data is correct at collection; the problem is in channel grouping. See cause 3.
    • DebugView shows different values from the URL: a GTM variable mapping, a consent-modelled artefact, or a redirect is rewriting the payload. See causes 9, 13.
    • DebugView shows no session_start: the hit is being blocked by consent, an ad blocker, or an in-app browser. See causes 9, 12.
  3. Cross-check the Default Channel Group rules. Admin > Data display > Channel groups. The built-in rules match case-insensitively, so Social still resolves. What does land in Unassigned is a non-matching value (a paid campaign tagged utm_medium=social when the Paid Social rule wants paid_social), or a custom rule using a case-sensitive operator.

If all three checks pass and the data still looks wrong, read on.

How GA4 reads a UTM at session start

GA4 treats every new session as a fresh attribution event. When the first page_view fires, GA4 resolves source, medium, campaign, term, and content and writes them as session-scoped dimensions. The rest of the session inherits those values, regardless of what happens to the URL after.

The resolution order is:

  1. UTM parameters in the landing URL. Read off the first page_view. Casing preserved exactly.
  2. Click identifiers. gclid, gbraid, wbraid, dclid attribute the session when no overriding UTM is present. UTMs take precedence over click IDs in source/medium fields.
  3. Referrer header. If the previous page was on another domain, GA4 reads the referring hostname and applies referral source rules. Known search engines map to organic. Known social domains map to organic social. Everything else is referral.
  4. Cookie continuation. A _ga cookie stitches the new session to a prior client ID. The session still gets its own source/medium; last non-direct logic uses cookie history only when computing conversion credit.
  5. Default. (direct) / (none) if nothing above resolved.

Three properties of this flow matter when diagnosing. First, GA4 reads the URL once. If an SPA changes the URL after the first page_view, the change is invisible to attribution. Second, casing is preserved exactly in reports and dimension values: utm_medium=CPC and utm_medium=cpc are two distinct rows in every dimension breakdown. The built-in Default Channel Group rules match case-insensitively, so both still resolve to Paid Search there, but any custom channel rule using a case-sensitive operator, or any warehouse query that groups on the raw value, will treat them as different. Third, channel grouping is a separate layer: two sessions with identical source/medium can land in different channels because rules are evaluated top-down and the first match wins.

Worth knowing.

The Default Channel Group feeds most pre-built reports; custom Channel Groups apply only in explorations and as overlay dimensions. Edits to the Default reclassify historical sessions when viewed through that group, but the underlying source, medium, and campaign values on each session are immutable once written. You can re-bucket the past via rules. You cannot rewrite the past values.

The 14 real causes ranked by frequency

These failure modes show up repeatedly across published GA4 diagnostics. Percentages are directional estimates, not measured data. The order is more reliable than the exact share.

#CauseShare of casesTypical signature
1Casing or naming drift (Facebook vs facebook)~18%Same campaign fragments across multiple source rows; warehouse joins on the raw value break
2Redirect chain strips the query string~14%UTMs present in click URL, gone by the final URL
3Channel grouping misconfiguration~12%Source/medium correct, channel bucket Unassigned or wrong
4Mis-typed parameter names (utm_source vs utmsource)~9%Sessions land in Direct despite a tagged-looking URL
5SPA hash routing or client-side URL rewrite~8%First page_view has no campaign params; URL bar clean within 200ms
6Cross-domain and hostname leakage~7%Mid-funnel hop creates Referral session with wrong source
7ESP / shortener / redirect stripping~6%Email or shortened links land with no UTMs or with ESP-injected ones
8Server-side redirect drops query string~5%Vanity URL or CMS 301 rule lacks query-preserve flag
9Consent mode v2 dropping hits~5%Specific geos show inflated Direct or (not set)
10(not set) from cardinality or late data~4%High-cardinality dimensions show (not set) for the tail
11Wrong source/medium pair (utm_medium=facebook)~4%Channel buckets behave oddly; source and medium swapped
12In-app browser referrer stripping~3%Mobile share of Direct or Referral much higher than desktop
13GTM variable mapping rewrites parameters~3%DebugView differs from URL; values come from a dataLayer push
14Long idle and tab reuse creating new sessions~2%Returning sessions on the same user show as Direct

The next sections cover the causes worth understanding in detail. The end-to-end diagnostic flow ties them together.

Case-sensitivity bugs (Facebook vs facebook vs FB)

This is the highest-frequency wrong-UTM-data cause across published GA4 diagnostics, and the one most teams underestimate because the URL “looks fine”.

You launch a Meta campaign tagged utm_source=Facebook&utm_medium=Paid Social. In Reports > Acquisition > Traffic acquisition, you see Facebook / Paid Social sitting alongside facebook / paid social from older campaigns. They look the same but split across rows. The channel still resolves: GA4’s built-in Paid Social rule matches case-insensitively, so the capitalised value buckets correctly. The real damage is elsewhere. Every report that groups on Session source fragments one campaign into multiple rows, exports to the warehouse carry the raw casing so joins keyed on the value silently miss, and any custom channel rule that uses a case-sensitive operator does drop the session into Unassigned. Over six months the drift compounds: Facebook, facebook, FB, fb, Meta, meta, Instagram, instagram, IG all coexist, splitting one campaign into nine rows.

Confirm: In Traffic acquisition, sort by Session source alphabetically and scan for near-duplicates that differ only by case. Build a quick exploration filtered by (?i)^facebook$ and compare to the sum of exact-case rows. If they match, you have casing drift.

Fix in two layers. Downstream: edit your Default Channel Group so every regex is explicitly case-insensitive and includes common variants. Upstream: define a controlled vocabulary per UTM dimension, enforce casing at link generation, and reject any value outside the dictionary. A marketing taxonomy governance platform like Terminus does this at the link-builder layer, so Facebook is auto-corrected to facebook or the save action is blocked. The downstream fix patches symptoms. The upstream fix prevents recurrence.

Channel grouping misconfiguration

Channel grouping is where correct source/medium turns into wrong channel attribution. The data is fine; the rule that interprets it is wrong.

A channel group is an ordered list of rules. GA4 walks the list top-down on each session, and the first matching rule wins. If nothing matches, the session lands in Unassigned. Four recurring misconfigurations:

Rule order is wrong. A custom Branded Search rule sits below the built-in Paid Search rule with an overlapping condition. Paid Search wins. Move the more specific rule above the more general one.

Regex anchors get dropped. A rule with matchesRegex(source, "facebook") matches anything containing facebook, including not-facebook.example.com. Use anchored patterns (^facebook$).

The Email rule fails on capitalised mediums. If a custom rule above Email uses a case-sensitive operator and your campaigns set utm_medium=Email, the session falls through to Referral or Unassigned.

Paid vs Organic confusion on social. The Paid Social rule requires medium in (cpc, paid social, paid-social, paidsocial) combined with a known social source. If a paid campaign uses utm_medium=social, the Organic Social rule matches first. Tag paid social as paid_social.

Confirm: In Traffic acquisition, set Default channel group as primary dimension and Session source/medium as secondary. Look at rows under Unassigned. If you see source/medium pairs that should obviously bucket somewhere, the channel rules are not matching them.

Fix: Restore Google’s built-in Default Channel Group definition if you have edited it and lost track. Prefer additive custom channel groups over edits to the Default; custom groups apply selectively and do not affect every pre-built report. Validate every new rule with a saved exploration before declaring it done.

Cross-domain and hostname leakage

Cross-domain misconfiguration produces wrong UTM data in two ways: it loses the campaign signal when a session crosses a domain boundary, and it injects the wrong source by treating an internal hop as a referral.

A user lands on www.example.com from a Google Ads click, correctly tagged google / cpc. They move to checkout.example.com to complete a purchase. GA4 fires a new session on checkout with source www.example.com and medium referral. The original paid attribution disappears from the session view.

Confirm: Click a link that crosses between your domains. If ?_gl=1*xxxxxx* is appended on landing, the linker is working. If not, cross-domain is off, the destination is missing from the configured list, or the click is a JavaScript navigation rather than a standard <a href>. In reports, filter Session source by your own hostnames. Many self-referred sessions mean hostname leakage.

Fix: Admin > Data streams > [your stream] > Configure tag settings > Configure your domains. Add every hostname that shares a measurement, and make sure both domains use the same measurement ID. Add your own hostnames to the unwanted referrals list. For form submissions and JavaScript navigations, use GTM event triggers to manually decorate the URL with _gl, or implement server-side session continuation via a shared first-party cookie.

SPA hash routing and client-side URL rewrite

Single-page applications are a steady source of wrong UTM data. UTMs arrive on the URL, the framework re-renders, the router rewrites the URL via history.replaceState, and the first page_view event GA4 sees has no campaign parameters.

A worse variant uses hash routing (example.com/#/landing). UTMs end up after the #, in the fragment, and GA4 cannot read URL fragments. The session lands in Direct.

Confirm: Open the live URL in incognito with network recording. The dl parameter on the GA4 collect request shows the URL GA4 saw. If dl has no UTMs but the original URL did, the SPA stripped them before the tag read them.

Fix. Three options, ordered from most to least reliable. First, capture early: add an inline script in <head> above the framework bootstrap that reads window.location.search, parses utm_* parameters, and writes them to sessionStorage and a global variable. Configure GTM (or the gtag config call) to pass those as event parameters on every event for the session. Second, fire the first page_view manually: disable the default automatic page_view, set a trigger that fires once on initial DOM ready before the router runs, and send a manual page_view with page_location set to the captured URL. Third, avoid hash routing for campaign landers: HTML5 history mode (/landing) is readable; /#/landing is not.

ESP / shortener / redirect strip

ESPs, link shorteners, and ad-network redirects are the most common reason UTMs survive the click but die before the landing.

Email sends from Marketo, HubSpot, Iterable, Klaviyo, Salesforce Marketing Cloud, and Mailchimp click through a tracker subdomain (track.example.com/click/abc123) and then redirect. If the ESP redirect drops or replaces the query string, the original UTMs do not survive. A second variant: the ESP injects its own UTMs that overwrite yours. Marketo’s mkt_tok, Mailchimp’s mc_cid and mc_eid, and HubSpot’s _hsenc and _hsmi are the recognisable signatures. Some ESPs append (harmless), some replace (fatal). Link shorteners (Bitly, Rebrandly, Branch, in-house shorteners) have the same risk: workspace defaults can overwrite your UTMs.

Confirm: Send a test email or short link with known UTMs. Click in incognito and watch every hop. A 301 or 302 with a Location header that drops ?utm_ or replaces it is the leak.

Fix (ESP-specific). Marketo: System > Email Defaults > Click Tracking. Enable “Append parameters to destination URL”. HubSpot: Marketing > Email > Settings > Tracking. Confirm tracking preserves query parameters. Klaviyo: Account > Settings > Email > UTM Tracking. Disable automatic UTM appending if you tag manually. Bitly: workspace settings > Default UTM Parameters. Set to off. For custom shorteners, forward the entire query string in the redirect handler, not just the path.

Consent Mode v2 became mandatory for personalised advertising in the EEA, UK, and Switzerland in March 2024. Misconfigured, it produces a specific signature of wrong UTM data: high (direct), high (not set), and inflated modelled conversions that do not reconcile with paid-platform reports. Direct share spikes in EEA, UK, and Swiss traffic but not in US or APAC. Some sessions land with source (not set) rather than (direct) / (none), which is the consent-modelled signature.

Confirm: Open the consent banner in an EU IP context (or VPN). Three checks. Are GA4 collect requests going out before consent is granted? Basic Consent Mode fires nothing until consent; advanced fires cookieless pings carrying only modelling-grade signal. Are all four required signals configured (ad_storage, analytics_storage, ad_user_data, ad_personalization)? Verify via GTM Preview. Is the Conversion Linker tag attached to an Initialization-All Pages trigger? If it fires after user interaction, gclid and dclid are not captured into the consent-aware first-party cookie.

Fix: Configure all four signals, default to denied before any tag fires, update to granted on consent. Use the official GTM template for your CMP (Cookiebot, OneTrust, Usercentrics, Iubenda) rather than hand-rolling. For advanced consent mode, validate modelled conversions in the Google Ads UI. Modelling requires a minimum daily traffic threshold; below that, declined consent becomes irrecoverable.

The “(not set)” tier and what it means

(not set) is the most misunderstood value in GA4. It is not (direct) / (none), and it does not always mean tagging failed. It means GA4 received the event but could not resolve the specific dimension being queried.

For source or medium. Rarer than you would think, because core source and medium populate at session start regardless of later events. The genuine causes are: late-arriving Google Ads cost data (the hit reported gclid but the next cost import has not run); and cardinality limits on high-dimensionality reports (rows beyond the cap collapse to (other) or (not set)). Note one thing this is not: a custom session-scoped dimension whose value is set after session_start shows (not set) for that custom dimension only. It does not push the built-in source/medium to (not set).

For landing page. The most common (not set) complaint. Three causes: session_start fired without a corresponding page_view; SPA delayed-page_view where the framework loaded after session_start arrived; GA4 internal lookup-table lag where new page URLs are not associated with sessions for several hours.

For channel. Almost always Default Channel Group misconfiguration. Source and medium are present, but no channel rule matched, so the channel field resolves to (not set) or Unassigned.

The non-fix that wastes the most time is treating (not set) as if it were the same problem as (direct) / (none). Confirm which dimension is (not set) before troubleshooting.

Diagnostic flow end-to-end

Run this when UTM data looks wrong. Each step either resolves the case or hands you off to the next.

  1. Identify the symptom precisely. Wrong source? Wrong medium? Wrong campaign? (not set)? Duplicate sessions? Misclassified channel? Different symptoms, different root causes.
  2. Open the live link in incognito with network recording. Trace every redirect hop. UTMs missing on final URL: redirect strip (causes 2, 7, 8). UTMs with wrong casing: source-side bug (causes 1, 4, 11). UTMs correct on final URL: continue.
  3. Verify GA4 received the parameters. DebugView, click the link, confirm session_start matches the URL. Match: skip to step 5. Mismatch: collection-layer rewrite (causes 9, 13). No session_start: hit blocked (causes 9, 12).
  4. Compare reports to DebugView. Build an exploration with Session source, medium, campaign, and Sessions. Source/medium correct but channel is Unassigned: channel grouping (cause 3). One channel split across rows: casing drift (cause 1). Your own hostname appears as source: cross-domain (cause 6).
  5. For SPA sites, check URL rewrite timing. Inspect the dl parameter on the first collect request. Differs from original URL: SPA stripped it (cause 5).
  6. Check consent mode if the problem is geo-specific. Inflated Direct or (not set) in EEA/UK only points at Consent Mode v2 misconfiguration (cause 9).
  7. For email campaigns, send a test and trace. ESPs are the most common UTM-mangler for owned channels (cause 7).
  8. For cross-domain funnels, walk the funnel. Track the _gl linker parameter on every hop. Missing linker means hostname leakage (cause 6).
  9. Quantify and prevent. Compare ad-platform clicks or send-side counts to GA4 sessions for the same period, then move the fix upstream so it does not recur.

Server-side fixes that hold up

Server-side fixes work because they normalise data before GA4 ever sees it. Once a value is wrong at session start, GA4 has no way to correct it. The only durable place to enforce a taxonomy is before the URL reaches the user’s browser. Four patterns hold up in production.

Edge enrichment at the landing page. Run a small handler at your CDN edge (Cloudflare Workers, Vercel Edge, Fastly Compute, AWS Lambda@Edge) that intercepts incoming requests, inspects the query string, normalises UTM values against a controlled vocabulary, and writes corrected values back to the URL before the page renders. Lowercase utm_source and utm_medium. Map synonyms (fb to facebook). Log unknown values for review rather than letting them through. The handler runs in single-digit milliseconds.

Server-side GTM with parameter normalisation. Add a normalisation step in the sGTM server container that lowercases incoming UTM values, maps synonyms, and forwards cleaned values to GA4. Trade-off: cleansing in sGTM does not fix the URL in the address bar, so client-side tools that read the URL directly still see the raw values.

Path-based campaign capture. For campaigns where you control the URL, encode campaign metadata in the URL path rather than the query string. Instead of example.com/landing?utm_source=google&utm_medium=cpc&utm_campaign=spring, use example.com/c/spring-google-cpc. Decode server-side, write canonical UTMs into a first-party cookie before the GA4 tag fires, and let GA4 read the cookie via a custom variable. This pattern survives iOS Link Tracking Protection, query-string-stripping redirects, and SPA URL rewrites.

Governance at link generation. The pattern that prevents the largest share of wrong UTM data is governance at link creation. A marketing taxonomy governance platform like Terminus enforces a controlled vocabulary at the moment a marketer creates a link: only allowed values per UTM dimension, casing rules applied automatically, cross-field validation (campaign-name regex; coherent source/medium pairs), and refusal to save a link that would land in (not set) or Unassigned.

Consider a paid acquisition team running campaigns across Google, Meta, LinkedIn, and TikTok. Without governance, the team produces ten variants of utm_source for each network within six months (facebook, Facebook, FB, meta, Meta, fb_ads, fbads, facebook_ads, paid_facebook, facebook-paid). With governance, every link carries one value: facebook. The downstream regex normalisation is a safety net. The upstream governance is the fix.

Three guardrails worth defining upfront:

  1. Casing. Always lowercase. Multi-word tokens use underscores (paid_social, not paid-social or paid social).
  2. Source/medium pairing. A source value implies legal medium values. google pairs with cpc, organic, display, youtube. google does not pair with email. The link builder enforces the pairing.
  3. Campaign naming convention. Campaign names match a regex like ^[a-z]{2}-[a-z0-9_]+-[0-9]{4}q[1-4]$ (geo, slug, fiscal quarter).

Together these remove causes 1, 4, and 11 from the top list. Edge enrichment and sGTM cleansing handle the rest of the URL-level failure modes. The diagnostic flow handles the long tail.

Frequently asked questions

Why is my UTM source showing as my own domain in GA4?

The session crossed a domain boundary without the cross-domain linker active. GA4 read the previous-page URL as referrer and recorded your own subdomain as source. Fix by adding the hostname to cross-domain configuration (Admin > Data streams > Configure tag settings > Configure your domains) and to the unwanted referrals list. The linker only works on standard <a href> clicks; JavaScript navigations and form submits need extra handling.

Why does GA4 show different campaign names than what I set in the URL?

Three possibilities. Casing drift, since GA4 preserves casing exactly and Spring_Promo and spring_promo are different campaigns. A redirect rewrote the query string between the click and the landing. Or an SPA stripped the parameters before the first page_view fired. Use DebugView to confirm what GA4 actually received, then trace upstream.

What is the difference between (not set) and (direct) / (none) in GA4?

(direct) / (none) means GA4 received the hit and could not find any source signal at all. (not set) means GA4 received the event but could not resolve the specific dimension being queried, often because of cardinality limits, late-arriving cost data, custom dimensions firing before value-set, or channel rules failing to match. Direct is a deliberate fallback. Not set is data missing for technical or timing reasons.

Why are my campaigns split across multiple rows in GA4 reports?

Casing drift. GA4 treats Facebook and facebook as two distinct sources, and the same applies to campaign names. The fix is governance at link generation (enforce one casing convention) and normalisation in channel grouping rules (use case-insensitive regex).

How do I check what GA4 actually received for a UTM-tagged session?

Use GA4 DebugView. Admin > Data display > DebugView. Install the GA4 debug Chrome extension or append &_dbg=1 to the test URL. Click the link in fresh incognito and watch session_start. Its parameters (source, medium, campaign, term, content) show exactly what GA4 read off the URL. Match means collection is correct and the issue is downstream. Mismatch means the issue is upstream in the link, redirect chain, or page-side script.

Can a channel grouping change fix wrong UTM data retroactively?

Edits to Default and custom channel groups apply to historical data when viewed through that group. You cannot retroactively change the underlying source, medium, or campaign values: once GA4 has written them on a session, they are immutable. You can only re-bucket sessions into different channels via grouping rules. Governance fixes the data going forward; grouping fixes the reporting view backward.

Why is GA4 showing organic social for a paid social campaign?

The utm_medium value you set does not match the Paid Social rule, but the utm_source value does match the Organic Social rule. Paid Social typically requires medium in (cpc, paid social, paid-social, paidsocial) combined with a known social source. If you tagged with utm_medium=social, the Organic Social rule matches first. Tag paid social campaigns with utm_medium=paid_social.

Yes, in two ways. Basic Consent Mode does not fire hits until consent is granted, so declined sessions are absent. Advanced Consent Mode fires cookieless pings that carry limited signal, and the modelled source/medium can land in (not set) or in a modelled fallback that differs from the actual click source. Configure all four required signals, default to denied before any tag fires, and validate consent state via GTM Preview on first hit.

How do I prevent UTM casing drift across a marketing team?

Two layers. Downstream: configure channel-grouping rules with case-insensitive regex so variants aggregate correctly. Upstream: enforce a controlled vocabulary at link generation. The downstream fix patches symptoms; the upstream governance prevents recurrence. A taxonomy governance platform makes the upstream layer practical at team scale.

Last updated: 23 July 2026.

Terminus helps you and your team be consistent in UTM tracking

Try Terminus risk-free for 21 days. Cancel anytime with 1 click.