LinkSanitizer

Automatically purges tracking parameters from URLs for privacy and pristine link sharing.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

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

})();