Invalid Percent Encoding in UTM Parameters: What Breaks and Why
"Our campaign URLs looked perfect. But Google Analytics showed garbled data like 'summer%2Gsale' and 'email%XYnewsletter.' Turns out our URL encoding tool was broken, generating invalid percent codes that browsers couldn't decode."
James Wilson, a marketing technologist at a SaaS company, spent two days debugging why 40% of their campaign traffic was showing corrupted parameter values. The culprit: malformed percent encoding.
What is Percent Encoding?
Percent encoding (also called URL encoding) is the method for representing special characters in URLs using the % symbol followed by two hexadecimal digits.
Valid percent encoding:
Space → %20
! → %21
" → %22
# → %23
$ → %24
% → %25
The rule: % followed by exactly two hexadecimal digits (0-9, A-F)
What Makes Percent Encoding Invalid?
Invalid Pattern 1: Non-Hexadecimal Characters
Valid hex digits: 0 1 2 3 4 5 6 7 8 9 A B C D E F
Invalid examples:
❌ %2G (G is not hex)
❌ %ZZ (Z is not hex)
❌ %XY (X and Y are not hex)
❌ %1K (K is not hex)
❌ %M5 (M is not hex)
What happens: Browser cannot decode, shows literal characters or throws error
🚨 Not sure what's breaking your tracking?
Run a free 60-second audit to check all 40+ ways UTM tracking can fail.
Scan Your Campaigns Free✓ No credit card ✓ See results instantly
Invalid Pattern 2: Incomplete Encoding
Missing digits:
❌ %2 (only one digit instead of two)
❌ % (percent sign alone)
❌ %A (only one digit)
What happens: Browser treats as malformed, unpredictable behavior
Invalid Pattern 3: Truncated Sequences
Cut-off encoding at end of value:
❌ utm_campaign=summer-sale%2
(Encoding started but not finished)
❌ utm_source=email%
(Percent with no following digits)
Invalid Pattern 4: Wrong Case for Reserved Percent
Lowercase hex digits in some contexts:
⚠️ %2f vs %2F
Generally works but not technically valid per RFC 3986
Should use uppercase: %2F
Real-World Failure Examples
Case 1: The Malformed Comma Encoding
Intended campaign: "2024, Summer Sale"
Someone tried to encode the comma:
❌ WRONG:
utm_campaign=2024%2G summer sale
(G is not hexadecimal)
Browser sees: "2024%2G summer sale" (literal characters, not decoded)
Correct:
✅ RIGHT:
utm_campaign=2024-summer-sale
(No encoding needed, use hyphens)
Or if you must encode:
utm_campaign=2024%2C%20summer%20sale
(Comma = %2C, Space = %20, both valid hex)
Case 2: The Truncated Encoding Error
Generated URL (automated tool bug):
❌ BROKEN:
https://example.com?utm_source=email&utm_campaign=spring%2
What happened:
- Tool started encoding (%)
- Crashed or truncated before finishing
- Left incomplete %2 sequence
Browser behavior:
- Chrome: Shows "spring%2" literally
- Firefox: May show "spring%" or error
- Safari: Unpredictable
- Analytics: Records gibberish
Impact:
- 2,847 sessions tracked with malformed campaign name
- Unable to attribute $12,600 in revenue
- Campaign appears as multiple different names in reports
Case 3: The Copy-Paste Encoding Corruption
Original URL:
utm_campaign=black-friday-sale
After copy-paste through multiple tools:
❌ CORRUPTED:
utm_campaign=black%XYfriday%ZZsale
Why it happened:
1. Pasted into Word document (auto-formatting applied)
2. Copied from Word to email (encoding changed)
3. Email client "helped" by trying to encode
4. Result: Invalid percent sequences
😰 Is this your only tracking issue?
This is just 1 of 40+ ways UTM tracking breaks. Most marketing teams have 8-12 critical issues they don't know about.
• 94% of sites have UTM errors
• Average: $8,400/month in wasted ad spend
• Fix time: 15 minutes with our report
✓ Connects directly to GA4 (read-only, secure)
✓ Scans 90 days of data in 2 minutes
✓ Prioritizes issues by revenue impact
✓ Shows exact sessions affected
How to Detect Invalid Percent Encoding
Visual Inspection
Red flags to look for:
❌ %2G, %XY, %ZZ (non-hex characters)
❌ Trailing % at end of value
❌ %A, %2, %F (single digit after %)
❌ Multiple %% characters
❌ % followed by space
JavaScript Detection
function detectInvalidPercentEncoding(url) {
const issues = [];
try {
const urlObj = new URL(url);
const params = urlObj.searchParams;
['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(param => {
const value = params.get(param);
if (!value) return;
// Pattern 1: % followed by non-hex characters
const invalidHex = value.match(/%[^0-9A-Fa-f]/g);
if (invalidHex) {
issues.push({
parameter: param,
issue: 'Invalid hex characters after %',
examples: invalidHex,
severity: 'CRITICAL'
});
}
// Pattern 2: % followed by only one digit
const incompleteEncoding = value.match(/%[0-9A-Fa-f](?![0-9A-Fa-f])/g);
if (incompleteEncoding) {
issues.push({
parameter: param,
issue: 'Incomplete percent encoding (single digit)',
examples: incompleteEncoding,
severity: 'CRITICAL'
});
}
// Pattern 3: % at end of string
if (value.endsWith('%')) {
issues.push({
parameter: param,
issue: 'Percent sign at end of value',
severity: 'CRITICAL'
});
}
// Pattern 4: % followed by space
if (value.includes('% ')) {
issues.push({
parameter: param,
issue: 'Percent followed by space',
severity: 'CRITICAL'
});
}
// Pattern 5: Multiple consecutive %
if (value.includes('%%')) {
issues.push({
parameter: param,
issue: 'Multiple consecutive percent signs',
severity: 'WARNING'
});
}
});
} catch (e) {
issues.push({
parameter: 'URL',
issue: 'Malformed URL',
error: e.message,
severity: 'CRITICAL'
});
}
return {
valid: issues.length === 0,
issues: issues
};
}
// Usage
const testURLs = [
'https://example.com?utm_campaign=sale%2Gspecial',
'https://example.com?utm_source=email%',
'https://example.com?utm_medium=social%2',
'https://example.com?utm_campaign=summer-sale' // This one is valid
];
testURLs.forEach(url => {
const result = detectInvalidPercentEncoding(url);
console.log(`URL: ${"{"}{"{"}url{"}"}{"}"}}`);
console.log(`Valid: ${result.valid}`);
if (!result.valid) {
console.log('Issues:', result.issues);
}
console.log('---');
});Browser Console Test
Quick manual check:
// Paste this in browser console with your URL
const testUrl = 'https://example.com?utm_campaign=sale%2Gspecial';
try {
const urlObj = new URL(testUrl);
const campaign = urlObj.searchParams.get('utm_campaign');
console.log('Campaign value:', campaign);
// Try to decode explicitly
try {
const decoded = decodeURIComponent(campaign);
console.log('Decoded:', decoded);
} catch (e) {
console.error('Decoding failed:', e.message);
console.log('⚠️ INVALID PERCENT ENCODING DETECTED');
}
} catch (e) {
console.error('Invalid URL:', e.message);
}Valid Percent Encoding Reference
Common Characters Properly Encoded
| Character | Name | Hex Value | Percent Encoded |
|---|---|---|---|
| Space | Space | 20 | %20 |
| ! | Exclamation | 21 | %21 |
| " | Quote | 22 | %22 |
| # | Hash | 23 | %23 |
| $ | Dollar | 24 | %24 |
| % | Percent | 25 | %25 |
| & | Ampersand | 26 | %26 |
| ' | Apostrophe | 27 | %27 |
| ( | Left Paren | 28 | %28 |
| ) | Right Paren | 29 | %29 |
| * | Asterisk | 2A | %2A |
| + | Plus | 2B | %2B |
| , | Comma | 2C | %2C |
| / | Slash | 2F | %2F |
| : | Colon | 3A | %3A |
| ; | Semicolon | 3B | %3B |
| = | Equals | 3D | %3D |
| ? | Question | 3F | %3F |
| @ | At | 40 | %40 |
Hexadecimal Reference
Valid hex digits (0-15):
0 1 2 3 4 5 6 7 8 9 A B C D E F
(Also lowercase: a b c d e f, though uppercase preferred)
Invalid (not hex):
G H I J K L M N O P Q R S T U V W X Y Z
(Any letter beyond F)
How to Fix Invalid Percent Encoding
Fix 1: Don't Encode at All
Best solution: Avoid characters that need encoding
❌ NEEDS ENCODING:
utm_campaign=Save 50%! Limited Time
✅ NO ENCODING NEEDED:
utm_campaign=save-50-percent-limited-time
Fix 2: Use Proper Encoding Function
JavaScript:
function properlyEncodeUTM(value) {
// First, clean to safe characters (best approach)
const cleaned = value
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/%/g, '-percent')
.replace(/&/g, '-and-')
.replace(/[^a-z0-9-_]/g, '');
return cleaned;
}
// If you MUST preserve original and encode:
function encodeUTMValue(value) {
return encodeURIComponent(value);
}
// Examples
console.log(properlyEncodeUTM('Save 50%! Limited Time'));
// Output: save-50-percent-limited-time
console.log(encodeURIComponent('Save 50%'));
// Output: Save%2050%25 (valid, but avoid this approach)PHP:
<?php
// Don't use raw urlencode on entire UTM value
// Instead, clean the value first
function cleanUTMValue($value) {
$cleaned = strtolower($value);
$cleaned = str_replace(' ', '-', $cleaned);
$cleaned = str_replace('%', '-percent', $cleaned);
$cleaned = preg_replace('/[^a-z0-9-_]/', '', $cleaned);
return $cleaned;
}
echo cleanUTMValue('Save 50%! Limited Time');
// Output: save-50-percent-limited-time
?>Python:
import re
from urllib.parse import quote
def clean_utm_value(value):
"""Best approach: clean to safe characters"""
cleaned = value.lower()
cleaned = cleaned.replace(' ', '-')
cleaned = cleaned.replace('%', '-percent')
cleaned = re.sub(r'[^a-z0-9-_]', '', cleaned)
return cleaned
# If you must encode:
def encode_utm_value(value):
"""Use only if absolutely necessary"""
return quote(value, safe='')
print(clean_utm_value('Save 50%! Limited Time'))
# Output: save-50-percent-limited-time
print(encode_utm_value('Save 50%'))
# Output: Save%2050%25 (valid but not recommended)Fix 3: Validate Before Use
function validateAndFixPercentEncoding(url) {
try {
const urlObj = new URL(url);
let fixed = false;
['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(param => {
const value = urlObj.searchParams.get(param);
if (!value) return;
// Check for invalid encoding
const hasInvalidEncoding =
/%[^0-9A-Fa-f]/.test(value) || // Non-hex after %
/%[0-9A-Fa-f](?![0-9A-Fa-f])/.test(value) || // Single digit after %
value.endsWith('%'); // % at end
if (hasInvalidEncoding) {
// Clean the value
const cleaned = value
.replace(/%[^0-9A-Fa-f]{0,2}/g, '') // Remove invalid sequences
.replace(/%$/, '') // Remove trailing %
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-_]/g, '');
urlObj.searchParams.set(param, cleaned);
fixed = true;
}
});
return {
original: url,
fixed: urlObj.toString(),
wasFixed: fixed
};
} catch (e) {
return {
original: url,
error: e.message
};
}
}
// Usage
const problematic = 'https://example.com?utm_campaign=sale%2Gspecial&utm_source=email%';
const result = validateAndFixPercentEncoding(problematic);
console.log('Original:', result.original);
console.log('Fixed:', result.fixed);
console.log('Was fixed:', result.wasFixed);Prevention Strategies
1. Never Manual Encode
Don't try to encode by hand:
❌ MANUAL ENCODING (error-prone):
Thinking: "Space is %20, so Summer Sale = Summer%20Sale"
Result: Often leads to %2G, %XY type errors
✅ USE FUNCTIONS:
encodeURIComponent('Summer Sale')
Result: Always correct
2. Use Clean Values Only
Best practice: avoid encoding entirely
// URL Builder that prevents encoding issues
function buildCleanUTMUrl(baseUrl, params) {
const url = new URL(baseUrl);
Object.keys(params).forEach(key => {
// Clean value to not need encoding
const clean = params[key]
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/%/g, '-percent')
.replace(/&/g, '-and-')
.replace(/[^a-z0-9-_]/g, '');
url.searchParams.set(key, clean);
});
return url.toString();
}
// Usage
const trackedUrl = buildCleanUTMUrl('https://example.com', {
utm_source: 'Email Newsletter',
utm_campaign: 'Save 50%! Limited Time'
});
console.log(trackedUrl);
// https://example.com?utm_source=email-newsletter&utm_campaign=save-50-percent-limited-time
// No percent encoding needed!3. Validate After Any Tool
Test all URLs from automation:
// After generating URL from any tool
function validateNoInvalidEncoding(url) {
const pattern = /%(?:[^0-9A-Fa-f]|[0-9A-Fa-f](?![0-9A-Fa-f])|$)/;
if (pattern.test(url)) {
throw new Error('Invalid percent encoding detected in URL: ' + url);
}
return true;
}
// Use in your workflow
const generated = urlBuilderTool.generate(params);
validateNoInvalidEncoding(generated); // Throws if invalid
deployurl(generated);4. Regular Audits
Check for encoding issues monthly:
-- Google Analytics 4 BigQuery query
SELECT
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'campaign_name') as campaign,
COUNT(*) as sessions
FROM `project.dataset.events_*`
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY))
AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
AND (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'campaign_name') LIKE '%\%%'
GROUP BY campaign
ORDER BY sessions DESC✅ Fixed this issue? Great! Now check the other 39...
You just fixed one tracking issue. But are your Google Ads doubling sessions? Is Facebook attribution broken? Are internal links overwriting campaigns?
• Connects to GA4 (read-only, OAuth secured)
• Scans 90 days of traffic in 2 minutes
• Prioritizes by revenue impact
• Free forever for monthly audits
Join 2,847 marketers fixing their tracking daily
FAQ
Q: What if my URL has %20 for spaces? Is that invalid?
A: %20 is valid (2 and 0 are hex digits). But it's better to avoid needing encoding by using hyphens instead of spaces.
Q: Can I fix invalid encoding by double-encoding?
A: No! That makes it worse. Fix by removing the encoding and using clean characters only.
Q: Why does %2G appear in my URLs?
A: Likely a broken encoding tool that's using the wrong character set or has a bug. Stop using that tool and switch to proper URL builders.
Q: Will browsers automatically fix invalid encoding?
A: No. Different browsers handle it differently, creating inconsistent data. Some show literal characters, others throw errors.
Q: How do I know if encoding is uppercase or lowercase hex?
A: Both work technically, but RFC 3986 recommends uppercase (%2F not %2f). Most importantly: ensure both characters after % are valid hex (0-9, A-F).
Q: Can analytics tools decode invalid percent encoding?
A: No. If the encoding is malformed, analytics receives and stores the malformed value. Garbage in, garbage out.
Q: What's the most common cause of invalid encoding?
A: Broken URL builder tools, manual encoding attempts, and copy-paste corruption through multiple systems.
Q: Should I decode, clean, then re-encode URLs?
A: No. Decode, clean to safe characters (a-z, 0-9, hyphens), and don't re-encode. This prevents all encoding issues.
Stop invalid percent encoding from breaking your campaign tracking. UTMGuard automatically detects malformed encoding patterns and validates all your UTM parameters. Start your free audit today.