Tutorial: Diagnosing and Fixing Double-Encoded UTM Parameters

Management Summary
UTM parameters are labels added to links so analytics can identify the campaign behind a website visit. If those labels are converted into a link-safe format more times than intended, one campaign can appear under several names and distort comparisons. The problem may arise while links are built or redirected. Correcting the faulty step keeps future reporting consistent, while older name variants can be grouped without discarding the original records. Unusual percent signs alone do not prove that a label is broken.
Jump to the interactive example ↓
Contents
A campaign expected as Sommer Aktion appears as Sommer%20Aktion. Double encoding is a possible cause, but the report alone cannot identify the faulty step or prove that attribution is otherwise correct. A literal percent sequence, a missing decoding step or an explicit campaign override can produce a similar result.
The reliable diagnosis compares the intended raw value, the generated link, each redirect target and the final collected value. Correct nesting of one URL inside another must be distinguished from unintended repeated encoding of the same field.

One layer of percent encoding
Percent encoding represents bytes as % followed by two hexadecimal digits. In a query value, a space can be %20, an ampersand %26, and UTF-8 ü %C3%BC. An ampersand separating query parameters has a structural role and remains a separator.
Applying encodeURIComponent to already encoded text turns a percent sign into %25. If the intended raw value is known and exactly one decoding pass follows, the results are predictable:
| Final query value | After one decoding pass |
|---|---|
Sommer%20Aktion | Sommer Aktion |
Sommer%2520Aktion | Sommer%20Aktion |
Sommer%252520Aktion | Sommer%2520Aktion |
These are controlled examples, not a method for counting all operations in a tracking chain. A correctly decoded wrapper removes its own encoding layer. A report containing a percent sign is a candidate for investigation, not proof of an error.
A correctly nested tracking URL
A click tracker can store a complete destination URL in a parameter such as u. Encoding that destination is necessary even when the destination already contains encoded query values. The wrapper parser restores the original destination before the redirect:
const destination = "https://shop.example/?utm_campaign=Sommer%20Aktion";
const tracker = new URL("https://click.example/r");
tracker.searchParams.set("u", destination);
const restored = tracker.searchParams.get("u");
console.log(restored === destination); // true
console.log(new URL(restored).searchParams.get("utm_campaign"));
// Sommer AktionThe outer URL contains Sommer%2520Aktion, yet the restored destination still contains the correct Sommer%20Aktion. The final campaign parses as Sommer Aktion. A fault occurs when a layer encodes or decodes contrary to the agreed format. The presence of %26utm_source%3D inside a nested parameter is therefore not evidence of a broken link.
Building a destination from raw values
The URL API handles query separators, existing parameters and the fragment. The inputs below are raw values, not strings preprocessed with encodeURIComponent:
const url = new URL("https://shop.example/sommer?lang=de#details");
url.searchParams.set("utm_source", "newsletter");
url.searchParams.set("utm_medium", "email");
url.searchParams.set("utm_campaign", "Sommer Aktion");
url.searchParams.set("utm_content", "header & title");
console.log(url.href);
// https://shop.example/sommer?lang=de&utm_source=newsletter&utm_medium=email&utm_campaign=Sommer+Aktion&utm_content=header+%26+title#detailsURLSearchParams serializes spaces as +. A literal plus is serialized as %2B, so A B and A+B remain distinguishable. Parsing applies the corresponding form rules. In contrast, decodeURIComponent("A+B") leaves the plus unchanged.
encodeURIComponent is suitable for individual components and escapes query delimiters such as & and =. Some punctuation, including !, remains unescaped; the function is not a strict RFC 3986 unreserved-only encoder. encodeURI preserves structural delimiters and is unsuitable for encoding an arbitrary parameter value. Neither function needs to be added around values already serialized by URLSearchParams.
Finding the faulty boundary
A practical trace records the raw campaign value, the link-builder output, any nested destination after parsing its wrapper, every HTTP Location header and the final landing URL. The outgoing analytics request and its decoded parameters then reveal what collection actually sends. Browser network tools also reveal JavaScript redirects that curl cannot execute.
For an authorized test URL, curl -sS -L -D - -o /dev/null 'https://shop.example/test?utm_campaign=Sommer%20Aktion' follows HTTP redirects with GET and prints response headers. This is a Unix-shell example. -I would use HEAD, which can behave differently. Testing a tracking link can register a click, so a test campaign is preferable.
Link builders, trackers, CMS output and server redirects are all possible boundaries. HTML escaping, for example & in an attribute, is a separate layer from percent encoding. A search for % in reports only finds candidates; missing parameters, literal pluses and already merged names can escape that filter.
Correcting a verified case
The first correction belongs at the faulty producer or boundary. Automatic repeated decoding is unsafe: a legitimate literal %20 can become a space, and an encoded delimiter can acquire structural meaning. Malformed escape sequences can also make decodeURIComponent throw.
A temporary GTM mapping can be limited to exact, verified input values. The example assumes that preview has confirmed the output of URL - utm_campaign at the selected event and that the two listed spellings belong to the same campaign:
function () {
var value = {{URL - utm_campaign}};
var approved = {
"Sommer%20Aktion": "Sommer Aktion",
"Sommer%2520Aktion": "Sommer Aktion"
};
if (typeof value !== "string") { return undefined; }
return Object.prototype.hasOwnProperty.call(approved, value)
? approved[value] : value;
}The variable only returns text. Applying the result requires an explicit collection configuration, such as the documented campaign_name field, before the relevant Google tag collects campaign data. That field overrides utm_campaign; a tag sent later does not retroactively repair the session. Campaign scope, auto-tagging and other acquisition fields must remain consistent. GA4 event-modification rules are not a general recursive URL decoder and do not change historical data.
Keeping historical repairs reversible
A documented mapping in an analysis view can preserve raw campaign values and group confirmed variants. The SQL example requires a prepared table with exactly one row per session and verified source, medium, campaign and session date. The date range and aliases are illustrative and must match the confirmed incident:
SELECT
CASE
WHEN source = 'newsletter' AND medium = 'email'
AND campaign IN ('Sommer%20Aktion', 'Sommer%2520Aktion')
THEN 'Sommer Aktion'
ELSE campaign
END AS campaign_clean,
COUNT(*) AS session_rows
FROM `project.dataset.verified_sessions`
WHERE session_date BETWEEN DATE '2026-07-01' AND DATE '2026-08-31'
GROUP BY campaign_clean
ORDER BY session_rows DESC;COUNT(*) counts rows. Running the same pattern directly over GA4 event exports would count events, not sessions. A comparison or segment can select variants without renaming the original dimension. Corrected analysis labels should remain distinguishable from collected values.
Questions and answers
Is a real percent sign or plus sign valid in a campaign name?
Yes. Raw 20%-off and A+B can be encoded correctly. Even a literal %20 can be intentional. A documented naming convention can simplify handling, but does not replace correct encoding or prove that a historic name was erroneous.
Does Apache’s NE flag always fix the problem?
No. Apache can escape an external redirect substitution, including percent signs already present there. [NE] suppresses this escaping, but can also expose characters such as # with structural meaning. Query copying, replacement, backreferences and flags need to be assessed together. A redirect can preserve, replace or discard a query string; the actual Location headers provide the evidence. Production rewrite rules should not receive NE indiscriminately.
Learn by trying
What happens when you encode twice?
Choose a campaign value and the number of encoding passes. Compare the URL value with the result after exactly one decoding pass.
- Original
- Value in the URL
- After one decoding pass
Shows percent-encoding of a single value, not a whole URL. Form encoding such as URLSearchParams can represent spaces as +. Literal percent signs are allowed; % in a name alone does not prove an error.
This module runs in your browser and does not send or store your input.
Learn by trying
One minute: can you spot the mistake?
Summer Sale becomes Summer%2520Sale in the URL. What remains after one decoding pass?
%25 becomes a literal percent sign. The text %20 remains. Another decoding pass would produce a space; the better fix is to remove the unnecessary encoding pass when building the link.