Fix Malformed Percent Encoding: Complete UTM Recovery Guide
"Monday morning: our biggest campaign of the year was live, but analytics showed '%2G' and '%XY' in campaign names. 8,000 email clicks were tracking to gibberish. I had 2 hours to fix it before the executive dashboard review."
This nightmare scenario hit Jennifer Park, director of digital marketing. Here's the exact process she used to fix malformed percent encoding and recover tracking in under 2 hours.
Emergency Fix: Stop the Bleeding
Step 1: Identify Affected Campaigns (5 minutes)
Quick scan in Google Analytics 4:
- Reports → Acquisition → Traffic Acquisition
- Secondary dimension: Session campaign
- Look for these patterns:
%followed by non-hex:%2G,%XY,%ZZ- Trailing
%:campaign% - Incomplete encoding:
%2,%A
Export data to CSV
Critical question: Is the campaign still running?
- YES → Proceed to Step 2 immediately
- NO → Document for data cleanup later
🚨 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
Step 2: Pause Affected Campaigns (2 minutes)
Email Marketing:
Mailchimp: Pause scheduled sends
HubSpot: Pause automation workflows
SendGrid: Pause API-triggered sends
Paid Advertising:
Google Ads: Pause campaigns
Facebook Ads: Pause ad sets
LinkedIn Ads: Pause campaigns
Social Scheduling:
Hootsuite: Delete scheduled posts
Buffer: Pause queue
Sprout Social: Remove from calendar
Step 3: Identify the Malformed Sequences (10 minutes)
Run detection script:
function findMalformedEncoding(urlList) {
const malformed = [];
urlList.forEach(({ id, url }) => {
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: Non-hex after %
const nonHex = value.match(/%[^0-9A-Fa-f]/g);
if (nonHex) {
malformed.push({
campaignId: id,
param,
issue: 'Non-hex characters after %',
examples: nonHex,
original: value
});
}
// Pattern 2: Incomplete encoding
const incomplete = value.match(/%[0-9A-Fa-f](?![0-9A-Fa-f])/g);
if (incomplete) {
malformed.push({
campaignId: id,
param,
issue: 'Incomplete percent encoding',
examples: incomplete,
original: value
});
}
// Pattern 3: Trailing %
if (value.endsWith('%')) {
malformed.push({
campaignId: id,
param,
issue: 'Trailing percent sign',
original: value
});
}
});
} catch (e) {
malformed.push({
campaignId: id,
issue: 'Invalid URL',
error: e.message
});
}
});
return malformed;
}
// Usage
const campaigns = [
{ id: 'EMAIL-001', url: 'https://example.com?utm_campaign=sale%2G' },
{ id: 'FB-123', url: 'https://example.com?utm_source=facebook%XY' },
{ id: 'EMAIL-002', url: 'https://example.com?utm_medium=email%' }
];
const issues = findMalformedEncoding(campaigns);
console.table(issues);Example output:
Campaign ID | Param | Issue | Original
-----------|---------------|--------------------------|------------------
EMAIL-001 | utm_campaign | Non-hex after % | sale%2G
FB-123 | utm_source | Non-hex after % | facebook%XY
EMAIL-002 | utm_medium | Trailing percent sign | email%
😰 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
Step 4: Fix the URLs (20 minutes)
Option A: Decode and Clean (Recommended)
function fixMalformedEncoding(url) {
try {
const urlObj = new URL(url);
const fixes = [];
['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(param => {
const original = urlObj.searchParams.get(param);
if (!original) return;
// Remove all percent encoding (malformed or not)
let cleaned = original.replace(/%[0-9A-Fa-f]{2}/g, match => {
try {
return decodeURIComponent(match);
} catch {
return match;
}
});
// Remove malformed encoding sequences
cleaned = cleaned.replace(/%[^0-9A-Fa-f]{0,2}/g, '');
cleaned = cleaned.replace(/%[0-9A-Fa-f](?![0-9A-Fa-f])/g, '');
cleaned = cleaned.replace(/%$/g, '');
// Clean to safe characters
cleaned = cleaned
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-_]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
if (cleaned !== original) {
fixes.push({ param, original, cleaned });
urlObj.searchParams.set(param, cleaned);
}
});
return {
original: url,
fixed: urlObj.toString(),
changes: fixes
};
} catch (e) {
return {
original: url,
error: e.message
};
}
}
// Example usage
const broken = 'https://example.com?utm_campaign=Summer%2GSale&utm_source=email%XY';
const result = fixMalformedEncoding(broken);
console.log('Original:', result.original);
console.log('Fixed:', result.fixed);
console.log('Changes:', result.changes);
// Output:
// Original: https://example.com?utm_campaign=Summer%2GSale&utm_source=email%XY
// Fixed: https://example.com?utm_campaign=summersale&utm_source=email
// Changes: [
// { param: 'utm_campaign', original: 'Summer%2GSale', cleaned: 'summersale' },
// { param: 'utm_source', original: 'email%XY', cleaned: 'email' }
// ]Option B: Pattern-Specific Fixes
function fixSpecificPatterns(value) {
// Fix %2G (should be %20 for space)
value = value.replace(/%2G/gi, '%20');
// Fix %XY (remove entirely)
value = value.replace(/%XY/gi, '');
// Fix %ZZ (remove entirely)
value = value.replace(/%ZZ/gi, '');
// Fix trailing %
value = value.replace(/%$/g, '');
// Fix incomplete %2 (should be %20 or removed)
value = value.replace(/%2(?![0-9A-Fa-f])/g, '%20');
// Decode any valid sequences
try {
value = decodeURIComponent(value);
} catch {
// If decoding fails, proceed with partial fix
}
// Clean final value
return value
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-_]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}Step 5: Update Platforms (30-60 minutes)
Systematic platform updates:
Email Platforms
Mailchimp:
1. Clone affected campaign
2. Update all tracking links with fixed URLs
3. Test send to seed list
4. Verify links in email
5. Check parameters in browser network tab
6. Resume sending with corrected version
HubSpot:
1. Edit workflow/email
2. Find & Replace URLs:
Find: utm_campaign=Summer%2GSale
Replace: utm_campaign=summer-sale
3. Review all replacements
4. Publish changes
5. Test with internal contact
6. Reactivate workflow
Paid Advertising
Google Ads:
1. Campaign Settings → Campaign URL options
2. Tracking template:
Before:
`{"{"}{"{"}lpurl{"}"}{"}"}}`?utm_source=google&utm_campaign=Sale%2G
After:
`{"{"}{"{"}lpurl{"}"}{"}"}}`?utm_source=google&utm_campaign=sale
3. Apply to all ad groups
4. Review changes
5. Reactivate campaign
Facebook Ads:
1. Edit ad creative
2. URL parameters:
Before:
utm_source=facebook%XY&utm_campaign=sale%2G
After:
utm_source=facebook&utm_campaign=sale
3. Publish changes
4. Re-activate ads
Step 6: Test Everything (15 minutes)
Pre-relaunch checklist:
[ ] Click each fixed URL in browser
[ ] Check browser network tab for parameters
[ ] Verify parameters in GA4 Realtime report
[ ] No % symbols in clean URLs
[ ] All parameters lowercase
[ ] All platforms updated
[ ] Seed test successful
[ ] Documentation updated
Testing script:
async function testFixedURLs(urls) {
const results = [];
for (const url of urls) {
try {
// Parse URL
const urlObj = new URL(url);
const params = urlObj.searchParams;
// Check for remaining encoding issues
let hasIssues = false;
const issues = [];
['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(param => {
const value = params.get(param);
if (!value) return;
// Check for % in final value
if (value.includes('%')) {
hasIssues = true;
issues.push(`${"{"}{"{"}param{"}"}{"}"}} still has % character`);
}
// Check for non-safe characters
if (!/^[a-z0-9-_]+$/.test(value)) {
hasIssues = true;
issues.push(`${"{"}{"{"}param{"}"}{"}"}} has non-safe characters: ${"{"}{"{"}value{"}"}{"}"}}`);
}
});
results.push({
url,
passed: !hasIssues,
issues
});
} catch (e) {
results.push({
url,
passed: false,
issues: ['Invalid URL: ' + e.message]
});
}
}
return results;
}
// Usage
const fixedURLs = [
'https://example.com?utm_campaign=summer-sale&utm_source=email',
'https://example.com?utm_campaign=qa-webinar&utm_source=facebook'
];
const testResults = await testFixedURLs(fixedURLs);
console.log('Test Results:');
testResults.forEach(result => {
console.log(`\nURL: ${result.url}`);
console.log(`Status: ${result.passed ? '✓ PASS' : '✗ FAIL'}`);
if (result.issues.length > 0) {
console.log('Issues:', result.issues.join(', '));
}
});Data Recovery: Cleaning Historical Data
Option 1: Create Data Transformations
Google Analytics 4 custom dimension:
1. Admin → Data Display → Custom Definitions
2. Create custom dimension: "Campaign (Cleaned)"
3. Scope: Event
4. Event parameter: campaign_name
5. Use lookup table or regex to map:
Original: summer%2Gsale → Cleaned: summer-sale
Original: email%XY → Cleaned: email
Option 2: Export and Reprocess
BigQuery data cleaning:
-- Create view with cleaned campaign names
CREATE OR REPLACE VIEW `project.dataset.cleaned_campaigns` AS
SELECT
event_date,
user_pseudo_id,
-- Clean campaign parameter
REGEXP_REPLACE(
LOWER(
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'campaign')
),
r'%[^0-9A-Fa-f]{0,2}|%[0-9A-Fa-f](?![0-9A-Fa-f])|%$',
''
) as cleaned_campaign,
-- Other fields...
FROM `project.dataset.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20240101' AND '20240131'Option 3: Accept Data Loss
When to move forward:
- Affected period < 7 days
- Traffic volume < 5% of total
- Impact < $1,000 in revenue
- Fix cost > value of recovered data
Document and exclude:
Campaign Report Note:
"Data from May 10-12 excluded due to malformed encoding.
Approximately 2,340 sessions affected.
Issue resolved May 13, data quality normal thereafter."
Prevention: Never Again
1. Automated Validation
Pre-deployment check:
function validateBeforeLaunch(url) {
const urlObj = new URL(url);
const params = urlObj.searchParams;
const errors = [];
['utm_source', 'utm_medium', 'utm_campaign'].forEach(param => {
const value = params.get(param);
if (!value) {
errors.push(`Missing ${"{"}{"{"}param{"}"}{"}"}}`);
return;
}
// Check for percent encoding
if (value.includes('%')) {
errors.push(`${"{"}{"{"}param{"}"}{"}"}} contains % (encoding not allowed)`);
}
// Check for safe characters only
if (!/^[a-z0-9-_]+$/.test(value)) {
errors.push(`${"{"}{"{"}param{"}"}{"}"}} contains unsafe characters`);
}
});
if (errors.length > 0) {
throw new Error('Validation failed:\n' + errors.join('\n'));
}
return true;
}
// Use in campaign workflow
try {
validateBeforeLaunch(campaignURL);
launchCampaign(campaignURL);
} catch (e) {
console.error('Cannot launch:', e.message);
// Block launch, require fixes
}2. URL Builder That Prevents Encoding
function buildEncodingFreeURL(baseUrl, params) {
const url = new URL(baseUrl);
Object.keys(params).forEach(key => {
// Clean value to never need encoding
const clean = params[key]
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/%/g, '-percent')
.replace(/&/g, '-and-')
.replace(/[^a-z0-9-_]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
url.searchParams.set(key, clean);
});
// Final validation
const finalURL = url.toString();
if (finalURL.includes('%')) {
throw new Error('URL contains encoding despite cleaning. Check baseUrl.');
}
return finalURL;
}
// This function makes encoding impossible
const safeURL = buildEncodingFreeURL('https://example.com', {
utm_source: 'Email Newsletter',
utm_campaign: 'Save 50%! Limited'
});
console.log(safeURL);
// https://example.com?utm_source=email-newsletter&utm_campaign=save-50-percent-limited3. Weekly Audit
// Run automatically every Monday
async function weeklyEncodingAudit() {
const lastWeekData = await fetchGAData({
startDate: '7daysAgo',
endDate: 'yesterday'
});
const encodingIssues = lastWeekData.filter(row =>
row.campaign.includes('%') ||
row.source.includes('%') ||
row.medium.includes('%')
);
if (encodingIssues.length > 0) {
sendAlert({
subject: '⚠️ Encoding Issues Detected',
body: `Found ${encodingIssues.length} campaigns with % encoding`,
issues: encodingIssues
});
}
}✅ 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: Can I just delete the malformed sequences and keep everything else?
A: Yes, that's often the safest approach. Remove %2G, %XY, etc., decode any valid sequences that remain, then clean to safe characters.
Q: Will fixing URLs break existing tracking links?
A: The old malformed links will continue to track malformed data. New fixed links track correctly. You want this separation to see clean data going forward.
Q: How do I fix encoding in URLs that are already shortened?
A: You need to access the destination URL in your link shortener (Bitly, ow.ly, etc.), fix it there, and update the short link's destination.
Q: What if the encoding is in the base URL, not the parameters?
A: That's a different issue (path encoding). For UTM parameters specifically, ensure clean parameter values. The base URL encoding should be handled by your CMS/site.
Q: Can Google Analytics auto-fix malformed encoding?
A: No. GA4 records exactly what it receives. If the encoding is malformed, that's what gets stored. Fix at the source.
Q: Should I pause ALL campaigns or just affected ones?
A: Only affected ones. Don't disrupt working campaigns. Identify specifically which have malformed encoding and fix those only.
Q: How long does the fix process typically take?
A: For a medium-sized operation: Detection (10 min), fixing URLs (30 min), platform updates (1-2 hours), testing (15 min). Total: 2-3 hours.
Q: What's the best way to prevent this from happening again?
A: Use a URL builder that cleans values to safe characters only (no encoding needed). Add validation that rejects any URL containing %. Train team to use the tool exclusively.
Stop fighting malformed percent encoding. UTMGuard detects encoding issues before they break your tracking and provides instant fixes. Start your free audit today.