URL Tracking Parameter Cleaner

Automatically strips known privacy-invading tracking parameters from all URLs, links, and network requests

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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 });
})();