1/** 2 * ============================================================================ 3 * lead-source-tracker.js 4 * ============================================================================ 5 * 6 * Zero-dependency, UTM-independent lead source attribution engine. 7 * 8 * @version 2.2.0 9 * @author Dynamic Dreamz 10 * @license MIT 11 */ 12(function (window, document) { 13 'use strict'; 14 15 if (window.__DDZ_LEAD_SOURCE_TRACKER_LOADED__) { 16 return; 17 } 18 window.__DDZ_LEAD_SOURCE_TRACKER_LOADED__ = true; 19 20 // ------------------------------------------------------------------------ 21 // 1. CONFIGURATION 22 // ------------------------------------------------------------------------ 23 24 const CONFIG = Object.freeze({ 25 VERSION: '2.2.0', 26 SESSION_KEY: 'ddz_visit_attribution', 27 LEGACY_STORAGE_KEY: 'ddz_lead_attribution', 28 DEBUG_NAMESPACE: 'DDZTracker', 29 CF7_FORM_SELECTOR: 'form.wpcf7-form, form[data-wpcf7]', 30 DIRECT_SOURCE: 'Direct', 31 DIRECT_MEDIUM: 'Direct' 32 }); 33 34 const CF7_FIELD_NAMES = Object.freeze([ 35 'utm_source', 36 'utm_medium', 37 'first_source', 38 'first_medium', 39 'referrer', 40 'landing_page', 41 'submission_page' 42 ]); 43 44 // ------------------------------------------------------------------------ 45 // 2. PLATFORM DATABASE 46 // ------------------------------------------------------------------------ 47 48 const REVIEW_DIRECTORIES = [ 49 { domains: ['clutch.co'], source: 'Clutch', medium: 'Review Directory', category: 'Review' }, 50 { domains: ['goodfirms.co'], source: 'GoodFirms', medium: 'Review Directory', category: 'Review' }, 51 { domains: ['designrush.com'], source: 'DesignRush', medium: 'Review Directory', category: 'Review' }, 52 { domains: ['trustpilot.com'], source: 'Trustpilot', medium: 'Review Directory', category: 'Review' }, 53 { domains: ['themanifest.com'], source: 'Manifest', medium: 'Review Directory', category: 'Review' }, 54 { domains: ['agencyspotter.com'], source: 'Agency Spotter', medium: 'Review Directory', category: 'Review' }, 55 { domains: ['techbehemoths.com'], source: 'TechBehemoths', medium: 'Review Directory', category: 'Review' }, 56 { domains: ['sortlist.com', 'sortlist.co.uk', 'sortlist.us'], source: 'Sortlist', medium: 'Review Directory', category: 'Review' }, 57 { domains: ['topdevelopers.co'], source: 'TopDevelopers', medium: 'Review Directory', category: 'Review' }, 58 { domains: ['extract.co'], source: 'Extract', medium: 'Review Directory', category: 'Review' }, 59 { domains: ['selectedfirms.co'], source: 'SelectedFirms', medium: 'Review Directory', category: 'Review' }, 60 { domains: ['itfirms.co'], source: 'IT Firms', medium: 'Review Directory', category: 'Review' }, 61 { domains: ['superbcompanies.com'], source: 'SuperbCompanies', medium: 'Review Directory', category: 'Review' }, 62 { domains: ['softwareworld.co'], source: 'SoftwareWorld', medium: 'Review Directory', category: 'Review' }, 63 { domains: ['digitalagencynetwork.com'], source: 'Digital Agency Network', medium: 'Review Directory', category: 'Review' }, 64 { domains: ['g2.com'], source: 'G2', medium: 'Review Directory', category: 'Review' }, 65 { domains: ['capterra.com'], source: 'Capterra', medium: 'Review Directory', category: 'Review' }, 66 { domains: ['bark.com'], source: 'Bark', medium: 'Review Directory', category: 'Review' }, 67 { domains: ['upcity.com'], source: 'UpCity', medium: 'Review Directory', category: 'Review' }, 68 { domains: ['getapp.com'], source: 'GetApp', medium: 'Review Directory', category: 'Review' }, 69 { domains: ['trustradius.com'], source: 'TrustRadius', medium: 'Review Directory', category: 'Review' }, 70 { domains: ['softwareadvice.com'], source: 'Software Advice', medium: 'Review Directory', category: 'Review' }, 71 { domains: ['crozdesk.com'], source: 'Crozdesk', medium: 'Review Directory', category: 'Review' }, 72 { domains: ['financesonline.com'], source: 'FinancesOnline', medium: 'Review Directory', category: 'Review' }, 73 { domains: ['provenexpert.com'], source: 'ProvenExpert', medium: 'Review Directory', category: 'Review' }, 74 { domains: ['threebestrated.com'], source: 'ThreeBestRated', medium: 'Review Directory', category: 'Review' }, 75 { domains: ['yelp.com'], source: 'Yelp', medium: 'Review Directory', category: 'Review' }, 76 { domains: ['bbb.org'], source: 'Better Business Bureau', medium: 'Review Directory', category: 'Review' }, 77 { domains: ['angi.com'], source: 'Angi', medium: 'Review Directory', category: 'Review' }, 78 { domains: ['homeadvisor.com'], source: 'HomeAdvisor', medium: 'Review Directory', category: 'Review' }, 79 { domains: ['thumbtack.com'], source: 'Thumbtack', medium: 'Review Directory', category: 'Review' }, 80 { domains: ['houzz.com'], source: 'Houzz', medium: 'Review Directory', category: 'Review' }, 81 { domains: ['saashub.com'], source: 'SaaSHub', medium: 'Review Directory', category: 'Review' }, 82 { domains: ['alternativeto.net'], source: 'AlternativeTo', medium: 'Review Directory', category: 'Review' }, 83 { domains: ['slant.co'], source: 'Slant', medium: 'Review Directory', category: 'Review' }, 84 { domains: ['stackshare.io'], source: 'StackShare', medium: 'Review Directory', category: 'Review' }, 85 { domains: ['glassdoor.com'], source: 'Glassdoor', medium: 'Review Directory', category: 'Review' } 86 ]; 87 88 const FREELANCE_PLATFORMS = [
89 { domains: ['upwork.com'], source: 'Upwork', medium: 'Freelance Marketplace', category: 'Freelance' }, 90 { domains: ['fiverr.com'], source: 'Fiverr', medium: 'Freelance Marketplace', category: 'Freelance' }, 91 { domains: ['freelancer.com'], source: 'Freelancer', medium: 'Freelance Marketplace', category: 'Freelance' }, 92 { domains: ['peopleperhour.com'], source: 'PeoplePerHour', medium: 'Freelance Marketplace', category: 'Freelance' }, 93 { domains: ['guru.com'], source: 'Guru', medium: 'Freelance Marketplace', category: 'Freelance' }, 94 { domains: ['toptal.com'], source: 'Toptal', medium: 'Freelance Marketplace', category: 'Freelance' }, 95 { domains: ['99designs.com'], source: '99designs', medium: 'Freelance Marketplace', category: 'Freelance' }, 96 { domains: ['designcrowd.com'], source: 'DesignCrowd', medium: 'Freelance Marketplace', category: 'Freelance' }, 97 { domains: ['behance.net'], source: 'Behance', medium: 'Portfolio Network', category: 'Freelance' }, 98 { domains: ['dribbble.com'], source: 'Dribbble', medium: 'Portfolio Network', category: 'Freelance' }, 99 { domains: ['flexjobs.com'], source: 'FlexJobs', medium: 'Job Board', category: 'Freelance' }, 100 { domains: ['weworkremotely.com'], source: 'We Work Remotely', medium: 'Job Board', category: 'Freelance' }, 101 { domains: ['remoteok.com'], source: 'RemoteOK', medium: 'Job Board', category: 'Freelance' }, 102 { domains: ['wellfound.com'], source: 'Wellfound', medium: 'Job Board', category: 'Freelance' }, 103 { domains: ['angel.co'], source: 'AngelList', medium: 'Job Board', category: 'Freelance' }, 104 { domains: ['simplyhired.com'], source: 'SimplyHired', medium: 'Job Board', category: 'Freelance' }, 105 { domains: ['indeed.com'], source: 'Indeed', medium: 'Job Board', category: 'Freelance' }, 106 { domains: ['servicescape.com'], source: 'ServiceScape', medium: 'Freelance Marketplace', category: 'Freelance' }, 107 { domains: ['contently.com'], source: 'Contently', medium: 'Freelance Marketplace', category: 'Freelance' } 108 ]; 109 110 const AI_PLATFORMS = [ 111 { domains: ['chat.openai.com', 'chatgpt.com'], source: 'ChatGPT', medium: 'AI Referral', category: 'AI' }, 112 { domains: ['perplexity.ai'], source: 'Perplexity', medium: 'AI Referral', category: 'AI' }, 113 { domains: ['claude.ai'], source: 'Claude', medium: 'AI Referral', category: 'AI' }, 114 { domains: ['gemini.google.com'], source: 'Gemini', medium: 'AI Referral', category: 'AI' }, 115 { domains: ['copilot.microsoft.com'], source: 'Microsoft Copilot', medium: 'AI Referral', category: 'AI' }, 116 { domains: ['you.com'], source: 'You.com', medium: 'AI Referral', category: 'AI' }, 117 { domains: ['phind.com'], source: 'Phind', medium: 'AI Referral', category: 'AI' }, 118 { domains: ['poe.com'], source: 'Poe', medium: 'AI Referral', category: 'AI' }, 119 { domains: ['character.ai'], source: 'Character.AI', medium: 'AI Referral', category: 'AI' }, 120 { domains: ['meta.ai'], source: 'Meta AI', medium: 'AI Referral', category: 'AI' }, 121 { domains: ['grok.com', 'x.ai'], source: 'Grok', medium: 'AI Referral', category: 'AI' }, 122 { domains: ['chat.deepseek.com'], source: 'DeepSeek', medium: 'AI Referral', category: 'AI' }, 123 { domains: ['chat.mistral.ai', 'mistral.ai'], source: 'Mistral Le Chat', medium: 'AI Referral', category: 'AI' }, 124 { domains: ['huggingface.co'], source: 'HuggingChat', medium: 'AI Referral', category: 'AI' }, 125 { domains: ['pi.ai'], source: 'Pi.ai', medium: 'AI Referral', category: 'AI' } 126 ]; 127 128 const SEARCH_ENGINES = [ 129 { domains: ['maps.google.com'], source: 'Google Maps', medium: 'Organic Search', category: 'Search' }, 130 { domains: ['news.google.com'], source: 'Google News', medium: 'Organic Search', category: 'Search' }, 131 { domains: ['images.google.com'], source: 'Google Images', medium: 'Organic Search', category: 'Search' }, 132 { domains: ['discover.google.com'], source: 'Google Discover', medium: 'Organic Search', category: 'Search' }, 133 { domains: ['google.com', 'google.co.uk', 'google.ca', 'google.com.au', 'google.de', 'google.fr', 'google.co.in'], source: 'Google', medium: 'Organic Search', category: 'Search' }, 134 { domains: ['bing.com'], source: 'Bing', medium: 'Organic Search', category: 'Search' }, 135 { domains: ['search.yahoo.com', 'yahoo.com'], source: 'Yahoo', medium: 'Organic Search', category: 'Search' }, 136 { domains: ['duckduckgo.com'], source: 'DuckDuckGo', medium: 'Organic Search', category: 'Search' }, 137 { domains: ['search.brave.com'], source: 'Brave Search', medium: 'Organic Search', category: 'Search' }, 138 { domains: ['yandex.com', 'yandex.ru'], source: 'Yandex', medium: 'Organic Search', category: 'Search' }, 139 { domains: ['baidu.com'], source: 'Baidu', medium: 'Organic Search', category: 'Search' }, 140 { domains: ['naver.com'], source: 'Naver', medium: 'Organic Search', category: 'Search' }, 141 { domains: ['ecosia.org'], source: 'Ecosia', medium: 'Organic Search', category: 'Search' }, 142 { domains: ['startpage.com'], source: 'Startpage', medium: 'Organic Search', category: 'Search' }, 143 { domains: ['search.aol.com'], source: 'AOL Search', medium: 'Organic Search', category: 'Search' }, 144 { domains: ['ask.com'], source: 'Ask.com', medium: 'Organic Search', category: 'Search' }, 145 { domains: ['wolframalpha.com'], source: 'WolframAlpha', medium: 'Organic Search', category: 'Search' }, 146 { domains: ['qwant.com'], source: 'Qwant', medium: 'Organic Search', category: 'Search' }, 147 { domains: ['sogou.com'], source: 'Sogou', medium: 'Organic Search', category: 'Search' }, 148 { domains: ['seznam.cz'], source: 'Seznam', medium: 'Organic Search', category: 'Search' } 149 ]; 150 151 const SOCIAL_PLATFORMS = [ 152 { domains: ['linkedin.com'], source: 'LinkedIn', medium: 'Social', category: 'Social' }, 153 { domains: ['facebook.com', 'fb.com'], source: 'Facebook', medium: 'Social', category: 'Social' }, 154 { domains: ['instagram.com'], source: 'Instagram', medium: 'Social', category: 'Social' }, 155 { domains: ['threads.net'], source: 'Threads', medium: 'Social', category: 'Social' }, 156 { domains: ['x.com', 'twitter.com', 't.co'], source: 'X', medium: 'Social', category: 'Social' }, 157 { domains: ['pinterest.com'], source: 'Pinterest', medium: 'Social', category: 'Social' }, 158 { domains: ['reddit.com'], source: 'Reddit', medium: 'Social', category: 'Social' }, 159 { domains: ['tiktok.com'], source: 'TikTok', medium: 'Social', category: 'Social' }, 160 { domains: ['youtube.com', 'youtu.be'], source: 'YouTube', medium: 'Social', category: 'Social' }, 161 { domains: ['quora.com'], source: 'Quora', medium: 'Social', category: 'Social' }, 162 { domains: ['snapchat.com'], source: 'Snapchat', medium: 'Social', category: 'Social' }, 163 { domains: ['tumblr.com'], source: 'Tumblr', medium: 'Social', category: 'Social' }, 164 { domains: ['whatsapp.com', 'wa.me'], source: 'WhatsApp', medium: 'Social', category: 'Social' }, 165 { domains: ['telegram.org', 't.me'], source: 'Telegram', medium: 'Social', category: 'Social' }, 166 { domains: ['discord.com', 'discord.gg'], source: 'Discord', medium: 'Social', category: 'Social' }, 167 { domains: ['mastodon.social'], source: 'Mastodon', medium: 'Social', category: 'Social' }, 168 { domains: ['bsky.app'], source: 'Bluesky', medium: 'Social', category: 'Social' }, 169 { domains: ['wechat.com'], source: 'WeChat', medium: 'Social', category: 'Social' }, 170 { domains: ['line.me'], source: 'Line', medium: 'Social', category: 'Social' }, 171 { domains: ['viber.com'], source: 'Viber', medium: 'Social', category: 'Social' }, 172 { domains: ['messenger.com'], source: 'Facebook Messenger', medium: 'Social', category: 'Social' }, 173 { domains: ['vk.com'], source: 'VK', medium: 'Social', category: 'Social' } 174 ]; 175 176 const DEVELOPER_PLATFORMS = [ 177 { domains: ['github.com'], source: 'GitHub', medium: 'Developer Community', category: 'Developer' }, 178 { domains: ['gitlab.com'], source: 'GitLab', medium: 'Developer Community', category: 'Developer' }, 179 { domains: ['stackoverflow.com'], source: 'StackOverflow', medium: 'Developer Community', category: 'Developer' }, 180 { domains: ['dev.to'], source: 'Dev.to', medium: 'Developer Community', category: 'Developer' }, 181 { domains: ['hashnode.com'], source: 'Hashnode', medium: 'Developer Community', category: 'Developer' }, 182 { domains: ['medium.com'], source: 'Medium', medium: 'Developer Community', category: 'Developer' }, 183 { domains: ['bitbucket.org'], source: 'Bitbucket', medium: 'Developer Community', category: 'Developer' }, 184 { domains: ['codepen.io'], source: 'CodePen', medium: 'Developer Community', category: 'Developer' }, 185 { domains: ['producthunt.com'], source: 'Product Hunt', medium: 'Developer Community', category: 'Developer' }, 186 { domains: ['news.ycombinator.com'], source: 'Hacker News', medium: 'Developer Community', category: 'Developer' }, 187 { domains: ['sourceforge.net'], source: 'SourceForge', medium: 'Developer Community', category: 'Developer' }, 188 { domains: ['npmjs.com'], source: 'npm', medium: 'Developer Community', category: 'Developer' }, 189 { domains: ['freecodecamp.org'], source: 'freeCodeCamp', medium: 'Developer Community', category: 'Developer' } 190 ]; 191 192 const EMAIL_PLATFORMS = [ 193 { domains: ['mail.google.com'], source: 'Gmail', medium: 'Email', category: 'Email' }, 194 { domains: ['outlook.com', 'outlook.live.com', 'outlook.office.com'], source: 'Outlook', medium: 'Email', category: 'Email' },
195 { domains: ['mail.yahoo.com'], source: 'Yahoo Mail', medium: 'Email', category: 'Email' }, 196 { domains: ['mail.proton.me', 'protonmail.com'], source: 'ProtonMail', medium: 'Email', category: 'Email' }, 197 { domains: ['mail.zoho.com'], source: 'Zoho Mail', medium: 'Email', category: 'Email' }, 198 { domains: ['mail.aol.com'], source: 'AOL Mail', medium: 'Email', category: 'Email' }, 199 { domains: ['icloud.com'], source: 'iCloud Mail', medium: 'Email', category: 'Email' }, 200 { domains: ['mail.ru'], source: 'Mail.ru', medium: 'Email', category: 'Email' }, 201 { domains: ['gmx.com'], source: 'GMX Mail', medium: 'Email', category: 'Email' } 202 ]; 203 204 const CMS_PLATFORMS = [ 205 { domains: ['wordpress.com'], source: 'WordPress.com', medium: 'CMS Referral', category: 'CMS' }, 206 { domains: ['wordpress.org'], source: 'WordPress.org', medium: 'CMS Referral', category: 'CMS' }, 207 { domains: ['shopify.com'], source: 'Shopify', medium: 'CMS Referral', category: 'CMS' }, 208 { domains: ['wix.com'], source: 'Wix', medium: 'CMS Referral', category: 'CMS' }, 209 { domains: ['squarespace.com'], source: 'Squarespace', medium: 'CMS Referral', category: 'CMS' }, 210 { domains: ['webflow.com'], source: 'Webflow', medium: 'CMS Referral', category: 'CMS' }, 211 { domains: ['weebly.com'], source: 'Weebly', medium: 'CMS Referral', category: 'CMS' }, 212 { domains: ['joomla.org'], source: 'Joomla', medium: 'CMS Referral', category: 'CMS' }, 213 { domains: ['drupal.org'], source: 'Drupal', medium: 'CMS Referral', category: 'CMS' }, 214 { domains: ['bigcommerce.com'], source: 'BigCommerce', medium: 'CMS Referral', category: 'CMS' }, 215 { domains: ['godaddy.com'], source: 'GoDaddy Website Builder', medium: 'CMS Referral', category: 'CMS' }, 216 { domains: ['elementor.com'], source: 'Elementor', medium: 'CMS Referral', category: 'CMS' }, 217 { domains: ['hubspot.com'], source: 'HubSpot', medium: 'CMS Referral', category: 'CMS' } 218 ]; 219 220 const MISC_PLATFORMS = [ 221 { domains: ['crunchbase.com'], source: 'Crunchbase', medium: 'Business Directory', category: 'Directory' }, 222 { domains: ['craigslist.org'], source: 'Craigslist', medium: 'Classifieds', category: 'Directory' }, 223 { domains: ['blogspot.com'], source: 'Blogger', medium: 'Blog Referral', category: 'Blog' }, 224 { domains: ['substack.com'], source: 'Substack', medium: 'Blog Referral', category: 'Blog' }, 225 { domains: ['notion.site'], source: 'Notion', medium: 'Referral', category: 'Productivity' }, 226 { domains: ['slack.com'], source: 'Slack', medium: 'Referral', category: 'Productivity' }, 227 { domains: ['teams.microsoft.com'], source: 'Microsoft Teams', medium: 'Referral', category: 'Productivity' }, 228 { domains: ['zoom.us'], source: 'Zoom', medium: 'Referral', category: 'Productivity' }, 229 { domains: ['amazon.com'], source: 'Amazon', medium: 'Marketplace', category: 'Marketplace' }, 230 { domains: ['appsumo.com'], source: 'AppSumo', medium: 'Marketplace', category: 'Marketplace' }, 231 { domains: ['play.google.com'], source: 'Google Play', medium: 'App Store', category: 'App Store' }, 232 { domains: ['apps.apple.com'], source: 'Apple App Store', medium: 'App Store', category: 'App Store' }, 233 { domains: ['business.google.com'], source: 'Google Business Profile', medium: 'Local Listing', category: 'Local' }, 234 { domains: ['bingplaces.com'], source: 'Bing Places', medium: 'Local Listing', category: 'Local' }, 235 { domains: ['foursquare.com'], source: 'Foursquare', medium: 'Local Listing', category: 'Local' }, 236 { domains: ['waze.com'], source: 'Waze', medium: 'Local Listing', category: 'Local' } 237 ]; 238 239 const PLATFORM_DATABASE = Object.freeze( 240 [].concat( 241 REVIEW_DIRECTORIES, 242 FREELANCE_PLATFORMS, 243 AI_PLATFORMS, 244 SEARCH_ENGINES, 245 SOCIAL_PLATFORMS, 246 DEVELOPER_PLATFORMS, 247 EMAIL_PLATFORMS, 248 CMS_PLATFORMS, 249 MISC_PLATFORMS 250 ).map((entry) => Object.freeze({ 251 domains: Object.freeze(entry.domains.slice()), 252 source: entry.source, 253 medium: entry.medium, 254 category: entry.category 255 })) 256 ); 257 258 const DOMAIN_LOOKUP = (function buildDomainLookup(database) { 259 const pairs = []; 260 database.forEach((platform) => { 261 platform.domains.forEach((domain) => { 262 pairs.push({ domain, platform }); 263 }); 264 }); 265 pairs.sort((a, b) => b.domain.length - a.domain.length); 266 return Object.freeze(pairs); 267 }(PLATFORM_DATABASE)); 268 269 const PAID_CLICK_ID_PARAMS = Object.freeze([ 270 { param: 'gclid', source: 'Google Ads', medium: 'Paid Search' }, 271 { param: 'gclsrc', source: 'Google Ads', medium: 'Paid Search' }, 272 { param: 'fbclid', source: 'Facebook Ads', medium: 'Paid Social' }, 273 { param: 'msclkid', source: 'Microsoft Ads', medium: 'Paid Search' }, 274 { param: 'ttclid', source: 'TikTok Ads', medium: 'Paid Social' }, 275 { param: 'li_fat_id', source: 'LinkedIn Ads', medium: 'Paid Social' } 276 ]); 277 278 const BOT_PATTERNS = Object.freeze([ 279 /googlebot/i, 280 /bingbot/i, 281 /facebookexternalhit|facebot/i, 282 /semrush/i, 283 /ahrefs/i, 284 /mj12bot/i, 285 /dotbot/i, 286 /yandexbot/i, 287 /slackbot/i, 288 /discordbot/i, 289 /twitterbot/i, 290 /linkedinbot/i 291 ]); 292 293 // ------------------------------------------------------------------------ 294 // 3. UTILITIES 295 // ------------------------------------------------------------------------ 296 297 const Utils = Object.freeze({ 298 299 getCurrentURL() { 300 return window.location.href; 301 }, 302 303 getReferrer() { 304 return document.referrer || ''; 305 }, 306 307 safeParseURL(url) { 308 if (!url) return null; 309 try { 310 return new URL(url); 311 } catch (error) { 312 return null; 313 } 314 }, 315 316 normalizeHostname(hostname) { 317 return (hostname || '') 318 .toLowerCase() 319 .replace(/^www\./, '') 320 .replace(/^m\./, ''); 321 }, 322 323 hostnameMatches(hostname, domain) { 324 const normalizedHost = Utils.normalizeHostname(hostname); 325 const normalizedDomain = Utils.normalizeHostname(domain); 326 return normalizedHost === normalizedDomain 327 || normalizedHost.endsWith(`.${normalizedDomain}`); 328 }, 329 330 normalizeLabel(value) { 331 return (value || '').toString().trim().toLowerCase(); 332 }, 333 334 getSiteHostname() { 335 return Utils.normalizeHostname(window.location.hostname); 336 }, 337 338 isSameSite(url) { 339 const parsed = Utils.safeParseURL(url); 340 if (!parsed || !parsed.hostname) return false;
341 return Utils.normalizeHostname(parsed.hostname) === Utils.getSiteHostname(); 342 }, 343 344 isExternalReferrer(referrerUrl) { 345 if (!referrerUrl) return false; 346 return !Utils.isSameSite(referrerUrl); 347 }, 348 349 hasUTMParams(url) { 350 const utm = Utils.extractUTMParams(url); 351 return Boolean(utm.utm_source || utm.utm_medium); 352 }, 353 354 isNewPlatformVisit(referrerUrl, currentUrl) { 355 return Utils.isExternalReferrer(referrerUrl) 356 || Utils.hasUTMParams(currentUrl) 357 || Utils.hasUTMParams(referrerUrl); 358 }, 359 360 buildAttributionRecord(referrerUrl, currentUrl, landingPage) { 361 const detection = PlatformDetector.detect(referrerUrl, currentUrl); 362 const utm = Utils.resolveUTMParams([currentUrl, referrerUrl]); 363 const source = Utils.normalizeLabel(detection.source); 364 const medium = Utils.normalizeLabel(detection.medium); 365 366 return { 367 utm_source: Utils.normalizeLabel(utm.utm_source || source), 368 utm_medium: Utils.normalizeLabel(utm.utm_medium || medium), 369 first_source: source, 370 first_medium: medium, 371 referrer: referrerUrl || '', 372 landing_page: landingPage || currentUrl 373 }; 374 }, 375 376 isBot() { 377 const ua = navigator.userAgent || ''; 378 return BOT_PATTERNS.some((pattern) => pattern.test(ua)); 379 }, 380 381 extractUTMParams(url) { 382 const parsed = Utils.safeParseURL(url); 383 if (!parsed) { 384 return { utm_source: '', utm_medium: '' }; 385 } 386 return { 387 utm_source: parsed.searchParams.get('utm_source') || '', 388 utm_medium: parsed.searchParams.get('utm_medium') || '' 389 }; 390 }, 391 392 mergeUTMParams(primary, fallback) { 393 return { 394 utm_source: primary.utm_source || fallback.utm_source || '', 395 utm_medium: primary.utm_medium || fallback.utm_medium || '' 396 }; 397 }, 398 399 resolveUTMParams(urls) { 400 const empty = { utm_source: '', utm_medium: '' }; 401 return urls.reduce((merged, url) => { 402 return Utils.mergeUTMParams(merged, Utils.extractUTMParams(url)); 403 }, empty); 404 } 405 }); 406 407 // ------------------------------------------------------------------------ 408 // 4. VISIT STORE (session-scoped, refreshes on each new platform visit) 409 // ------------------------------------------------------------------------ 410 411 class VisitStore { 412 413 constructor(key) { 414 this.key = key; 415 } 416 417 load() { 418 try { 419 const raw = window.sessionStorage.getItem(this.key); 420 return raw ? JSON.parse(raw) : null; 421 } catch (error) { 422 return null; 423 } 424 } 425 426 save(record) { 427 try { 428 window.sessionStorage.setItem(this.key, JSON.stringify(record)); 429 } catch (error) { 430 // Fail silently. 431 } 432 } 433 434 clear() { 435 try { 436 window.sessionStorage.removeItem(this.key); 437 } catch (error) { 438 // Fail silently. 439 } 440 } 441 442 static clearLegacyStorage() { 443 try { 444 window.localStorage.removeItem(CONFIG.LEGACY_STORAGE_KEY); 445 } catch (error) { 446 // Fail silently. 447 } 448 try { 449 document.cookie = `${CONFIG.LEGACY_STORAGE_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax`; 450 } catch (error) { 451 // Fail silently. 452 } 453 } 454 } 455 456 // ------------------------------------------------------------------------ 457 // 5. PLATFORM DETECTOR 458 // ------------------------------------------------------------------------ 459 460 class PlatformDetector { 461 462 static detect(referrerUrl, currentUrl) { 463 const utmMatch = PlatformDetector._matchUTM(currentUrl) 464 || PlatformDetector._matchUTM(referrerUrl); 465 if (utmMatch) { 466 return Object.assign({ referrerUrl }, utmMatch); 467 } 468
469 const paidMatch = PlatformDetector._matchPaidClickId(currentUrl); 470 if (paidMatch) { 471 return Object.assign({ referrerUrl }, paidMatch); 472 } 473 474 const referrerMatch = PlatformDetector._matchReferrer(referrerUrl); 475 if (referrerMatch) { 476 return Object.assign({ referrerUrl }, referrerMatch); 477 } 478 479 return { 480 referrerUrl, 481 source: CONFIG.DIRECT_SOURCE, 482 medium: CONFIG.DIRECT_MEDIUM, 483 category: 'Direct' 484 }; 485 } 486 487 static _matchUTM(url) { 488 const parsed = Utils.safeParseURL(url); 489 if (!parsed) return null; 490 491 const utmSource = parsed.searchParams.get('utm_source'); 492 if (!utmSource) return null; 493 494 return { 495 source: utmSource, 496 medium: parsed.searchParams.get('utm_medium') || 'Campaign', 497 category: 'Campaign' 498 }; 499 } 500 501 static _matchPaidClickId(currentUrl) { 502 const parsed = Utils.safeParseURL(currentUrl); 503 if (!parsed) return null; 504 505 const found = PAID_CLICK_ID_PARAMS.find((entry) => parsed.searchParams.has(entry.param)); 506 if (!found) return null; 507 508 return { source: found.source, medium: found.medium, category: 'Paid' }; 509 } 510 511 static _matchReferrer(referrerUrl) { 512 const parsed = Utils.safeParseURL(referrerUrl); 513 if (!parsed || !parsed.hostname) return null; 514 515 const match = DOMAIN_LOOKUP.find((entry) => ( 516 Utils.hostnameMatches(parsed.hostname, entry.domain) 517 )); 518 519 return match 520 ? { 521 source: match.platform.source, 522 medium: match.platform.medium, 523 category: match.platform.category 524 } 525 : null; 526 } 527 } 528 529 // ------------------------------------------------------------------------ 530 // 6. ATTRIBUTION TRACKER 531 // ------------------------------------------------------------------------ 532 533 class AttributionTracker { 534 535 constructor() { 536 this.store = new VisitStore(CONFIG.SESSION_KEY); 537 this.visitData = null; 538 this.isBot = Utils.isBot(); 539 VisitStore.clearLegacyStorage(); 540 } 541 542 captureVisit(referrerUrl, currentUrl) { 543 const existing = this.visitData || this.store.load(); 544 const isNewVisit = Utils.isNewPlatformVisit(referrerUrl, currentUrl); 545 const landingPage = isNewVisit || !existing 546 ? currentUrl 547 : existing.landing_page; 548 549 this.visitData = Utils.buildAttributionRecord(referrerUrl, currentUrl, landingPage); 550 this.store.save(this.visitData); 551 return this.visitData; 552 } 553 554 init() { 555 if (this.isBot) return null; 556 557 const referrerUrl = Utils.getReferrer(); 558 const currentUrl = Utils.getCurrentURL(); 559 const existing = this.store.load(); 560 561 if (Utils.isNewPlatformVisit(referrerUrl, currentUrl) || !existing) { 562 return this.captureVisit(referrerUrl, currentUrl); 563 } 564 565 this.visitData = existing; 566 return this.visitData; 567 } 568 569 getFullAttributionData() { 570 if (this.isBot) return null; 571 572 const currentUrl = Utils.getCurrentURL(); 573 const referrerUrl = Utils.getReferrer(); 574 575 if (Utils.isNewPlatformVisit(referrerUrl, currentUrl)) { 576 this.captureVisit(referrerUrl, currentUrl); 577 } else { 578 this.visitData = this.visitData || this.store.load() || this.captureVisit(referrerUrl, currentUrl); 579 } 580 581 if (!this.visitData) return null; 582 583 const liveUtm = Utils.resolveUTMParams([currentUrl, referrerUrl]); 584 const source = this.visitData.first_source; 585 const medium = this.visitData.first_medium; 586 587 return { 588 utm_source: Utils.normalizeLabel(liveUtm.utm_source || this.visitData.utm_source || source), 589 utm_medium: Utils.normalizeLabel(liveUtm.utm_medium || this.visitData.utm_medium || medium), 590 first_source: source, 591 first_medium: medium, 592 referrer: this.visitData.referrer, 593 landing_page: this.visitData.landing_page, 594 submission_page: currentUrl 595 }; 596 } 597 598 reset() { 599 try { 600 this.store.clear(); 601 this.visitData = null; 602 VisitStore.clearLegacyStorage(); 603 } catch (error) { 604 // Fail silently. 605 } 606 } 607 } 608 609 // ------------------------------------------------------------------------ 610 // 7. CONTACT FORM 7 INTEGRATION 611 // ------------------------------------------------------------------------ 612 613 class CF7Integration { 614 615 constructor(tracker) { 616 this.tracker = tracker; 617 this._observer = null; 618 } 619 620 populateAllForms() { 621 try { 622 const forms = document.querySelectorAll(CONFIG.CF7_FORM_SELECTOR);
623 forms.forEach((form) => this.populateForm(form)); 624 } catch (error) { 625 // Fail silently. 626 } 627 } 628 629 populateForm(form) { 630 try { 631 if (!form) return; 632 633 const data = this.tracker.getFullAttributionData(); 634 if (!data) return; 635 636 CF7_FIELD_NAMES.forEach((fieldName) => { 637 const input = form.querySelector(`[name="${fieldName}"]`); 638 if (input) { 639 input.value = data[fieldName] != null ? data[fieldName] : ''; 640 } 641 }); 642 } catch (error) { 643 // Fail silently. 644 } 645 } 646 647 _resolveFormFromEvent(event) { 648 if (!event) return null; 649 if (event.target && event.target.matches && event.target.matches(CONFIG.CF7_FORM_SELECTOR)) { 650 return event.target; 651 } 652 if (event.detail && event.detail.contactFormId) { 653 const unit = document.querySelector(`.wpcf7[data-wpcf7-id="${event.detail.contactFormId}"]`); 654 if (unit) { 655 const form = unit.querySelector('form'); 656 if (form) return form; 657 } 658 } 659 return null; 660 } 661 662 _handleFormEvent(event) { 663 const form = this._resolveFormFromEvent(event); 664 if (form) { 665 this.populateForm(form); 666 return; 667 } 668 this.populateAllForms(); 669 } 670 671 bindEvents() { 672 this.populateAllForms(); 673 674 document.addEventListener('wpcf7beforesubmit', (event) => { 675 this._handleFormEvent(event); 676 }); 677 678 document.addEventListener('wpcf7submit', (event) => { 679 this._handleFormEvent(event); 680 }); 681 682 document.addEventListener('wpcf7mailsent', (event) => { 683 this._handleFormEvent(event); 684 }); 685 686 document.addEventListener('submit', (event) => { 687 const form = event.target; 688 if (form && form.matches && form.matches(CONFIG.CF7_FORM_SELECTOR)) { 689 this.populateForm(form); 690 } 691 }, true); 692 693 this._observeDynamicForms(); 694 } 695 696 _observeDynamicForms() { 697 if (typeof MutationObserver === 'undefined') return; 698 699 try { 700 this._observer = new MutationObserver((mutations) => { 701 const hasNewForms = mutations.some((mutation) => { 702 return Array.from(mutation.addedNodes).some((node) => { 703 if (node.nodeType !== 1) return false; 704 if (node.matches && node.matches(CONFIG.CF7_FORM_SELECTOR)) return true; 705 return node.querySelector && node.querySelector(CONFIG.CF7_FORM_SELECTOR); 706 }); 707 }); 708 if (hasNewForms) { 709 this.populateAllForms(); 710 } 711 }); 712 713 this._observer.observe(document.documentElement, { 714 childList: true, 715 subtree: true 716 }); 717 } catch (error) { 718 // Fail silently. 719 } 720 } 721 722 destroy() { 723 if (this._observer) { 724 this._observer.disconnect(); 725 this._observer = null; 726 } 727 } 728 } 729 730 // ------------------------------------------------------------------------ 731 // 8. DEBUG API â window.DDZTracker 732 // ------------------------------------------------------------------------ 733 734 class DebugAPI { 735 736 constructor(tracker, cf7) { 737 this._tracker = tracker; 738 this._cf7 = cf7; 739 } 740 741 debug() { 742 const data = this._tracker.getFullAttributionData(); 743 /* eslint-disable no-console */ 744 console.groupCollapsed('%c[DDZTracker] Debug Snapshot', 'color:#6c5ce7;font-weight:bold;'); 745 console.log('Version:', CONFIG.VERSION); 746 console.log('Is bot:', this._tracker.isBot); 747 console.log('Raw referrer:', Utils.getReferrer() || '(none â direct visit)'); 748 console.log('Storage:', this.storage()); 749 if (data) console.table(data); 750 console.groupEnd(); 751 /* eslint-enable no-console */ 752 return data; 753 } 754 755 show() { 756 const data = this._tracker.getFullAttributionData(); 757 /* eslint-disable no-console */ 758 if (data) console.table(data); 759 /* eslint-enable no-console */ 760 return data; 761 } 762 763 clear() { 764 this._tracker.reset(); 765 /* eslint-disable no-console */ 766 console.info('[DDZTracker] Attribution data cleared.'); 767 /* eslint-enable no-console */ 768 } 769 770 version() { 771 return CONFIG.VERSION; 772 } 773 774 storage() { 775 return { 776 backend: 'sessionStorage', 777 key: CONFIG.SESSION_KEY, 778 hasData: Boolean(this._tracker.store.load()), 779 platformCount: PLATFORM_DATABASE.length, 780 isBot: this._tracker.isBot 781 }; 782 } 783 } 784 785 // ------------------------------------------------------------------------ 786 // 9. BOOTSTRAP 787 // ------------------------------------------------------------------------ 788 789 function bootstrap() { 790 try { 791 const tracker = new AttributionTracker(); 792 tracker.init(); 793 794 const cf7 = new CF7Integration(tracker); 795 cf7.bindEvents(); 796 797 window[CONFIG.DEBUG_NAMESPACE] = Object.freeze(new DebugAPI(tracker, cf7)); 798 } catch (error) { 799 // Never break the website. 800 } 801 } 802 803 if (document.readyState === 'loading') { 804 document.addEventListener('DOMContentLoaded', bootstrap); 805 } else { 806 bootstrap(); 807 } 808 809}(window, document));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.