/** * Known tracking-only query parameters that Warden strips from URLs * before launching the chosen browser. * * Three tiers: * 1. Global deny-list (TRACKING_PARAMS) — safe to strip everywhere * 2. Prefix-based deny-list (TRACKING_PREFIXES) — catches vendor families * 3. Per-site rules (SITE_TRACKING_RULES) — host-specific with preserve * lists so functional params (q, v, t, sort, etc.) are never stripped * on the wrong platform. * * Based on hand-vetted rules from FixupXer, Brave, and ClearURLs. */ // ──────────────────────────────────────────────────────────────────────────── // Global deny-list — safe to strip on every domain // ──────────────────────────────────────────────────────────────────────────── export const TRACKING_PARAMS: ReadonlySet = new Set([ 'utm_id', 'utm_name', 'utm_brand', 'gclid', 'gclsrc', 'dclid', 'gbraid', 'wbraid', '_ga', '_gl', 'gad_source', 'gad_source_', 'guccounter', 'guce_referrer', 'guce_referrer_sig', 'fbclid', 'fb_action_ids', 'fb_action_types', 'fb_ref', 'fb_source', 'msclkid', 'twclid', '__twitter_impression', 'yclid', '_openstat', 'mc_cid', 'mc_eid', '_hsenc', '_hsmi', '__hssc', '__hstc', 'hsCtaTracking', 'hsctatracking', 'mkt_tok', 'sfmc_activityid', 'elqTrackId', 'elqTrack', 'pk_keyword', 'vero_conv', 'vero_id', '_branch_match_id', '_branch_referrer', 's_cid', 's_kwcid', 'sc_campaign', 'sc_channel', 'sc_content', 'epik', 'pp', 'igshid', 'igsh', 'tt_medium', 'tt_content', 'trk', 'trkCampaign', 'ko_click_id', 'sscid', 'zanpid', 'sharedid', 'ranmid', 'raneaid', 'ransiteid', 'shareasale_site_id', 'shareasale_user_id', 'spjobid', 'spmailingid', 'spreportid', 'spuserid', 'irclickid', 'irgwc', 'rb_clickid', '_bta_tid', '_bta_c', 'ml_subscriber', 'ml_subscriber_hash', 'oly_anon_id', 'oly_enc_id', 'wickedid', '_ke', '_kx', '__s', 'vgo_ee', ]); export const TRACKING_PREFIXES: readonly string[] = [ 'utm_', 'pk_', 'mtm_', 'wt.', 'ga_', 'cid_', 'matomo_', 'piwik_', 'wt_', 'itm_', 'elq', 'xtor', 'at_', 'fb_', ]; export type SiteRuleEntry = { remove: ReadonlySet; preserve?: ReadonlySet; paths?: readonly string[]; }; export const SITE_TRACKING_RULES: ReadonlyMap = new Map([ // ─── Amazon ─── ['amazon.com', { remove: new Set([ 'tag', 'linkCode', 'linkId', 'camp', 'creative', 'creativeASIN', 'adId', 'th', 'psc', 'ref', 'ref_', 'pf_rd_p', 'pf_rd_r', 'pf_rd_s', 'pf_rd_t', 'pd_rd_w', 'pd_rd_r', 'pd_rd_i', 'pd_rd_wg', 'qid', 'rank', 'spIA', 'spLa', 'sr', 'ie', 'sprefix', 'sp_csd', 'coliid', 'colid', 'refRID', 'mgh', 'pebp', 'redirectId', 'sourceid', 'dib', 'dib_tag', 'dsd', 'psd', 'crid', 'cri', 'cv_ct_id', 'cv_ct_pg', 'imprToken', 'mrntc', 'corrId', 'node', 'refTagSuffix', 'refSrcId', 'reftag', 'aaxitk', 'ascs', 'ascsubtag', 'asc_campaign', 'asc_source', 'asc_refurl', 'sa-no-redirect', 'xpid', 'bestSellersRank', 'recommendationSource', 'recommendationType', 'useRedirectOnSuccess', 'rw_html_to_wsrp', 'rw_useCurrentProtocol', ]), preserve: new Set([ 'keywords', 'k', 'i', 'rh', 's', 'page', 'node', 'field-keywords', 'field-brand', 'field-price-range', 'field-availability', 'sort', 'lo', 'fs', ]) }], ['amzn.to', { remove: new Set(['tag', 'linkCode', 'ref']), preserve: new Set() }], ['amzn.eu', { remove: new Set(['tag', 'linkCode', 'ref']), preserve: new Set() }], ['amzn.asia', { remove: new Set(['tag', 'linkCode', 'ref']), preserve: new Set() }], // ─── Google Search ─── ['google.com', { remove: new Set([ 'ved', 'ei', 'usg', 'sa', 'source', 'sourceid', 'sxsrf', 'rlz', 'biw', 'bih', 'sei', 'gs_lcp', 'gs_l', 'oq', 'aqs', 'cs', 'output', 'bg', 'dpr', 'sca_esv', 'sca_upv', 'si', 'ictx', 'uact', 'cd', 'd', 'sclient', 'gs_ri', 'gs_mss', 'sig2', 'sig', 'itctx', 'cad', 'authuser', 'es_sm', 'imgar', 'imgsz', 'imgtype', 'imgc', 'imgdii', 'amp', 'amp_gsa', 'amp_js_v', 'gws_rd', 'gfe_rd', ]), preserve: new Set([ 'q', 'tbm', 'tbs', 'start', 'num', 'hl', 'lr', 'safe', 'nfpr', 'filter', 'as_q', 'as_epq', 'as_oq', 'as_eq', 'as_qdr', 'as_rights', 'imgsz', 'imgtype', 'imgc', 'gl', 'cr', 'search', 'udm', ]) }], // ─── YouTube ─── ['youtube.com', { remove: new Set([ 'si', 'feature', 'pp', 'ab_channel', 'app', 'gclid', 'ytclid', 'embeds_referring_euri', 'source_ve_path', 'webpage', 'bpctr', 'has_verified', 'themeRefresh', 'embeds_euri', 'embeds_origin', 'persist_app', 'persist_gl', 'sourceid', 'sid', 'search_source', 'session_id', 'ei', 'ved', 'surl', 'gws_rd', 'gfe_rd', 'lc', 'h', 'w', 'ucbcb', 'gvs', 'android_app', 'ios_app', 'vq', 'cq', 'embed_config', 'continuePlayback', 'watch_next', 'notif_id', 'notif_t', 'sp', 'ad', 'adformat', 'adsense', 'yt_ad', 'sub_confirmation', 'shelf_id', 'playnext', 'hl', 'gl', 'persist_hl', 'annotation_id', 'annotation_prefix', 'annotation_properties', ]), preserve: new Set([ 'v', 't', 'list', 'index', 'start', 'end', 'search_query', 'q', 'radio', 'p', 'page', 'sort', 'view', 'shelf_id', 'nolist', 'si', ]) }], ['youtu.be', { remove: new Set([ 'si', 'feature', 'pp', 'ab_channel', 't', 'app', ]), preserve: new Set(['v', 't', 'list', 'index']) }], ['youtube-nocookie.com', { remove: new Set([ 'si', 'feature', 'pp', 'ab_channel', 't', 'app', ]), preserve: new Set(['v', 't', 'list', 'index']) }], // ─── Twitter / X ─── ['twitter.com', { remove: new Set([ 's', 't', 'ref_src', 'ref_url', 'via', 'cn', 'src', 'mx', 'mxId', 'tw_campaign', 'tw_source', 'tw_term', 'tw_adid', 'cxt', 'ct', 'cta', 'widget_id', 'original_referer', 'card', 'cards_platform', 'conversation_id', 'promoted_content_id', 'amp', 'amp_js_v', 'amp_gsa', 'ads', 'ad_id', 'ads_tracking', 'ad_tracking', 'prefetchTimestamp', 'view', 'focused', 'scroll', 'react', 'reaction', ]), preserve: new Set([ 'lang', 'theme', 'display', 'tz', 'include_profile_interstitial_type', 'include_blocking', 'include_blocked_by', 'include_followed_by', 'include_want_retweets', 'include_mute_edge', 'include_can_dm', ]) }], ['x.com', { remove: new Set([ 's', 't', 'ref_src', 'ref_url', 'via', 'cn', 'src', ]), preserve: new Set(['lang', 'theme']) }], // ─── Instagram ─── ['instagram.com', { remove: new Set([ 'igsh', 'igshid', 'ig_cache_key', 'ig_mid', 'ig_share_sheet', '__a', '__d', '_rdr', 'hl', 'ig_share', 'ig_share_id', 'ig_share_source', 'ig_shortcode', 'ig_media_id', 'ig_source', 'ig_campaign_id', 'ig_ad_id', 'mibextid', 'epa', 'xtor', 'trk', 'trkCampaign', 'trkInfo', 'ref', 'referral', 'referral_source', 'trackId', 'trackingId', 'trackingid', 'requestId', 'sessionId', 'e', 'f', 'g', 'h', 'device_id', 'os', 'platform', 'feed_type', 'pinned', 'st', 'preview', 'draft', 'aft', 'csr', ]), preserve: new Set([ 'img_index', 'story_media_id', 'h', 'w', 'carousel_index', 'media_id', 'reel_ids', 'highlight_reel_ids', 'lang', ]) }], // ─── Facebook ─── ['facebook.com', { remove: new Set([ '__cft__', '__tn__', '__xts__', '__user', '__a', '__req', 'mibextid', 'epa', '_gl', 'rc', 'ref', 'referral', 'refsrc', 'comment_tracking', 'hc_location', 'hc_ref', 'av', 'v', 'source', 'sourceid', 'sr', 'sk', 'set', 'type', 'theater', 'locale', 'eid', 'ft', 'ftid', 'ftype', 'acontext', 'aref', 'arefid', 'feed', 'feed_sub', 'feed_story', 'feedtype', 'timeline_context_item_source', 'section_header_type', 'timeline', 'focus', 'focused_comment_id', 'reaction', 'reaction_comment_id', 'notif', 'notif_id', 'notif_t', 'notifications', 'suggested', 'suggestion', 'co', 'coid', 'co_referrer', 'highlight', 'highlight_id', 'promoted', 'promoted_id', 'promoted_post', 'ads', 'ad_id', 'adset_id', 'campaign_id', 'creative', 'post', 'post_id', 'story_fbid', 'comment_id', 'fbid', 'id', 'share', 'share_id', 'share_source', 'tab', 'section', 'filter', 'sort', 'order', 'cursor', 'after', 'before', 'pp', 'pp_source', 'pp_campaign', 'pa', 'pa_source', 'pa_campaign', 'sk', 'skey', 'skeys', 'location', 'lat', 'lng', 'near', 'event', 'event_time', 'event_id', 'device', 'device_id', 'os', 'app', 'session', 'session_id', 'request', 'req', 'req_id', 'track', 'tracking', 'tracker', 'click', 'click_id', 'click_tracking', 'impression', 'impression_id', 'te', 'te_source', 'te_campaign', ]), preserve: new Set([ 'id', 'story_fbid', 'fbid', 'set', 'type', 'theater', 'av', 'v', 'locale', 'ref', 'hc_location', 'comment_id', 'reply_comment_id', 'notif_id', 'notif_t', 'tab', 'sk', 'filter', 'section', ]) }], ['fb.com', { remove: new Set(['__cft__', '__tn__', 'mibextid', 'epa', 'ref', 'rc']), preserve: new Set(['id', 'locale', 'ref']) }], // ─── Reddit ─── ['reddit.com', { remove: new Set([ 'context', 'correlator', 'rdt_cid', 'share_id', 'source', 'ref', 'ref_source', 'ref_url', 'sort', 't', 'type', 'restrict_sr', 'include_over_18', 'after', 'before', 'limit', 'count', 'show', 'raw_json', 'depth', 'showmore', 'sr_detail', 'sr', 'is_multi', 'comment_sort', 'comment_limit', 'feed', 'feed_length', 'feed_sort', 'user', 'username', 'action', 'reason', 'rule', 'embed', 'embed_type', 'theme', 'lang', 'locale', 'device', 'device_id', 'session', 'session_id', 'tracking', 'tracking_id', 'ads', 'ad_id', 'campaign', 'campaign_id', 'creative', 'experiment', 'variant', 'st', 'sk', 'token', 'geo_filter', 'geo_filter_id', ]), preserve: new Set([ 'context', 'sort', 't', 'q', 'type', 'restrict_sr', 'include_over_18', 'after', 'before', 'limit', 'count', 'show', 'raw_json', 'depth', 'showmore', 'sr_detail', 'sr', 'is_multi', ]) }], ['redd.it', { remove: new Set(['context', 'correlator', 'share_id']), preserve: new Set() }], // ─── TikTok ─── ['tiktok.com', { remove: new Set([ '_r', '_t', '_d', 'tt_from', 'share_source', 'is_from_webapp', 'is_copy_url', 'language', 'lang', 'region', 'traffic_type', 'traffic_source', 'campaign_id', 'adgroup_id', 'creative_id', 'ad_id', 'ad_name', 'ad_platform', 'previous_page', 'refer', 'referer', 'referrer', 'source', 'source_type', 'source_app', 'session_id', 'request_id', 'server_id', 'device_id', 'os', 'platform', 'app_version', 'app_name', 'carrier', 'connection_type', 'network_type', 'screen_width', 'screen_height', 'density', 'timezone', 'time', 'timestamp', 'ts', 'nav_source', 'tab', 'section', 'feed_type', 'content_type', 'sort_type', 'display_method', 'is_ads', 'ad_tracking', 'ads_tracking', 'A/B', 'abtest', 'experiment', 'link_id', 'click_id', 'impression_id', 'view_id', 'search_id', 'search_keyword', 'hashtag', 'sound', 'effect', 'creation_time', 'modal', 'is_fullscreen', 'embed', 'is_playlist', 'playlist_id', 'live', 'live_id', 'stream_id', 'commerce', 'product_id', 'shop_id', 'social', 'social_source', 'social_share', 'follow', 'following', 'user_id', ]), preserve: new Set([ 'lang', 'q', 't', 'embed', 'video_id', 'item_id', 'modal', 'search_keyword', 'sort_type', 'content_type', 'creation_time', 'hashtag', 'sound', 'effect', 'display_method', 'is_fullscreen', ]) }], ['tiktokcdn.com', { remove: new Set(['_r', '_t', '_d', 'tt_from', 'share_source']), preserve: new Set() }], ['tiktokv.com', { remove: new Set(['_r', '_t', '_d', 'tt_from', 'share_source']), preserve: new Set() }], // ─── LinkedIn ─── ['linkedin.com', { remove: new Set([ 'trk', 'trkInfo', 'trkEmail', 'li_theme', 'li_ft', 'li_mc', 'li_tc', 'li_uuid', 'li_at', 'li_rm', 'trackingId', 'tracking', 'tracker', 'tracking_source', 'ref', 'refId', 'refUrl', 'original_referer', 'originalSubdomain', 'source', 'source_app', 'source_type', 'sessionId', 'session_redirect', 'session_redirect_count', 'eid', 'mid', 'midToken', 'requestToken', 'geoUrn', 'region', 'feature', 'feature_id', 'feature_type', 'event', 'event_id', 'event_name', 'messageId', 'conversationId', 'threadId', 'recommendationId', 'recommendationType', 'companyId', 'groupId', 'articleId', 'articleUrn', 'activityId', 'searchId', 'searchOrigin', 'searchType', 'ads', 'adId', 'campaignId', 'creativeId', 'learning', 'learningPathId', 'courseId', ]), preserve: new Set([ 'keywords', 'origin', 'q', 'start', 'count', 'filters', 'sortBy', 'facet', 'geoUrn', 'network', 'schoolFilter', 'companySize', 'datePosted', 'jobType', 'location', 'redirectUrl', 'response_type', 'client_id', 'state', 'scope', ]) }], ['lnkd.in', { remove: new Set(['trk', 'trkInfo']), preserve: new Set() }], // ─── Substack ─── ['substack.com', { remove: new Set([ 'r', 'ref', 'referrer', 'referral', 'referral_code', 'token', 'access_token', 'token_id', 'source', 'source_app', 'source_type', 'open', 'openId', 'click', 'clickId', 'session', 'session_id', 'device', 'device_id', 'os', 'email', 'email_id', 'email_type', 'email_source', 'subscriber', 'subscriber_id', 'publication', 'post', 'post_id', 'comment_id', 'search', 'search_query', 'search_id', 'sort', 'order', 'section', 'tag', 'ab_test', 'variant', 'experiment', ]), preserve: new Set(['publication_id', 'post_id']) }], // ─── Lighter platforms ─── ['wikipedia.org', { remove: new Set(['wprov']), preserve: new Set() }], ['threads.net', { remove: new Set(['igshid', 'xmt']), preserve: new Set() }], ['threads.com', { remove: new Set(['igshid', 'xmt']), preserve: new Set() }], ['twitch.tv', { remove: new Set(['tt_medium', 'tt_content', 'ref', 'referrer']), preserve: new Set() }], ['spotify.com', { remove: new Set(['si', 'dl_branch', 'dl_mobileapp']), preserve: new Set(['context', 'uri']) }], ['pinterest.com', { remove: new Set(['e_t', 'e_t_s', 'e_t_cs', 'ouuid', 'pin_unauth', 'pp']), preserve: new Set() }], ['snapchat.com', { remove: new Set(['share_id']), preserve: new Set() }], ['whatsapp.com', { remove: new Set(['link_source', 'link_medium']), preserve: new Set() }], ['medium.com', { remove: new Set(['source', 'sk', 'gi']), preserve: new Set() }], ['bing.com', { remove: new Set(['cvid', 'form', 'pq', 'sc', 'qs', 'sk', 'PC', 'ghc', 'ghacc', 'ghpl']), preserve: new Set(['q', 'search']) }], ['duckduckgo.com', { remove: new Set(['t', 'atb', 'ia', 'iax', 'iar']), preserve: new Set(['q', 'kp', 'kac', 'kaf', 'k1', 'k9', 'ko', 'kav']) }], ['store.google.com', { remove: new Set(['hl', 'selections']), preserve: new Set() }], ['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']) }], ['ebay.co.uk', { remove: new Set(['mkevt', 'mkcid', 'mkrid', 'campid', 'customid', 'toolid', '_trkparms', '_trksid']), preserve: new Set(['var', 'hash', 'epid']) }], ['ebay.de', { remove: new Set(['mkevt', 'mkcid', 'mkrid', 'campid', 'customid', 'toolid', '_trkparms', '_trksid']), preserve: new Set(['var', 'hash', 'epid']) }], ['netflix.com', { remove: new Set(['trkid', 'tctx', 'jbv', 'so', 'ctx']), preserve: new Set() }], ['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']) }], ['bilibili.com', { remove: new Set(['vd_source', 'seid', 'share_source', 'copy_link']), preserve: new Set() }], ['maps.google.com', { remove: new Set(['ved', 'ei', 'usg', 'sa', 'source', 'sourceid', 'hl', 'gl', 'authuser']), preserve: new Set() }], ['photos.google.com', { remove: new Set(['key', 'ved', 'ei', 'usg']), preserve: new Set() }], ['drive.google.com', { remove: new Set(['usp', 'urp']), preserve: new Set() }], ]); function siteRulesFor(host: string, pathname: string = '/'): SiteRuleEntry | undefined { const lower = host.toLowerCase(); for (const [domain, rules] of SITE_TRACKING_RULES) { if (lower === domain || lower.endsWith('.' + domain)) { if (rules.paths && rules.paths.length > 0) { if (!rules.paths.some((p) => pathname.startsWith(p))) continue; } return rules; } } return undefined; } export function stripTrackingParams(input: string): { url: string; stripped: string[] } { try { const u = new URL(input); if (!u.search) return { url: input, stripped: [] }; const siteRules = siteRulesFor(u.hostname, u.pathname); const preserve = siteRules?.preserve; const removed: string[] = []; for (const key of Array.from(u.searchParams.keys())) { const lower = key.toLowerCase(); if (preserve?.has(lower)) continue; if (siteRules?.remove.has(lower) || TRACKING_PARAMS.has(lower) || TRACKING_PREFIXES.some((p) => lower.startsWith(p))) { u.searchParams.delete(key); removed.push(key); } } if (removed.length === 0) return { url: input, stripped: [] }; return { url: u.toString(), stripped: removed }; } catch { return { url: input, stripped: [] }; } }