Automatically purges tracking parameters from URLs for privacy and pristine link sharing.
// ==UserScript==
// @name LinkSanitizer
// @namespace https://github.com/local/universal-detracker
// @version 1.0.0
// @description Automatically purges tracking parameters from URLs for privacy and pristine link sharing.
// @author r0cketdev1
// @license MIT
// @match *://*/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
'use strict';
// ─── Tracking parameter list ────────────────────────────────────────────────
// Covers: Google Analytics/Ads, Meta/Facebook, Microsoft/Bing, Twitter/X,
// HubSpot, Mailchimp, Marketo, Klaviyo, Drip, SendGrid, TikTok, Pinterest,
// LinkedIn, Snapchat, Yahoo, Reddit, Amazon, and many more.
const TRACKING_PARAMS = new Set([
// Google / YouTube
'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
'utm_id', 'utm_source_platform', 'utm_creative_format', 'utm_marketing_tactic',
'gclid', 'gclsrc', 'gad_source', 'gbraid', 'wbraid', 'dclid',
'_ga', '_gl',
// Meta / Facebook / Instagram
'fbclid', 'fb_action_ids', 'fb_action_types', 'fb_ref', 'fb_source',
'fbaid', 'mc_cid', 'mc_eid',
// Microsoft / Bing
'msclkid',
// Twitter / X
'twclid',
// TikTok
'ttclid',
// Pinterest
'epik', 'pin_unshorten',
// Snapchat
'sccid', 'sc_irclid',
// LinkedIn
'li_fat_id', 'trk', 'trkInfo',
// Yahoo
'yclid',
// Reddit
'rdt_cid',
// Amazon
'tag', 'ascsubtag', 'linkCode', 'linkId', 'ref_',
// HubSpot
'_hsenc', '_hsmi', 'hsa_acc', 'hsa_ad', 'hsa_cam', 'hsa_grp',
'hsa_kw', 'hsa_mt', 'hsa_net', 'hsa_src', 'hsa_tgt', 'hsa_ver',
// Mailchimp
'mc_cid', 'mc_eid',
// Marketo
'mkt_tok',
// Klaviyo
'_kx',
// Drip
'__s',
// SendGrid / SparkPost
'sg_ehash',
// Vero
'vero_conv', 'vero_id',
// Iterable
'iterableEmailCampaignId', 'iterableTemplateId', 'iterableMessageId',
// Braze (Appboy)
'abe',
// Adobe Analytics / Experience Cloud
's_cid', 's_kwcid', 'ef_id', 'msid',
// Outbrain
'obOrigUrl',
// Taboola
'tblci',
// General
'ref', 'referrer', 'source', 'campaign', 'affiliate', 'aff_id',
'click_id', 'clickid', 'cmpid', 'adid', 'adgroupid', 'creative',
'matchtype', 'network', 'placement', 'position', 'device',
'devicemodel', 'keyword', 'ad_type', 'ad_format', 'partner_id',
'irclickid', 'irgwc', 'subid', 'sub_id', 'sub1', 'sub2', 'sub3',
]);
// Regex for wildcard prefix matches (e.g. utm_*, _ga*, etc.)
const TRACKING_PREFIXES = [
/^utm_/i,
/^hsa_/i,
/^fb_/i,
/^yclid/i,
/^mc_/i,
];
// strip tracking params from a URL string
function cleanURL(rawURL) {
let url;
try {
url = new URL(rawURL);
} catch {
return rawURL; // not a valid URL, return unchanged
}
const before = url.search;
const params = url.searchParams;
const toDelete = [];
for (const key of params.keys()) {
const lower = key.toLowerCase();
if (
TRACKING_PARAMS.has(lower) ||
TRACKING_PARAMS.has(key) ||
TRACKING_PREFIXES.some(rx => rx.test(key))
) {
toDelete.push(key);
}
}
toDelete.forEach(k => params.delete(k));
const after = url.search;
return before !== after ? url.toString() : rawURL;
}
// Phase 1: Initial sweep on page load
function sweepCurrentURL() {
const clean = cleanURL(window.location.href);
if (clean !== window.location.href) {
try {
window.history.replaceState(
window.history.state,
document.title,
clean
);
} catch (e) {
// Cross-origin or restricted — silently skip
}
}
}
sweepCurrentURL();
// Phase 2: Dynamic monitoring for SPAs
// Override history.pushState
const originalPushState = history.pushState.bind(history);
history.pushState = function (state, title, url) {
const cleanedURL = url ? cleanURL(String(url)) : url;
return originalPushState(state, title, cleanedURL);
};
// Override history.replaceState
const originalReplaceState = history.replaceState.bind(history);
history.replaceState = function (state, title, url) {
const cleanedURL = url ? cleanURL(String(url)) : url;
return originalReplaceState(state, title, cleanedURL);
};
// Listen for popstate (back/forward navigation)
window.addEventListener('popstate', () => {
sweepCurrentURL();
});
// Phase 3: Copy interceptor
document.addEventListener('copy', function (e) {
// Only intercept if clipboard API is writable
if (!e.clipboardData) return;
// Try to get selected text
const selection = window.getSelection();
if (!selection || selection.isCollapsed) return;
const selectedText = selection.toString().trim();
if (!selectedText) return;
// Check if the selected text looks like a URL
let isURL = false;
try {
const parsed = new URL(selectedText);
isURL = parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch {
isURL = false;
}
if (!isURL) return;
const cleanedText = cleanURL(selectedText);
if (cleanedText === selectedText) return; // nothing to clean
e.preventDefault();
e.clipboardData.setData('text/plain', cleanedText);
e.clipboardData.setData('text/html', cleanedText);
}, true);
// Phase 3b: Right-click link copy (contextmenu)
document.addEventListener('mousedown', function (e) {
if (e.button !== 2) return; // right-click only
const anchor = e.target.closest('a[href]');
if (!anchor) return;
const originalHref = anchor.getAttribute('href');
if (!originalHref) return;
let fullURL;
try {
fullURL = new URL(originalHref, window.location.href).toString();
} catch {
return;
}
const cleanedHref = cleanURL(fullURL);
if (cleanedHref === fullURL) return;
// Temporarily swap the href, restore after menu closes
anchor.setAttribute('href', cleanedHref);
const restore = () => {
anchor.setAttribute('href', originalHref);
document.removeEventListener('mouseup', restore);
};
document.addEventListener('mouseup', restore);
}, true);
})();