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:

// These go out automatically with every click — no rules needed
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:

// Enable Activity Map click data collection
s.trackInlineStats = true;

// Tell AppMeasurement your domain so it knows internal vs exit links
s.linkInternalFilters = location.hostname;
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
// Global config — runs at Library Loaded, order 1 (before all other rules)
// Alternative when extension custom code editor is not accessible
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.

// Check DOM for logged-in indicators
if (document.querySelector(
  '.logged-in, .authenticated, [data-logged-in], ' +
  '.user-menu, .account-nav, .my-account'
)) return 'authenticated';

// Check meta tags
var meta = document.querySelector('meta[name="user-status"]');
if (meta) return meta.content;

// Check cookies
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.

Search Terms (multiple types)

Internal site search

// Data element: search.term
// Captures the search query from URL parameters or the search input field.
// Common parameter names vary by platform — check all known patterns.
var params = new URLSearchParams(location.search);

// Check URL parameters (most common — search results pages use query strings)
var term = params.get('q')         // Google-style
  || params.get('query')     // Explicit
  || params.get('search')    // Generic
  || params.get('s')         // WordPress default
  || params.get('keyword')   // E-commerce
  || params.get('searchTerm') // CMS platforms
  || params.get('k')         // Shortened
  || '';

// If no URL param, check the search input field value (SPA or pre-submit)
if (!term) {
  var input = document.querySelector(
    'input[type="search"], input[name="q"], input[name="query"], ' +
    'input[name="search"], .search-input, [data-search-input]'
  );
  if (input && input.value) term = input.value;
}

// Clean: lowercase, trim, limit length for reporting
return term ? term.trim().toLowerCase().substring(0, 255) : '';

Search results count

// Data element: search.resultsCount
// Captures how many results were returned. Useful for zero-results analysis.
var el = document.querySelector(
  '[data-results-count], [data-total-results], ' +
  '.results-count, .search-results-count, .hits-count'
);
if (el) {
  var count = el.getAttribute('data-results-count')
    || el.getAttribute('data-total-results')
    || el.textContent.replace(/[^0-9]/g, '');
  return count || '0';
}
return '';

Search type (to differentiate search contexts)

// Data element: search.type
// Identifies WHERE the search originated — global nav, category filter, etc.
var path = location.pathname.toLowerCase();
var params = new URLSearchParams(location.search);

if (params.get('category') || params.get('cat')) return 'filtered';
if (path.includes('/search')) return 'site search';
if (params.get('q') || params.get('query')) return 'site search';
if (document.querySelector('.autocomplete-results:not(:empty)')) return 'predictive';
return '';

Site Sections (hierarchical)

Primary section (level 1)

// Data element: site.section
// First directory segment of the URL path. Maps to s.channel.
var segments = location.pathname.split('/').filter(Boolean);
return segments[0] || 'home';

Sub-section (level 2)

// Data element: site.subSection
// Second directory segment. Useful for prop/eVar hierarchy reporting.
var segments = location.pathname.split('/').filter(Boolean);
return segments[1] || '';

Full breadcrumb path

// Data element: site.hierarchy
// Full path as a delimited string. Maps to s.hier1 or a prop.
// Produces: "products|electronics|headphones"
var segments = location.pathname.split('/').filter(Boolean);
return segments.join('|') || 'home';

Section from DOM (breadcrumb or nav)

// Data element: site.sectionFromDOM
// Falls back to reading the breadcrumb or active nav element when
// the URL structure doesn't reflect the true site section.
var breadcrumb = document.querySelector(
  'nav[aria-label="breadcrumb"] a, .breadcrumb a, ' +
  '.breadcrumbs a, [data-breadcrumb] a'
);
if (breadcrumb) return breadcrumb.textContent.trim();

// Active nav item
var activeNav = document.querySelector(
  'nav .active, nav [aria-current="page"], ' +
  '.nav-item.active, .navigation .current'
);
if (activeNav) return activeNav.textContent.trim();

// URL fallback
return location.pathname.split('/').filter(Boolean)[0] || 'home';

Customer Status

Customer type / tier

// Data element: customer.type
// Identifies the customer segment: new, returning, member, VIP, etc.
// Priority: data layer → DOM indicators → cookie
var dl = window.digitalData || {};
var user = dl.user || dl.customer || {};

// Data layer (most reliable if populated)
if (user.type) return user.type;
if (user.segment) return user.segment;
if (user.tier) return user.tier;
if (user.customerType) return user.customerType;

// DOM indicators (class names or data attributes on body/container)
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');

// Cookie-based (if the platform sets a customer segment cookie)
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
// Determines logged-in vs logged-out state from available signals.
// Priority: data layer → DOM classes → meta tags → cookies
var dl = window.digitalData || {};
var user = dl.user || {};

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

// DOM: look for elements that only render for authenticated users
if (document.querySelector(
  '.logged-in, .authenticated, [data-logged-in], ' +
  '.user-menu, .account-nav, .my-account, ' +
  '[data-auth-state="authenticated"], .user-avatar'
)) return 'logged in';

// Meta tags (some platforms set these server-side)
var meta = document.querySelector(
  'meta[name="user-status"], meta[name="login-status"], meta[name="auth-state"]'
);
if (meta && meta.content) return meta.content;

// Cookies (common auth indicators)
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)

Retrieve the ECID from the Visitor API

// Data element: visitor.ecid
// The ECID (Marketing Cloud Visitor ID) is set by the Adobe Visitor ID Service.
// It is available via the Visitor API object after the service initializes.

// Method 1: Visitor API (standard — works when ECID Service extension is installed)
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;
  }
}

// Method 2: Read from the AMCV cookie directly (fallback)
// The AMCV cookie contains the ECID in a pipe-delimited format.
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 tracking code (multi-parameter)

// Data element: campaign.code
// Checks multiple common campaign parameters.
// Priority order matters — cid is the Adobe standard, utm_campaign is Google.
var params = new URLSearchParams(location.search);

return params.get('cid')            // Adobe standard
  || params.get('s_cid')          // Adobe alternate
  || params.get('utm_campaign')   // Google
  || params.get('campaign')       // Generic
  || params.get('promo')          // Promotional
  || '';

Traffic source / medium

// Data element: traffic.source
// Identifies how the visitor arrived: organic, paid, direct, social, email, referral.
var params = new URLSearchParams(location.search);
var ref = document.referrer;

// Paid campaigns (has tracking parameters)
if (params.get('utm_medium')) return params.get('utm_medium');
if (params.get('cid') || params.get('gclid') || params.get('fbclid')) return 'paid';

// No referrer = direct
if (!ref) return 'direct';

// Search engines
if (/google\.|bing\.|yahoo\.|duckduckgo\.|baidu\./.test(ref)) return 'organic';

// Social
if (/facebook\.|twitter\.|linkedin\.|instagram\.|tiktok\.|youtube\./.test(ref)) return 'social';

// Email (common referrer patterns)
if (/mail\.|outlook\.|gmail\./.test(ref)) return 'email';

// Same domain = internal (exclude from reporting)
if (ref.includes(location.hostname)) return 'internal';

return 'referral';

Additional useful elements

Page load time (performance)

// Data element: performance.pageLoadTime
// Captures page load duration in milliseconds using the Performance API.
// Best used with a 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
// Simple responsive breakpoint detection — matches CSS media query logic.
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
// Reads the content category from meta tags, schema markup, or DOM.
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');

// Schema.org breadcrumb
var schema = document.querySelector('[itemtype*="BreadcrumbList"] [itemprop="name"]');
if (schema) return schema.textContent.trim();

// URL segment fallback
var segments = location.pathname.split('/').filter(Boolean);
return segments[0] || '';
Best practices for all data elements:

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

// Hybrid data element: always returns a value
// Priority: data layer → DOM → sensible default

var dl = window.digitalData || {};
var page = dl.page || {};

// Use data layer value if it exists and is non-empty
if (page.name && page.name.trim()) return page.name.trim();

// Fall back to DOM scraping
return document.title.replace(' | SiteName', '').trim();

Applied to Each Data Element

page.name (hybrid)

var dl = window.digitalData || {};
var page = dl.page || {};
// Data layer first, then DOM fallback.
// The 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();

// DOM/URL fallback — same checks as the DOM-only version
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';

// DOM fallback — check common logged-in indicators
if (document.querySelector(
  '.logged-in, .authenticated, [data-logged-in], ' +
  '.user-menu, .account-nav, .my-account'
)) return 'authenticated';

// Check meta tags
var meta = document.querySelector('meta[name="user-status"]');
if (meta) return meta.content;

// Check cookies
if (document.cookie.includes('loggedIn=') ||
    document.cookie.includes('session=') ||
    document.cookie.includes('auth='))
  return 'authenticated';

return 'anonymous';

search.term (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;

// URL/DOM fallback
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
  || '';
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
// Set error-specific variables BEFORE the page view rule fires.
// Rule 1 (order 50) handles the s.t() call — do NOT add Send Beacon here.
// Adding s.t() here would cause two page view beacons on error pages.
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
// Set search-specific variables BEFORE the page view rule fires.
// Rule 1 (order 50) handles the s.t() call — do NOT add Send Beacon here.
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
// Launch's Core > Click event binds 'this' to the clicked DOM element.
// No addEventListener or handler function is needed — Launch provides the context.
// 'this' might be a child element (e.g., a <span> or <svg> icon inside
// a <button>), so we walk up the DOM tree to find the actual
// interactive parent element.
var el = this;

// Walk up to find the actual clickable element (button, link, etc.)
// Stops when it finds an element matching one of the interactive selectors.
while (el && !el.matches('a, button, [role="button"], input[type="submit"]')) {
  el = el.parentElement;
}
// If no matching parent was found, fall back to the original clicked element.
if (!el) el = this;

// Grab the best label available
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';

// Grab the region/section of the page
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();
}

// Grab the destination URL
var href = el.getAttribute('href') || '';

// Set the tracking variables for this click.
// eVars persist across the visit; props are hit-level only.
// Using both gives flexibility in reporting.
s.eVar10 = label;    // What was clicked (persists)
s.prop10 = label;    // What was clicked (this hit only)
s.eVar11 = region || 'unknown';  // Where on the page
s.prop11 = region || 'unknown';  // Where on the page (hit)
s.eVar12 = href;     // Destination URL (if applicable)

// linkTrackVars: MUST list every variable to include in the s.tl() call.
// Any variable not listed here will be silently excluded from the beacon.
s.linkTrackVars = 'eVar10,prop10,eVar11,prop11,eVar12,events';

// linkTrackEvents: MUST list every event to include.
s.linkTrackEvents = 'event10';
s.events = 'event10';

// Fire the beacon. Per Adobe docs, the first argument controls delay:
//   this  = wait up to 500ms for the beacon before allowing navigation
//   true  = do not wait (fire and forget)
// Using 'this' to ensure the beacon completes before any page 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()
// In an Adobe Analytics Custom Code action triggered by a Core > Form Submission
// event, 'this' refers to the <form> element that was submitted.
// Scrape the form name from whatever attributes exist on the element.
// Priority: name attr → id attr → action attr → submit button text → fallback
var formName = this.getAttribute('name')
  || this.getAttribute('id')
  || this.getAttribute('action')
  || (this.querySelector('[type="submit"]') || {}).textContent?.trim()
  || 'unknown form';

// Set the variables that will be sent with the beacon.
// eVar13 persists across the visit; prop13 is hit-level only.
s.eVar13 = formName;
s.prop13 = formName;

// linkTrackVars tells AppMeasurement which variables to include
// in this s.tl() call. Without this, the eVars/props are ignored.
s.linkTrackVars = 'eVar13,prop13,events';
s.linkTrackEvents = 'event11';
s.events = 'event11';

// Fire the beacon directly from this code block.
// 'this' is the form element — passing it as the first arg tells
// AppMeasurement to wait up to 500ms for the beacon before navigating.
// Do NOT use a separate Send Beacon action — it cannot access
// the formName variable defined above.
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()

// Returns the current resolved value of a data element
_satellite.getVar('page.name');
_satellite.getVar('page.type');
_satellite.getVar('user.authState');
_satellite.getVar('campaign.trackingCode');
_satellite.getVar('search.term');

Method 2: Dump all data elements at once

// List every data element name and its current value
var names = Object.keys(_satellite._container.dataElements || {});
names.forEach(function(name) {
  console.log(name + ': ' + _satellite.getVar(name));
});

Method 3: Test specific custom code logic

// Run the same code a custom code data element would run
// Useful for debugging fallback chains

// Example: test the hybrid page.name 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 4: Check the data layer object directly

// See what the data layer contains (or if it exists at all)
console.log('digitalData:', window.digitalData);
console.log('dataLayer:', window.dataLayer);
console.log('utag_data:', window.utag_data);

// Deep inspect a specific path
console.table(window.digitalData && window.digitalData.page);

Method 5: 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.
// After the page loads and the page view rule fires,
// inspect what AppMeasurement actually sent
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);

// Or dump all populated props and eVars
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 6: Enable Launch debug logging

// Enables verbose console output for all Launch rule evaluations,
// data element resolutions, and beacon calls
_satellite.setDebug(true);

// Disable when done
_satellite.setDebug(false);
_satellite.setDebug(true) persists across page loads by setting a cookie. It will remain active until explicitly turned off or the cookie is cleared. Useful for multi-page debugging sessions.

Method 7: Test a click tracking rule manually

// Simulate what the universal click tracker would capture
// for a specific element — without actually firing a beacon
var el = document.querySelector('#sign-up-btn'); // target element

// Same priority chain as Rule 4's actual code
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';

// Same region detection as Rule 4
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.