tracking-params.ts raw

   1  /**
   2   * Known tracking-only query parameters that Warden strips from URLs
   3   * before launching the chosen browser.
   4   *
   5   * Three tiers:
   6   *   1. Global deny-list (TRACKING_PARAMS) — safe to strip everywhere
   7   *   2. Prefix-based deny-list (TRACKING_PREFIXES) — catches vendor families
   8   *   3. Per-site rules (SITE_TRACKING_RULES) — host-specific with preserve
   9   *      lists so functional params (q, v, t, sort, etc.) are never stripped
  10   *      on the wrong platform.
  11   *
  12   * Based on hand-vetted rules from FixupXer, Brave, and ClearURLs.
  13   */
  14  
  15  // ────────────────────────────────────────────────────────────────────────────
  16  // Global deny-list — safe to strip on every domain
  17  // ────────────────────────────────────────────────────────────────────────────
  18  export const TRACKING_PARAMS: ReadonlySet<string> = new Set([
  19    'utm_id', 'utm_name', 'utm_brand',
  20    'gclid', 'gclsrc', 'dclid', 'gbraid', 'wbraid',
  21    '_ga', '_gl', 'gad_source', 'gad_source_',
  22    'guccounter', 'guce_referrer', 'guce_referrer_sig',
  23    'fbclid', 'fb_action_ids', 'fb_action_types', 'fb_ref', 'fb_source',
  24    'msclkid', 'twclid', '__twitter_impression',
  25    'yclid', '_openstat',
  26    'mc_cid', 'mc_eid',
  27    '_hsenc', '_hsmi', '__hssc', '__hstc', 'hsCtaTracking', 'hsctatracking',
  28    'mkt_tok', 'sfmc_activityid',
  29    'elqTrackId', 'elqTrack',
  30    'pk_keyword',
  31    'vero_conv', 'vero_id',
  32    '_branch_match_id', '_branch_referrer',
  33    's_cid', 's_kwcid',
  34    'sc_campaign', 'sc_channel', 'sc_content',
  35    'epik', 'pp',
  36    'igshid', 'igsh',
  37    'tt_medium', 'tt_content',
  38    'trk', 'trkCampaign',
  39    'ko_click_id', 'sscid', 'zanpid', 'sharedid',
  40    'ranmid', 'raneaid', 'ransiteid',
  41    'shareasale_site_id', 'shareasale_user_id',
  42    'spjobid', 'spmailingid', 'spreportid', 'spuserid',
  43    'irclickid', 'irgwc', 'rb_clickid',
  44    '_bta_tid', '_bta_c', 'ml_subscriber', 'ml_subscriber_hash',
  45    'oly_anon_id', 'oly_enc_id', 'wickedid',
  46    '_ke', '_kx', '__s', 'vgo_ee',
  47  ]);
  48  
  49  export const TRACKING_PREFIXES: readonly string[] = [
  50    'utm_', 'pk_', 'mtm_', 'wt.', 'ga_', 'cid_',
  51    'matomo_', 'piwik_', 'wt_', 'itm_', 'elq', 'xtor', 'at_', 'fb_',
  52  ];
  53  
  54  export type SiteRuleEntry = {
  55    remove: ReadonlySet<string>;
  56    preserve?: ReadonlySet<string>;
  57    paths?: readonly string[];
  58  };
  59  
  60  export const SITE_TRACKING_RULES: ReadonlyMap<string, SiteRuleEntry> = new Map([
  61  
  62    // ─── Amazon ───
  63    ['amazon.com', { remove: new Set([
  64      'tag', 'linkCode', 'linkId', 'camp', 'creative', 'creativeASIN', 'adId',
  65      'th', 'psc', 'ref', 'ref_', 'pf_rd_p', 'pf_rd_r', 'pf_rd_s', 'pf_rd_t',
  66      'pd_rd_w', 'pd_rd_r', 'pd_rd_i', 'pd_rd_wg',
  67      'qid', 'rank', 'spIA', 'spLa', 'sr', 'ie', 'sprefix', 'sp_csd',
  68      'coliid', 'colid',
  69      'refRID', 'mgh', 'pebp', 'redirectId', 'sourceid',
  70      'dib', 'dib_tag', 'dsd', 'psd', 'crid', 'cri', 'cv_ct_id', 'cv_ct_pg',
  71      'imprToken', 'mrntc', 'corrId',
  72      'node', 'refTagSuffix', 'refSrcId', 'reftag',
  73      'aaxitk', 'ascs', 'ascsubtag', 'asc_campaign', 'asc_source', 'asc_refurl',
  74      'sa-no-redirect', 'xpid',
  75      'bestSellersRank', 'recommendationSource', 'recommendationType',
  76      'useRedirectOnSuccess', 'rw_html_to_wsrp', 'rw_useCurrentProtocol',
  77    ]), preserve: new Set([
  78      'keywords', 'k', 'i', 'rh', 's', 'page', 'node',
  79      'field-keywords', 'field-brand', 'field-price-range',
  80      'field-availability', 'sort', 'lo', 'fs',
  81    ]) }],
  82  
  83    ['amzn.to', { remove: new Set(['tag', 'linkCode', 'ref']), preserve: new Set() }],
  84    ['amzn.eu', { remove: new Set(['tag', 'linkCode', 'ref']), preserve: new Set() }],
  85    ['amzn.asia', { remove: new Set(['tag', 'linkCode', 'ref']), preserve: new Set() }],
  86  
  87    // ─── Google Search ───
  88    ['google.com', { remove: new Set([
  89      'ved', 'ei', 'usg', 'sa', 'source', 'sourceid', 'sxsrf',
  90      'rlz', 'biw', 'bih', 'sei', 'gs_lcp', 'gs_l',
  91      'oq', 'aqs', 'cs', 'output', 'bg', 'dpr',
  92      'sca_esv', 'sca_upv', 'si', 'ictx', 'uact',
  93      'cd', 'd', 'sclient', 'gs_ri', 'gs_mss',
  94      'sig2', 'sig', 'itctx', 'cad', 'authuser', 'es_sm',
  95      'imgar', 'imgsz', 'imgtype', 'imgc', 'imgdii',
  96      'amp', 'amp_gsa', 'amp_js_v', 'gws_rd', 'gfe_rd',
  97    ]), preserve: new Set([
  98      'q', 'tbm', 'tbs', 'start', 'num', 'hl', 'lr', 'safe',
  99      'nfpr', 'filter', 'as_q', 'as_epq', 'as_oq', 'as_eq',
 100      'as_qdr', 'as_rights', 'imgsz', 'imgtype', 'imgc', 'gl', 'cr',
 101      'search', 'udm',
 102    ]) }],
 103  
 104    // ─── YouTube ───
 105    ['youtube.com', { remove: new Set([
 106      'si', 'feature', 'pp', 'ab_channel',
 107      'app', 'gclid', 'ytclid', 'embeds_referring_euri',
 108      'source_ve_path', 'webpage', 'bpctr',
 109      'has_verified', 'themeRefresh', 'embeds_euri', 'embeds_origin',
 110      'persist_app', 'persist_gl',
 111      'sourceid', 'sid', 'search_source',
 112      'session_id', 'ei', 'ved',
 113      'surl', 'gws_rd', 'gfe_rd',
 114      'lc', 'h', 'w', 'ucbcb', 'gvs',
 115      'android_app', 'ios_app',
 116      'vq', 'cq', 'embed_config',
 117      'continuePlayback', 'watch_next',
 118      'notif_id', 'notif_t', 'sp',
 119      'ad', 'adformat', 'adsense', 'yt_ad',
 120      'sub_confirmation', 'shelf_id', 'playnext',
 121      'hl', 'gl', 'persist_hl',
 122      'annotation_id', 'annotation_prefix', 'annotation_properties',
 123    ]), preserve: new Set([
 124      'v', 't', 'list', 'index', 'start', 'end',
 125      'search_query', 'q', 'radio', 'p', 'page', 'sort',
 126      'view', 'shelf_id', 'nolist', 'si',
 127    ]) }],
 128  
 129    ['youtu.be', { remove: new Set([
 130      'si', 'feature', 'pp', 'ab_channel', 't', 'app',
 131    ]), preserve: new Set(['v', 't', 'list', 'index']) }],
 132  
 133    ['youtube-nocookie.com', { remove: new Set([
 134      'si', 'feature', 'pp', 'ab_channel', 't', 'app',
 135    ]), preserve: new Set(['v', 't', 'list', 'index']) }],
 136  
 137    // ─── Twitter / X ───
 138    ['twitter.com', { remove: new Set([
 139      's', 't', 'ref_src', 'ref_url', 'via', 'cn', 'src',
 140      'mx', 'mxId',
 141      'tw_campaign', 'tw_source', 'tw_term', 'tw_adid',
 142      'cxt', 'ct', 'cta',
 143      'widget_id', 'original_referer',
 144      'card', 'cards_platform',
 145      'conversation_id', 'promoted_content_id',
 146      'amp', 'amp_js_v', 'amp_gsa',
 147      'ads', 'ad_id', 'ads_tracking', 'ad_tracking',
 148      'prefetchTimestamp', 'view',
 149      'focused', 'scroll', 'react', 'reaction',
 150    ]), preserve: new Set([
 151      'lang', 'theme', 'display', 'tz',
 152      'include_profile_interstitial_type', 'include_blocking',
 153      'include_blocked_by', 'include_followed_by',
 154      'include_want_retweets', 'include_mute_edge', 'include_can_dm',
 155    ]) }],
 156  
 157    ['x.com', { remove: new Set([
 158      's', 't', 'ref_src', 'ref_url', 'via', 'cn', 'src',
 159    ]), preserve: new Set(['lang', 'theme']) }],
 160  
 161    // ─── Instagram ───
 162    ['instagram.com', { remove: new Set([
 163      'igsh', 'igshid', 'ig_cache_key', 'ig_mid', 'ig_share_sheet',
 164      '__a', '__d', '_rdr', 'hl',
 165      'ig_share', 'ig_share_id', 'ig_share_source',
 166      'ig_shortcode', 'ig_media_id',
 167      'ig_source', 'ig_campaign_id', 'ig_ad_id',
 168      'mibextid', 'epa', 'xtor',
 169      'trk', 'trkCampaign', 'trkInfo',
 170      'ref', 'referral', 'referral_source',
 171      'trackId', 'trackingId', 'trackingid',
 172      'requestId', 'sessionId',
 173      'e', 'f', 'g', 'h',
 174      'device_id', 'os', 'platform',
 175      'feed_type', 'pinned', 'st',
 176      'preview', 'draft', 'aft', 'csr',
 177    ]), preserve: new Set([
 178      'img_index', 'story_media_id', 'h', 'w',
 179      'carousel_index', 'media_id', 'reel_ids', 'highlight_reel_ids', 'lang',
 180    ]) }],
 181  
 182    // ─── Facebook ───
 183    ['facebook.com', { remove: new Set([
 184      '__cft__', '__tn__', '__xts__', '__user', '__a', '__req',
 185      'mibextid', 'epa', '_gl',
 186      'rc', 'ref', 'referral', 'refsrc', 'comment_tracking',
 187      'hc_location', 'hc_ref',
 188      'av', 'v', 'source', 'sourceid', 'sr', 'sk',
 189      'set', 'type', 'theater', 'locale',
 190      'eid', 'ft', 'ftid', 'ftype', 'acontext', 'aref', 'arefid',
 191      'feed', 'feed_sub', 'feed_story', 'feedtype',
 192      'timeline_context_item_source', 'section_header_type',
 193      'timeline', 'focus', 'focused_comment_id',
 194      'reaction', 'reaction_comment_id',
 195      'notif', 'notif_id', 'notif_t', 'notifications',
 196      'suggested', 'suggestion', 'co', 'coid', 'co_referrer',
 197      'highlight', 'highlight_id',
 198      'promoted', 'promoted_id', 'promoted_post',
 199      'ads', 'ad_id', 'adset_id', 'campaign_id', 'creative',
 200      'post', 'post_id', 'story_fbid', 'comment_id',
 201      'fbid', 'id', 'share', 'share_id', 'share_source',
 202      'tab', 'section', 'filter', 'sort', 'order',
 203      'cursor', 'after', 'before',
 204      'pp', 'pp_source', 'pp_campaign',
 205      'pa', 'pa_source', 'pa_campaign',
 206      'sk', 'skey', 'skeys',
 207      'location', 'lat', 'lng', 'near',
 208      'event', 'event_time', 'event_id',
 209      'device', 'device_id', 'os', 'app',
 210      'session', 'session_id', 'request', 'req', 'req_id',
 211      'track', 'tracking', 'tracker',
 212      'click', 'click_id', 'click_tracking',
 213      'impression', 'impression_id',
 214      'te', 'te_source', 'te_campaign',
 215    ]), preserve: new Set([
 216      'id', 'story_fbid', 'fbid', 'set', 'type', 'theater', 'av', 'v',
 217      'locale', 'ref', 'hc_location', 'comment_id', 'reply_comment_id',
 218      'notif_id', 'notif_t', 'tab', 'sk', 'filter', 'section',
 219    ]) }],
 220  
 221    ['fb.com', { remove: new Set(['__cft__', '__tn__', 'mibextid', 'epa', 'ref', 'rc']), preserve: new Set(['id', 'locale', 'ref']) }],
 222  
 223    // ─── Reddit ───
 224    ['reddit.com', { remove: new Set([
 225      'context', 'correlator', 'rdt_cid', 'share_id',
 226      'source', 'ref', 'ref_source', 'ref_url',
 227      'sort', 't', 'type', 'restrict_sr', 'include_over_18',
 228      'after', 'before', 'limit', 'count',
 229      'show', 'raw_json', 'depth', 'showmore', 'sr_detail',
 230      'sr', 'is_multi', 'comment_sort', 'comment_limit',
 231      'feed', 'feed_length', 'feed_sort',
 232      'user', 'username', 'action', 'reason', 'rule',
 233      'embed', 'embed_type', 'theme', 'lang', 'locale',
 234      'device', 'device_id', 'session', 'session_id',
 235      'tracking', 'tracking_id',
 236      'ads', 'ad_id', 'campaign', 'campaign_id', 'creative',
 237      'experiment', 'variant', 'st', 'sk', 'token',
 238      'geo_filter', 'geo_filter_id',
 239    ]), preserve: new Set([
 240      'context', 'sort', 't', 'q', 'type', 'restrict_sr',
 241      'include_over_18', 'after', 'before', 'limit', 'count',
 242      'show', 'raw_json', 'depth', 'showmore', 'sr_detail',
 243      'sr', 'is_multi',
 244    ]) }],
 245  
 246    ['redd.it', { remove: new Set(['context', 'correlator', 'share_id']), preserve: new Set() }],
 247  
 248    // ─── TikTok ───
 249    ['tiktok.com', { remove: new Set([
 250      '_r', '_t', '_d', 'tt_from', 'share_source',
 251      'is_from_webapp', 'is_copy_url',
 252      'language', 'lang', 'region',
 253      'traffic_type', 'traffic_source',
 254      'campaign_id', 'adgroup_id', 'creative_id',
 255      'ad_id', 'ad_name', 'ad_platform',
 256      'previous_page', 'refer', 'referer', 'referrer',
 257      'source', 'source_type', 'source_app',
 258      'session_id', 'request_id', 'server_id',
 259      'device_id', 'os', 'platform', 'app_version', 'app_name',
 260      'carrier', 'connection_type', 'network_type',
 261      'screen_width', 'screen_height', 'density',
 262      'timezone', 'time', 'timestamp', 'ts',
 263      'nav_source', 'tab', 'section',
 264      'feed_type', 'content_type', 'sort_type', 'display_method',
 265      'is_ads', 'ad_tracking', 'ads_tracking',
 266      'A/B', 'abtest', 'experiment',
 267      'link_id', 'click_id', 'impression_id', 'view_id',
 268      'search_id', 'search_keyword',
 269      'hashtag', 'sound', 'effect', 'creation_time',
 270      'modal', 'is_fullscreen', 'embed',
 271      'is_playlist', 'playlist_id',
 272      'live', 'live_id', 'stream_id',
 273      'commerce', 'product_id', 'shop_id',
 274      'social', 'social_source', 'social_share',
 275      'follow', 'following', 'user_id',
 276    ]), preserve: new Set([
 277      'lang', 'q', 't', 'embed', 'video_id', 'item_id',
 278      'modal', 'search_keyword', 'sort_type', 'content_type',
 279      'creation_time', 'hashtag', 'sound', 'effect',
 280      'display_method', 'is_fullscreen',
 281    ]) }],
 282  
 283    ['tiktokcdn.com', { remove: new Set(['_r', '_t', '_d', 'tt_from', 'share_source']), preserve: new Set() }],
 284    ['tiktokv.com', { remove: new Set(['_r', '_t', '_d', 'tt_from', 'share_source']), preserve: new Set() }],
 285  
 286    // ─── LinkedIn ───
 287    ['linkedin.com', { remove: new Set([
 288      'trk', 'trkInfo', 'trkEmail',
 289      'li_theme', 'li_ft', 'li_mc', 'li_tc', 'li_uuid', 'li_at', 'li_rm',
 290      'trackingId', 'tracking', 'tracker', 'tracking_source',
 291      'ref', 'refId', 'refUrl', 'original_referer', 'originalSubdomain',
 292      'source', 'source_app', 'source_type',
 293      'sessionId', 'session_redirect', 'session_redirect_count',
 294      'eid', 'mid', 'midToken', 'requestToken',
 295      'geoUrn', 'region',
 296      'feature', 'feature_id', 'feature_type',
 297      'event', 'event_id', 'event_name',
 298      'messageId', 'conversationId', 'threadId',
 299      'recommendationId', 'recommendationType',
 300      'companyId', 'groupId',
 301      'articleId', 'articleUrn', 'activityId',
 302      'searchId', 'searchOrigin', 'searchType',
 303      'ads', 'adId', 'campaignId', 'creativeId',
 304      'learning', 'learningPathId', 'courseId',
 305    ]), preserve: new Set([
 306      'keywords', 'origin', 'q', 'start', 'count', 'filters',
 307      'sortBy', 'facet', 'geoUrn', 'network',
 308      'schoolFilter', 'companySize', 'datePosted', 'jobType',
 309      'location', 'redirectUrl', 'response_type',
 310      'client_id', 'state', 'scope',
 311    ]) }],
 312  
 313    ['lnkd.in', { remove: new Set(['trk', 'trkInfo']), preserve: new Set() }],
 314  
 315    // ─── Substack ───
 316    ['substack.com', { remove: new Set([
 317      'r', 'ref', 'referrer', 'referral', 'referral_code',
 318      'token', 'access_token', 'token_id',
 319      'source', 'source_app', 'source_type',
 320      'open', 'openId', 'click', 'clickId',
 321      'session', 'session_id',
 322      'device', 'device_id', 'os',
 323      'email', 'email_id', 'email_type', 'email_source',
 324      'subscriber', 'subscriber_id',
 325      'publication', 'post', 'post_id', 'comment_id',
 326      'search', 'search_query', 'search_id',
 327      'sort', 'order', 'section', 'tag',
 328      'ab_test', 'variant', 'experiment',
 329    ]), preserve: new Set(['publication_id', 'post_id']) }],
 330  
 331    // ─── Lighter platforms ───
 332    ['wikipedia.org',   { remove: new Set(['wprov']), preserve: new Set() }],
 333    ['threads.net',      { remove: new Set(['igshid', 'xmt']), preserve: new Set() }],
 334    ['threads.com',      { remove: new Set(['igshid', 'xmt']), preserve: new Set() }],
 335    ['twitch.tv',        { remove: new Set(['tt_medium', 'tt_content', 'ref', 'referrer']), preserve: new Set() }],
 336    ['spotify.com',      { remove: new Set(['si', 'dl_branch', 'dl_mobileapp']), preserve: new Set(['context', 'uri']) }],
 337    ['pinterest.com',    { remove: new Set(['e_t', 'e_t_s', 'e_t_cs', 'ouuid', 'pin_unauth', 'pp']), preserve: new Set() }],
 338    ['snapchat.com',     { remove: new Set(['share_id']), preserve: new Set() }],
 339    ['whatsapp.com',     { remove: new Set(['link_source', 'link_medium']), preserve: new Set() }],
 340    ['medium.com',       { remove: new Set(['source', 'sk', 'gi']), preserve: new Set() }],
 341    ['bing.com',         { remove: new Set(['cvid', 'form', 'pq', 'sc', 'qs', 'sk', 'PC', 'ghc', 'ghacc', 'ghpl']), preserve: new Set(['q', 'search']) }],
 342    ['duckduckgo.com',   { remove: new Set(['t', 'atb', 'ia', 'iax', 'iar']), preserve: new Set(['q', 'kp', 'kac', 'kaf', 'k1', 'k9', 'ko', 'kav']) }],
 343    ['store.google.com', { remove: new Set(['hl', 'selections']), preserve: new Set() }],
 344    ['ebay.com',         { remove: new Set(['mkevt', 'mkcid', 'mkrid', 'campid', 'customid', 'toolid', 'ssspo', 'sssrc', 'ssuid', 'widget_ver', 'media', '_trkparms', '_trksid']), preserve: new Set(['var', 'selected', 'hash', 'epid']) }],
 345    ['ebay.co.uk',       { remove: new Set(['mkevt', 'mkcid', 'mkrid', 'campid', 'customid', 'toolid', '_trkparms', '_trksid']), preserve: new Set(['var', 'hash', 'epid']) }],
 346    ['ebay.de',          { remove: new Set(['mkevt', 'mkcid', 'mkrid', 'campid', 'customid', 'toolid', '_trkparms', '_trksid']), preserve: new Set(['var', 'hash', 'epid']) }],
 347    ['netflix.com',      { remove: new Set(['trkid', 'tctx', 'jbv', 'so', 'ctx']), preserve: new Set() }],
 348    ['aliexpress.com',   { remove: new Set(['spm', 'srcSns', 'businessType', 'templateKey', 'aff_fcid', 'aff_fsk', 'aff_platform', 'aff_trace_key', 'terminal_id', 'afSmartRedirect', 'utparam-url', 'scm', 'scm-url', 'scm_id', 'pvid', 'gps-id', 'bizType', 'dp', 'is_c', 'is_d', 'traffic_source', 'algo_pvid', 'algo_exp_id', 'btsid', 'categoryId', 'storeId']), preserve: new Set(['sku_id', 'currency', 'language', 'gatewayAdapt']) }],
 349    ['bilibili.com',     { remove: new Set(['vd_source', 'seid', 'share_source', 'copy_link']), preserve: new Set() }],
 350    ['maps.google.com',  { remove: new Set(['ved', 'ei', 'usg', 'sa', 'source', 'sourceid', 'hl', 'gl', 'authuser']), preserve: new Set() }],
 351    ['photos.google.com', { remove: new Set(['key', 'ved', 'ei', 'usg']), preserve: new Set() }],
 352    ['drive.google.com', { remove: new Set(['usp', 'urp']), preserve: new Set() }],
 353  ]);
 354  
 355  function siteRulesFor(host: string, pathname: string = '/'): SiteRuleEntry | undefined {
 356    const lower = host.toLowerCase();
 357    for (const [domain, rules] of SITE_TRACKING_RULES) {
 358      if (lower === domain || lower.endsWith('.' + domain)) {
 359        if (rules.paths && rules.paths.length > 0) {
 360          if (!rules.paths.some((p) => pathname.startsWith(p))) continue;
 361        }
 362        return rules;
 363      }
 364    }
 365    return undefined;
 366  }
 367  
 368  export function stripTrackingParams(input: string): { url: string; stripped: string[] } {
 369    try {
 370      const u = new URL(input);
 371      if (!u.search) return { url: input, stripped: [] };
 372      const siteRules = siteRulesFor(u.hostname, u.pathname);
 373      const preserve = siteRules?.preserve;
 374      const removed: string[] = [];
 375      for (const key of Array.from(u.searchParams.keys())) {
 376        const lower = key.toLowerCase();
 377        if (preserve?.has(lower)) continue;
 378        if (siteRules?.remove.has(lower) ||
 379            TRACKING_PARAMS.has(lower) ||
 380            TRACKING_PREFIXES.some((p) => lower.startsWith(p))) {
 381          u.searchParams.delete(key);
 382          removed.push(key);
 383        }
 384      }
 385      if (removed.length === 0) return { url: input, stripped: [] };
 386      return { url: u.toString(), stripped: removed };
 387    } catch {
 388      return { url: input, stripped: [] };
 389    }
 390  }
 391