
This is one of those problems that makes you question your own sanity. You built the link. You can see utm_source=newsletter&utm_medium=email sitting right there in the address bar. And yet GA4 insists the session came from “Direct,” or files the source as “(not set).”
The UTMs are there. GA4 is ignoring them. What’s going on?
I’ve debugged this more times than I can count, and here’s the reassuring part: it’s almost never random. There’s a specific, findable reason every single time — and once you understand the one principle underneath all of them, you can trace it fast.
Let me give you that principle first, then walk through every cause in the order I actually check them, then show you how to stop the biggest category of these problems from ever happening.
The one thing that explains almost every case
GA4 reads UTM parameters once — at the moment the session starts — and whatever it reads becomes canonical for the rest of that session.
That single sentence is the key to nearly every version of this bug. GA4 doesn’t continuously watch the URL. It resolves source, medium, and campaign on the first page_view of a session, writes them as session-scoped dimensions, and moves on. Anything that happens to the URL after that read is invisible to attribution.
So the real question is never “are my UTMs correct?” It’s:
Were my UTMs still intact, in the query string, at the exact moment GA4 read them?
Reframe it that way and most of these cases solve themselves. A redirect that fired before the read. A typo that made the value unreadable. A session that restarted mid-journey with no UTMs. An SPA that rewrote the URL before GA4 got to it. Every one is a variation of “the parameters weren’t intact at the moment of the read.”
Now let’s go find yours.
Group 1: The parameters got stripped before GA4 read them
This is the most common category by far. Start here.
Server-side redirects
If your link passes through any redirect before landing on the final page, there’s a strong chance the parameters get dropped. HTTP → HTTPS. Non-www → www. A trailing-slash normalization rule. A vanity URL or CMS 301 that rewrites the path. Any of these can strip the query string unless the redirect is explicitly configured to preserve it.
The user clicks a link with perfect UTMs, the redirect fires, and the URL that finally loads has been cleaned. GA4 reads that URL, sees nothing, and falls back to Direct.
Catch it: Click your own link in a fresh incognito window with the DevTools network tab open and “preserve log” enabled. Trace every redirect hop. If the parameters are on the click URL but gone by the final URL, a redirect ate them.
Fix it: Configure every redirect in the chain to pass the query string through — most redirect rules can, they’re just not set up to by default. And wherever you can, link directly to the final destination so there’s no hop to worry about.
JavaScript redirects
These happen inside the browser after the page loads. A landing page that immediately bounces the user somewhere else via JavaScript — common on custom-built landers and some page builders — can lose the UTMs before GA4 reads them if the redirect fires fast enough.
Catch it: Same live test. If the URL visibly changes a beat after landing and the parameters vanish, a client-side script did it.
The website strips the query string for “cleanliness”
Some sites automatically scrub everything after the ? on load, for aesthetic or security reasons. The visitor still reaches the right page, but the campaign data is gone — and if that scrub runs before GA4’s tag fires, the attribution goes with it.
The web server truncates or alters the URL
Long URLs can get truncated by server length limits, cutting off the parameters at the end (utm_campaign and utm_content are usually last, so they’re the first casualties). Security configs and firewalls sometimes strip query parameters they deem unnecessary. Work with whoever owns the server config to confirm UTMs survive any rewrite or normalization rule.
Group 2: The UTMs are malformed (and you can’t see it)
UTM parameters are more fragile than they look, and a single formatting mistake can make GA4 quietly ignore the whole thing. This is one of the two highest-frequency causes across every GA4 diagnostic I’ve read — and the one people stare straight past, because the URL looks fine.
Here’s the catalog of what breaks it:
- A typo in the parameter name —
utm_sorce,utm-source(hyphen instead of underscore),utmsource. GA4 doesn’t recognize it and ignores it. - A missing
&between parameters —?utm_source=google utm_medium=cpccollapses into one broken value. - A missing
?before the first parameter — the server can’t tell the page path from the parameters. - A stray space —
utm_source=google &utm_medium=cpc(space before the&) means onlyutm_sourceparses; everything after the space is dropped. - An unencoded space in a value —
utm_campaign=spring saleneeds to bespring_saleorspring%20sale. A raw space is an invalid URL character. - An unencoded
&inside a value —utm_campaign=spring&summersplits into two parameters; the&needs to be%26. - Smart quotes or curly characters pasted from a doc instead of straight ones.
Casing drift — the silent report-wrecker
This one deserves its own spotlight because it’s the single most common wrong-UTM cause, and it does damage even when the URL is “valid.”
GA4 preserves casing exactly. utm_source=Facebook, facebook, and FB are three different sources. Over six months, a team with no shared standard produces Facebook, facebook, FB, fb, Meta, meta — and one campaign fragments across six rows in every report. Warehouse joins keyed on the raw value silently miss. And any custom channel rule using a case-sensitive operator drops the session into Unassigned.
The maddening part: GA4’s built-in channel rules match case-insensitively, so the traffic often still buckets into the right channel — which means the drift hides in plain sight until you try to actually analyze a campaign and find it scattered across a dozen near-duplicate rows.
Catch it: In Traffic acquisition, sort by Session source alphabetically and scan for near-duplicates that differ only by case.
The real fix is prevention — which is where a UTM builder earns its place. Enforcing consistent lowercase values, correct encoding, and valid syntax at the moment the link is created eliminates this entire category before it can ship. This is exactly what UTM Manager is built to do: it standardizes casing automatically, encodes values correctly, and won’t let a malformed or off-taxonomy link out the door. If you’re hand-typing UTMs into a spreadsheet, this class of bug is essentially inevitable at scale — a dedicated builder takes it off the table entirely.
Group 3: You used a parameter or value GA4 doesn’t understand
An unsupported UTM parameter
GA4 only recognizes a specific set: utm_id, utm_source, utm_medium, utm_campaign, utm_term, utm_content, utm_source_platform, utm_campaign_id, utm_creative_format, and utm_marketing_tactic. Invent something like utm_channel or utm_name and GA4 ignores it unless you’ve configured a custom dimension to catch it.
A value that doesn’t match GA4’s channel rules
This is why “Unassigned” shows up even when your source and medium are technically recorded. GA4 groups traffic into channels using a fixed rule set tied to system-defined mediums. Tag a Facebook ad with utm_medium=fb-ad and GA4 has no rule for “fb-ad” — so it lands in Unassigned. Tag it utm_medium=paid (or paid_social with a social source) and it buckets correctly into Paid Social.
The takeaway: use system-defined sources and mediums wherever you can, and make sure everyone tagging links in your organization knows the channel rules. This is another thing a good builder enforces for you — pairing valid mediums with sources so links resolve to a real channel instead of Unassigned. UTM Manager can hold that taxonomy so your team isn’t relying on memory.
The “shop” trap
A genuinely obscure one worth knowing: if utm_campaign contains the word “shop” (case-insensitive) and utm_medium starts with “paid,” GA4 forces the session into Paid Shopping — regardless of source. So utm_campaign=shop_launch&utm_medium=paidsocial&utm_source=facebook gets mislabeled as Paid Shopping, not Paid Social. Avoid “shop” in campaign names (use “store” or “promo”), or use a medium that doesn’t start with “paid” (like social_paid).
UTMs in the fragment instead of the query string
Parameters must live in the query string (after the ?). If they end up after a # — common with hash-routing SPAs — GA4 can’t read URL fragments at all, and the session lands in Direct.
Group 4: The parameters were intact, but GA4 read the wrong URL — or didn’t read at all
The GA4 tag fired too late
If your GA4 configuration tag fires after another script has changed the URL, it reads the already-cleaned version. The fix is to fire it as early as possible — the Initialization – All Pages trigger in GTM is designed to run before everything else, which is exactly where your Google tag belongs.
Single-page apps rewriting the URL
On React, Vue, and similar frameworks, the page doesn’t fully reload between views. The router often rewrites the URL via history.replaceState after first paint — so the first page_view GA4 sees has no campaign parameters. Worse, hash-routing SPAs put the UTMs after the #, where GA4 can’t read them.
Catch it: Inspect the dl (document location) parameter on the GA4 collect request in the network tab. If dl has no UTMs but the original URL did, the SPA stripped them before the tag read them.
Fix it: Capture the entry URL early — an inline script in the <head>, above the framework bootstrap, that reads window.location.search, parses the utm_ parameters, and stashes them before the router can rewrite anything. Then pass those captured values to GA4. And avoid hash routing for campaign landers — HTML5 history mode (/landing) is readable; /#/landing is not.
Missing or invalid tracking code
Obvious but worth confirming: if the landing page is missing the GA4 tag entirely, has an invalid Measurement ID, or the tag simply fails to fire (JavaScript errors, restrictive triggers, script conflicts), then no page_view fires — and no page_view means no attribution, no matter how perfect the URL. Because attribution is set on the landing page, a missing tag there poisons the whole session even if every subsequent page is tagged correctly.
GA4 inside an iframe
If the GA4 code sits in a child iframe rather than the parent frame, it can’t read the parent’s URL — so it can’t see the UTMs. Keep the tag in the parent frame, or pass the parameters into the iframe explicitly.
Group 5: The session itself was re-attributed
Cross-domain gaps
If a user’s journey crosses a domain boundary — main site to a separate checkout domain, a payment processor, a subdomain — and cross-domain tracking isn’t configured, GA4 splits the journey into multiple sessions. The second session starts fresh with no UTMs and gets attributed to Direct or, worse, to your own hostname as a referral.
The classic version: a user lands with perfect UTMs, moves to a third-party checkout, returns to a thank-you page, and the purchase session shows as Direct — because the UTMs were several page loads and a domain-hop ago.
Catch it: Click a link that crosses your domains and check for the _gl linker parameter on landing. No _gl means the linker isn’t firing. In reports, filter Session source by your own hostnames — lots of self-referrals means hostname leakage.
Fix it: Add every hostname to your cross-domain configuration (and confirm they share one Measurement ID), and add your own domains plus payment processors to the unwanted-referrals list. For JavaScript-navigation buttons and form submits, decorate the URL with _gl manually.
Clicking a link mid-session
GA4 sets campaign attribution at session start. If someone is already browsing your site and clicks a UTM link within the same active session, GA4 may not start a new session — so the new UTMs get ignored. This trips up marketers constantly during internal testing. Test in a fresh incognito window every time.
Long idle / tab reuse
Sessions time out after 30 minutes of inactivity by default. A user who lands with your UTMs, leaves the tab open, and returns 45 minutes later triggers a new session with no UTMs — attributed to Direct. Not a bug, just GA4 working as designed, but it explains a slice of unexpected Direct traffic.
Internal links tagged with UTMs
Never put UTMs on internal links. If a user arrives via Google organic and then clicks an internal link carrying UTMs, those parameters overwrite the original source — so real acquisition data gets replaced by your own internal tag. Use events to track internal navigation, not UTMs.
Group 6: Something is blocking or rewriting the hit
Consent banners
If analytics is gated behind a consent banner and the user hasn’t granted consent, GA4 may not fire at all — so even with perfect UTMs in the URL, nothing is recorded. And critically, if consent is granted after the page loads, the UTMs from that initial load are not captured retroactively. The fix is to store the UTMs in a first-party cookie the moment the page loads (before consent), then send them to GA4 once consent is granted. In the EEA/UK, misconfigured Consent Mode v2 produces a recognizable signature: inflated Direct and “(not set)” specifically in those regions.
Ad blockers and privacy extensions
These can block the GA4 hit entirely, strip the UTMs from the URL, or even replace the values with “anonymized” placeholders. There’s no clean fix for user-side blocking, but it’s worth knowing as an explanation when a chunk of traffic — often mobile or privacy-conscious segments — shows up as Direct.
In-app browsers
Traffic from inside social apps (Instagram, Facebook, TikTok, LinkedIn) sometimes handles URLs and referrers differently, stripping parameters as a “feature.” If your unexpectedly-Direct traffic skews heavily mobile-social, this is a likely contributor.
Server-side tagging
If you’ve moved to server-side GTM, UTMs aren’t automatically forwarded with server-side events. You have to capture them client-side first (URL variables in web GTM), pass them to your server container, and read them there as event-data variables. Skip that and your server-side tags won’t have the campaign data at all.
“(not set)” is not the same as “Direct”
One clarification that saves a lot of wasted troubleshooting: these two values mean different things.
“(direct) / (none)” means GA4 received the hit but found no source signal at all — it’s a deliberate fallback.
“(not set)” means GA4 received the event but couldn’t resolve the specific dimension you’re looking at. Common causes: cardinality limits collapsing the long tail of high-dimensionality reports, late-arriving Google Ads cost data, a custom dimension whose value gets set after session_start, or a channel rule that failed to match. Before you troubleshoot, confirm which dimension is showing “(not set)” — the cause for a “(not set)” landing page is completely different from a “(not set)” channel.
A fast 3-step diagnostic
When the data looks wrong, don’t guess. Test in order:
1. Test the link live. Open the exact UTM URL in a fresh incognito window with the network tab recording. Confirm the parameters survive to the final URL. If they vanish → it’s a redirect or a script (Groups 1 and 4).
2. Check GA4 DebugView. Click the link again with DebugView open and inspect the session_start parameters. If DebugView shows the right values but your reports don’t → the problem is downstream in channel grouping. If DebugView shows different values than the URL → something rewrote the hit. If there’s no session_start at all → the hit is being blocked (Group 6).
3. Check standard reports after 24–48 hours. DebugView and Realtime update instantly, but standard reports lag. Give it up to two days before concluding the data is truly missing rather than just processing.
Stop the biggest category at the source
Step back and almost every cause here is a variation of the same thing: the UTM parameters weren’t intact, in the query string, at the moment GA4 read them. Redirects, cross-domain hops, SPA rewrites, and blocked hits are infrastructure problems you fix at the infrastructure level.
But the single largest slice — malformed parameters, casing drift, invalid mediums that land in Unassigned, the “shop” trap, unsupported parameters — all come from the same root: inconsistent, hand-built UTMs with no shared standard. And that category you can eliminate entirely, before it ever reaches a URL.
That’s exactly what UTM Manager is for. Instead of every team member typing links into a spreadsheet and quietly introducing Facebook vs facebook vs FB, you build every link from one governed system:
- Consistent casing, enforced automatically — so one campaign never fragments across six rows
- Correct encoding by default — no raw spaces, no broken
&, no smart quotes - Valid, system-defined mediums — so links resolve to a real channel instead of Unassigned
- A single source of truth the whole team builds from, instead of tribal knowledge and copy-paste
It won’t fix a redirect that strips your query string or a cross-domain gap — those are separate infrastructure issues you’ll still handle with the diagnostics above. But it removes the entire malformed-and-inconsistent-UTM category, which in my experience is a bigger share of “why is GA4 saying Direct?” than most teams realize.
If you’re running campaigns at any real volume, moving your UTM creation out of spreadsheets and into a dedicated tool is one of the highest-leverage hygiene fixes available to you. Build your links clean and consistent from the start with UTM Manager →
