Launch + AppMeasurement Clean Implementation Guide

Complete implementation reference for Adobe Analytics via Launch with AppMeasurement.

Contents

  1. Extension Configuration
  2. Activity Map
  3. Data Elements
  4. Page Type Detection
  5. User State Detection
  6. Search Term Detection
  7. Common Data Elements
  8. Hybrid Data Layer + DOM Fallback
  9. Rules
  10. Click Tracking
  11. Form Tracking
  12. Data Attribute Pattern
  13. CSS Selector Strategy
  14. Cleanup Checklist
  15. Console Testing
  16. Validation
  17. Coverage Matrix
  18. API Reference

1. Extension Configuration

Adobe Analytics Extension

SettingValueDocs
Report Suite IDProduction + Staging (use environment-specific)Report suites
Tracking ServerYour tracking servers.trackingServer
Character SetUTF-8s.charSet
Currency CodeUSDs.currencyCode

Full extension reference →

Link Tracking Settings

SettingValueWhyDocs
Enable Activity MapONAuto-tracks every click with link text, region, and pageActivity Map
Track download linksONAuto-tracks file downloadss.trackDownloadLinks
Track outbound linksONAuto-tracks exit linkss.trackExternalLinks
Download extensionsdoc,docx,eps,jpg,png,pdf,pptx,svg,xls,xlsx,zipFile types to track as downloadss.linkDownloadFileTypes
Activity Map gives you click tracking for free. It auto-captures every link click with the link text, the region (parent container ID), and the page. Shows click heatmaps in Adobe Analytics. No rules, no data elements needed. See Activity Map Deep Dive below.

2. Activity Map

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.

What Activity Map sends on every click

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.

Extension settings that must be ON

SettingWhereMust beDocs
Enable ClickMap / Activity MapAA Extension > Link TrackingONGetting started
Enable Link TrackingAA Extension > Link TrackingONtrackExternalLinks
Track download linksAA Extension > Link TrackingONtrackDownloadLinks
Track outbound linksAA Extension > Link TrackingONtrackExternalLinks

Code settings (no checkbox in the UI)

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:

Option A: Extension Custom Code (requires extension configuration access)

AA Extension > Configure Tracker Using Custom Code:

s.trackInlineStats = true;
s.linkInternalFilters = location.hostname; // distinguishes internal vs exit links
This is the preferred method. The extension custom code runs once when the library loads, before any rules fire. This guarantees Activity Map is active before the first click.

Option B: Rule-based config (when extension configuration access is unavailable)

When the AA extension settings cannot be edited directly, create a dedicated config rule instead:

ComponentConfigurationDocs
Rule nameGlobal ConfigRules
EventCore > Library Loaded (Page Top)Core events
Order1 (lowest fires first — must run before all other rules)Rule ordering
ConditionNone
ActionAdobe Analytics > Custom Code
s.trackInlineStats = true;
s.linkInternalFilters = location.hostname;
Why Library Loaded (Page Top) with Order 1? This is the earliest rule event — fires as soon as the Launch library loads, before DOM Ready and Window Loaded. Setting the order to 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.
Rule ordering reference: Lower numbers fire first. Event-based rules (click, form submit) fire on their trigger regardless of order — order only matters for rules sharing the same event type.

What Activity Map tracks vs doesn't

Element typeAuto-tracked?Notes
<a href="...">YesAll anchor links with href
<a> with onclickYesJavaScript-driven links
<button>SometimesOnly if AppMeasurement recognizes it as interactive. Not guaranteed.
<div onclick="...">NoNot a standard link element
[role="button"]NoARIA roles not recognized by Activity Map
<input type="submit">SometimesDepends on form structure
Activity Map only reliably tracks <a> tags. For buttons, divs with onclick, role="button", and other non-link interactive elements, the custom s.tl() rule (Rule 4 below) is required. Activity Map + Rule 4 together cover everything.

Why clicks might not appear

  1. s.trackInlineStats is false — the main toggle. Must be true.
  2. Link tracking globally disabled — check "Enable Link Tracking" in extension settings.
  3. The element isn't an <a> tag — Activity Map only auto-fires on elements AppMeasurement identifies as links. Use Rule 4 for everything else.
  4. 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.
  5. The link text is empty — icon-only links with no text content, no aria-label, and no title won't produce a useful Activity Map entry.

Activity Map in Adobe Analytics reporting

Once enabled, these dimensions are available in Analysis Workspace:

DimensionWhat it shows
Activity Map LinkThe text/label of the clicked element
Activity Map RegionThe container ID where the click occurred
Activity Map PageThe page where the click happened
Activity Map Link By RegionLink + 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.

Activity Map + custom s.tl() rule = full coverage

ElementActivity Map handles it?Rule 4 handles it?
Standard <a> linksYesYes
ButtonsMaybeYes
role="button" divsNoYes
Form submitsNoYes
onclick handlersNoYes
Icon-only elementsNo labelYes (reads aria-label)
Use both. Activity Map provides baseline coverage. Rule 4 catches everything Activity Map misses. They complement each other with no conflict when mapped to separate eVars.

3. Data Elements

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 →

Page-Level

NameTypeSourceMaps toDocs
page.nameCustom Codereturn document.title.replace(' | SiteName', '').trim();s.pageNamepageName
page.sectionCustom Codereturn location.pathname.split('/').filter(Boolean)[0] || 'home';s.channelchannel
page.typeCustom CodeSee Page Type Detections.pageType / s.prop1pageType
page.headingCustom Codevar h = document.querySelector('h1'); return h ? h.textContent.trim().substring(0, 255) : document.title.trim().substring(0, 255);s.prop2
page.urlCore > URLFull URLURL type
page.pathCore > URLPath only
page.queryStringCore > URLQuery string
page.domainCore > URLHostnames.serverserver

User-Level

NameTypeSourceMaps toDocs
user.authStateCustom CodeSee User State Detections.eVar1eVars

Campaign

NameTypeSourceMaps toDocs
campaign.trackingCodeQuery String Parametercid or utm_campaigns.campaigncampaign

Search

NameTypeSourceMaps toDocs
search.termCustom CodeSee Search Term Detections.eVar5 / s.prop5eVars

Environment

NameTypeSourceMaps toDocs
env.nameCustom Codereturn location.hostname.includes('staging') ? 'staging' : 'production';
Delete everything else. If a %value% references a data element that doesn't exist and isn't in this list, kill it.

4. Page Type Detection

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';
Customize the URL patterns to match the site structure. Look at the actual URL structure and adjust the includes() checks to match. The logic above covers the most common patterns.

5. User State Detection

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
  || '';

7. Common Data Elements — Best Practices

Reference patterns for frequently tracked values. Each example shows the recommended approach with fallback chains, proper return statements, and inline documentation.

Query Parameters

Full query string capture

// 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=priceq: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.


Search

Search results count

// 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 '';

Site Sections (hierarchical)

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 typeURLSection
E-commerce/products/shoes/runningproducts
SaaS/docs/api/authenticationdocs
Media/sports/nfl/scoressports
Corporate/about/careers/engineeringabout
Support/help/billing/refundshelp

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

Locale from URL (standalone utility)

// 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() : '';

Language code

// 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 '';

Primary section (level 1)

// 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';
Locale prefix handling. A known locale list is more reliable than regex-only detection. The regex /^[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.
URLFirst segmentIn locale list?Result
/en/products/shoesenYes — skipproducts
/fr-ca/aidefr-caYes — skipaide
/enenYes — skiphome (locale-only = homepage)
/de/deYes — skiphome
/products/shoesproductsNoproducts
/us/pricingusNo (not in list)us (treated as a section)
/(none)home
Keep the locale list updated. If a new locale is added to the site but not to the 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.

Sub-section (level 2)

// 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]) || '';

Full breadcrumb path

// 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';

Section from DOM (breadcrumb or nav)

// 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';

Customer Status

Customer type / tier

// 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';

Login Status

Authentication state

// 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 (Experience Cloud ID)

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.

WhereName usedExample
Adobe docs (current)ECIDExperience Cloud ID
Adobe docs (older)MCID / MIDMarketing Cloud ID — same value
Cookie nameAMCV_AMCV_ORGID%40AdobeOrg
Cookie value keyMCMIDMCMID|12345678901234567890
Visitor API methodgetMarketingCloudVisitorID()Returns the ECID as a string
Reporting (Workspace)Experience Cloud IDDimension name

Retrieve the ECID from the Visitor API

// 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 '';
Replace 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 / Traffic Source

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.

ProblemExampleRisk
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 → /productsCampaign value is gone on the destination page
SPA navigation loses paramsInitial load has ?cid=abc, user navigates, params disappearOnly the first page view gets attribution
Multiple params present?cid=email123&utm_campaign=google456Inconsistent attribution if priority isn't defined
Encoded characters?cid=summer%2026%20saleNeeds decoding for clean reporting

Campaign tracking code

// 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 '';
Why sessionStorage for campaign persistence?

Campaign channel breakdown

// 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 '';

Full UTM parameter capture

// 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('|') : '';

Wiring campaign data elements to Adobe Analytics variables

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 ElementMaps toWhat it capturesDocs
campaign.codes.campaignPrimary campaign ID with session persistencecampaign
campaign.channels.eVar6Marketing channel derived from click IDs and UTM mediumeVars
campaign.utmFulls.eVar7Full UTM parameter string for granular breakdowns
Two ways to reference data elements in Launch rules: Both return the same result. Set Variables UI is simpler for straightforward mappings. Custom Code is needed when logic depends on the value (conditionals, formatting, combining multiple elements).

Traffic source / medium

// 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';

Additional useful elements

Page load time (performance)

// 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 '';

Viewport / device category

// Data element: device.category
var w = window.innerWidth;
if (w < 768) return 'mobile';
if (w < 1024) return 'tablet';
return 'desktop';

Content group (for editorial/blog sites)

// 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] || '';
Best practices for all data elements:

Writing to the Data Layer from Custom Code (Stopgap)

When 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.

This is a temporary measure. Writing to the data layer from tag management is backwards — the data layer should be populated by the site before tags fire. This pattern bridges the gap while waiting for proper implementation. Remove it once the site code handles population.

Pattern: Initialize and populate at Order 1

// 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';
}

Key principles

PrincipleWhy
Check if (!window.digitalData.page.name) before writingPreserves 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 1Ensures 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 useData 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 valuesServer-rendered values are always more accurate than DOM scraping. The guard ensures scraped values are fallback only.
Comment that it's temporaryWhen someone asks "where is this coming from?" the answer is findable. Include a removal date target if possible.

For SPA (route changes)

// 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();

Cleanup plan

  1. Verify site-populated values match what the custom code was scraping
  2. Confirm data elements still resolve (same object path — should be seamless)
  3. Delete the custom code writes from the Global Config rule
  4. The if (!value) guards allow gradual removal — one value at a time

8. Hybrid Data Layer + DOM Fallback

The 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.

Why this pattern matters. Data layers are often incomplete, inconsistently populated across page types, or deployed in phases. A hybrid approach ensures tracking works on day one regardless of data layer readiness, and automatically upgrades to structured data as it becomes available.

The Pattern

var dl = window.digitalData || {};
var page = dl.page || {};
if (page.name && page.name.trim()) return page.name.trim();
return document.title.replace(' | SiteName', '').trim();

Applied to Each Data Element

page.name (hybrid)

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();

page.section (hybrid)

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';

page.type (hybrid)

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';

user.authState (hybrid)

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';

page.queryParams (hybrid)

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, ':');
Adapting the data layer object name. These examples reference 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.

When to use which approach

ScenarioApproach
Data layer exists and is fully populatedUse data layer values directly — DOM fallbacks will never fire
Data layer exists but is partially populatedHybrid — use data layer where available, DOM fills the gaps
Data layer does not exist or is emptyHybrid gracefully degrades to full DOM scraping
Data layer is planned but not yet deployedStart with DOM scraping now, add data layer checks later — no reconfiguration needed

9. Rules

Rule 1: Page View — All Pages

ComponentConfigurationDocs
EventCore > Library Loaded (Page Top)Core events
Order50 (fires after Rules 2-3 at order 40 so their variables are set first)Rule ordering
ConditionNone
Action 1Adobe Analytics > Set VariablesRules
s.pageName  = %page.name%
s.channel   = %page.section%
s.pageType  = %page.type%
s.server    = %page.domain%
s.prop1     = %page.type%
s.prop2     = %page.heading%
s.campaign  = %campaign.trackingCode%
s.eVar1     = %user.authState%
Action 2Adobe Analytics > Send Beacon — s.t() page views.t()

Rule 2: Error Page (Set Variables Only)

ComponentConfigurationDocs
EventCore > Library LoadedCore events
Order40 (must fire BEFORE Rule 1 at order 50)Rule ordering
ConditionCore > Value Comparison: %page.type% equals errorConditions
Action 1Adobe Analytics > Set Variables
// Do NOT add Send Beacon here — Rule 1 fires s.t()
s.pageType = "errorPage"
s.events   = "event20"
No Send Beacon on Rules 2 and 3. Rule 1 (Page View) fires 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.

Rule 3: Internal Search (Set Variables Only)

ComponentConfigurationDocs
EventCore > Library LoadedCore events
Order40 (must fire BEFORE Rule 1 at order 50)Rule ordering
ConditionCore > Value Comparison: %search.term% is not emptyConditions
Action 1Adobe Analytics > Set Variables
// Do NOT add Send Beacon here — Rule 1 fires s.t()
s.eVar5  = %search.term%
s.prop5  = %search.term%
s.events = "event5"

10. Click Tracking

One rule covers every clickable element. Reads whatever data exists on the element automatically.

How click handling works in Launch. The Core > Click event creates the click listener. When an element matching the CSS selector is clicked, Launch executes the Custom Code action with 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.

Rule 4: Universal Click Tracker

ComponentConfigurationDocs
EventCore > ClickCore extension
Elements matching CSS: a, button, [role="button"], input[type="submit"], [onclick]
ConditionNone
Action 1Adobe Analytics > Custom Codes.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);
Important: ONE action, not two. The 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.
The first argument to s.tl() controls navigation delay.
What this captures for every click:

11. Form Tracking

Rule 5: Form Submit

ComponentConfigurationDocs
EventCore > Form Submission (or Custom Event for SPA)Core events
Elements matching CSS: form
ConditionNone
Action 1Adobe 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);
Same pattern as Rule 4: fire 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.

12. Data Attribute Pattern

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.

13. CSS Selector Strategy

When targeting a specific element beyond the universal click tracker, use this priority for selectors:

PrioritySelector PatternExampleReliability
1ID#sign-up-btnBest
2Data attribute[data-action="subscribe"]Best
3Aria label[aria-label="Close dialog"]Good
4Unique class + tagbutton.cta-primaryOK
5Container + tag#hero-section buttonOK
6Structural pathnav > ul > li:nth-child(3) > aFragile
Avoid Tailwind/utility classes as selectors. Classes like .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.

14. Cleanup Checklist

  1. Delete every data element that references %garbage% or doesn't exist
  2. Delete every rule that references deleted data elements
  3. Configure the AA extension settings (Activity Map ON, link tracking ON)
  4. Create the clean data elements from the table above
  5. Create the rules (5 core rules + 1 global config rule if using Option B)
  6. Build to staging
  7. Validate with Adobe Debugger (see below)
  8. Build to production

15. Console Testing

Launch 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.

Method 1: _satellite.getVar()

_satellite.getVar('page.name');
_satellite.getVar('page.type');
_satellite.getVar('user.authState');
_satellite.getVar('campaign.trackingCode');
_satellite.getVar('search.term');

Method 2: Check known data elements (production-safe)

[
  '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)'));
});

Method 3: Dump ALL data elements (debug-only)

// _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)'));
});
Method 2 vs Method 3:

Method 4: Test specific custom code logic

(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');
})();

Method 5: Check the data layer object directly

console.log('digitalData:', window.digitalData);
console.log('dataLayer:', window.dataLayer);
console.log('utag_data:', window.utag_data);
console.table(window.digitalData && window.digitalData.page);

Method 6: Validate the s object after a page view fires

When is the s object available? The 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]);
}

Method 7: Enable Launch debug logging

_satellite.setDebug(true);
_satellite.setDebug(false);
Persists across page loads via cookie. Stays active until turned off or cookie is cleared.

Method 8: Build and environment info

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.

Method 9: Test a click tracking rule manually

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');

16. Validation

Using the AA Debugger or Adobe Experience Platform Debugger extension:

CheckExpected
Page load firess.t() with pageName and channel populated
Click on any link/buttonActivity Map data in network request (a.activitymap.*)
Click on tracked elements.tl() with eVar10 (label) and eVar11 (region) populated
Form submits.tl() with eVar13 (form name) populated
Search pageeVar5 populated with search term
Error pages.pageType = "errorPage", event20 fires
ConsoleNo undefined warnings for %value% references

17. Coverage Matrix

WhatHowDeveloper needed?
Page namedocument.titleNo
Page typeURL pattern matchingNo
Site sectionFirst path segmentNo
User stateDOM class / cookie sniffingNo
Search termURL params or input valueNo
All clicksActivity MapNo
Smart clicksUniversal click rule (DOM scraping)No
Form submitsForm name/id/action scrapingNo
ErrorsDOM class detectionNo
CampaignQuery string paramsNo
DownloadsBuilt-in link trackingNo
Exit linksBuilt-in link trackingNo
Time on pageBuilt into AppMeasurementNo
Scroll depthCore > Scroll eventNo

18. API Reference

Variable Mapping Summary

VariablePurposeSet byDocs
s.pageNamePage nameRule 1pageName
s.channelSite sectionRule 1channel
s.pageTypePage typeRule 1 / Rule 2pageType
s.campaignCampaign tracking codeRule 1campaign
s.eVar1User auth stateRule 1eVars
s.prop1Page type (hit-level)Rule 1Props
s.prop2Page headingRule 1
s.serverDomain / hostnameRule 1server
s.eVar5 / s.prop5Internal search termRule 3eVars
s.eVar10 / s.prop10Click labelRule 4eVars
s.eVar11 / s.prop11Click regionRule 4
s.eVar12Click destination URLRule 4
s.eVar13 / s.prop13Form nameRule 5
event5Internal searchRule 3Events
event10Click eventRule 4
event11Form submitRule 5
event20Error pageRule 2

Beacon Types

CallWhenTypeDocs
s.t()Page view, error page, search pagePage view beacons.t() method
s.tl(this, 'o', name)Click tracking, form submitCustom link beacons.tl() method

Complete variable reference →

Zero storage. Zero tracking. This page runs entirely in your browser.