Automatically strips known privacy-invading tracking parameters from all URLs, links, and network requests
// ==UserScript==
// @name URL Tracking Parameter Cleaner
// @namespace VVJMIFRyYWNraW5nIFBhcmFtZXRlciBDbGVhbmVy
// @version 1.0
// @description Automatically strips known privacy-invading tracking parameters from all URLs, links, and network requests
// @author smed79
// @license GPLv3
// @icon https://i25.servimg.com/u/f25/11/94/21/24/utpc10.png
// @match *://*/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Tracking parameters - wildcard * supported
const rawParams = `action_ref_*, action_type_*, hsa_*, itm_*, matomo_*, mkt_*, mtm_*, ns_*, piwik_*, pk_*,
sb_referer_*, sms_*, trk_*, tw_*, url_bnm_*, utm_*, zone_*, __s, _branch_match_id, _bta_c,
_bta_tid, _ga, _gac, _gl, _hsenc, _hsmi, _ke, _openstat, action_object_map, ad_id,
adgroupid, adjust_campaign, adjust_tracker, af_campaign, af_channel, af_keyword, af_medium,
af_source, awc, bclid, campaign_id, campid, cid, cj_event, cjevent, click_id, cmpid,
customid, dclid, dm_i, ef_id, elqTrack, epik, fb_action_ids, fb_action_types, fb_ref,
fb_source, fbclid, gbraid, gclid, gclsrc, gdffi, gdfms, gdftrk, hc_location, hc_ref,
hootPostID, hsCtaTracking, hubspotUtk, idzone, igshid, irclid, lptoken, mc_cid, mc_eid,
mkcid, mkevt, mkrid, mkwid, msclkid, ndclid, pcampaignid, pcrid, psid, pub_id, pubfeed,
publisherid, rdt, ref, ref_campaign, ref_source, s_kwcid, scm, scontext_r, si, srsltid,
toolid, ttclid, twclid, uclick, wbraid, wprov, wt_mc, WT.mc_id, WT.nav, yclid, zanpid,
zoneid`.split(',').map(p => p.trim().toLowerCase());
// Pre-compile Regexes for performance
const trackingRegexes = rawParams.map(pattern => {
if (pattern.includes('*')) {
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
const regex = escaped.replace(/\*/g, '[^&=]*');
return new RegExp(`^${regex}$`, 'i');
}
return new RegExp(`^${pattern}$`, 'i');
});
function isTrackingParam(paramName) {
return trackingRegexes.some(regex => regex.test(paramName));
}
function cleanUrl(url) {
if (!url || typeof url !== 'string' || url.startsWith('blob:') || url.startsWith('data:') || url.startsWith('javascript:')) return url; // Check to skip blob and data URLs
try {
const urlObj = new URL(url, window.location.origin);
const params = new URLSearchParams(urlObj.search);
// Safely iterate by converting keys to an array first
const keysToDelete = Array.from(params.keys()).filter(isTrackingParam);
if (keysToDelete.length === 0) return url; // No changes needed
keysToDelete.forEach(key => params.delete(key));
urlObj.search = params.toString();
// Return relative path if original URL was relative (started with /)
if (url.startsWith('/') && !url.startsWith('//')) {
return urlObj.pathname + urlObj.search + urlObj.hash;
}
return urlObj.toString();
} catch (e) {
return url;
}
}
function cleanElementAttribute(element, attribute) {
if (element.hasAttribute(attribute)) {
const url = element.getAttribute(attribute);
// Relaxed the check to allow relative URLs like "/path?utm_..."
if (url && !url.startsWith('javascript:') && !url.startsWith('mailto:')) {
const cleaned = cleanUrl(url);
if (cleaned !== url) {
element.setAttribute(attribute, cleaned);
}
}
}
}
function cleanPage() {
document.querySelectorAll('a[href]').forEach(el => cleanElementAttribute(el, 'href'));
document.querySelectorAll('form[action]').forEach(el => cleanElementAttribute(el, 'action'));
document.querySelectorAll('iframe[src], script[src], img[src]').forEach(el => cleanElementAttribute(el, 'src'));
// Clean img/picture srcset
document.querySelectorAll('[srcset]').forEach(el => {
let srcset = el.getAttribute('srcset');
if (srcset) {
const cleanedSrcset = srcset.split(',').map(part => {
const [url, ...rest] = part.trim().split(' ');
return cleanUrl(url) + (rest.length ? ' ' + rest.join(' ') : '');
}).join(',');
if (cleanedSrcset !== srcset) el.setAttribute('srcset', cleanedSrcset);
}
});
// Clean meta refresh
document.querySelectorAll('meta[http-equiv="refresh" i]').forEach(meta => {
const content = meta.getAttribute('content');
if (content) {
const match = content.match(/url=([^;]+)/i);
if (match) {
const url = match[1].replace(/^['"]|['"]$/g, '');
const cleanedUrl = cleanUrl(url);
if (cleanedUrl !== url) {
meta.setAttribute('content', content.replace(url, cleanedUrl));
}
}
}
});
// Clean data attributes
document.querySelectorAll('[data-url], [data-href], [data-src]').forEach(element => {
['data-url', 'data-href', 'data-src'].forEach(attr => cleanElementAttribute(element, attr));
});
}
function injectPageScript() {
// Passing our functions into the page context
const pageScript = `
(function() {
const trackingRegexes = [${trackingRegexes.map(r => r.toString()).join(',')}];
function isTrackingParam(paramName) {
return trackingRegexes.some(regex => regex.test(paramName));
}
function cleanUrl(url) {
if (!url || typeof url !== 'string' || url.startsWith('blob:') || url.startsWith('data:') || url.startsWith('javascript:')) return url; // Check to skip blob and data URLs
try {
const urlObj = new URL(url, window.location.origin);
const params = new URLSearchParams(urlObj.search);
const keysToDelete = Array.from(params.keys()).filter(isTrackingParam);
if (keysToDelete.length === 0) return url;
keysToDelete.forEach(key => params.delete(key));
urlObj.search = params.toString();
if (url.startsWith('/') && !url.startsWith('//')) {
return urlObj.pathname + urlObj.search + urlObj.hash;
}
return urlObj.toString();
} catch (e) {
return url;
}
}
// Intercept window.location
const originalLocationAssign = window.location.assign;
const originalLocationReplace = window.location.replace;
try {
const originalHrefSetter = Object.getOwnPropertyDescriptor(Location.prototype, 'href').set;
Object.defineProperty(Location.prototype, 'href', {
set: function(url) {
originalHrefSetter.call(this, cleanUrl(url));
},
get: Object.getOwnPropertyDescriptor(Location.prototype, 'href').get
});
} catch(e) {} // Fails safely in strict environments
window.location.assign = function(url) {
originalLocationAssign.call(window.location, cleanUrl(url));
};
window.location.replace = function(url) {
originalLocationReplace.call(window.location, cleanUrl(url));
};
// Intercept XHR
const originalXhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
const stringUrl = typeof url === 'string' ? url : url.toString();
const cleaned = cleanUrl(stringUrl);
// Only modify if the URL actually changed
if (cleaned !== stringUrl) {
return originalXhrOpen.call(this, method, cleaned, ...rest);
}
return originalXhrOpen.call(this, method, url, ...rest);
};
})();
`;
const script = document.createElement('script');
script.textContent = pageScript;
if (document.documentElement) document.documentElement.appendChild(script);
}
// Intercept fetch
const originalFetch = window.fetch;
window.fetch = function(...args) {
if (args[0]) {
if (typeof args[0] === 'string') {
const cleaned = cleanUrl(args[0]);
if (cleaned !== args[0]) {
args[0] = cleaned;
}
} else if (typeof args[0] === 'object' && args[0].url) {
const cleaned = cleanUrl(args[0].url);
// ONLY clone the Request object if the URL had tracking params removed
if (cleaned !== args[0].url) {
args[0] = new Request(cleaned, args[0]);
}
}
}
return originalFetch.apply(this, args);
};
// Clean current page URL in address bar if tracking params exist
const cleanedCurrentUrl = cleanUrl(window.location.href);
if (cleanedCurrentUrl !== window.location.href) {
window.history.replaceState(null, '', cleanedCurrentUrl);
}
// Debounce function to prevent lag from MutationObserver
let timeout;
const observer = new MutationObserver((mutations) => {
const hasAdditions = mutations.some(m => m.addedNodes.length > 0);
if (hasAdditions) {
clearTimeout(timeout);
timeout = setTimeout(() => cleanPage(), 300); // Wait 300ms after DOM settles
}
});
injectPageScript();
// Initial Run on DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', cleanPage);
} else {
cleanPage();
}
observer.observe(document.documentElement || document.body, { childList: true, subtree: true });
})();