Why Is My UTM Data Wrong in GA4? The 14 Real Causes

PC

Puru Choudhary

Last updated · published

UTM data goes wrong in GA4 when the values reaching session_start are not the values you set. The cause is almost always upstream: a miscased parameter on the ad, a redirect that dropped the query string, a single-page app router that rewrote the URL, an ESP that appended its own, or a channel rule that re-bucketed an honest source.

Start with the five-minute triage below. It tells you whether the problem lives in the link, the page, or the configuration, and that alone resolves most cases.

For paid traffic specifically landing in (direct) / (none), use the paid-as-direct diagnostic. Everything else belongs here: wrong source, wrong medium, wrong campaign, (not set), and channels that make no sense.

TL;DR

  • GA4 resolves source, medium and campaign once, at session start. Whatever it reads then is canonical for the session.
  • Triage: open the live link in a private window, confirm DebugView shows the parameters you expect on session_start, then check the channel rules.
  • When a click carries both a click ID and manual UTMs, the auto-tagged values win. Your UTMs land in GA4’s Manual dimensions.
  • Casing does not break channels, because channel definitions are not case sensitive. It splits your source and medium reports instead.
  • utm_medium=social on a paid campaign lands in Organic Social, not Unassigned, so paid spend reports as organic.
  • (not set) is not Direct. GA4 received the event and could not resolve that particular dimension.

The Five-Minute Triage

Three checks, in order. Stop when one of them explains your symptom.

1. Open the live link in a private window with the network tab recording. Copy the destination URL out of the ad platform or ESP, open it, and follow every redirect to the final URL.

  • Parameters missing at the end: a redirect dropped them. Causes 2, 7, 8.
  • Parameters present with the wrong values (Facebook rather than facebook): a link-source bug. Causes 1, 4, 11.
  • Parameters present, then the address bar cleans itself a moment after load: the app router rewrote the URL. Cause 5.

2. Click the same link with DebugView open. Admin, then Data display, then DebugView. Read the source, medium and campaign parameters on session_start.

  • Matches the URL but reports disagree: the collection is fine and the problem is channel rules. Cause 3.
  • Differs from the URL: a tag variable, a consent artefact, or a redirect rewrote the payload. Causes 9, 13.
  • No session_start at all: the hit is blocked by consent, a blocker, or an in-app browser. Causes 9, 12.

3. Read the channel rules. Admin, then Data display, then Channel groups. The built-in definitions are not case sensitive, so Social resolves exactly as social does. What misfiles a session is a value that matches a different rule, such as a paid campaign tagged utm_medium=social matching Organic Social before anything paid can claim it.

If all three pass and the data still looks wrong, work down the causes.

How GA4 Resolves a Session

Every new session gets one attribution event. When the first page_view fires, GA4 resolves the traffic dimensions and writes them as session-scoped values. Everything later in the session inherits them.

The order it works through:

  1. Click identifiers. gclid, gbraid, wbraid and dclid win when present. This is the rule most teams have backwards: if a click carries both a click ID and manual UTMs, GA4 uses the auto-tagged values for source, medium and campaign, and your manual values populate the Manual dimensions instead.
  2. UTM parameters in the landing URL, read off that first page_view, casing preserved.
  3. The referrer, when the previous page was on another domain. Known search engines map to organic, known social domains to organic social, everything else to referral.
  4. Prior non-direct source. A returning visitor with no new signal inherits the source of their previous non-direct session rather than becoming Direct.
  5. (direct) / (none) when nothing above resolves.

Three consequences worth holding onto. GA4 reads the URL once, so a router that rewrites it later changes nothing. Casing is preserved in dimension values but ignored by channel definitions, so CPC and cpc are two rows in one channel. And channel grouping is a separate layer evaluated top down, first match wins, which is why two sessions with identical source and medium can land in different channels.

One clarification on channel groups, because the earlier version of this page had it wrong. You cannot edit GA4’s default channel group. What you can do is create a custom channel group, which applies retroactively and works as a primary dimension in standard reports, not only in explorations. Standard properties allow two.

The 14 Causes, in the Order Worth Checking

#CauseTypical signature
1Casing or naming driftOne campaign split across several source rows; warehouse joins miss
2Redirect chain strips the query stringParameters in the click URL, gone on landing
3Channel grouping mismatchSource and medium correct, channel wrong or Unassigned
4Mistyped parameter namesSessions land in Direct despite a tagged-looking URL
5App router rewrites the URLFirst page_view carries no campaign values
6Cross-domain leakageYour own hostname appears as the source
7ESP or shortener rewrites linksOwned-channel links land bare or with vendor parameters
8Server-side redirect drops the queryVanity URLs and CMS rules without a preserve flag
9Consent handling drops hitsOne region shows inflated Direct or (not set)
10Cardinality or late-arriving dataHigh-cardinality reports show a (not set) tail
11Source and medium swappedutm_medium=facebook, and channels behave oddly
12In-app browser strips the referrerMobile Direct much higher than desktop
13Tag variable mapping overwrites valuesDebugView disagrees with the URL
14Auto-tagging and manual UTMs togetherReports show the Google Ads campaign, not your utm_campaign

The sections below cover the ones worth understanding rather than just fixing.

Casing Drift Splits Reports, Not Channels

This is the most common cause and the most misdiagnosed, because the channel report looks healthy.

Launch a campaign tagged utm_source=Facebook&utm_medium=Paid Social and the session still files under Paid Social, since channel definitions ignore case. What breaks is everything keyed on the value itself. Traffic acquisition shows Facebook / Paid Social beside facebook / paid social from older campaigns. Warehouse joins on the raw string miss half the rows. Six months later you have Facebook, facebook, FB, fb, Meta and meta describing one campaign.

Confirm it: sort Traffic acquisition by source and scan for near-duplicates. Filter an exploration with (?i)^facebook$ and compare that total against the exact-case rows.

Fix it in one place. Lowercase everything at link creation and reject anything outside the vocabulary. Terminus, the marketing taxonomy governance platform, applies casing rules and picklists when the link is built, which is the only point where the fix is cheap. Case-insensitive channel rules are a downstream patch for data that is already inconsistent.

Channel Grouping Puts Good Data in the Wrong Bucket

The data is right, the rule reading it is wrong. Four recurring shapes:

Paid social filed as organic. GA4’s Paid Social rule wants a source on its social list and a medium matching a paid pattern (anything containing cp, or ppc, retargeting, or beginning paid). A campaign tagged utm_medium=social matches Organic Social first, so paid spend reports as organic. paid-social and paid_social both work, so pick one and stay on it.

A rule that matches too much. GA4’s regex matching is full-match by default rather than “contains”. If you deliberately write a partial pattern, .*facebook.* matches not-facebook.example.com too. Anchor deliberately.

Rule order. A specific rule below a general one never fires. Custom groups evaluate top down, first match wins.

Missing rules. Values matching nothing at all land in Unassigned, which is a different diagnosis from landing in the wrong channel.

Fix: build a custom channel group rather than trying to edit the default, which GA4 does not allow. Custom groups apply to historical data as soon as you save them, so you can validate against last month rather than waiting.

Cross-Domain Leakage

Two failure shapes: losing campaign data at a domain boundary, and inventing a referral from your own site.

A visitor lands on example.com from a paid click, then moves to examplecheckout.com. Without cross-domain configuration, GA4 starts a session on the second domain with your first domain as the referrer, and the paid attribution disappears.

Subdomains of one site share the _ga cookie already, so www.example.com to checkout.example.com needs nothing. Configure cross-domain when the registrable domain changes.

Confirm: click through the boundary and look for _gl on the landing URL. Present means the linker fired.

Fix: Admin, Data streams, your stream, then Data collection and modification, then Configure your domains. List every hostname sharing the measurement, and add your own hostnames to the unwanted referrals list. JavaScript navigations and form posts need explicit decoration.

App Routers Eat Parameters

Single-page apps rewrite the URL after render. If history.replaceState runs before the first page_view, GA4 never sees the parameters. Hash routing is worse: anything after # is a fragment, and GA4 cannot read fragments at all.

Confirm: inspect the dl parameter on the first collect request. That is the URL GA4 saw. If it is missing the UTMs the original had, the app stripped them.

Fix, best first. Capture early with an inline script in <head> that reads location.search before the framework boots and stashes the values. Or send the first page_view manually once the captured URL is available. And keep campaign landing pages off hash routing.

ESPs and Shorteners

Email platforms route clicks through their own tracker before redirecting. Two distinct problems get confused here.

The first is a redirect that drops your query string. That kills your UTMs outright.

The second is not UTM rewriting at all: ESPs append their own tracking parameters, such as mkt_tok, mc_cid and mc_eid, or _hsenc and _hsmi. Those sit alongside your UTMs rather than replacing them. They look alarming in a URL and are usually harmless.

Shorteners are the riskier case, because several offer a workspace default that adds UTM values to every link, which does overwrite yours.

Confirm: send yourself a test, click it, and watch each hop for a Location header that drops or rewrites utm_.

Fix: turn off any automatic UTM appending in the ESP or shortener, and make sure custom redirect handlers forward the whole query string rather than the path alone. Check the setting in your own account rather than trusting a menu path from an article, including this one.

Consent Mode v2 became mandatory for personalised advertising in the EEA and UK in March 2024. Misconfigured, it produces a recognisable signature: Direct and (not set) inflated in affected regions and nowhere else.

Confirm: load your banner from an affected region and check three things. Are hits going out before consent? Are all four signals configured (ad_storage, analytics_storage, ad_user_data, ad_personalization)? Does the Conversion Linker fire on initialisation rather than after interaction?

Fix: default everything to denied before any tag fires, update on consent, and use your consent platform’s official template rather than hand-rolling. Modelling has published thresholds, and they are specific: at least 1,000 events per day with analytics_storage denied for at least seven days, plus at least 1,000 daily users with consent granted on seven of the previous 28 days. Below those, declined sessions are simply gone.

What (not set) Actually Means

It is not Direct. GA4 received the event and could not resolve the dimension you asked for.

For source or medium it is rarer than people think, because both populate at session start. The real causes are late-arriving Google Ads cost data and cardinality limits collapsing the tail of a high-cardinality report.

For landing page it usually means session_start arrived without a matching page_view, which is the app-router problem again.

For channel it means no rule matched.

The time-waster is treating it as the same problem as (direct) / (none). Establish which dimension is unresolved first.

Fix It Upstream

Every durable fix happens before GA4 sees the data, because once a value is written on a session it is immutable. You can re-bucket the past with a custom channel group. You cannot rewrite what was recorded.

Normalise at the edge. A worker at your CDN can lowercase values, map known synonyms, and log anything unrecognised before the page renders.

Normalise in server-side tagging. A step in your server container can clean values before forwarding, with one caveat: the address bar still shows the raw values, so client-side tools reading the URL see the original.

Encode in the path. For campaigns you fully control, example.com/c/spring-google-cpc survives query stripping, redirects and router rewrites. Decode it server-side into a cookie before the tag fires.

Govern at link creation. This removes causes 1, 4 and 11 outright, because the bad value never exists. Picklists per dimension, casing applied automatically, campaign names checked against a pattern, and source and medium pairs validated together so google with email never ships.

Say a paid team runs Google, Meta, LinkedIn and TikTok without governance. Within six months there are ten spellings of the Meta source alone. With governance there is one, and the regex in your channel rules goes back to being a safety net rather than load-bearing infrastructure.

Two conventions worth settling before anyone builds a link: one casing rule, applied everywhere, and one campaign naming pattern with a regex behind it.

FAQ

Why does GA4 show my own domain as the source?

The session crossed a registrable domain boundary without cross-domain configuration, so GA4 treated your own site as the referrer. Add every hostname sharing the measurement under Data collection and modification, and add your hostnames to the unwanted referrals list.

Why do reports show a different campaign than the URL?

Three possibilities: casing drift splitting one campaign into several rows, a redirect rewriting the query string, or an app router stripping parameters before the first page_view. DebugView tells you which, because it shows what GA4 actually received.

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

(direct) / (none) means no source signal existed. (not set) means the event arrived but that particular dimension could not be resolved, usually through cardinality limits, late-arriving cost data, or a channel rule matching nothing.

Which values win when a click has both a click ID and UTMs?

The auto-tagged values. Source, medium and campaign come from the click ID, and your manual values populate the Manual dimensions. This surprises teams who tag Google Ads links manually and never see those values in standard reports.

How do I see what GA4 actually received?

DebugView, under Admin, Data display. Switch debugging on with the Google Analytics Debugger extension, with Tag Assistant, or by sending debug_mode with your configuration. Then click your link in a fresh private window and read the session_start parameters.

Can a channel grouping change fix past data?

It can re-bucket past sessions, since custom channel groups apply retroactively. It cannot change the source, medium or campaign values recorded on those sessions. Grouping fixes the view backwards; governance fixes the data forwards.

Why is my paid social showing as organic social?

Your medium does not match the paid pattern while your source does match the social list, so the organic rule claims the session first. Tag paid social with a medium beginning paid or containing cp.

Does casing break my channel reports?

No. Channel definitions are not case sensitive. Casing splits your source and medium reports and breaks warehouse joins, which is a quieter failure and usually a more expensive one.

Rarely, and not for UTMs, which survive it. It strips click identifiers in Mail, Messages and Private Browsing by default. See what link tracking protection strips before attributing a reporting gap to Apple.

How do I stop casing drift across a team?

Enforce the vocabulary where links are made. Everything downstream is compensation for values that should never have existed.

Terminus helps you and your team be consistent in UTM tracking

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