Why UTM parameters get stripped, and a 5-step test to find the hop that does it
UTM parameters get stripped by redirects, malformed links and a few privacy features. A 5-step test finds the hop that drops yours, with the fix for each.
Muzahid Maruf, FounderUpdated
On this page
Explore with AI
Opens this article inside the chosen assistant with a ready-made prompt.
UTM parameters get stripped wherever a hop rebuilds the destination URL without its query string: a redirect rule, a shortener, a login redirect on your own site, or a router that rewrites the address bar before the analytics tag reads it.
Safari 17 and Firefox 102 strip tracking parameters too, but Firefox's list has 8 entries and none is a utm_ tag, and Apple's documentation names no parameters.
When Google Analytics 4 files a tagged click under Direct, trace the link's path first.
Google Analytics Help names redirects, URL shorteners such as bit.ly and ad blockers among the causes of (direct) / (none) traffic. In a revenue report that credits renewals to the first click, one lost tag misfiles every later payment.
Key takeaways
- Tags vanish where a hop rebuilds the URL, and 5 checks (private window, curl, iPhone apps, server log, Google Analytics 4 sessions) find the hop.
- Firefox 102 strips 8 named parameters in Strict mode and none is a utm_ tag. Apple names none, so test a tagged link in Messages, Mail and Private Browsing.
- Save the first-touch tags in a cookie your server sets, then copy them into Stripe metadata (50 keys, 500 characters per value) so renewals keep their source.
Where UTM parameters get stripped
| Where it disappears | What happens | Test with | Fix |
|---|---|---|---|
| Redirect with a fixed destination | The Location header holds only the configured URL | Step 2 | Forward the query |
| Hand-built link | A second ?, a raw & or a # before the ? splits or hides tags | Step 1 | Use the free UTM builder |
| Value encoded twice | spring%20sale becomes spring%2520sale | Step 2 | Encode once |
| Login, locale or canonical redirect | A hop inside your app drops the query | Step 2 | Link to the final URL |
| Router or late tag | history.replaceState or a consent wait leaves the tag a clean URL | Step 4 | Read tags before the router runs |
| Safari 17 and Firefox 102 features | Known tracking parameters are removed in Messages, Mail, Private Browsing and Strict mode | Step 3 | Capture tags on your server |
| Half-stripped tags | One surviving utm_ value makes Google Analytics 4 read every source dimension from UTMs alone | Step 5 | Set all 3 core tags |
| Return from Stripe Checkout | The processor's domain can count as a referral | Step 5 | Add it to unwanted referrals |
Redirects that rebuild the URL
A redirect answers with a Location header, and the browser requests exactly that URL, so a rule that names a destination without a query turns /spring?utm_source=news into a plain /pricing request.
RFC 9110 (June 2022) section 10.2.2 carries a fragment across a redirect when the Location has none and sets no such rule for the query.
| Tool | Original query string | Change it with | Status code |
|---|---|---|---|
nginx rewrite | Previous arguments are appended after new ones | End the replacement with ? | 302 with redirect, 301 with permanent |
nginx return 301 | Dropped: the written URL is the whole Location | Append $is_args$args | The one you write |
Apache RewriteRule | Copied unless the new URL has its own query | Add [QSA] to merge | 302, or 301 with R=301 |
| Cloudflare single redirects | Dropped, since Preserve query string is off by default | Turn it on | 301 by default, or 302, 307, 308 |
Next.js redirects() | Passed through to the destination | Nothing to change | 308 permanent, 307 temporary |
Behavior as documented by nginx, Apache 2.4, Cloudflare and Next.js 16, October 2026.
# Drops it: the Location header is exactly this URLlocation = /spring { return 301 https://www.example.com/pricing;} # Keeps it: $is_args adds the ? and $args adds the original querylocation = /summer { return 301 https://www.example.com/pricing$is_args$args;}Links that are built wrong
- A second
?./pricing?plan=pro?utm_source=newsyields 1 parameter,plan, with the valuepro?utm_source=news. - A
#before the?. RFC 3986 (January 2005) section 3.5 splits the fragment off before the request goes out, so/pricing#plans?utm_source=newsreaches your server as/pricing. - An unescaped
&inside a value.utm_campaign=q&aends the campaign atqand opens a parameter nameda. Write%26. - A value encoded twice. RFC 3986 section 2.4 requires a percent sign used as data to be written
%25, so a second pass turnsspring%20saleintospring%2520sale. Any hop that wraps your whole URL in one of its own parameters encodes it again.
Wrong: https://example.com/pricing#plans?utm_source=news&utm_campaign=q&a
Right: https://example.com/pricing?utm_source=news&utm_campaign=q%26a#plansPrivacy features in Safari and Firefox
| Feature | Where it applies | Parameters named | utm_ tags stripped? | Source |
|---|---|---|---|---|
| Safari 17 Link Tracking Protection | Messages, Mail and Private Browsing | None named by Apple | Stape reports no, in Private Browsing | Apple, June 2023; WebKit, Safari 17.0; Stape, iOS 26 |
| Firefox 102 query stripping | Strict mode of Firefox's tracking protection | 8, including mc_eid, mkt_tok and fbclid | No | Mozilla engineering docs; release notes, June 28, 2022 |
Either list can grow in any release, so confirm your own links with step 3. Our iOS 17 Link Tracking Protection post covers what else changed.
What Google Analytics 4 does with what arrives
When any UTM parameter is present, Google Analytics 4 derives all cross-channel source dimensions from UTMs exclusively, per its traffic-source documentation. A link that keeps utm_campaign but loses utm_source can show (not set).
Stripe Checkout runs on checkout.stripe.com unless you set up a custom domain, and Google's unwanted referrals page, which allows 50 domains per data stream, uses payment processors as its first example.
Between 2 domains you own, cross-domain measurement carries the visitor ID in a _gl parameter, and a query-dropping redirect breaks it.
A 5-step test for the hop that drops them
| Step | What to do | If the tags are missing |
|---|---|---|
| 1 | Open the link from its real placement, such as the newsletter draft or the ad's URL field, in a private window | A hand-built link or a redirect: go to step 2 |
| 2 | Trace every hop with the curl command below | The first hop without the query string needs a forwarding setting |
| 3 | Open it from Messages, Mail and Private Browsing on an iPhone, and through Microsoft Safe Links, which rewrites email links onto a safelinks.protection.outlook.com address | A privacy feature or a scanner hop |
| 4 | Read the landing request in the access log or an edge function | Tags in the log but not the address bar mean a router or script removed them |
| 5 | Compare your link tool's clicks with Google Analytics 4 sessions per source; values are case sensitive | A gap that reappears as Direct sessions on the same days; a small, steady gap is bots and ad blockers |
curl -sL -o /dev/null -D - "https://go.example.com/spring?utm_source=news&utm_medium=email" \
| grep -i -E "^(HTTP|location):"Capture the first touch on your own server
Redirect fixes protect the tags only as far as the landing page. I prefer to read them on the first request, because nothing after that can rewrite what the server stored.
This Express middleware saves the 5 standard tags in an HTTP-only cookie on the first tagged visit:
const KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"]; app.use((req, res, next) => { const tags = {}; for (const key of KEYS) { if (typeof req.query[key] === "string") tags[key] = req.query[key]; } if (Object.keys(tags).length > 0 && !req.cookies.first_touch) { res.cookie("first_touch", JSON.stringify(tags), { maxAge: 90 * 24 * 60 * 60 * 1000, // 90 days, in milliseconds httpOnly: true, secure: true, sameSite: "lax", }); } next();});At checkout, copy the cookie into the Stripe session.
Stripe's metadata documentation allows 50 key-value pairs per object, with keys up to 40 characters and values up to 500, and does not copy Checkout Session metadata to the subscription, so set subscription_data.metadata too.
Each invoice the subscription creates then carries a snapshot. The Stripe metadata attribution setup post covers the field mapping.
const tags = JSON.parse(req.cookies.first_touch ?? "{}");const attribution = Object.fromEntries( Object.entries(tags).map(([key, value]) => [key, String(value).slice(0, 500)]),); const session = await stripe.checkout.sessions.create({ mode: "subscription", line_items: [{ price: "price_123", quantity: 1 }], success_url: "https://example.com/welcome", metadata: attribution, subscription_data: { metadata: attribution },});Why a tag that survives can still lose the sale
WebKit's tracking prevention page caps JavaScript-created cookies at 24 hours after link decoration from a tracker, and deletes them, with other script-writeable storage, after 7 days without user interaction.
A visitor who clicks on a Monday and subscribes 8 days later, with no visit in between, has lost the cookie.
The server-set cookie above sits outside both rules as written, since WebKit caps HTTP-response cookies at 7 days only for CNAME and IP-address cloaking. Our Safari ITP post has more.
| Tags lost | Share of cohort | MRR credited to Direct | First-year revenue credited to Direct |
|---|---|---|---|
| 5 | 12.5% | $195 | $2,340 |
| 10 | 25% | $390 | $4,680 |
| 15 | 37.5% | $585 | $7,020 |
| 20 | 50% | $780 | $9,360 |
Invented numbers: 40 signups a month on a $39 plan with no cancellations, so each customer pays $468 in 12 months and the cohort is worth $18,720. MRR is what the misfiled customers pay each month; first-year revenue is their 12-month total, and the two should not share a line in a report.
What TrackRev does with tags and clicks
TrackRev is SaaS affiliate software with link tracking built in, and it stores the channel on the link, so a click lands in the right channel even when no query string survives.
| Step | What the TrackRev redirect does |
|---|---|
| 1 | Looks up the link and checks its expiry and password |
| 2 | Logs the click on the server, with the link's channel (Smart Links infer it from the referrer) |
| 3 | Adds the link's saved UTM tags to the destination, skipping any the destination already has |
| 4 | Adds a _vid visitor parameter that the TrackRev pixel reads, and sets a vid cookie for new visitors |
| 5 | Sends a 302 to the destination |
Query parameters appended to a short link at send time are not forwarded, so per-send tags belong in the saved link.
A redirect of yours that drops the query string drops _vid too, and the pixel falls back to a vid cookie when it finds one.
On Stripe the visitor ID rides in client_reference_id, which the pixel adds to Payment Links and Buy Buttons.
The Free plan covers 50 links and 1,000 tracked events a month with revenue hidden, and the paid plans are Indie at $29 a month (1 workspace, 1 tracked domain) and Growth at $299 a year (up to 10 workspaces, 10 tracked domains) (pricing).
If you only need sessions in Google Analytics 4, forwarding redirects and the naming rules in the UTM parameters guide fix this without a new tool.
Found this useful? Share it.
Frequently asked questions
- Reports say no for Private Browsing, and Apple names no parameters. Test your own link in Messages and Mail on iOS 17 and iOS 26.
- A source, medium or campaign value was missing or misspelled when the session started. Add Session source / medium as a secondary dimension in Traffic acquisition and compare each row with the links you sent.
- None forwards it on its own, because the Location header decides. The codes differ on caching and on whether the request method survives: 307 and 308 keep it.
- Google's UTM best-practices help page says to always use utm_source, utm_medium and utm_campaign. With only some of them, Google Analytics 4 reads a partial set.
- No. The referrer is an HTTP header and the UTM tags are part of the URL. A visit with neither is filed as direct in Google Analytics 4.
- Partly. Server logs and the landing URL sometimes show where a visitor came from. Otherwise file that signup under Direct, as Google Analytics 4 would.

Written by
Founder, TrackRev.io & Contant.io
Muzahid Maruf founded TrackRev.io, SaaS affiliate software with no limit on tracked revenue, and Contant.io. He writes about affiliate programs.
Writes about Marketing attribution · Link tracking · Revenue analytics · SaaS growth
Stop guessing where your revenue comes from.
Set up TrackRev in about five minutes. The free plan covers 1,000 events a month, no card needed.
Start free