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 — Founder of TrackRev.io

Muzahid MarufUpdated

UTM tracking · 7 min read
On this page
  1. 01Where UTM parameters get stripped
  2. 02A 5-step test for the hop that drops them
  3. 03Capture the first touch on your own server
  4. 04Why a tag that survives can still lose the sale
  5. 05What TrackRev does with tags and clicks

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 disappearsWhat happensTest withFix
Redirect with a fixed destinationThe Location header holds only the configured URLStep 2Forward the query
Hand-built linkA second ?, a raw & or a # before the ? splits or hides tagsStep 1Use the free UTM builder
Value encoded twicespring%20sale becomes spring%2520saleStep 2Encode once
Login, locale or canonical redirectA hop inside your app drops the queryStep 2Link to the final URL
Router or late taghistory.replaceState or a consent wait leaves the tag a clean URLStep 4Read tags before the router runs
Safari 17 and Firefox 102 featuresKnown tracking parameters are removed in Messages, Mail, Private Browsing and Strict modeStep 3Capture tags on your server
Half-stripped tagsOne surviving utm_ value makes Google Analytics 4 read every source dimension from UTMs aloneStep 5Set all 3 core tags
Return from Stripe CheckoutThe processor's domain can count as a referralStep 5Add 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.

ToolOriginal query stringChange it withStatus code
nginx rewritePrevious arguments are appended after new onesEnd the replacement with ?302 with redirect, 301 with permanent
nginx return 301Dropped: the written URL is the whole LocationAppend $is_args$argsThe one you write
Apache RewriteRuleCopied unless the new URL has its own queryAdd [QSA] to merge302, or 301 with R=301
Cloudflare single redirectsDropped, since Preserve query string is off by defaultTurn it on301 by default, or 302, 307, 308
Next.js redirects()Passed through to the destinationNothing to change308 permanent, 307 temporary

Behavior as documented by nginx, Apache 2.4, Cloudflare and Next.js 16, October 2026.

nginx: the first rule drops the query string, the second keeps it
# 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;}
  • A second ?. /pricing?plan=pro?utm_source=news yields 1 parameter, plan, with the value pro?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=news reaches your server as /pricing.
  • An unescaped & inside a value. utm_campaign=q&a ends the campaign at q and opens a parameter named a. 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 turns spring%20sale into spring%2520sale. Any hop that wraps your whole URL in one of its own parameters encodes it again.
The same link, built wrong and built right
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#plans

Privacy features in Safari and Firefox

FeatureWhere it appliesParameters namedutm_ tags stripped?Source
Safari 17 Link Tracking ProtectionMessages, Mail and Private BrowsingNone named by AppleStape reports no, in Private BrowsingApple, June 2023; WebKit, Safari 17.0; Stape, iOS 26
Firefox 102 query strippingStrict mode of Firefox's tracking protection8, including mc_eid, mkt_tok and fbclidNoMozilla 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

StepWhat to doIf the tags are missing
1Open the link from its real placement, such as the newsletter draft or the ad's URL field, in a private windowA hand-built link or a redirect: go to step 2
2Trace every hop with the curl command belowThe first hop without the query string needs a forwarding setting
3Open 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 addressA privacy feature or a scanner hop
4Read the landing request in the access log or an edge functionTags in the log but not the address bar mean a router or script removed them
5Compare your link tool's clicks with Google Analytics 4 sessions per source; values are case sensitiveA gap that reappears as Direct sessions on the same days; a small, steady gap is bots and ad blockers
Step 2: print the status line and Location header of every hop (GET, because some servers answer HEAD differently)
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:

Store the first tagged visit in a cookie your server sets (needs cookie-parser)
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.

Pass the stored tags to Stripe Checkout
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 lostShare of cohortMRR credited to DirectFirst-year revenue credited to Direct
512.5%$195$2,340
1025%$390$4,680
1537.5%$585$7,020
2050%$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.

StepWhat the TrackRev redirect does
1Looks up the link and checks its expiry and password
2Logs the click on the server, with the link's channel (Smart Links infer it from the referrer)
3Adds the link's saved UTM tags to the destination, skipping any the destination already has
4Adds a _vid visitor parameter that the TrackRev pixel reads, and sets a vid cookie for new visitors
5Sends 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.

PostLinkedIn

Frequently asked questions

Muzahid Maruf — Founder of TrackRev.io

Written by

Muzahid Maruf

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