Adobe AEM

AEM with Adobe Analytics, Target & the Adobe Client Data Layer: The Complete Guide

27 min read

How AEM, Adobe Experience Platform Tags, Adobe Analytics, and Adobe Target fit together — the Adobe Client Data Layer and its API, the Core Components data layer, custom component data via Sling Models, the Tags cloud configuration, AppMeasurement vs. Web SDK, page view and click tracking, Experience Fragment offers, flicker, consent, debugging, and caching. Includes code, a cheat sheet, best practices, and do's & don'ts.

AEMAdobe AnalyticsAdobe TargetData LayerTagsWeb SDK
AEM with Adobe Analytics, Target & the Adobe Client Data Layer: The Complete Guide

Sooner or later every AEM project gets the same ticket: "marketing needs page views, CTA clicks, and an A/B test on the homepage hero." It lands on the AEM developer, because what decides whether tracking is reliable isn't the analytics tool — it's the markup and data your components render. If the page doesn't expose clean, predictable data, every tag rule becomes brittle CSS-selector scraping that breaks the next time someone renames a class.

This guide walks the whole chain: how AEM, Adobe Experience Platform Tags, Adobe Analytics, and Adobe Target divide the work; the Adobe Client Data Layer (ACDL); the Core Components data layer and data-layer JSON for your own components; connecting Tags to AEM; AppMeasurement vs. the Web SDK; page view and click rules; Experience Fragments as Target offers; and flicker, consent, debugging, and caching. A cheat sheet, best practices, and do's & don'ts close it out.

It builds on the Component Development tutorial (Sling Models and HTL), the Frontend Integration guide (clientlibs and page JavaScript), and the Dispatcher guide (why cacheability matters). For the Experience Fragments you'll export to Target, see the Content Fragments & Experience Fragments guide.

The big picture: who does what

Most confusion comes from blurring four systems' responsibilities:

LayerOwnerResponsibility
AEMYou (AEM dev)Renders the HTML, emits data-layer data about the page and components, includes the Tags script
Adobe Client Data LayerYou (contract)A JavaScript object on the page (window.adobeDataLayer) holding page/component data and events
Adobe Experience Platform TagsAnalytics / martech teamThe tag manager: rules that listen to data-layer events and fire Analytics, Target, and third-party tags
Analytics / TargetAnalysts, optimization teamReporting, testing, and personalization — they consume what Tags sends them

Tags was formerly Adobe Experience Platform Launch. You'll still see "Launch" in AEM's UI — the cloud configuration is named Adobe Launch Configurations — but the product is now Tags, managed in the Data Collection UI.

The mental model: AEM publishes facts, Tags decides what to do with them. A component should never hard-code an Analytics beacon or a Target call. It exposes "this is a page with this title and template" and "this teaser was clicked," and Tags maps those facts to whatever tools marketing uses this year. The data layer is the contract between the two teams.

Browser loads an AEM page (cached by Dispatcher/CDN)
   │
   ├─ AEM markup pushes page + component data  ──►  window.adobeDataLayer
   │                                                 (events: cmp:show, cmp:click, …)
   │
   └─ Tags library (loaded async) ── rules listen to data-layer events
            │
            ├─► Web SDK / AppMeasurement ──► Adobe Analytics
            └─► Web SDK / at.js          ──► Adobe Target (fetch + render offers)

The Adobe Client Data Layer (ACDL)

The Adobe Client Data Layer is a small open-source library (adobe/adobe-client-data-layer, on npm as @adobe/adobe-client-data-layer) that turns a plain array, window.adobeDataLayer, into an event-driven store: you push data and events, it merges data into a computed state and notifies listeners.

The key design decision: push is the only method you can rely on before the library loads. Because it starts as a normal array, any code can push into it at any time, and the library processes the queue once it loads — which is what makes it safe with async loading.

// Always initialise defensively — it may or may not exist yet
window.adobeDataLayer = window.adobeDataLayer || [];

Pushing data

A push with plain properties merges into the computed state:

window.adobeDataLayer.push({
  page: {
    title: "Getting Started"
  }
});

To delete a key, push it with null:

window.adobeDataLayer.push({
  component: {
    "map-1": null
  }
});

Pushing events

A push with an event property is an event; the optional eventInfo object carries details. It can carry data too — the data is merged and the event fires:

window.adobeDataLayer.push({
  event: "click",
  eventInfo: {
    reference: "component.hero-1"
  }
});

Reading state and listening

The other methods only exist once the library has loaded, so you reach them by pushing a function, which the library calls with the data layer as its argument:

window.adobeDataLayer.push(function (dl) {
  // Whole merged state
  var state = dl.getState();

  // One branch, using dot notation
  var hero = dl.getState("component.hero-1");

  // Listen for an event
  dl.addEventListener("click", myHandler);

  // Listen for data changes on one path only
  dl.addEventListener("adobeDataLayer:change", myHandler, {
    path: "component.accordion-1"
  });
});

removeEventListener(type, listener) undoes a registration (omit the listener to remove all for that type).

Two reserved event names are worth knowing: adobeDataLayer:change fires whenever data is pushed, and adobeDataLayer:event fires whenever any event is pushed.

The options object also accepts a scope: "past", "future", or "all" — the default. That default is ACDL's quiet superpower: a listener registered after the page view event was pushed still receives it, so you don't lose the first event just because Tags loaded late.

Important: Treat window.adobeDataLayer as a queue you only push into. Don't splice, sort, or otherwise modify it with native array methods — that corrupts the computed state.

The Core Components data layer

If you use the Core Components, you get ACDL integration out of the box — it just has to be switched on.

Enabling it

The data layer is enabled per site through a context-aware configuration named com.adobe.cq.wcm.core.components.internal.DataLayerConfig. Create it under your site's /conf folder:

/conf/mysite
  └─ sling:configs                                              (nt:unstructured)
      └─ com.adobe.cq.wcm.core.components.internal.DataLayerConfig   (nt:unstructured)
           enabled = true   (Boolean)

As the .content.xml of that node in your ui.content package:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured"
    enabled="{Boolean}true"/>

The site root must point at that configuration with sling:configRef (for example, sling:configRef="/conf/mysite" on /content/mysite/jcr:content). The AEM Project Archetype generates all of this by default (its datalayer option defaults to y). The config has three properties:

PropertyDefaultMeaning
enabledfalseTurn the Core Components data layer on for this site
skipClientlibIncludefalseIf true, the Page component doesn't include the ACDL library itself
nameadobeDataLayerThe global variable name of the data layer

skipClientlibInclude matters more than it looks. The Core Page component includes the ACDL library as a clientlib (core.wcm.components.commons.datalayer.acdl) alongside its own integration script (core.wcm.components.commons.datalayer.v2). If your Tags property also installs the Adobe Client Data Layer extension, which ships its own ACDL, you load the library twice. Pick one owner.

When it's working, body carries data-cmp-data-layer-enabled and adobeDataLayer.getState() in the console returns page and component objects. If the attribute is missing, the config isn't resolving for that page — check the sling:configRef chain first.

What gets pushed

The Page component pushes the page's data and a cmp:show event as soon as the body opens, and the integration script then pushes each component's data and fires cmp:loaded. Every entry is keyed by the component's generated ID. A page entry looks like this:

{
  "page": {
    "page-2eee4f8914": {
      "@type": "mysite/components/page",
      "repo:modifyDate": "2026-08-31T21:02:21Z",
      "dc:title": "Adventures and Travel",
      "dc:description": "Trips, guides and gear reviews.",
      "repo:path": "/content/mysite/us/en.html",
      "xdm:template": "/conf/mysite/settings/wcm/templates/landing-page-template",
      "xdm:language": "en-US",
      "xdm:tags": ["Attract"]
    }
  }
}

The component schema shared by all Core Components uses these fields:

FieldMeaning
@typeResource type
repo:modifyDateLast modified date
dc:title, dc:descriptionTitle, description
xdm:text, xdm:linkURLText, link URL
parentIdID of the parent component (or page)

Specialised schemas add fields on top: the page adds xdm:tags, repo:path, xdm:template, and xdm:language; the image component adds an image object with asset details (repo:id, repo:path, xdm:tags, and more); containers such as Accordion, Tabs, and Carousel add shownItems; the Content Fragment component adds elements.

The events

ComponentEvent(s)
Pagecmp:show
Accordion, Tabs, Carouselcmp:show, cmp:hide
Button, Teaser, Navigation, Breadcrumbcmp:click
Any element with data-cmp-clickablecmp:click
Global, once component data is pushedcmp:loaded

Every event's eventInfo.path points at the data of the component that fired it (page.page-2eee4f8914, component.teaser-a1b2c3d4e5). That's the pattern everywhere: the event tells you what happened, getState(eventInfo.path) tells you what it happened to.

function logEventObject(event) {
  if (event.eventInfo && event.eventInfo.path) {
    var dataObject = window.adobeDataLayer.getState(event.eventInfo.path);
    console.log(event.event, dataObject["@type"], dataObject["dc:title"]);
  }
}

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push(function (dl) {
  dl.addEventListener("cmp:show", logEventObject);
  dl.addEventListener("cmp:click", logEventObject);
});

Adding data-layer JSON to custom components

Your own components should speak the same contract. The Core Components expose a public API for it: ComponentData (the model), DataLayerBuilder (a fluent builder), and ComponentUtils (ID generation and the enabled check).

The Sling Model

For a promo banner with a title and CTA, the model exposes getId() and getData():

package com.mysite.core.models;

import com.adobe.cq.wcm.core.components.models.datalayer.ComponentData;

public interface PromoBanner {
    String getId();
    String getTitle();
    String getCtaLink();
    String getCtaLabel();
    ComponentData getData();
}
package com.mysite.core.models.impl;

import java.util.Date;
import javax.annotation.PostConstruct;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import com.adobe.cq.wcm.core.components.models.datalayer.ComponentData;
import com.adobe.cq.wcm.core.components.models.datalayer.builder.DataLayerBuilder;
import com.adobe.cq.wcm.core.components.util.ComponentUtils;
import com.mysite.core.models.PromoBanner;

@Model(
    adaptables = SlingHttpServletRequest.class,
    adapters = PromoBanner.class,
    resourceType = PromoBannerImpl.RESOURCE_TYPE,
    defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class PromoBannerImpl implements PromoBanner {

    static final String RESOURCE_TYPE = "mysite/components/promo-banner";

    @SlingObject
    private Resource resource;

    @ValueMapValue
    private String title;

    @ValueMapValue
    private String ctaLink;

    @ValueMapValue
    private String ctaLabel;

    private String id;

    @PostConstruct
    protected void init() {
        // Same format Core Components use: <prefix>-<10-char SHA-1 hash of the path>
        id = ComponentUtils.generateId("promo-banner", resource.getPath());
    }

    @Override public String getId() { return id; }
    @Override public String getTitle() { return title; }
    @Override public String getCtaLink() { return ctaLink; }
    @Override public String getCtaLabel() { return ctaLabel; }

    @Override
    public ComponentData getData() {
        // Respect the site's DataLayerConfig: no data layer, no JSON
        if (!ComponentUtils.isDataLayerEnabled(resource)) {
            return null;
        }
        return DataLayerBuilder.forComponent()
            .withId(() -> id)
            .withType(() -> RESOURCE_TYPE)
            .withTitle(() -> title)
            .withLinkUrl(() -> ctaLink)
            .withLastModifiedDate(() -> resource.getValueMap().get("jcr:lastModified", Date.class))
            .build();
    }
}

Worth understanding:

  • DataLayerBuilder.forComponent() forces withId(...) first — the ID is the key in the data layer. Every other with... method takes a Supplier, so values are computed lazily at serialisation.
  • ComponentUtils.isDataLayerEnabled(resource) reads the same DataLayerConfig, so your component switches on and off with the Core Components.
  • If your component wraps a Core Component via delegation, use DataLayerBuilder.extending(delegate.getData()) and override only what changes (for example .asComponent().withTitle(...)). There are also forContainer(), forImageComponent(), forPage(), and forAsset(...).
  • ComponentData.getJson() serialises to the {"<id>": {...}} shape the client script expects.

Tip: The builder covers the standard schema. If analytics needs a genuinely custom field (say, a campaign ID), you can emit your own JSON in the same {"<id>": {...}} shape — the client script just parses the attribute. Agree on field names with the analytics team; the data layer is a contract, not a dumping ground.

The HTL

Two attributes wire the component into the data layer: data-cmp-data-layer on the root element (the JSON) and data-cmp-clickable on anything whose click should fire cmp:click:

<div data-sly-use.banner="com.mysite.core.models.PromoBanner"
     id="${banner.id}"
     class="cmp-promo-banner"
     data-cmp-data-layer="${banner.data.json}">
    <h2 class="cmp-promo-banner__title">${banner.title}</h2>
    <a class="cmp-promo-banner__cta"
       href="${banner.ctaLink}"
       data-cmp-clickable="${banner.data ? true : false}">${banner.ctaLabel}</a>
</div>

On page load, the Core Components script pushes every [data-cmp-data-layer] element's JSON under component, filling in parentId from the closest data-layer ancestor (or body) if unset. Each [data-cmp-clickable] element gets a click listener that pushes cmp:click with eventInfo.path set to component.<id> of the enclosing component. When the data layer is disabled, getData() returns null, HTL drops the empty attribute, and nothing is pushed.

Give the root element id="${banner.id}" so the DOM ID and data-layer key match — parentId resolution relies on the element IDs. To test the model, see the unit testing guide.

Connecting Tags to AEM

AEM doesn't need you to paste the Tags embed code into a template. It has a cloud configuration that injects the right library for you.

The setup

  1. Create a Tags property in the Data Collection UI, add extensions (Web SDK or Analytics/Target, optionally the Adobe Client Data Layer extension), and publish a library.
  2. Verify the IMS configuration. AEM talks to Tags through Adobe IMS. On AEM as a Cloud Service, an IMS configuration named Adobe Launch is provisioned automatically. On 6.5 you create it in Tools → Security → Adobe IMS Configurations, backed by an Adobe Developer Console project.
  3. Create the configuration: Tools → Cloud Services → Adobe Launch Configurations → your site's /conf folder → Create. Choose the IMS configuration, the Company, and the Property.
  4. Review the Staging and Production tabs. Each has a Library URI (the embed URL for that Tags environment) and a Load Library Asynchronously toggle.
  5. Publish the configuration, and make sure the site root's page properties (Advanced tab → Cloud Configuration) point at the /conf folder.

The Core Page component's head renders the cloud-config script tags (cq/cloudconfig/components/scripttags/header), so templates built on it pick up the library once the config resolves.

Note: Adobe Developer Console JWT credentials are deprecated; IMS integrations now use OAuth Server-to-Server. If you inherit a 6.5 setup on JWT, plan the migration.

Staging vs production libraries

The integration picks the library by instance type, not by Cloud Manager environment:

AEM instanceTags library loaded
Author (all environments)Staging library
PublishProduction library

Authors preview unapproved rules; visitors get the approved build. The catch on AEM as a Cloud Service: there is no dev- or stage-specific setting, so you can't map a Cloud Manager dev environment to the Tags Development environment through this config. Test development builds with the Debugger's library replacement instead (see Debugging).

Important: If publish loads nothing, check that the AEM config is published, the Tags library is published to Production (not just Staging), and the site's Cloud Configuration resolves to the right /conf folder.

Adobe Analytics: AppMeasurement vs. the Web SDK

AppMeasurement (AppMeasurement.js, via the Adobe Analytics extension) is the classic library. You set variables on the s object (s.pageName, s.eVar5, s.events) and send a page view with s.t() or a link hit with s.tl(). Target (at.js) and the Experience Cloud ID service (VisitorAPI.js) are separate libraries making separate calls.

The Web SDK (alloy.js, via the Adobe Experience Platform Web SDK extension) is one solution-agnostic library. It sends one request per event to the Edge Network, where a datastream fans the data out to Analytics, Target, Audience Manager, Experience Platform, and others. Data is shaped as XDM (Experience Data Model), with a free-form data object alongside. It replaces AppMeasurement, at.js, VisitorAPI.js, and DIL.js.

AppMeasurement + at.jsWeb SDK
LibrariesOne per productOne (alloy.js)
RequestsOne per product per eventOne per event, fanned out at the Edge
Data formatAnalytics variables (s.eVar1)XDM + data object
RoutingIn the pageIn the datastream (server-side config)
StatusSupported, legacy pathAdobe's recommended method for new implementations

Adobe's docs call the Web SDK extension in Tags "the standardized and recommended method to implement Adobe Analytics for new customers." A working AppMeasurement setup can migrate incrementally — even page by page — so it's a planned migration, not an emergency.

How the Web SDK tells page views from clicks

The Web SDK has a single command, sendEvent, for everything. Analytics decides the hit type from the fields you send:

  • Page view: xdm.web.webPageDetails.name (or URL) is present without xdm.web.webInteraction.type — or xdm.eventType is web.webpagedetails.pageViews.
  • Link hit: xdm.web.webInteraction.type is present along with a name or URL. Valid types are other (custom link), download, and exit.

In the data object, the equivalents are data.__adobe.analytics.pageName for page views and linkType plus linkName/linkURL for links. Either way, xdm._experience.analytics.customDimensions.eVars.eVar1 (or data.__adobe.analytics.eVar1) sets eVars.

Tip: The Web SDK's clickCollectionEnabled defaults to true, so it automatically collects internal, download, and exit link clicks — a common source of double-counted clicks once you also build cmp:click rules. Decide which mechanism owns link tracking and turn the other off (or filter it via the clickCollection options).

Page view and click rules on ACDL events

This is where the data layer pays for itself. Instead of "when DOM ready, scrape the h1," rules say "when the data layer says a page was shown, send its data."

Option 1: the Adobe Client Data Layer extension

The Adobe Client Data Layer Tags extension provides events (Listen to all data changes, Listen to all events, Listen to specific event, each with scope all by default, future, or past), data elements (Computed State for the whole state or a dot-notation path, and Data Layer Size), and actions (Push to Data Layer and Reset & Set Computed State, which keeps the array small in SPAs). It's the lowest-code option for rules that don't need to know which component fired.

Option 2: a custom code event that resolves the component

For rules that need the triggering component's data — most of them — Adobe's AEM tutorials use a Custom Code event that resolves eventInfo.path and calls Tags' trigger() with an object that conditions and data elements then read as event:

// Tags rule → Event: Core → Custom Code
var pageShownEventHandler = function (evt) {
  if (evt.eventInfo && evt.eventInfo.path) {
    trigger({
      path: evt.eventInfo.path,
      component: window.adobeDataLayer.getState(evt.eventInfo.path)
    });
  }
};

window.adobeDataLayer = window.adobeDataLayer || [];
window.adobeDataLayer.push(function (dl) {
  dl.addEventListener("cmp:show", pageShownEventHandler);
});

Because cmp:show fires for accordion panels and tabs too, add a condition so only the page counts as a page view:

// Tags rule → Condition: Core → Custom Code
return event.component
    && typeof event.component["@type"] === "string"
    && event.component["@type"].indexOf("mysite/components/page") === 0;

Then define data elements (Custom Code) that read from the triggered event:

// Data element: "Page Name"
if (event && event.component && event.component.hasOwnProperty("dc:title")) {
  return event.component["dc:title"];
}

The action depends on your library. With AppMeasurement it's Set Variables then Send Beacon (s.t()). With the Web SDK it's Update variable then Send event, and the payload the extension sends is equivalent to:

alloy("sendEvent", {
  xdm: {
    eventType: "web.webpagedetails.pageViews",
    web: {
      webPageDetails: {
        name: pageName,          // from the data element
        URL: window.location.href,
        pageViews: { value: 1 }
      }
    }
  },
  data: {
    __adobe: {
      analytics: {
        eVar5: pageTemplate      // e.g. xdm:template
      }
    }
  }
});

A click rule is the same pattern on cmp:click: filter by @type (teasers, your promo banner) and send a link hit — s.tl() for AppMeasurement, or a Web SDK event with web.webInteraction.name, type: "other", and linkClicks: { value: 1 }. The link name and URL come straight from dc:title and xdm:linkURL, not from brittle selectors.

Important: The page's cmp:show is pushed by an inline script at the top of body, long before an async Tags library arrives. It still reaches your rule because ACDL listeners default to scope all and replay past events. Switch a listener to future and you'll silently lose page views.

Adobe Target with AEM

The AEM developer's part in Target is smaller than in Analytics, but the performance and caching consequences are larger. Vocabulary: an offer is content (HTML or JSON) Target can deliver; an activity decides which audience sees which offer — A/B Test, Experience Targeting (XT) for rule-based audience-to-experience mapping, Multivariate Test, and the AI-driven Auto-Allocate, Auto-Target, and Automated Personalization.

Exporting Experience Fragments to Target

The most useful AEM–Target integration lets authors build offers in AEM, with real components and styling, and push them to Target:

  1. Integrate AEM with Target: Tools → Cloud Services → Adobe Target, using an IMS configuration (OAuth Server-to-Server). You'll enter the Tenant ID and Client Code — for most customers they're identical; backend calls use the Tenant ID and client-side calls use the Client Code.
  2. Add the Target Cloud Configuration to the Experience Fragment or its folder, choosing the export format — HTML (default, for web), JSON (headless), or HTML & JSON — and a Target workspace.
  3. Configure the Externalizer on author so links and asset references inside the fragment become absolute publish URLs. Without it, the offer renders on your site with broken relative links.
  4. Select the fragment and choose Export to Adobe Target (or Update in Adobe Target after the first export), either without publishing or with Publish.

The fragment appears on Target's Offers page for use in activities. If an author deletes a fragment that's live in an activity, AEM warns but doesn't block the deletion — build that into your governance.

Tip: An exported XF offer is a snapshot. Edit the fragment in AEM and nothing changes in Target until someone runs Update in Adobe Target. Put that step in the authoring checklist.

at.js vs. the Web SDK for personalization

On the page, Target decisions are fetched and rendered client-side by at.js (via the Adobe Target v2 extension) or the Web SDK — always through Tags, not by AEM directly. AEM's docs still show the at.js path, but for new builds the Web SDK recommendation applies: with Target enabled in the datastream, one sendEvent carries the page view to Analytics and fetches Target decisions. Set renderDecisions: true (default false) to have the SDK render eligible visual offers automatically.

Flicker and the prehiding snippet

Client-side personalization has a physics problem: default content paints, then Target swaps it — flicker. Both libraries solve it by hiding, then revealing. Loaded synchronously (with the global mbox auto-created), at.js 2.x sets body opacity to 0 until Target responds, and the Web SDK's prehidingStyle option does the same. But Adobe recommends loading asynchronously, and then you must add a prehiding snippet before the library in the head. The Web SDK version from Adobe's docs:

<script>
  !function(e,a,n,t){
    if (a) return;
    var i=e.head;if(i){
    var o=e.createElement("style");
    o.id="alloy-prehiding",o.innerText=n,i.appendChild(o),
    setTimeout(function(){o.parentNode&&o.parentNode.removeChild(o)},t)}}
    (document, document.location.href.indexOf("adobe_authoring_enabled") !== -1, "body { opacity: 0 !important }", 3000);
</script>

The snippet injects a hiding style and removes it after at most 3000 ms — or immediately when the SDK has applied the decisions. at.js has an equivalent snippet with the same default timeout.

Important: Don't hide the whole body unless you personalize the whole page. Change body { opacity: 0 !important } to target only personalized regions (for example .cmp-hero). Hiding the body for up to 3 seconds hurts real users and your Largest Contentful Paint.

In AEM, place the snippet in your page component's customheaderlibs.html so it renders early in head, before the Tags script.

ContextHub

ContextHub is AEM's own client-side context framework: stores of visitor data, segments evaluated in the browser, and an author-only toolbar for previewing personalization. It powers AEM's native targeting mode and can feed Target through the ContextHub Tags extension. It's still documented on AEM as a Cloud Service, but most new personalization work happens in Target rather than AEM-native segments. If you don't use AEM targeting, don't load ContextHub on publish.

Analytics and Target set cookies and collect behavioural data, so in most jurisdictions they must wait for consent — decided in the tag layer, not scattered across components. With the Web SDK, set defaultConsent and call setConsent from your consent manager's callback:

alloy("configure", {
  datastreamId: "ebebf826-a01f-4458-8cec-ef61de241c93",
  orgId: "ADB3LETTERSANDNUMBERS@AdobeOrg",
  defaultConsent: "pending"
});

// Later, from the consent banner's "accept" callback
alloy("setConsent", {
  consent: [{
    standard: "Adobe",
    version: "1.0",
    value: { general: "in" }
  }]
});
defaultConsentBehaviour
"in"Collect normally until the user opts out
"pending"Queue events locally until setConsent opts in
"out"Discard data until the user opts in

setConsent also accepts the Adobe 2.0 standard and IAB TCF 2.0 strings. defaultConsent doesn't persist between navigations — it's configured on every page load (the Tags extension does this for you). On AppMeasurement setups, gate rules with consent conditions so nothing fires before opt-in.

On the AEM side: never put PII in the data layer — every script on the page can read it, and it ends up in analytics. And since the consent banner is client-side, it doesn't affect cacheability.

Debugging the implementation

ToolWhat it's for
Browser consoleadobeDataLayer.getState() for the contract; adobeDataLayer itself for the push history
_satellite.setDebug(true)Tags console logging (rules, conditions); run it in the console, never in a rule
Adobe Experience Platform DebuggerBrowser extension: Tags property/environment/build, Analytics report suites, Target activities, Web SDK requests
Adobe Experience Platform AssuranceThe best view of data flowing into and out of the Edge Network
Network tabWeb SDK requests hit /ee/ Edge endpoints

The Debugger's most underrated feature is replacing the Tags embed code (Experience Platform Tags → Configuration → Actions → Replace): load your property's Development library on any environment without touching AEM. That's the practical answer to AEM's "only Staging and Production" mapping.

Triage "nothing is tracked" in order: (1) is data-cmp-data-layer-enabled on body and does getState() return data — if not, it's AEM config; (2) is the Tags library on the page — if not, it's the cloud config or an unpublished library; (3) does the rule fire with debugging on — if not, check the event name and the @type condition; (4) does the hit leave the browser and get processed — if not, check the datastream, report suite, or consent state.

Performance and caching

Tracking and personalization run on every page view, so small mistakes multiply:

  • Load Tags asynchronously. Turn on Load Library Asynchronously in both tabs. ACDL's push queue exists precisely so async loading doesn't lose events.
  • Keep pages cacheable. Target personalization is fetched in the browser, so the HTML from Dispatcher and the CDN is identical for everyone. Personalize on the server per user (a Sling Model reading a cookie) and you get an uncacheable page — or worse, a cache serving one user's variant to another. The Dispatcher guide explains why that's a security issue too.
  • Keep data-layer JSON about content, not visitors. Core Components data is static per page, so it caches. Push user-specific data (logged-in state, customer tier) client-side after an uncached call.
  • Prehide narrowly and briefly, and don't double-load libraries — one ACDL, one Target library, one Analytics library. The Web SDK replaces three at once.
  • Prune the Tags library. Every extension and rule ships in it, on every page.

To measure the result, use the Performance Troubleshooting guide and the Website Audit tool.

AEM as a Cloud Service vs. 6.5

TopicAEM 6.5AEM as a Cloud Service
IMS configuration for TagsCreate manually (Developer Console project)Provisioned automatically ("Adobe Launch")
CredentialsLegacy setups often on JWT (deprecated)OAuth Server-to-Server
Analytics integrationLegacy framework-based integration still existsFramework-based integration not supported — use Tags
Tags library per instanceStaging on author, Production on publishSame, with no dev/stage-specific mapping
Cache in front of pagesDispatcher (+ your CDN)Dispatcher + Adobe-managed CDN

Target configurations migrated from the Classic UI moved from /etc/cloudservices/testandtarget/ to /conf/<tenant>/settings/cloudconfigs/target/; legacy ones still work but can't be created or edited in the Touch UI. For the broader move, see the 6.5 to Cloud Service migration guide.

Cheat sheet

TaskHow
Enable Core Components data layerCA config com.adobe.cq.wcm.core.components.internal.DataLayerConfig → enabled=true under /conf/<site>/sling:configs
Avoid loading ACDL twiceskipClientlibInclude=true when Tags' ACDL extension ships the library
Check it's ondata-cmp-data-layer-enabled on body; adobeDataLayer.getState()
Push an eventadobeDataLayer.push({ event: "x", eventInfo: { path: "component.id" } })
Call API safelyadobeDataLayer.push(function (dl) { ... })
Listendl.addEventListener("cmp:click", fn, { path, scope }) — scope all by default
Component data from eventadobeDataLayer.getState(event.eventInfo.path)
Core eventscmp:show, cmp:hide, cmp:click, cmp:loaded
Custom component dataDataLayerBuilder.forComponent().withId(...).withType(...).build()
HTL attributesdata-cmp-data-layer="${model.data.json}", data-cmp-clickable
Tags config in AEMTools → Cloud Services → Adobe Launch Configurations
Library per instanceAuthor → Staging, Publish → Production
New Analytics buildWeb SDK extension + datastream (recommended)
Link hit (Web SDK)web.webInteraction.type = other / download / exit
XF to TargetTarget cloud config on XF folder → Externalizer → Export to Adobe Target
Flicker (async)Prehiding snippet before the library, 3000 ms default
Consent (Web SDK)defaultConsent: "pending" + setConsent
Tags debug logging_satellite.setDebug(true) in the console

Best practices

  • ✅ Treat the data layer as a documented contract with the analytics team, with agreed field names.
  • ✅ Use the Core Components data layer and DataLayerBuilder rather than hand-rolled scripts.
  • ✅ Build Tags rules on data-layer events, never on CSS selectors or DOM scraping.
  • ✅ Use the Web SDK for new Analytics and Target implementations.
  • ✅ Load Tags asynchronously and keep ACDL listeners on scope all.
  • ✅ Keep personalization client-side so Dispatcher and CDN caching stay intact.
  • ✅ Gate collection on consent in the tag layer.
  • ✅ Keep IMS integrations on OAuth Server-to-Server credentials.

Do's and Don'ts

Do

  • ✅ Check ComponentUtils.isDataLayerEnabled before emitting data-layer JSON.
  • ✅ Filter cmp:show by @type so accordion and tab panels aren't counted as page views.
  • ✅ Decide whether Web SDK automatic click collection or cmp:click rules owns link tracking.
  • ✅ Configure the Externalizer before exporting Experience Fragments to Target.
  • ✅ Re-export XF offers after editing them — Target holds a snapshot.
  • ✅ Publish both the Tags library and the AEM cloud configuration.

Don't

  • ❌ Don't hard-code Analytics beacons or Target calls into components.
  • ❌ Don't put PII in the data layer.
  • ❌ Don't mutate window.adobeDataLayer with native array methods.
  • ❌ Don't load ACDL twice (Core Components clientlib and the Tags extension).
  • ❌ Don't render per-user variants on the server for pages that go through Dispatcher.
  • ❌ Don't hide the whole body with a long prehiding timeout "just in case."
  • ❌ Don't run _satellite.setDebug inside a Tags rule.

Wrapping up

The AEM side of analytics and personalization comes down to one discipline: publish facts, don't implement tracking. Enable the Core Components data layer, give custom components the same ComponentData contract through DataLayerBuilder, and let window.adobeDataLayer carry page and component data plus cmp:show and cmp:click events. Connect Tags through the cloud configuration, build rules on data-layer events, and send new work through the Web SDK to Analytics and Target. Export Experience Fragments as offers, prehide narrowly, gate on consent, and keep personalization client-side so pages stay cacheable. Get the contract right and marketing can change tools and rules without filing another AEM ticket.

Continue with the Component Development tutorial for the Sling Model and HTL fundamentals behind custom data-layer components, the Content Fragments & Experience Fragments guide for building the fragments you export to Target, the Dispatcher guide for keeping personalized pages cacheable, and the SEO guide for the other half of making pages perform in the real world.

Share this article

Discussion

By commenting you agree to the Privacy Policy. Guest comments are reviewed before they appear.

Loading discussion…

Subscribe to the Newsletter

Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.

Back to Blog