9 changed files with 491 additions and 27 deletions
@ -0,0 +1,258 @@ |
|||||||
|
/** |
||||||
|
* Utility to clean tracking parameters from URLs |
||||||
|
* Removes common tracking query parameters like utm_*, ref, fbclid, gclid, etc. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* List of common tracking parameters to remove from URLs |
||||||
|
*/ |
||||||
|
const TRACKING_PARAMS = new Set([ |
||||||
|
// UTM parameters
|
||||||
|
'utm_source', |
||||||
|
'utm_medium', |
||||||
|
'utm_campaign', |
||||||
|
'utm_term', |
||||||
|
'utm_content', |
||||||
|
'utm_id', |
||||||
|
'utm_cid', |
||||||
|
|
||||||
|
// Generic tracking
|
||||||
|
'ref', |
||||||
|
'source', |
||||||
|
'campaign', |
||||||
|
'referrer', |
||||||
|
'referer', |
||||||
|
|
||||||
|
// Social media tracking
|
||||||
|
'fbclid', // Facebook
|
||||||
|
'gclid', // Google Ads
|
||||||
|
'msclkid', // Microsoft
|
||||||
|
'twclid', // Twitter
|
||||||
|
'li_fat_id', // LinkedIn
|
||||||
|
'igshid', // Instagram
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
'_ga', // Google Analytics
|
||||||
|
'_gid', // Google Analytics
|
||||||
|
'_gl', // Google Analytics Linker
|
||||||
|
'mc_cid', // MailChimp
|
||||||
|
'mc_eid', // MailChimp
|
||||||
|
'icid', // Various
|
||||||
|
'ncid', // Various
|
||||||
|
|
||||||
|
// Other common trackers
|
||||||
|
'affiliate_id', |
||||||
|
'affid', |
||||||
|
'affiliate', |
||||||
|
'partner_id', |
||||||
|
'partner', |
||||||
|
'click_id', |
||||||
|
'clickid', |
||||||
|
'clickId', |
||||||
|
'click', |
||||||
|
'tracking_id', |
||||||
|
'trackingId', |
||||||
|
'tracking', |
||||||
|
'track', |
||||||
|
'tid', |
||||||
|
'trk', |
||||||
|
'trkid', |
||||||
|
|
||||||
|
// E-commerce
|
||||||
|
'promo', |
||||||
|
'promocode', |
||||||
|
'promo_code', |
||||||
|
'discount', |
||||||
|
'coupon', |
||||||
|
'voucher', |
||||||
|
|
||||||
|
// Email marketing
|
||||||
|
'email_source', |
||||||
|
'email_campaign', |
||||||
|
'email_medium', |
||||||
|
|
||||||
|
// Content
|
||||||
|
'content_id', |
||||||
|
'contentId', |
||||||
|
'content', |
||||||
|
|
||||||
|
// A/B testing
|
||||||
|
'ab_test', |
||||||
|
'abtest', |
||||||
|
'variant', |
||||||
|
|
||||||
|
// Time-based
|
||||||
|
'timestamp', |
||||||
|
'ts', |
||||||
|
'time', |
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
|
'hash', |
||||||
|
'anchor', |
||||||
|
'position', |
||||||
|
'pos', |
||||||
|
'placement', |
||||||
|
'placement_id', |
||||||
|
'placementId', |
||||||
|
'widget_id', |
||||||
|
'widgetId', |
||||||
|
'widget', |
||||||
|
'context', |
||||||
|
'ctx', |
||||||
|
'origin', |
||||||
|
'orig', |
||||||
|
'return', |
||||||
|
'return_to', |
||||||
|
'returnTo', |
||||||
|
'redirect', |
||||||
|
'redirect_to', |
||||||
|
'redirectTo', |
||||||
|
'next', |
||||||
|
'continue', |
||||||
|
'callback', |
||||||
|
'cb', |
||||||
|
'state', |
||||||
|
'session_id', |
||||||
|
'sessionId', |
||||||
|
'sid', |
||||||
|
'token', |
||||||
|
'key', |
||||||
|
'api_key', |
||||||
|
'apikey', |
||||||
|
'apiKey', |
||||||
|
'auth', |
||||||
|
'auth_token', |
||||||
|
'authToken', |
||||||
|
'access_token', |
||||||
|
'accessToken', |
||||||
|
'refresh_token', |
||||||
|
'refreshToken', |
||||||
|
]); |
||||||
|
|
||||||
|
/** |
||||||
|
* Check if a URL fragment (hash) contains tracking parameters |
||||||
|
*
|
||||||
|
* @param fragment - The URL fragment (without the #) |
||||||
|
* @returns true if the fragment contains tracking parameters |
||||||
|
*/ |
||||||
|
function isTrackingFragment(fragment: string): boolean { |
||||||
|
if (!fragment) return false; |
||||||
|
|
||||||
|
// Check if fragment is in key=value format (e.g., "ref=rss")
|
||||||
|
const equalIndex = fragment.indexOf('='); |
||||||
|
if (equalIndex > 0) { |
||||||
|
const key = fragment.substring(0, equalIndex).toLowerCase(); |
||||||
|
// Check if it's a known tracking parameter
|
||||||
|
if (TRACKING_PARAMS.has(key)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
// Check for tracking patterns
|
||||||
|
if ( |
||||||
|
key.startsWith('utm_') || |
||||||
|
key.startsWith('tracking_') || |
||||||
|
key.startsWith('track_') || |
||||||
|
key.startsWith('click_') || |
||||||
|
key.startsWith('affiliate_') || |
||||||
|
key.startsWith('partner_') || |
||||||
|
key.startsWith('ref_') || |
||||||
|
key.startsWith('source_') |
||||||
|
) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} else { |
||||||
|
// Fragment is just a key (e.g., "ref")
|
||||||
|
const keyLower = fragment.toLowerCase(); |
||||||
|
if (TRACKING_PARAMS.has(keyLower)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
// Check for tracking patterns
|
||||||
|
if ( |
||||||
|
keyLower.startsWith('utm_') || |
||||||
|
keyLower.startsWith('tracking_') || |
||||||
|
keyLower.startsWith('track_') || |
||||||
|
keyLower.startsWith('click_') || |
||||||
|
keyLower.startsWith('affiliate_') || |
||||||
|
keyLower.startsWith('partner_') || |
||||||
|
keyLower.startsWith('ref_') || |
||||||
|
keyLower.startsWith('source_') |
||||||
|
) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Clean tracking parameters from a URL |
||||||
|
* Removes tracking parameters from both query string and URL fragment (hash) |
||||||
|
*
|
||||||
|
* @param url - The URL to clean |
||||||
|
* @returns The cleaned URL with tracking parameters removed |
||||||
|
*/ |
||||||
|
export function cleanTrackingParams(url: string): string { |
||||||
|
if (!url || typeof url !== 'string') { |
||||||
|
return url; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
const urlObj = new URL(url); |
||||||
|
|
||||||
|
// Get all search parameters
|
||||||
|
const params = urlObj.searchParams; |
||||||
|
const keysToDelete: string[] = []; |
||||||
|
|
||||||
|
// Find all tracking parameters (case-insensitive)
|
||||||
|
for (const [key, value] of params.entries()) { |
||||||
|
const keyLower = key.toLowerCase(); |
||||||
|
|
||||||
|
// Check if this is a known tracking parameter
|
||||||
|
if (TRACKING_PARAMS.has(keyLower)) { |
||||||
|
keysToDelete.push(key); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// Also check for common patterns (e.g., keys starting with tracking prefixes)
|
||||||
|
if ( |
||||||
|
keyLower.startsWith('utm_') || |
||||||
|
keyLower.startsWith('tracking_') || |
||||||
|
keyLower.startsWith('track_') || |
||||||
|
keyLower.startsWith('click_') || |
||||||
|
keyLower.startsWith('affiliate_') || |
||||||
|
keyLower.startsWith('partner_') || |
||||||
|
keyLower.startsWith('ref_') || |
||||||
|
keyLower.startsWith('source_') || |
||||||
|
keyLower.endsWith('_id') && ( |
||||||
|
keyLower.includes('track') || |
||||||
|
keyLower.includes('click') || |
||||||
|
keyLower.includes('affiliate') || |
||||||
|
keyLower.includes('partner') || |
||||||
|
keyLower.includes('campaign') || |
||||||
|
keyLower.includes('source') |
||||||
|
) |
||||||
|
) { |
||||||
|
keysToDelete.push(key); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Remove all identified tracking parameters
|
||||||
|
for (const key of keysToDelete) { |
||||||
|
params.delete(key); |
||||||
|
} |
||||||
|
|
||||||
|
// Reconstruct the URL
|
||||||
|
urlObj.search = params.toString(); |
||||||
|
|
||||||
|
// Check and remove tracking parameters from URL fragment (hash)
|
||||||
|
const fragment = urlObj.hash.substring(1); // Remove the # character
|
||||||
|
if (fragment && isTrackingFragment(fragment)) { |
||||||
|
urlObj.hash = ''; // Remove the fragment
|
||||||
|
} |
||||||
|
|
||||||
|
return urlObj.toString(); |
||||||
|
} catch (error) { |
||||||
|
// If URL parsing fails, return the original URL
|
||||||
|
console.warn('Failed to parse URL for cleaning:', url, error); |
||||||
|
return url; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue