Complete implementation reference for Adobe Analytics via Launch with AppMeasurement.
| Setting | Value | Docs |
|---|---|---|
| Report Suite ID | Production + Staging (use environment-specific) | Report suites |
| Tracking Server | Your tracking server | s.trackingServer |
| Character Set | UTF-8 | s.charSet |
| Currency Code | USD | s.currencyCode |
| Setting | Value | Why | Docs |
|---|---|---|---|
| Enable Activity Map | ON | Auto-tracks every click with link text, region, and page | Activity Map |
| Track download links | ON | Auto-tracks file downloads | s.trackDownloadLinks |
| Track outbound links | ON | Auto-tracks exit links | s.trackExternalLinks |
| Download extensions | doc,docx,eps,jpg,png,pdf,pptx,svg,xls,xlsx,zip | File types to track as downloads | s.linkDownloadFileTypes |
Activity Map is built into AppMeasurement. When enabled, it automatically fires an s.tl() server call on every click — it's not just a visual heatmap overlay. Every click sends real data to your report suite.
Each click fires s.tl() with these context data variables:
c.a.activitymap.page = "Home Page" // s.pageName value
c.a.activitymap.link = "Sign Up Now" // link text / aria-label
c.a.activitymap.region = "hero-section" // parent container ID
c.a.activitymap.pageIDType = "1" // page ID type
You can see these in the Adobe Debugger network tab on every click.
| Setting | Where | Must be | Docs |
|---|---|---|---|
| Enable ClickMap / Activity Map | AA Extension > Link Tracking | ON | Getting started |
| Enable Link Tracking | AA Extension > Link Tracking | ON | trackExternalLinks |
| Track download links | AA Extension > Link Tracking | ON | trackDownloadLinks |
| Track outbound links | AA Extension > Link Tracking | ON | trackExternalLinks |
s.trackInlineStats is NOT a checkbox in the Launch extension UI. It's a code-level setting. Two ways to set it depending on your access level:
AA Extension > Configure Tracker Using Custom Code:
s.trackInlineStats = true;
s.linkInternalFilters = location.hostname; // distinguishes internal vs exit links
When the AA extension settings cannot be edited directly, create a dedicated config rule instead:
| Component | Configuration | Docs |
|---|---|---|
| Rule name | Global Config | Rules |
| Event | Core > Library Loaded (Page Top) | Core events |
| Order | 1 (lowest fires first — must run before all other rules) | Rule ordering |
| Condition | None | |
| Action | Adobe Analytics > Custom Code |
s.trackInlineStats = true;
s.linkInternalFilters = location.hostname;
1 guarantees it runs before your page view rule (which should use order 50 or higher). This ensures Activity Map and link tracking config are set before the first page view beacon or click event.
| Element type | Auto-tracked? | Notes |
|---|---|---|
<a href="..."> | Yes | All anchor links with href |
<a> with onclick | Yes | JavaScript-driven links |
<button> | Sometimes | Only if AppMeasurement recognizes it as interactive. Not guaranteed. |
<div onclick="..."> | No | Not a standard link element |
[role="button"] | No | ARIA roles not recognized by Activity Map |
<input type="submit"> | Sometimes | Depends on form structure |
s.tl() rule (Rule 4 below) is required. Activity Map + Rule 4 together cover everything.
s.trackInlineStats is false — the main toggle. Must be true.s.pageName isn't set — Activity Map needs a page name to associate the click. If your page view rule isn't firing first, Activity Map has no context.Once enabled, these dimensions are available in Analysis Workspace:
| Dimension | What it shows |
|---|---|
| Activity Map Link | The text/label of the clicked element |
| Activity Map Region | The container ID where the click occurred |
| Activity Map Page | The page where the click happened |
| Activity Map Link By Region | Link + Region combined |
You can also use the Activity Map browser overlay (separate browser extension from Adobe) to see a visual heatmap of clicks directly on the live page.
| Element | Activity Map handles it? | Rule 4 handles it? |
|---|---|---|
| Standard <a> links | Yes | Yes |
| Buttons | Maybe | Yes |
| role="button" divs | No | Yes |
| Form submits | No | Yes |
| onclick handlers | No | Yes |
| Icon-only elements | No label | Yes (reads aria-label) |
Every data element has ONE job. No data layer needed — everything comes from the URL, DOM, or cookies that already exist on the page.
Name format: lowercase.dot.notation
Data elements in Tags reference → | Core extension data element types →
| Name | Type | Source | Maps to | Docs |
|---|---|---|---|---|
page.name | Custom Code | return document.title.replace(' | SiteName', '').trim(); | s.pageName | pageName |
page.section | Custom Code | return location.pathname.split('/').filter(Boolean)[0] || 'home'; | s.channel | channel |
page.type | Custom Code | See Page Type Detection | s.pageType / s.prop1 | pageType |
page.heading | Custom Code | var h = document.querySelector('h1'); return h ? h.textContent.trim().substring(0, 255) : document.title.trim().substring(0, 255); | s.prop2 | |
page.url | Core > URL | Full URL | URL type | |
page.path | Core > URL | Path only | ||
page.queryString | Core > URL | Query string | ||
page.domain | Core > URL | Hostname | s.server | server |
| Name | Type | Source | Maps to | Docs |
|---|---|---|---|---|
user.authState | Custom Code | See User State Detection | s.eVar1 | eVars |
| Name | Type | Source | Maps to | Docs |
|---|---|---|---|---|
campaign.trackingCode | Query String Parameter | cid or utm_campaign | s.campaign | campaign |
| Name | Type | Source | Maps to | Docs |
|---|---|---|---|---|
search.term | Custom Code | See Search Term Detection | s.eVar5 / s.prop5 | eVars |
| Name | Type | Source | Maps to | Docs |
|---|---|---|---|---|
env.name | Custom Code | return location.hostname.includes('staging') ? 'staging' : 'production'; |
%value% references a data element that doesn't exist and isn't in this list, kill it.
Custom Code data element: page.type
Infers page type from URL patterns and DOM elements. No data layer needed.
var path = location.pathname.toLowerCase();
if (path === '/' || path === '/index') return 'home';
if (path.includes('/product')) return 'product';
if (path.includes('/cart') || path.includes('/basket')) return 'cart';
if (path.includes('/checkout')) return 'checkout';
if (path.includes('/confirm') || path.includes('/thank')) return 'confirmation';
if (path.includes('/search')) return 'search';
if (path.includes('/login') || path.includes('/signin')) return 'login';
if (path.includes('/account') || path.includes('/profile')) return 'account';
if (path.includes('/blog') || path.includes('/article') || path.includes('/news')) return 'content';
if (path.includes('/faq') || path.includes('/help') || path.includes('/support')) return 'support';
if (path.includes('/contact')) return 'contact';
if (path.includes('/category') || path.includes('/collection')) return 'category';
if (document.querySelector('.error-page, .page-404, [data-error]')) return 'error';
return 'general';
includes() checks to match. The logic above covers the most common patterns.
Custom Code data element: user.authState
Looks for common DOM patterns indicating logged-in state.
if (document.querySelector(
'.logged-in, .authenticated, [data-logged-in], ' +
'.user-menu, .account-nav, .my-account'
)) return 'authenticated';
var meta = document.querySelector('meta[name="user-status"]');
if (meta) return meta.content;
if (document.cookie.includes('loggedIn=') ||
document.cookie.includes('session=') ||
document.cookie.includes('auth='))
return 'authenticated';
return 'anonymous';
Custom Code data element: search.term
var params = new URLSearchParams(location.search);
return params.get('q')
|| params.get('query')
|| params.get('search')
|| params.get('s')
|| params.get('keyword')
|| (document.querySelector(
'input[type="search"], input[name="q"], .search-input'
) || {}).value
|| '';
Reference patterns for frequently tracked values. Each example shows the recommended approach with fallback chains, proper return statements, and inline documentation.
// Data element: page.queryParams
// Captures everything after ? as key:value pairs
// Change '|' to ',' or ';' to match reporting preferences
var qs = location.search.substring(1);
if (!qs) return '';
return decodeURIComponent(qs).replace(/&/g, '|').replace(/=/g, ':');
?q=shoes&category=mens&sort=price → q:shoes|category:mens|sort:price
Use this for full visibility into what's on the URL. Campaign-specific extraction (below) filters for known campaign params only.
// Data element: search.resultsCount
var el = document.querySelector(
'[data-results-count], [data-total-results], ' +
'.results-count, .search-results-count, .hits-count'
);
if (el) {
return el.getAttribute('data-results-count')
|| el.getAttribute('data-total-results')
|| el.textContent.replace(/[^0-9]/g, '')
|| '0';
}
return '';
The site section is the first logical content grouping — it answers "what part of the site is this?" It typically maps to s.channel.
| Site type | URL | Section |
|---|---|---|
| E-commerce | /products/shoes/running | products |
| SaaS | /docs/api/authentication | docs |
| Media | /sports/nfl/scores | sports |
| Corporate | /about/careers/engineering | about |
| Support | /help/billing/refunds | help |
The typical hierarchy in reporting:
s.channel = "products" // site section (level 1)
s.prop1 = "shoes" // sub-section (level 2)
s.prop2 = "running" // sub-sub-section (level 3)
s.hier1 = "products|shoes|running" // full hierarchy, pipe-delimited
// Data element: site.locale
var LOCALES = [
'en', 'en-us', 'en-gb', 'en-au', 'en-ca',
'fr', 'fr-ca', 'fr-fr',
'de', 'de-de', 'de-at',
'es', 'es-mx', 'es-es',
'pt', 'pt-br',
'ja', 'ko', 'zh', 'zh-cn', 'zh-tw',
'it', 'nl', 'ru', 'sv', 'nb', 'da', 'fi'
];
var first = location.pathname.split('/').filter(Boolean)[0] || '';
return LOCALES.indexOf(first.toLowerCase()) !== -1 ? first.toLowerCase() : '';
// Data element: site.language
var LOCALES = [
'en', 'en-us', 'en-gb', 'en-au', 'en-ca',
'fr', 'fr-ca', 'fr-fr',
'de', 'de-de', 'de-at',
'es', 'es-mx', 'es-es',
'pt', 'pt-br',
'ja', 'ko', 'zh', 'zh-cn', 'zh-tw',
'it', 'nl', 'ru', 'sv', 'nb', 'da', 'fi'
];
var first = location.pathname.split('/').filter(Boolean)[0] || '';
if (LOCALES.indexOf(first.toLowerCase()) !== -1) {
return first.toLowerCase().substring(0, 2);
}
var htmlLang = document.documentElement.getAttribute('lang');
if (htmlLang) return htmlLang.substring(0, 2).toLowerCase();
return '';
// Data element: site.section
var LOCALES = ['en','en-us','en-gb','en-au','en-ca','fr','fr-ca','fr-fr','de','de-de','de-at','es','es-mx','es-es','pt','pt-br','ja','ko','zh','zh-cn','zh-tw','it','nl','ru','sv','nb','da','fi'];
var segments = location.pathname.split('/').filter(Boolean);
var hasLocale = segments.length > 0 && LOCALES.indexOf(segments[0].toLowerCase()) !== -1;
return (hasLocale ? segments[1] : segments[0]) || 'home';
/^[a-z]{2}(-[a-z]{2})?$/ would match any two-letter segment — including real sections like /us/pricing (a "US" section) or /de/about (a "DE" region page on an English site). The known list avoids these false positives.
| URL | First segment | In locale list? | Result |
|---|---|---|---|
/en/products/shoes | en | Yes — skip | products |
/fr-ca/aide | fr-ca | Yes — skip | aide |
/en | en | Yes — skip | home (locale-only = homepage) |
/de/ | de | Yes — skip | home |
/products/shoes | products | No | products |
/us/pricing | us | No (not in list) | us (treated as a section) |
/ | (none) | — | home |
LOCALES array in site.locale, it will be treated as a site section instead of being skipped. Review the list when new languages or regions are launched.
// Data element: site.subSection
var LOCALES = ['en','en-us','en-gb','en-au','en-ca','fr','fr-ca','fr-fr','de','de-de','de-at','es','es-mx','es-es','pt','pt-br','ja','ko','zh','zh-cn','zh-tw','it','nl','ru','sv','nb','da','fi'];
var segments = location.pathname.split('/').filter(Boolean);
var hasLocale = segments.length > 0 && LOCALES.indexOf(segments[0].toLowerCase()) !== -1;
return (hasLocale ? segments[2] : segments[1]) || '';
// Data element: site.hierarchy
var LOCALES = ['en','en-us','en-gb','en-au','en-ca','fr','fr-ca','fr-fr','de','de-de','de-at','es','es-mx','es-es','pt','pt-br','ja','ko','zh','zh-cn','zh-tw','it','nl','ru','sv','nb','da','fi'];
var segments = location.pathname.split('/').filter(Boolean);
var hasLocale = segments.length > 0 && LOCALES.indexOf(segments[0].toLowerCase()) !== -1;
if (hasLocale) segments = segments.slice(1);
return segments.join('|') || 'home';
// Data element: site.sectionFromDOM
var breadcrumb = document.querySelector(
'nav[aria-label="breadcrumb"] a, .breadcrumb a, ' +
'.breadcrumbs a, [data-breadcrumb] a'
);
if (breadcrumb) return breadcrumb.textContent.trim();
var activeNav = document.querySelector(
'nav .active, nav [aria-current="page"], ' +
'.nav-item.active, .navigation .current'
);
if (activeNav) return activeNav.textContent.trim();
return location.pathname.split('/').filter(Boolean)[0] || 'home';
// Data element: customer.type
var dl = window.digitalData || {};
var user = dl.user || dl.customer || {};
if (user.type) return user.type;
if (user.segment) return user.segment;
if (user.tier) return user.tier;
if (user.customerType) return user.customerType;
var body = document.body;
if (body.classList.contains('vip') || body.classList.contains('premium')) return 'premium';
if (body.classList.contains('member')) return 'member';
if (body.getAttribute('data-customer-type')) return body.getAttribute('data-customer-type');
var cookies = document.cookie.split('; ');
for (var i = 0; i < cookies.length; i++) {
if (cookies[i].startsWith('customer_type=')) return cookies[i].split('=')[1];
if (cookies[i].startsWith('user_segment=')) return cookies[i].split('=')[1];
}
return 'unknown';
// Data element: user.loginStatus
var dl = window.digitalData || {};
var user = dl.user || {};
if (user.authState) return user.authState;
if (user.loginStatus) return user.loginStatus;
if (user.isLoggedIn === true) return 'logged in';
if (user.isLoggedIn === false) return 'logged out';
if (document.querySelector(
'.logged-in, .authenticated, [data-logged-in], ' +
'.user-menu, .account-nav, .my-account, ' +
'[data-auth-state="authenticated"], .user-avatar'
)) return 'logged in';
var meta = document.querySelector(
'meta[name="user-status"], meta[name="login-status"], meta[name="auth-state"]'
);
if (meta && meta.content) return meta.content;
var c = document.cookie;
if (c.includes('loggedIn=true') || c.includes('auth=1') ||
c.includes('is_authenticated=1')) return 'logged in';
return 'logged out';
ECID, MCID, and MID all refer to the same identifier. Adobe rebranded "Marketing Cloud" to "Experience Cloud" in 2017. The ID itself never changed — only the name.
| Where | Name used | Example |
|---|---|---|
| Adobe docs (current) | ECID | Experience Cloud ID |
| Adobe docs (older) | MCID / MID | Marketing Cloud ID — same value |
| Cookie name | AMCV_ | AMCV_ORGID%40AdobeOrg |
| Cookie value key | MCMID | MCMID|12345678901234567890 |
| Visitor API method | getMarketingCloudVisitorID() | Returns the ECID as a string |
| Reporting (Workspace) | Experience Cloud ID | Dimension name |
// Data element: visitor.ecid
if (typeof Visitor !== 'undefined') {
var visitor = Visitor.getInstance('INSERT-ORG-ID-HERE@AdobeOrg');
if (visitor && visitor.getMarketingCloudVisitorID) {
var ecid = visitor.getMarketingCloudVisitorID();
if (ecid) return ecid;
}
}
// Fallback: extract from AMCV cookie (pipe-delimited, MCMID key)
var cookies = document.cookie.split('; ');
for (var i = 0; i < cookies.length; i++) {
if (cookies[i].startsWith('AMCV_')) {
var val = decodeURIComponent(cookies[i].split('=').slice(1).join('='));
var match = val.match(/MCMID\|(\d+)/);
if (match) return match[1];
}
}
return '';
INSERT-ORG-ID-HERE@AdobeOrg with the actual Adobe Organization ID. This is found in the Adobe Admin Console or in the ECID Service extension configuration.
Campaign codes arrive through different parameters depending on the channel, can be lost on redirects, and need to persist in SPAs where navigation strips the URL.
| Problem | Example | Risk |
|---|---|---|
| Different param names per channel | ?cid= (email), ?utm_campaign= (Google), ?gclid= (paid search) | Checking only one misses the others |
| Redirect strips params | /landing?cid=abc → 301 → /products | Campaign value is gone on the destination page |
| SPA navigation loses params | Initial load has ?cid=abc, user navigates, params disappear | Only the first page view gets attribution |
| Multiple params present | ?cid=email123&utm_campaign=google456 | Inconsistent attribution if priority isn't defined |
| Encoded characters | ?cid=summer%2026%20sale | Needs decoding for clean reporting |
// Data element: campaign.code
// Only captures known campaign parameters — ignores unrelated query params
// like page, sort, filter, etc. that would pollute s.campaign.
var CAMPAIGN_PARAMS = [
'cid', // Adobe standard
's_cid', // Adobe alternate
'utm_campaign', // Google campaign name
'utm_source', // Google traffic source
'utm_medium', // Google channel
'utm_term', // Google paid keyword
'utm_content', // Google ad variation
'gclid', // Google Ads click ID
'fbclid', // Meta/Facebook click ID
'msclkid', // Microsoft Ads click ID
'ttclid', // TikTok click ID
'campaign', // Generic
'promo', // Promotional
'icid' // Internal campaign
];
var params = new URLSearchParams(location.search);
var found = [];
CAMPAIGN_PARAMS.forEach(function(key) {
var val = params.get(key);
if (val) found.push(key + ':' + val);
});
if (found.length) {
// Change '|' to match reporting preferences
var code = found.join('|');
try { sessionStorage.setItem('_campaign', code); } catch(e) {}
return code;
}
// No campaign params in URL — check sessionStorage (survives redirects, clears on tab close)
try {
var stored = sessionStorage.getItem('_campaign');
if (stored) return stored;
} catch(e) {}
return '';
s.campaign (session-level attribution).// Data element: campaign.channel
var params = new URLSearchParams(location.search);
if (params.get('gclid')) return 'paid search';
if (params.get('fbclid')) return 'paid social';
if (params.get('msclkid')) return 'paid search';
if (params.get('ttclid')) return 'paid social';
if (params.get('utm_medium')) return params.get('utm_medium');
if (params.get('cid') || params.get('s_cid')) return 'email';
if (params.get('icid')) return 'internal';
return '';
// Data element: campaign.utmFull
// Format: "source|medium|campaign|term|content"
var params = new URLSearchParams(location.search);
var parts = [
params.get('utm_source') || '',
params.get('utm_medium') || '',
params.get('utm_campaign') || '',
params.get('utm_term') || '',
params.get('utm_content') || ''
];
var hasUtm = parts.some(function(p) { return p !== ''; });
return hasUtm ? parts.join('|') : '';
Defining the data element captures the value. The rule must consume it. Add these to the Page View rule (Rule 1) Set Variables action:
// In Rule 1 (Page View) — Adobe Analytics > Set Variables
s.campaign = "%campaign.code%";
s.eVar6 = "%campaign.channel%";
s.eVar7 = "%campaign.utmFull%";
Or if using Custom Code in Rule 1 instead of Set Variables UI fields:
s.campaign = _satellite.getVar('campaign.code');
s.eVar6 = _satellite.getVar('campaign.channel');
s.eVar7 = _satellite.getVar('campaign.utmFull');
| Data Element | Maps to | What it captures | Docs |
|---|---|---|---|
campaign.code | s.campaign | Primary campaign ID with session persistence | campaign |
campaign.channel | s.eVar6 | Marketing channel derived from click IDs and UTM medium | eVars |
campaign.utmFull | s.eVar7 | Full UTM parameter string for granular breakdowns |
%data_element_name% syntax in the field value. Launch resolves it before setting the variable._satellite.getVar('data_element_name') to programmatically retrieve the resolved value.// Data element: traffic.source
var params = new URLSearchParams(location.search);
var ref = document.referrer;
if (params.get('utm_medium')) return params.get('utm_medium');
if (params.get('cid') || params.get('gclid') || params.get('fbclid')) return 'paid';
if (!ref) return 'direct';
if (/google\.|bing\.|yahoo\.|duckduckgo\.|baidu\./.test(ref)) return 'organic';
if (/facebook\.|twitter\.|linkedin\.|instagram\.|tiktok\.|youtube\./.test(ref)) return 'social';
if (/mail\.|outlook\.|gmail\./.test(ref)) return 'email';
if (ref.includes(location.hostname)) return 'internal';
return 'referral';
// Data element: performance.pageLoadTime
// Use with Window Loaded event (not Library Loaded) so timing is complete
var nav = performance.getEntriesByType('navigation')[0];
if (nav) return Math.round(nav.loadEventEnd - nav.startTime).toString();
return '';
// Data element: device.category
var w = window.innerWidth;
if (w < 768) return 'mobile';
if (w < 1024) return 'tablet';
return 'desktop';
// Data element: content.category
var meta = document.querySelector(
'meta[property="article:section"], meta[name="category"], ' +
'meta[name="content-type"], [data-content-category]'
);
if (meta) return meta.content || meta.getAttribute('data-content-category');
var schema = document.querySelector('[itemtype*="BreadcrumbList"] [itemprop="name"]');
if (schema) return schema.textContent.trim();
var segments = location.pathname.split('/').filter(Boolean);
return segments[0] || '';
return — without it, the data element resolves to undefinedundefined or null.substring(0, 255)) to stay within Adobe reporting limits.toLowerCase())site.section, user.loginStatus) for clear organizationWhen the data layer doesn't exist or is incomplete, values can be written from Launch as a bridge until proper site-level implementation is in place.
// TEMPORARY — remove once site code populates the data layer properly.
// The if(!value) guards preserve server-rendered values when they arrive.
window.digitalData = window.digitalData || {};
window.digitalData.page = window.digitalData.page || {};
window.digitalData.user = window.digitalData.user || {};
if (!window.digitalData.page.name) {
window.digitalData.page.name = document.title.replace(/\s*[\|–—]\s*[^|–—]+$/, '').trim();
}
if (!window.digitalData.page.section) {
window.digitalData.page.section = location.pathname.split('/').filter(Boolean)[0] || 'home';
}
if (!window.digitalData.page.type) {
var path = location.pathname.toLowerCase();
if (path === '/') window.digitalData.page.type = 'home';
else if (path.includes('/product')) window.digitalData.page.type = 'product';
else if (path.includes('/cart')) window.digitalData.page.type = 'cart';
else if (path.includes('/search')) window.digitalData.page.type = 'search';
else window.digitalData.page.type = 'general';
}
if (!window.digitalData.user.loginStatus) {
window.digitalData.user.loginStatus = document.querySelector(
'.logged-in, .authenticated, .user-menu'
) ? 'logged in' : 'logged out';
}
| Principle | Why |
|---|---|
Check if (!window.digitalData.page.name) before writing | Preserves any value the site already set. When proper implementation arrives, it takes priority automatically — no changes needed. |
| Write in the Global Config rule at Order 1 | Ensures values exist before any data element resolves. Data elements read from the data layer — if the layer is empty at read time, they get nothing. |
| Use the same object path the eventual implementation will use | Data elements already reference digitalData.page.name. When the site populates that path, the custom code's if guard skips it and nothing else changes. |
| Never overwrite existing values | Server-rendered values are always more accurate than DOM scraping. The guard ensures scraped values are fallback only. |
| Comment that it's temporary | When someone asks "where is this coming from?" the answer is findable. Include a removal date target if possible. |
// SPA route change — re-scrape and fire beacon
window.digitalData.page.name = document.title.replace(/\s*[\|–—]\s*[^|–—]+$/, '').trim();
window.digitalData.page.section = location.pathname.split('/').filter(Boolean)[0] || 'home';
s.pageName = window.digitalData.page.name;
s.channel = window.digitalData.page.section;
s.t();
if (!value) guards allow gradual removal — one value at a timeThe most resilient implementation uses a layered approach: read from the data layer when available, fall back to DOM scraping when it's not. This eliminates the dependency on a perfectly populated data layer while still using it when present.
var dl = window.digitalData || {};
var page = dl.page || {};
if (page.name && page.name.trim()) return page.name.trim();
return document.title.replace(' | SiteName', '').trim();
var dl = window.digitalData || {};
var page = dl.page || {};
// Regex strips common title suffixes like " | Site Name" or " — Brand"
return (page.name && page.name.trim())
|| (page.pageName && page.pageName.trim())
|| document.title.replace(/\s*[\|–—]\s*[^|–—]+$/, '').trim();
var dl = window.digitalData || {};
var page = dl.page || {};
return (page.section && page.section.trim())
|| (page.siteSection && page.siteSection.trim())
|| (page.category && page.category.trim())
|| location.pathname.split('/').filter(Boolean)[0] || 'home';
var dl = window.digitalData || {};
var page = dl.page || {};
if (page.type && page.type.trim()) return page.type.trim();
if (page.pageType && page.pageType.trim()) return page.pageType.trim();
var path = location.pathname.toLowerCase();
if (path === '/' || path === '/index') return 'home';
if (path.includes('/product')) return 'product';
if (path.includes('/cart') || path.includes('/basket')) return 'cart';
if (path.includes('/checkout')) return 'checkout';
if (path.includes('/confirm') || path.includes('/thank')) return 'confirmation';
if (path.includes('/search')) return 'search';
if (path.includes('/login') || path.includes('/signin')) return 'login';
if (path.includes('/account') || path.includes('/profile')) return 'account';
if (path.includes('/blog') || path.includes('/article') || path.includes('/news')) return 'content';
if (path.includes('/faq') || path.includes('/help') || path.includes('/support')) return 'support';
if (path.includes('/contact')) return 'contact';
if (path.includes('/category') || path.includes('/collection')) return 'category';
if (document.querySelector('.error-page, .page-404, [data-error]')) return 'error';
return 'general';
var dl = window.digitalData || {};
var user = dl.user || {};
if (user.authState) return user.authState;
if (user.loginStatus) return user.loginStatus;
if (user.authenticated !== undefined) return user.authenticated ? 'authenticated' : 'anonymous';
if (document.querySelector(
'.logged-in, .authenticated, [data-logged-in], ' +
'.user-menu, .account-nav, .my-account'
)) return 'authenticated';
var meta = document.querySelector('meta[name="user-status"]');
if (meta) return meta.content;
if (document.cookie.includes('loggedIn=') ||
document.cookie.includes('session=') ||
document.cookie.includes('auth='))
return 'authenticated';
return 'anonymous';
var dl = window.digitalData || {};
if (dl.search && dl.search.term) return dl.search.term;
if (dl.page && dl.page.searchTerm) return dl.page.searchTerm;
// Fallback: grab all query params as key:value pairs
var qs = location.search.substring(1);
if (!qs) return '';
return decodeURIComponent(qs).replace(/&/g, '|').replace(/=/g, ':');
window.digitalData. Replace with the actual data layer variable name used on the site — common alternatives include window.dataLayer, window.utag_data, window.pageData, or any custom object. The fallback logic remains the same regardless of the variable name.
| Scenario | Approach |
|---|---|
| Data layer exists and is fully populated | Use data layer values directly — DOM fallbacks will never fire |
| Data layer exists but is partially populated | Hybrid — use data layer where available, DOM fills the gaps |
| Data layer does not exist or is empty | Hybrid gracefully degrades to full DOM scraping |
| Data layer is planned but not yet deployed | Start with DOM scraping now, add data layer checks later — no reconfiguration needed |
| Component | Configuration | Docs |
|---|---|---|
| Event | Core > Library Loaded (Page Top) | Core events |
| Order | 50 (fires after Rules 2-3 at order 40 so their variables are set first) | Rule ordering |
| Condition | None | |
| Action 1 | Adobe Analytics > Set Variables | Rules |
| ||
| Action 2 | Adobe Analytics > Send Beacon — s.t() page view | s.t() |
| Component | Configuration | Docs |
|---|---|---|
| Event | Core > Library Loaded | Core events |
| Order | 40 (must fire BEFORE Rule 1 at order 50) | Rule ordering |
| Condition | Core > Value Comparison: %page.type% equals error | Conditions |
| Action 1 | Adobe Analytics > Set Variables | |
| ||
s.t() on every page — including error and search pages. Rules 2 and 3 only set additional variables. If they also fired s.t(), error and search pages would generate two page view beacons, inflating page view counts. Set the order lower than Rule 1 so the variables are in place before the beacon fires.
| Component | Configuration | Docs |
|---|---|---|
| Event | Core > Library Loaded | Core events |
| Order | 40 (must fire BEFORE Rule 1 at order 50) | Rule ordering |
| Condition | Core > Value Comparison: %search.term% is not empty | Conditions |
| Action 1 | Adobe Analytics > Set Variables | |
| ||
One rule covers every clickable element. Reads whatever data exists on the element automatically.
this bound to the clicked DOM element. The code below runs inside that context — there is no need to write an addEventListener or wrap the code in a handler function. Launch handles that.
| Component | Configuration | Docs |
|---|---|---|
| Event | Core > Click | Core extension |
Elements matching CSS: a, button, [role="button"], input[type="submit"], [onclick] | ||
| Condition | None | |
| Action 1 | Adobe Analytics > Custom Code | s.tl() · linkTrackVars · linkTrackEvents |
// 'this' may be a child element (span/svg inside a button), so walk up
var el = this;
while (el && !el.matches('a, button, [role="button"], input[type="submit"]')) {
el = el.parentElement;
}
if (!el) el = this;
var label = el.getAttribute('aria-label')
|| el.getAttribute('title')
|| el.getAttribute('data-track-label')
|| el.getAttribute('data-analytics')
|| el.getAttribute('data-name')
|| el.textContent.trim().substring(0, 100)
|| el.getAttribute('alt')
|| el.value
|| 'unlabeled';
var region = '';
var parent = el.closest(
'[id], [role="navigation"], [role="banner"], [role="main"], ' +
'[role="contentinfo"], nav, header, footer, aside, main, [data-region]'
);
if (parent) {
region = parent.getAttribute('data-region')
|| parent.getAttribute('id')
|| parent.getAttribute('role')
|| parent.tagName.toLowerCase();
}
var href = el.getAttribute('href') || '';
s.eVar10 = label;
s.prop10 = label;
s.eVar11 = region || 'unknown';
s.prop11 = region || 'unknown';
s.eVar12 = href;
// linkTrackVars: unlisted variables are silently excluded from the beacon
s.linkTrackVars = 'eVar10,prop10,eVar11,prop11,eVar12,events';
s.linkTrackEvents = 'event10';
s.events = 'event10';
// 'this' as first arg = wait up to 500ms for beacon before navigation
s.tl(this, 'o', label);
s.tl() call MUST be inside the Custom Code action — not as a separate "Send Beacon" action. Launch's Send Beacon action wants the link name as a static field or data element, and it can't read the label variable from the preceding custom code. Firing s.tl() directly in the code block solves this.
s.tl() controls navigation delay.
this — Wait up to 500ms for the beacon to send before allowing navigation. This is the documented default value.true — Do not wait. The beacon fires but navigation proceeds immediately. Suitable for non-navigation interactions.| Component | Configuration | Docs |
|---|---|---|
| Event | Core > Form Submission (or Custom Event for SPA) | Core events |
Elements matching CSS: form | ||
| Condition | None | |
| Action 1 | Adobe Analytics > Custom Code (fires s.tl() inside) | s.tl() |
// 'this' is the submitted form element
var formName = this.getAttribute('name')
|| this.getAttribute('id')
|| this.getAttribute('action')
|| (this.querySelector('[type="submit"]') || {}).textContent?.trim()
|| 'unknown form';
s.eVar13 = formName;
s.prop13 = formName;
s.linkTrackVars = 'eVar13,prop13,events';
s.linkTrackEvents = 'event11';
s.events = 'event11';
s.tl(this, 'o', formName);
s.tl() inside the Custom Code.
Do not add a separate Send Beacon action. The formName variable is local to this code block and cannot be accessed by a Send Beacon action's static link name field.
If developers eventually add data attributes to the HTML, you can target specific elements without changing Launch:
<!-- Any clickable element -->
<a href="/products" data-track-click data-track-label="Nav: Products">Products</a>
<button data-track-click data-track-label="Hero: Learn More">Learn More</button>
<!-- Any form -->
<form data-track-form="Contact Form" action="/submit">...</form>
The smart click rule (Rule 4) already checks for data-track-label and data-analytics attributes in its priority chain. So when devs add them, the tracking automatically gets more specific — no Launch changes needed.
When targeting a specific element beyond the universal click tracker, use this priority for selectors:
| Priority | Selector Pattern | Example | Reliability |
|---|---|---|---|
| 1 | ID | #sign-up-btn | Best |
| 2 | Data attribute | [data-action="subscribe"] | Best |
| 3 | Aria label | [aria-label="Close dialog"] | Good |
| 4 | Unique class + tag | button.cta-primary | OK |
| 5 | Container + tag | #hero-section button | OK |
| 6 | Structural path | nav > ul > li:nth-child(3) > a | Fragile |
.flex, .p-4, .w-full, .text-sm are not semantic — they exist on hundreds of elements and change frequently. Target structural or semantic classes only.
%garbage% or doesn't existLaunch data elements can be tested directly in the browser console without publishing changes. These methods return the resolved value of any %data_element% in real time.
_satellite.getVar('page.name');
_satellite.getVar('page.type');
_satellite.getVar('user.authState');
_satellite.getVar('campaign.trackingCode');
_satellite.getVar('search.term');
[
'page.name',
'page.section',
'page.type',
'page.heading',
'user.loginStatus',
'customer.type',
'search.term',
'campaign.code',
'campaign.channel',
'campaign.utmFull',
'visitor.ecid',
'traffic.source',
'env.name'
].forEach(function(name) {
var val = _satellite.getVar(name);
console.log(name + ': ' + (val || '(empty)'));
});
// _satellite._container is INTERNAL/UNDOCUMENTED — debug only, not for production
var names = Object.keys(_satellite._container.dataElements || {});
console.log('Data elements found: ' + names.length);
names.sort().forEach(function(name) {
var val = _satellite.getVar(name);
console.log(name + ': ' + (val || '(empty)'));
});
_satellite.getVar() API only. Safe, stable, supported. Requires knowing the data element names in advance._satellite._container. Useful when inheriting a property with unknown data elements. Internal API — use for debugging only, not in production code.(function() {
var dl = window.digitalData || {};
var page = dl.page || {};
var result = (page.name && page.name.trim())
|| document.title.replace(/\s*[\|–—]\s*[^|–—]+$/, '').trim();
console.log('page.name would resolve to:', result);
console.log('Source:', (page.name && page.name.trim()) ? 'data layer' : 'DOM fallback');
})();
console.log('digitalData:', window.digitalData);
console.log('dataLayer:', window.dataLayer);
console.log('utag_data:', window.utag_data);
console.table(window.digitalData && window.digitalData.page);
s object is created when the Adobe Analytics extension initializes, which happens at Library Loaded (Page Top) — in the <head>, before the DOM is parsed. Page-level variables (s.pageName, s.channel, etc.) are populated when the page view rule fires, also at Library Loaded. By the time the console is opened manually, the s object is fully populated with all page-level values. Click and form tracking variables are only populated after those interactions occur.
console.log('pageName:', s.pageName);
console.log('channel:', s.channel);
console.log('campaign:', s.campaign);
console.log('eVar1:', s.eVar1);
console.log('events:', s.events);
for (var i = 1; i <= 75; i++) {
if (s['prop' + i]) console.log('prop' + i + ': ' + s['prop' + i]);
}
for (var i = 1; i <= 250; i++) {
if (s['eVar' + i]) console.log('eVar' + i + ': ' + s['eVar' + i]);
}
_satellite.setDebug(true);
_satellite.setDebug(false);
console.log('Build:', _satellite.buildInfo);
console.log('Property:', _satellite.property);
console.log('Environment:', _satellite.environment);
_satellite.buildInfo returns:
{
buildDate: "2026-04-15T20:30:00Z",
environment: "production",
turbineVersion: "27.5.0",
turbineBuildDate: "2026-03-01T00:00:00Z"
}
Useful for confirming which build is live, when it was published, and whether the page is running the staging or production library.
var el = document.querySelector('#sign-up-btn');
var label = el.getAttribute('aria-label')
|| el.getAttribute('title')
|| el.getAttribute('data-track-label')
|| el.getAttribute('data-analytics')
|| el.getAttribute('data-name')
|| el.textContent.trim().substring(0, 100)
|| el.getAttribute('alt')
|| el.value
|| 'unlabeled';
var parent = el.closest(
'[id], [role="navigation"], [role="banner"], [role="main"], ' +
'[role="contentinfo"], nav, header, footer, aside, main'
);
var region = parent
? (parent.getAttribute('id') || parent.getAttribute('role') || parent.tagName.toLowerCase())
: 'unknown';
console.log('Label (eVar10):', label);
console.log('Region (eVar11):', region);
console.log('Href (eVar12):', el.getAttribute('href') || 'none');
Using the AA Debugger or Adobe Experience Platform Debugger extension:
| Check | Expected |
|---|---|
| Page load fires | s.t() with pageName and channel populated |
| Click on any link/button | Activity Map data in network request (a.activitymap.*) |
| Click on tracked element | s.tl() with eVar10 (label) and eVar11 (region) populated |
| Form submit | s.tl() with eVar13 (form name) populated |
| Search page | eVar5 populated with search term |
| Error page | s.pageType = "errorPage", event20 fires |
| Console | No undefined warnings for %value% references |
| What | How | Developer needed? |
|---|---|---|
| Page name | document.title | No |
| Page type | URL pattern matching | No |
| Site section | First path segment | No |
| User state | DOM class / cookie sniffing | No |
| Search term | URL params or input value | No |
| All clicks | Activity Map | No |
| Smart clicks | Universal click rule (DOM scraping) | No |
| Form submits | Form name/id/action scraping | No |
| Errors | DOM class detection | No |
| Campaign | Query string params | No |
| Downloads | Built-in link tracking | No |
| Exit links | Built-in link tracking | No |
| Time on page | Built into AppMeasurement | No |
| Scroll depth | Core > Scroll event | No |
| Variable | Purpose | Set by | Docs |
|---|---|---|---|
s.pageName | Page name | Rule 1 | pageName |
s.channel | Site section | Rule 1 | channel |
s.pageType | Page type | Rule 1 / Rule 2 | pageType |
s.campaign | Campaign tracking code | Rule 1 | campaign |
s.eVar1 | User auth state | Rule 1 | eVars |
s.prop1 | Page type (hit-level) | Rule 1 | Props |
s.prop2 | Page heading | Rule 1 | |
s.server | Domain / hostname | Rule 1 | server |
s.eVar5 / s.prop5 | Internal search term | Rule 3 | eVars |
s.eVar10 / s.prop10 | Click label | Rule 4 | eVars |
s.eVar11 / s.prop11 | Click region | Rule 4 | |
s.eVar12 | Click destination URL | Rule 4 | |
s.eVar13 / s.prop13 | Form name | Rule 5 | |
event5 | Internal search | Rule 3 | Events |
event10 | Click event | Rule 4 | |
event11 | Form submit | Rule 5 | |
event20 | Error page | Rule 2 |
| Call | When | Type | Docs |
|---|---|---|---|
s.t() | Page view, error page, search page | Page view beacon | s.t() method |
s.tl(this, 'o', name) | Click tracking, form submit | Custom link beacon | s.tl() method |