Sign in

cJS – Traffic Source

cJS – Traffic Source

GTM Custom JavaScript Variable Last updated: April 2026


Overview

This GTM Custom JavaScript variable detects and returns the traffic source for a website visitor. It is designed to work for both lead generation (form submissions) and ecommerce (Purchase events) by populating a traffic_source or source custom parameter on conversion tags — most commonly the Meta Pixel Lead and Purchase events.

The variable uses last-touch attribution within a session: each new entry to the site with a fresh click ID or UTM overwrites the prior value, so the conversion is credited to the channel that most recently brought the user back. SessionStorage carries the resolved signals through multi-page funnels so attribution is preserved through popup forms on internal pages and multi-step checkouts.


How It Works

Detection Priority

The variable checks signals in the following order. The first match wins.

PrioritySignalReturns
1utm_medium=paid_social + utm_source=facebookfacebook_ads
2utm_medium=paid_social + utm_source=instagraminstagram_ads
3utm_medium=paid_social + utm_source=linkedinlinkedin_ads
4utm_medium=paid_social + utm_source=tiktoktiktok_ads
5utm_medium=paid_social + any other source{source}_ads
6gclid in URL or sessionStoragegoogle_ads
7_gcl_aw cookie presentgoogle_ads
8msclkid in URL or sessionStoragemicrosoft_ads
9ttclid in URL or sessionStoragetiktok_ads
10Any other utm_source presentutmSource_utmMedium (e.g. email_newsletter)
11fbclid in URL or sessionStorage (no paid UTM)facebook_organic
12Referrer contains google.google_organic
13Referrer contains bing.bing_organic
14Referrer contains yahoo.yahoo_organic
15Referrer contains duckduckgo.duckduckgo_organic
16Referrer contains facebook. or fb.facebook_organic
17Referrer contains instagram.instagram_organic
18Referrer contains linkedin.linkedin_organic
19Referrer contains twitter. or x.comtwitter_organic
20Referrer contains youtube.youtube_organic
21Referrer contains reddit.reddit_organic
22Referrer contains pinterest.pinterest_organic
23Referrer contains tiktok.tiktok_organic
24No referrer or same-domain referrerdirect
25Anything elsereferral

Attribution Model

This variable uses last-touch attribution within a session. Each pageview captures any fresh signals from the URL (click IDs, UTMs) and stores them in sessionStorage, overwriting any prior values. The most recent identifiable source wins.

This means:

  • A user who clicks a Google Ad, leaves, and returns via a Facebook Ad will be attributed to Facebook on conversion.
  • A user who clicks a Facebook Ad, browses to checkout over multiple pages, and converts is still attributed to Facebook because sessionStorage carries the click ID through the funnel.
  • A new browser session (closed tab, new browser) starts fresh — no localStorage means no cross-session attribution carryover.

To switch to first-touch attribution: change the click ID and UTM capture blocks to only write if a value is not already stored, and add a localStorage layer for cross-session persistence.

Funnel Persistence via SessionStorage

Without persistence, multi-page funnels would lose attribution. By the second page of a checkout, document.referrer becomes your own domain, and click IDs disappear from the URL once the user navigates internally.

The variable stores the following in sessionStorage on the landing page:

  • gclid, msclkid, fbclid, ttclid (raw click IDs)
  • utm_source, utm_medium
  • original_referrer (the first external referrer seen this session)

On every subsequent page in the same tab, these stored values are used in the resolution logic, ensuring consistent attribution from landing through conversion.


Why UTMs Are Required for Paid Social

Facebook (and Instagram) append fbclid to all outbound links — both paid ads and organic posts. Relying on fbclid or the _fbc cookie alone would incorrectly label organic Facebook traffic as paid.

To distinguish paid from organic, paid social ad URLs must include UTMs following this convention:

Channelutm_sourceutm_medium
Facebook Adsfacebookpaid_social
Instagram Adsinstagrampaid_social
LinkedIn Adslinkedinpaid_social
TikTok Ads (paid)tiktokpaid_social

Any click with fbclid that does NOT have utm_medium=paid_social is treated as facebook_organic. Without consistent UTM tagging on ad URLs, paid and organic Facebook traffic will be misattributed.

For Google, Microsoft, and TikTok, the platform-specific click IDs (gclid, msclkid, ttclid) are unique to paid traffic and don't have this collision problem. UTMs are optional for those channels but recommended for GA4 reporting consistency.


Google Ads Attribution

Google Ads detection works in three layers:

  1. gclid in the URL — present on the landing page when a user clicks a Google Ad.
  2. gclid in sessionStorage — preserved from the landing page so Google Ads attribution survives multi-page funnels.
  3. _gcl_aw cookie — set automatically by gtag.js when auto-tagging is enabled in Google Ads. This catches cases where gclid is stripped from the URL by redirects or link shorteners.

For the _gcl_aw cookie fallback to work, the Google Ads account must have auto-tagging enabled and gtag.js must be firing on the site. If only GTM is used without a Google Ads tag, this fallback won't work and gclid in the URL is the only signal.


Lead Gen vs. Ecommerce Use

The same variable works for both conversion types because the underlying attribution problem is the same: a user enters with a source signal, navigates internally, and converts on a deeper page.

For lead gen (popup forms, contact forms):

  • Used in Meta Pixel Lead event as traffic_source parameter
  • Captures source even when the popup appears on a different page than the landing page

For ecommerce (multi-step checkout):

  • Used in Meta Pixel Purchase event as source parameter
  • Captures source through cart → checkout → confirmation flow

Both use cases benefit from the sessionStorage persistence layer — without it, attribution would collapse to direct on internal pages.


Cross-Tab Behavior

SessionStorage is scoped to a single browser tab. If a user opens checkout in a new tab partway through a funnel, the new tab starts fresh and re-detects from the URL and referrer at that moment.

For most flows this is fine — the new tab usually opens with the source URL intact. If cross-tab persistence is required (e.g. multi-tab purchase comparison flows), add a localStorage layer that writes alongside sessionStorage and reads as a fallback.


Known Limitations

  • No cross-session attribution. Closing the browser ends the session. A user who visits via Facebook Ads on Monday and converts via direct on Wednesday will be attributed to direct on Wednesday. This is intentional last-touch behavior.
  • iOS Safari ITP restricts cookie and storage lifetimes on Apple devices. The _fbc and _gcl_aw cookies may have shortened lifespans, which can affect Google Ads cookie fallback specifically.
  • fbclid on organic Facebook posts will return facebook_organic correctly — but only because the variable specifically excludes fbclid-only signals from paid attribution. This is by design.
  • Same-domain navigation with no UTM signals returns direct. If a user lands on a marketing landing page without UTMs, then navigates to checkout, the conversion is direct. UTM-tag your landing pages.
  • Referrer-based organic detection relies on document.referrer being captured on the landing page and stored as original_referrer. If the landing page is reached via a redirect that strips the referrer, organic source detection fails.

Usage in GTM

Variable Setup

  • Variable type: Custom JavaScript
  • Variable name: cJS - Traffic Source
  • Return type: String

Tag Implementation — Lead Event

The code

cjs-traffic-source.js
javascript
function() {
  // Last-touch traffic source for both lead gen and ecommerce conversions
  // Uses sessionStorage to persist source through multi-page funnels (checkout flows, popup forms on internal pages)
  // Distinguishes paid Facebook from organic via UTM convention since fbclid alone is unreliable
  
  var urlParams = new URLSearchParams(window.location.search);
  var referrer = document.referrer.toLowerCase();
  
  // Helper to safely read from sessionStorage with fallback for private browsing/blocked storage
  function getStored(key) {
    try { return sessionStorage.getItem(key) || ''; } catch(e) { return ''; }
  }
  
  // Helper to safely write to sessionStorage
  function setStored(key, value) {
    try { sessionStorage.setItem(key, value); } catch(e) {}
  }
  
  // STEP 1: Capture any fresh signals from current URL into sessionStorage
  // This ensures last-touch works correctly when a user re-enters the site with a new click ID
  
  // Click ID capture - each platform's unique click identifier
  if (urlParams.get('gclid')) setStored('gclid', urlParams.get('gclid'));
  if (urlParams.get('msclkid')) setStored('msclkid', urlParams.get('msclkid'));
  if (urlParams.get('fbclid')) setStored('fbclid', urlParams.get('fbclid'));
  if (urlParams.get('ttclid')) setStored('ttclid', urlParams.get('ttclid'));
  
  // UTM capture - source and medium together let us distinguish paid social from organic social
  if (urlParams.get('utm_source')) setStored('utm_source', urlParams.get('utm_source').toLowerCase());
  if (urlParams.get('utm_medium')) setStored('utm_medium', urlParams.get('utm_medium').toLowerCase());
  
  // Capture the original referrer on first pageview so organic detection works on deep funnel pages
  // Only set this once per session - if storage already has it, this is not the landing page
  if (!getStored('original_referrer') && referrer && referrer.indexOf(window.location.hostname) === -1) {
    setStored('original_referrer', referrer);
  }
  
  // STEP 2: Resolve traffic source using all available signals (URL + stored)
  
  var utmSource = urlParams.get('utm_source') ? urlParams.get('utm_source').toLowerCase() : getStored('utm_source');
  var utmMedium = urlParams.get('utm_medium') ? urlParams.get('utm_medium').toLowerCase() : getStored('utm_medium');
  
  // Paid social detection - requires explicit UTM tagging since fbclid alone catches organic posts too
  // Convention: utm_medium=paid_social with utm_source identifying the platform
  if (utmMedium === 'paid_social') {
    if (utmSource === 'facebook') return 'facebook_ads';
    if (utmSource === 'instagram') return 'instagram_ads';
    if (utmSource === 'linkedin') return 'linkedin_ads';
    if (utmSource === 'tiktok') return 'tiktok_ads';
    // Fallback for any other paid social platform we haven't bucketed
    return utmSource + '_ads';
  }
  
  // Google Ads - check URL gclid, then stored gclid, then _gcl_aw cookie set by gtag auto-tagging
  if (urlParams.get('gclid') || getStored('gclid')) return 'google_ads';
  if (document.cookie.indexOf('_gcl_aw=') > -1) return 'google_ads';
  
  // Microsoft Ads - msclkid is reliable on its own
  if (urlParams.get('msclkid') || getStored('msclkid')) return 'microsoft_ads';
  
  // TikTok Ads - ttclid is reliable on its own (TikTok organic posts don't append it)
  if (urlParams.get('ttclid') || getStored('ttclid')) return 'tiktok_ads';
  
  // Generic UTM source fallback - catches email, partnerships, custom campaigns
  // Combines source and medium for clarity (e.g. email_newsletter, partner_referral)
  if (utmSource) {
    return utmMedium ? utmSource + '_' + utmMedium : utmSource;
  }
  
  // Facebook organic - fbclid present without paid_social UTM means it came from an organic post
  if (urlParams.get('fbclid') || getStored('fbclid')) return 'facebook_organic';
  
  // STEP 3: Organic referrer detection - use original referrer from session storage if available
  // This makes organic attribution survive multi-page funnels where document.referrer becomes self-referential
  var sourceReferrer = getStored('original_referrer') || referrer;
  
  if (sourceReferrer.indexOf('google.') > -1) return 'google_organic';
  if (sourceReferrer.indexOf('bing.') > -1) return 'bing_organic';
  if (sourceReferrer.indexOf('yahoo.') > -1) return 'yahoo_organic';
  if (sourceReferrer.indexOf('duckduckgo.') > -1) return 'duckduckgo_organic';
  if (sourceReferrer.indexOf('facebook.') > -1 || sourceReferrer.indexOf('fb.') > -1) return 'facebook_organic';
  if (sourceReferrer.indexOf('instagram.') > -1) return 'instagram_organic';
  if (sourceReferrer.indexOf('linkedin.') > -1) return 'linkedin_organic';
  if (sourceReferrer.indexOf('twitter.') > -1 || sourceReferrer.indexOf('x.com') > -1) return 'twitter_organic';
  if (sourceReferrer.indexOf('youtube.') > -1) return 'youtube_organic';
  if (sourceReferrer.indexOf('reddit.') > -1) return 'reddit_organic';
  if (sourceReferrer.indexOf('pinterest.') > -1) return 'pinterest_organic';
  if (sourceReferrer.indexOf('tiktok.') > -1) return 'tiktok_organic';
  
  // No referrer at all = direct (typed URL, bookmark, app, dark social, email client)
  if (!sourceReferrer) return 'direct';
  
  // Same-domain referrer with no other signals = direct (internal navigation from unknown origin)
  if (sourceReferrer.indexOf(window.location.hostname) > -1) return 'direct';
  
  // External referring site we don't have a specific bucket for
  return 'referral';
}