1(() => { 2 const DEVICE_ID_COOKIE_NAME = "clutch_device_id"; 3 const DEVICE_ID_STORAGE_KEY = "clutch_device_id"; 4 const FIRST_TOUCH_STORAGE_KEY = "clutch_first_touch_attribution"; 5 const LAST_TOUCH_STORAGE_KEY = "clutch_last_touch_attribution"; 6 const EVENT_CACHE_PREFIX = "clutch_attribution_event"; 7 const LAST_CAPTURE_SESSION_KEY = "clutch_last_attribution_capture_key"; 8 const DEFAULT_BASE_URL = "https://clutch-app.co"; 9 const OUTBOUND_QUERY_KEYS = [ 10 "device_id", 11 "utm_source", 12 "utm_medium", 13 "utm_campaign", 14 "utm_content", 15 "utm_term", 16 "utm_id", 17 "cl_ref", 18 "gclid", 19 "gbraid", 20 "wbraid", 21 "fbclid", 22 "ttclid", 23 "msclkid", 24 "ref", 25 "referral_code", 26 ]; 27 const REFERRER_RULES = [ 28 { 29 hosts: ["reddit.com"], 30 source: "reddit", 31 medium: "community", 32 campaign: "inferred_reddit", 33 }, 34 { 35 hosts: ["discord.com"], 36 source: "discord", 37 medium: "community", 38 campaign: "inferred_discord", 39 }, 40 { 41 hosts: ["tiktok.com"], 42 source: "tiktok", 43 medium: "organic_social", 44 campaign: "inferred_tiktok", 45 }, 46 { 47 hosts: ["youtube.com", "youtu.be"], 48 source: "youtube", 49 medium: "organic_video", 50 campaign: "inferred_youtube", 51 }, 52 { 53 hosts: ["instagram.com"], 54 source: "instagram", 55 medium: "organic_social", 56 campaign: "inferred_instagram", 57 }, 58 { 59 hosts: ["facebook.com", "l.facebook.com"], 60 source: "facebook", 61 medium: "organic_social", 62 campaign: "inferred_facebook", 63 }, 64 { 65 hosts: ["google.com"], 66 source: "google", 67 medium: "organic_or_paid_unknown", 68 campaign: "inferred_google", 69 }, 70 { 71 hosts: ["bing.com"], 72 source: "bing", 73 medium: "organic_or_paid_unknown", 74 campaign: "inferred_bing", 75 }, 76 ]; 77 78 function normalizeValue(value) { 79 if (typeof value !== "string") return null; 80 const trimmed = value.trim(); 81 return trimmed ? trimmed : null; 82 } 83 84 function getAttributionCaptureUrl() { 85 return normalizeValue(window.CLUTCH_ATTRIBUTION_CAPTURE_URL); 86 } 87 88 function logDevelopmentWarning(message, extra) { 89 const host = window.location.hostname; 90 const isDevHost = 91 host === "localhost" || host === "127.0.0.1" || host.endsWith(".local"); 92 93 if (!isDevHost) return; 94 console.warn(message, extra); 95 } 96 97 function readCookie(name) { 98 const parts = document.cookie ? document.cookie.split(";") : []; 99 for (const part of parts) { 100 const [rawName, ...rawValue] = part.trim().split("="); 101 if (rawName === name) { 102 return normalizeValue(decodeURIComponent(rawValue.join("="))); 103 } 104 } 105 return null; 106 } 107 108 function writeCookie(name, value) { 109 const secure = window.location.protocol === "https:" ? "; Secure" : ""; 110 const expiresAt = new Date( 111 Date.now() + 365 * 24 * 60 * 60 * 1000, 112 ).toUTCString(); 113 document.cookie = [ 114 `${name}=${encodeURIComponent(value)}`, 115 "Path=/", 116 "SameSite=Lax", 117 `Expires=${expiresAt}`, 118 secure, 119 ].join("; "); 120 } 121 122 function readJsonStorage(key) { 123 try { 124 const raw = window.localStorage.getItem(key); 125 return raw ? JSON.parse(raw) : null; 126 } catch { 127 return null; 128 } 129 } 130 131 function writeJsonStorage(key, value) { 132 try { 133 window.localStorage.setItem(key, JSON.stringify(value)); 134 } catch {} 135 } 136 137 function writeStorage(key, value) { 138 try { 139 window.localStorage.setItem(key, value); 140 } catch {} 141 } 142 143 function createDeviceId() { 144 if (window.crypto && typeof window.crypto.randomUUID === "function") { 145 return window.crypto.randomUUID(); 146 } 147 return `clutch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; 148 } 149 150 function getOrCreateDeviceId() { 151 const cookieValue = readCookie(DEVICE_ID_COOKIE_NAME); 152 let storageValue = null; 153 try { 154 storageValue = normalizeValue( 155 window.localStorage.getItem(DEVICE_ID_STORAGE_KEY), 156 ); 157 } catch {} 158 const deviceId = cookieValue || storageValue || createDeviceId(); 159 writeCookie(DEVICE_ID_COOKIE_NAME, deviceId); 160 writeStorage(DEVICE_ID_STORAGE_KEY, deviceId); 161 return deviceId; 162 } 163 164 function getReferrerInference(referrer) { 165 const normalizedReferrer = normalizeValue(referrer); 166 if (!normalizedReferrer) return null; 167 168 try { 169 const hostname = new URL(normalizedReferrer).hostname.toLowerCase(); 170 const match = REFERRER_RULES.find((rule) => 171 rule.hosts.some( 172 (host) => hostname === host || hostname.endsWith(`.${host}`), 173 ), 174 ); 175 if (!match) return null; 176 return { 177 utm_source: match.source, 178 utm_medium: match.medium, 179 utm_campaign: match.campaign, 180 }; 181 } catch { 182 return null; 183 } 184 } 185 186 function hasTrackedFields(payload) { 187 return Boolean( 188 payload.utm_source || 189 payload.utm_medium || 190 payload.utm_campaign ||
191 payload.utm_content || 192 payload.utm_term || 193 payload.utm_id || 194 payload.cl_ref || 195 payload.gclid || 196 payload.gbraid || 197 payload.wbraid || 198 payload.fbclid || 199 payload.ttclid || 200 payload.msclkid || 201 payload.ref || 202 payload.referral_code, 203 ); 204 } 205 206 function getAttributionConfidence(payload, inferred) { 207 if (hasTrackedFields(payload)) return "tracked"; 208 if (inferred) return "inferred"; 209 return "unknown"; 210 } 211 212 function readAttributionFromUrl(input) { 213 const url = new URL(input || window.location.href, DEFAULT_BASE_URL); 214 const params = url.searchParams; 215 return { 216 utm_source: normalizeValue(params.get("utm_source")), 217 utm_medium: normalizeValue(params.get("utm_medium")), 218 utm_campaign: normalizeValue(params.get("utm_campaign")), 219 utm_content: normalizeValue(params.get("utm_content")), 220 utm_term: normalizeValue(params.get("utm_term")), 221 utm_id: normalizeValue(params.get("utm_id")), 222 cl_ref: normalizeValue(params.get("cl_ref")), 223 gclid: normalizeValue(params.get("gclid")), 224 gbraid: normalizeValue(params.get("gbraid")), 225 wbraid: normalizeValue(params.get("wbraid")), 226 fbclid: normalizeValue(params.get("fbclid")), 227 ttclid: normalizeValue(params.get("ttclid")), 228 msclkid: normalizeValue(params.get("msclkid")), 229 ref: normalizeValue(params.get("ref")), 230 referral_code: normalizeValue(params.get("referral_code")), 231 }; 232 } 233 234 function getCurrentAttributionPayload() { 235 const url = new URL(window.location.href); 236 const fromUrl = readAttributionFromUrl(url.toString()); 237 const referrer = normalizeValue(document.referrer); 238 const inferred = hasTrackedFields(fromUrl) 239 ? null 240 : getReferrerInference(referrer); 241 return { 242 device_id: getOrCreateDeviceId(), 243 utm_source: 244 fromUrl.utm_source || (inferred && inferred.utm_source) || null, 245 utm_medium: 246 fromUrl.utm_medium || (inferred && inferred.utm_medium) || null, 247 utm_campaign: 248 fromUrl.utm_campaign || (inferred && inferred.utm_campaign) || null, 249 utm_content: fromUrl.utm_content || null, 250 utm_term: fromUrl.utm_term || null, 251 utm_id: fromUrl.utm_id || null, 252 cl_ref: fromUrl.cl_ref || null, 253 gclid: fromUrl.gclid || null, 254 gbraid: fromUrl.gbraid || null, 255 wbraid: fromUrl.wbraid || null, 256 fbclid: fromUrl.fbclid || null, 257 ttclid: fromUrl.ttclid || null, 258 msclkid: fromUrl.msclkid || null, 259 ref: fromUrl.ref || null, 260 referral_code: fromUrl.referral_code || null, 261 referrer, 262 landing_page: url.toString(), 263 pathname: url.pathname, 264 user_agent: normalizeValue(navigator.userAgent), 265 timezone: (() => { 266 try { 267 return Intl.DateTimeFormat().resolvedOptions().timeZone || null; 268 } catch { 269 return null; 270 } 271 })(), 272 captured_at: new Date().toISOString(), 273 attribution_confidence: getAttributionConfidence(fromUrl, inferred), 274 }; 275 } 276 277 function getStoredFirstTouchAttribution() { 278 return readJsonStorage(FIRST_TOUCH_STORAGE_KEY); 279 } 280 281 function getStoredLastTouchAttribution() { 282 return readJsonStorage(LAST_TOUCH_STORAGE_KEY); 283 } 284 285 function shouldSendGaEvent(eventName, pageKey) { 286 try { 287 const key = `${EVENT_CACHE_PREFIX}:${eventName}:${pageKey}`; 288 if (window.sessionStorage.getItem(key) === "1") return false; 289 window.sessionStorage.setItem(key, "1"); 290 return true; 291 } catch { 292 return true; 293 } 294 } 295 296 function getHubSlugFromPath(pathname) { 297 const targetPath = normalizeValue(pathname) || window.location.pathname; 298 const match = targetPath.match(/^\/(?:hubs|lfg)\/([^/?#]+)/i); 299 return (match && match[1]) || null; 300 } 301 302 function getHubLandingSlugFromPath(pathname) { 303 const targetPath = normalizeValue(pathname) || window.location.pathname; 304 const match = targetPath.match(/^\/hubs\/([^/?#]+)/i); 305 return (match && match[1]) || null; 306 } 307 308 function getCaptureEventName(pathname) { 309 return getHubLandingSlugFromPath(pathname) 310 ? "hub_landing_view" 311 : "website_attribution_captured"; 312 } 313 314 function buildAttributionCaptureRequest(payload) { 315 const hubSlug = getHubLandingSlugFromPath(payload.pathname); 316 317 return { 318 device_id: payload.device_id, 319 utm_source: payload.utm_source, 320 utm_medium: payload.utm_medium, 321 utm_campaign: payload.utm_campaign,
322 utm_content: payload.utm_content, 323 utm_term: payload.utm_term, 324 utm_id: payload.utm_id, 325 cl_ref: payload.cl_ref, 326 gclid: payload.gclid, 327 gbraid: payload.gbraid, 328 wbraid: payload.wbraid, 329 fbclid: payload.fbclid, 330 ttclid: payload.ttclid, 331 msclkid: payload.msclkid, 332 ref: payload.ref, 333 referral_code: payload.referral_code, 334 referrer: payload.referrer, 335 landing_page: payload.landing_page, 336 pathname: payload.pathname, 337 timezone: payload.timezone, 338 user_agent: payload.user_agent, 339 attribution_confidence: payload.attribution_confidence, 340 event_name: getCaptureEventName(payload.pathname), 341 hub_slug: hubSlug, 342 hub_id: null, 343 }; 344 } 345 346 function getAttributionCaptureDedupeKey(payload) { 347 return [ 348 payload.device_id, 349 payload.landing_page, 350 payload.utm_campaign || "", 351 payload.utm_content || "", 352 payload.cl_ref || "", 353 ].join("|"); 354 } 355 356 function shouldSendAttributionCapture(payload) { 357 const key = getAttributionCaptureDedupeKey(payload); 358 359 try { 360 const previousKey = window.sessionStorage.getItem( 361 LAST_CAPTURE_SESSION_KEY, 362 ); 363 if (previousKey === key) { 364 return false; 365 } 366 367 window.sessionStorage.setItem(LAST_CAPTURE_SESSION_KEY, key); 368 return true; 369 } catch { 370 return true; 371 } 372 } 373 374 async function sendAttributionCapture(payload) { 375 const captureUrl = getAttributionCaptureUrl(); 376 if (!captureUrl) { 377 return { ok: false }; 378 } 379 380 try { 381 const response = await fetch(captureUrl, { 382 method: "POST", 383 headers: { 384 "Content-Type": "application/json", 385 }, 386 body: JSON.stringify(payload), 387 }); 388 389 if (!response.ok) { 390 logDevelopmentWarning("Attribution capture request failed.", { 391 status: response.status, 392 }); 393 return { ok: false }; 394 } 395 396 return { ok: true }; 397 } catch (error) { 398 logDevelopmentWarning("Attribution capture request failed.", error); 399 return { ok: false }; 400 } 401 } 402 403 function sendGaEvent(eventName, payload) { 404 if (typeof window.gtag !== "function") return; 405 window.gtag("event", eventName, { 406 device_id: payload.device_id, 407 utm_source: payload.utm_source || undefined, 408 utm_medium: payload.utm_medium || undefined, 409 utm_campaign: payload.utm_campaign || undefined, 410 utm_content: payload.utm_content || undefined, 411 utm_term: payload.utm_term || undefined, 412 utm_id: payload.utm_id || undefined, 413 cl_ref: payload.cl_ref || undefined, 414 ref: payload.ref || undefined, 415 referral_code: payload.referral_code || undefined, 416 landing_page: payload.landing_page, 417 pathname: payload.pathname, 418 hub_slug: getHubSlugFromPath(payload.pathname) || undefined, 419 }); 420 } 421 422 function persistAttributionFromCurrentUrl(options = {}) { 423 const payload = getCurrentAttributionPayload(); 424 const firstTouch = getStoredFirstTouchAttribution() || payload; 425 const lastTouch = payload; 426 427 if (!getStoredFirstTouchAttribution()) { 428 writeJsonStorage(FIRST_TOUCH_STORAGE_KEY, firstTouch); 429 } 430 writeJsonStorage(LAST_TOUCH_STORAGE_KEY, lastTouch); 431 432 if (options.trackGa4 !== false) { 433 const pageKey = `${payload.pathname}${window.location.search}`; 434 if (shouldSendGaEvent("website_attribution_captured", pageKey)) { 435 sendGaEvent("website_attribution_captured", payload); 436 } 437 if ( 438 payload.attribution_confidence !== "unknown" && 439 shouldSendGaEvent("campaign_landing_view", pageKey) 440 ) { 441 sendGaEvent("campaign_landing_view", payload); 442 } 443 if ( 444 getHubSlugFromPath(payload.pathname) && 445 shouldSendGaEvent("hub_landing_view", pageKey) 446 ) { 447 sendGaEvent("hub_landing_view", payload); 448 } 449 } 450 451 return { payload, firstTouch, lastTouch }; 452 } 453 454 function buildAttributionQueryParams(payload) { 455 const source = 456 payload || 457 getStoredLastTouchAttribution() || 458 getStoredFirstTouchAttribution() || 459 getCurrentAttributionPayload(); 460 const params = new URLSearchParams();
461 OUTBOUND_QUERY_KEYS.forEach((key) => { 462 const value = source && source[key]; 463 if (typeof value === "string" && value.length > 0) { 464 params.set(key, value); 465 } 466 }); 467 return params; 468 } 469 470 function appendAttributionToUrl(rawUrl, payload) { 471 const normalizedUrl = normalizeValue(rawUrl); 472 if ( 473 !normalizedUrl || 474 /^(mailto:|tel:|sms:|javascript:)/i.test(normalizedUrl) 475 ) { 476 return rawUrl; 477 } 478 479 const isAbsolute = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(normalizedUrl); 480 const url = isAbsolute 481 ? new URL(normalizedUrl) 482 : new URL(normalizedUrl, window.location.origin); 483 const params = buildAttributionQueryParams(payload); 484 485 params.forEach((value, key) => { 486 if (!url.searchParams.has(key)) { 487 url.searchParams.set(key, value); 488 } 489 }); 490 491 return isAbsolute 492 ? url.toString() 493 : `${url.pathname}${url.search}${url.hash}`; 494 } 495 496 function decorateDocumentAttributionLinks(root = document) { 497 const selector = [ 498 'a[href^="clutchapp://"]', 499 'a[href*="apps.apple.com"]', 500 'a[href*="play.google.com"]', 501 'a[href^="/download.html"]', 502 'a[href*="/download.html"]', 503 'a[data-clutch-attribution-link="true"]', 504 ].join(","); 505 506 root.querySelectorAll(selector).forEach((link) => { 507 const href = link.getAttribute("href"); 508 if (!href) return; 509 const nextHref = appendAttributionToUrl(href); 510 if (nextHref !== href) { 511 link.setAttribute("href", nextHref); 512 } 513 }); 514 } 515 516 function observeAttributionLinks() { 517 const observer = new MutationObserver(() => { 518 decorateDocumentAttributionLinks(document); 519 }); 520 observer.observe(document.documentElement, { 521 subtree: true, 522 childList: true, 523 attributes: true, 524 attributeFilter: ["href"], 525 }); 526 } 527 528 window.ClutchAttribution = { 529 appendAttributionToUrl, 530 captureCurrentAttribution(options = {}) { 531 const result = persistAttributionFromCurrentUrl(options); 532 if (options.decorateLinks !== false) { 533 decorateDocumentAttributionLinks(document); 534 } 535 536 const capturePayload = buildAttributionCaptureRequest(result.payload); 537 if (shouldSendAttributionCapture(capturePayload)) { 538 void sendAttributionCapture(capturePayload); 539 } 540 541 return result; 542 }, 543 decorateDocumentAttributionLinks, 544 getCurrentAttributionPayload, 545 getOrCreateDeviceId, 546 getStoredFirstTouchAttribution, 547 getStoredLastTouchAttribution, 548 sendAttributionCapture, 549 }; 550 551 const boot = () => { 552 window.ClutchAttribution.captureCurrentAttribution(); 553 decorateDocumentAttributionLinks(document); 554 observeAttributionLinks(); 555 }; 556 557 if (document.readyState === "loading") { 558 document.addEventListener("DOMContentLoaded", boot, { once: true }); 559 } else { 560 boot(); 561 } 562})();
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.