PageSourceSearch

https://clutch-app.co/attribution.js

js clutch-app.co collected 2026-10-06 13:16:06 UTC 16,524 bytes, 562 lines download raw bytes

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.