DEV Community

Cover image for How to Preserve UTM Parameters Through Redirects Without Losing Attribution
Redirhub
Redirhub

Posted on

How to Preserve UTM Parameters Through Redirects Without Losing Attribution

How to Preserve UTM Parameters Through Redirects Without Losing Attribution

A practical guide to forwarding campaign query strings through redirects, testing every hop, and keeping GA4 attribution useful.

You can build a clean campaign URL and still lose attribution before the visitor reaches the landing page.

The usual culprit is not GA4, the ad platform, or the frontend tag. It is a redirect that sends the browser to the right page while quietly dropping the query string.

This matters for developers because redirects often live in infrastructure: Nginx, Apache, edge middleware, short-link tools, CMS routing, or application code. If one hop in the chain forgets ?utm_source=..., the final page cannot recover it.

This guide walks through redirect patterns that preserve UTM parameters without creating duplicate keys, unsafe destinations, or analytics noise.


The Failure Mode

Start with a tagged campaign URL:

https://go.example.com/spring-sale?utm_source=linkedin&utm_medium=social&utm_campaign=spring_sale
Enter fullscreen mode Exit fullscreen mode

A broken redirect might return:

HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/spring-sale
Enter fullscreen mode Exit fullscreen mode

The redirect is valid HTTP. The user lands on the right page. But the destination URL no longer contains utm_source, utm_medium, or utm_campaign, so GA4 sees a pageview without the campaign context you expected.

The correct redirect preserves the query string:

HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/spring-sale?utm_source=linkedin&utm_medium=social&utm_campaign=spring_sale
Enter fullscreen mode Exit fullscreen mode

Status code correctness and attribution correctness are separate checks. A 301, 302, 307, or 308 can all preserve or drop parameters depending on how the Location header is built.


Choose the Redirect Type First

Use a permanent redirect such as 301 or 308 when the source URL has permanently moved. Use a temporary redirect such as 302 or 307 for short-lived campaigns, experiments, or links whose destination may change.

Do not use a permanent redirect just because it works in a quick test. Browsers, crawlers, and intermediate systems may cache permanent redirects, which makes later campaign changes harder to reason about.

The query-forwarding rule should be intentional either way: if the campaign URL arrives with tracking parameters, every redirect hop that owns the route should preserve them.


Server Examples

Nginx

For a destination with no existing query string, append the incoming query safely with $is_args$args:

location = /spring {
    return 302 https://www.example.com/spring-sale$is_args$args;
}
Enter fullscreen mode Exit fullscreen mode

$is_args expands to ? only when the incoming request has a query string. $args contains the raw incoming query string. That avoids producing a trailing ? for untagged visits.

If the destination already has parameters, do not add a second question mark:

location = /spring {
    return 302 https://www.example.com/spring-sale?region=us&$args;
}
Enter fullscreen mode Exit fullscreen mode

That simple version is fine only if you have tested the empty-query case and accept the trailing &. For stricter output, split tagged and untagged cases or move the logic into application/edge code where URL parsing is clearer.

Apache

With mod_rewrite, QSA appends the original query string to the replacement URL:

RewriteEngine On
RewriteRule ^spring$ https://www.example.com/spring-sale [R=302,L,NE,QSA]
Enter fullscreen mode Exit fullscreen mode

QSA means query string append. It is especially important when the replacement URL includes its own query string, because without explicit handling the original parameters may be replaced.

Application Code

In application code, parse URLs instead of concatenating strings:

app.get("/spring", (req, res) => {
  const incoming = new URL(req.originalUrl, "https://go.example.com");
  const destination = new URL("https://www.example.com/spring-sale");

  incoming.searchParams.forEach((value, key) => {
    destination.searchParams.append(key, value);
  });

  res.redirect(302, destination.toString());
});
Enter fullscreen mode Exit fullscreen mode

This preserves duplicate keys exactly as received. If you prefer one value per UTM key, use set() for known campaign keys instead of append():

for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"]) {
  const value = incoming.searchParams.get(key);
  if (value) destination.searchParams.set(key, value);
}
Enter fullscreen mode Exit fullscreen mode

The important part is that URL encoding is handled by URL and URLSearchParams. Do not build redirect URLs by stitching raw request values into a string.


Decide Your Duplicate-Key Policy

Duplicate parameters are easy to create:

https://www.example.com/spring-sale?utm_source=newsletter&utm_source=linkedin
Enter fullscreen mode Exit fullscreen mode

Different tools may choose the first value, the last value, or expose both. That ambiguity is bad for attribution.

Pick a policy before launch:

  • Source-owned UTMs win: the incoming campaign parameters replace destination defaults.
  • Destination-owned UTMs win: the landing page keeps its predefined campaign values.
  • Reject duplicates: the redirect refuses or normalizes links with conflicting campaign keys.

For most campaign redirects, source-owned UTMs are easiest to reason about because the distributed URL defines the campaign. There are exceptions, especially when a destination uses query parameters for product filters, locales, or experiments.


Multi-Hop Redirects

Campaign links often pass through several systems:

ad platform -> short link -> branded redirect domain -> landing page
Enter fullscreen mode Exit fullscreen mode

Attribution survives only if every hop preserves the query string. If the short link keeps the parameters but the branded redirect drops them, the final page still loses.

Trace the whole chain:

curl -sSIL \
  'https://go.example.com/spring?utm_source=linkedin&utm_medium=social&utm_campaign=spring_sale'
Enter fullscreen mode Exit fullscreen mode

Read every Location header. Confirm:

  • The expected number of redirects occurs.
  • Each hop stays on an expected hostname.
  • HTTPS does not downgrade to HTTP.
  • Required UTM parameters are still present.
  • Existing destination parameters are merged intentionally.
  • Duplicate UTM keys are absent or match your policy.

To follow the redirect chain and limit loops:

curl -sSIL -L --max-redirs 10 \
  'https://go.example.com/spring?utm_source=linkedin&utm_medium=social&utm_campaign=spring_sale'
Enter fullscreen mode Exit fullscreen mode

An unexpectedly long chain is not just inefficient. It increases the chance that one system will strip or rewrite parameters.


Open-Redirect Safety

Avoid patterns like this:

res.redirect(req.query.next);
Enter fullscreen mode Exit fullscreen mode

That can turn a tracking route into an open redirect, where an attacker sends users through your domain to a malicious site.

Use an allowlist for destinations:

const allowedDestinations = {
  spring: "https://www.example.com/spring-sale",
  pricing: "https://www.example.com/pricing"
};

const target = allowedDestinations[req.params.slug];
if (!target) return res.sendStatus(404);

const destination = new URL(target);
Enter fullscreen mode Exit fullscreen mode

Then copy only the parameters you intend to support. Many teams preserve utm_*, gclid, fbclid, and a few product parameters while rejecting unrelated keys. The right list depends on your analytics and privacy requirements.


Production Checklist

Before a campaign goes live, test these cases:

/spring?utm_source=linkedin&utm_medium=social&utm_campaign=spring_sale
/spring?utm_source=newsletter
/spring
/spring?utm_campaign=spring%20sale
/spring?utm_source=one&utm_source=two
Enter fullscreen mode Exit fullscreen mode

Verify at two layers.

First, inspect network behavior with curl or browser developer tools. The final URL should contain the expected parameters, encoded correctly, with no surprise hostnames or loops.

Second, test the analytics result. In GA4 DebugView or realtime reports, confirm the visit is attributed to the expected source, medium, and campaign. A correct final URL is necessary, but analytics configuration can still affect what appears in reports.


Managing Redirects at Scale

A few hand-written rules are easy to audit. Hundreds of campaign routes, vanity domains, migration URLs, and partner links need a process.

Treat redirects as managed configuration:

  • Store source URL, destination URL, status code, owner, and query-forwarding policy.
  • Test representative tagged and untagged URLs after bulk changes.
  • Monitor failed destinations, redirect loops, and unexpected hostnames.
  • Keep campaign links stable even when the landing page changes.
  • Document whether utm_*, ad click IDs, and product parameters are forwarded.

If you use a redirect management platform, verify query forwarding explicitly. Do not assume every tool preserves parameters by default. For a deeper walkthrough, see this guide on how to preserve UTM parameters through redirects.

Top comments (1)

Collapse
 
kris_boblea_47c941c7cfa5 profile image
Kris Bob Lea

that's very helpful!
thanks!