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