AEM: Analytics Implementation Guide | OpsBlu Docs

AEM: Analytics Implementation Guide

Technical guide to implementing analytics on Adobe Experience Manager, covering clientlibs, HTL templates, Adobe Launch integration, and dispatcher...

Analytics Architecture on AEM

Adobe Experience Manager uses a Java-based content repository (JCR) with a component-driven rendering model. Analytics scripts are deployed through the clientlib (client library) system, which manages JavaScript and CSS dependencies across components and templates.

The request lifecycle that affects tracking:

Request → CDN → Dispatcher (Apache/IIS cache) → AEM Publish → Sling Resolution → HTL Rendering → HTML

The Dispatcher cache sits between the client and AEM Publish instances. Like any reverse proxy cache, it serves static HTML to anonymous users. Analytics scripts embedded in the HTML still execute client-side, but any server-side personalization or data layer logic only runs on cache misses unless you use SDI (Sling Dynamic Include) or client-side fetches.

AEM Cloud Service manages Dispatcher configuration through the Cloud Manager pipeline. On-premise AEM (6.5) uses manually configured Apache/IIS Dispatcher modules.


Installing Tracking Scripts

Method 1: Client Library (clientlib)

Create a clientlib category that loads on every page:

/apps/mysite/clientlibs/analytics/
├── .content.xml
├── js.txt
└── analytics.js
<!-- .content.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
          jcr:primaryType="cq:ClientLibraryFolder"
          categories="[mysite.analytics]"
          dependencies="[mysite.base]"/>
// analytics.js
window.dataLayer = window.dataLayer || [];
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXX');

Include the clientlib in your page template's HTL:

<!-- page.html (HTL/Sightly template) -->
<sly data-sly-use.clientLib="${'/libs/granite/sightly/templates/clientlib.html'}">
  <sly data-sly-call="${clientLib.all @ categories='mysite.analytics'}"/>
</sly>

Method 2: HTL Template Direct Injection

For scripts that must load before any clientlib processing, inject directly in the <head> of your base page component:

<!-- /apps/mysite/components/page/head.html -->
<head data-sly-use.page="com.mysite.core.models.PageModel">
  <meta charset="UTF-8"/>
  <script>
    window.digitalData = ${page.dataLayerJson @ context='unsafe'};
  </script>
  <sly data-sly-use.clientLib="${'/libs/granite/sightly/templates/clientlib.html'}">
    <sly data-sly-call="${clientLib.js @ categories='mysite.analytics'}"/>
  </sly>
</head>

The @context='unsafe' directive is required to output raw JSON. Use it only for trusted server-generated data.

Method 3: Adobe Launch (Adobe Tags)

AEM integrates natively with Adobe Launch (now Adobe Experience Platform Tags). Configure the integration through AEM Cloud Service configuration:

  1. Create a Cloud Configuration at /conf/mysite/settings/cloudconfigs/launch
  2. Map the Launch property ID and environment
  3. AEM automatically injects the Launch embed script on pages that reference the configuration
/conf/mysite/settings/cloudconfigs/launch/
└── jcr:content
    ├── libraryUri = "//assets.adobedtm.com/launch-XXXXX.min.js"
    └── reportSuiteId = "mysite-prod"

Apply the cloud configuration to your site's page hierarchy:

/content/mysite/jcr:content
└── cq:cloudserviceconfigs = ["/conf/mysite/settings/cloudconfigs/launch"]

Data Layer Implementation

Sling Model Approach

Create a Sling Model that exposes page data as JSON:

// core/src/main/java/com/mysite/core/models/AnalyticsDataLayer.java

@Model(adaptables = SlingHttpServletRequest.class,
       defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class AnalyticsDataLayer {

    @ScriptVariable
    private Page currentPage;

    @SlingObject
    private Resource resource;

    @Self
    private SlingHttpServletRequest request;

    public String getDataLayerJson() {
        JsonObject data = new JsonObject();
        data.addProperty("platform", "aem");
        data.addProperty("pageTitle", currentPage.getTitle());
        data.addProperty("pagePath", currentPage.getPath());
        data.addProperty("pageTemplate", currentPage.getTemplate().getPath());
        data.addProperty("language", currentPage.getLanguage().toString());
        data.addProperty("lastModified",
            currentPage.getLastModified() != null
                ? currentPage.getLastModified().toInstant().toString()
                : null);

        // Component inventory on the page
        JsonArray components = new JsonArray();
        Resource content = currentPage.getContentResource();
        if (content != null) {
            collectComponents(content, components);
        }
        data.add("components", components);

        return data.toString();
    }

    private void collectComponents(Resource resource, JsonArray components) {
        String type = resource.getResourceType();
        if (type != null && type.startsWith("mysite/")) {
            components.add(type);
        }
        for (Resource child : resource.getChildren()) {
            collectComponents(child, components);
        }
    }
}

Reference it in HTL:

<sly data-sly-use.analytics="com.mysite.core.models.AnalyticsDataLayer">
  <script>
    window.digitalData = ${analytics.dataLayerJson @ context='unsafe'};
  </script>
</sly>

AEM Core Components Data Layer

AEM Core Components (v2.4+) include a built-in data layer. Enable it in the page policy:

{
  "dataLayer": {
    "enabled": true
  }
}

This populates adobeDataLayer on every page with component-level data, including component IDs, types, and content metadata. It uses the Adobe Client Data Layer standard, which integrates directly with Adobe Launch rules.


Editable Templates and Script Injection

AEM's editable templates (introduced in AEM 6.3) define page structure through policies. Assign clientlib categories at the template policy level:

/conf/mysite/settings/wcm/templates/content-page/policies/
└── jcr:content
    └── root
        └── responsivegrid
            └── cq:policy → /conf/mysite/settings/wcm/policies/wcm/foundation/components/page/policy_analytics

The policy node:

<policy_analytics
    jcr:primaryType="nt:unstructured"
    clientlibs="[mysite.analytics,mysite.base]"
    clientlibsJsHead="[mysite.analytics]"/>

This ensures every page using the template includes the analytics clientlib, without requiring authors to add it manually.


Common Issues

Dispatcher Cache Serving Stale Data Layer

The Dispatcher caches full HTML pages. If your data layer contains dynamic values (user segment, A/B test variant), those values are cached with the first response.

Solutions:

  • Use Sling Dynamic Include (SDI) to mark the data layer component as uncacheable:
<!-- In the page component HTL -->
<sly data-sly-resource="${'datalayer' @ resourceType='mysite/components/datalayer',
     decorationTagName='sly',
     addSelectors='nocache'}"/>

Configure the SDI filter in the Dispatcher to fetch that component via ESI or SSI.

  • Alternatively, fetch dynamic data client-side:
fetch('/content/mysite/en/jcr:content.analytics.json')
  .then(r => r.json())
  .then(data => {
    window.digitalData = Object.assign(window.digitalData || {}, data);
    window.adobeDataLayer.push({ event: 'context-loaded' });
  });

Clientlib Not Loading After Deployment

After deploying via Cloud Manager, clientlibs may not appear if:

  • The clientlib category name does not match what the template references
  • The js.txt manifest file is missing or has incorrect paths
  • The clientlib is not compiled (check for build errors in Cloud Manager logs)

Debug clientlibs by appending ?debugClientLibs=true to any page URL on the author instance, or check /libs/granite/ui/content/dumplibs.html.

HTL Context Filtering Breaks JSON Output

HTL escapes output by default. If your data layer JSON is being HTML-encoded, you are missing the context directive:

<!-- Wrong: JSON will be escaped -->
<script>var data = ${model.json};</script>

<!-- Correct: raw JSON output -->
<script>var data = ${model.json @ context='unsafe'};</script>

Only use unsafe for server-generated, trusted content.

Adobe Launch Firing Before Data Layer

If Launch rules execute before digitalData is populated, the data layer values are undefined. Ensure the data layer script is in <head> before the Launch embed, or use Launch's Library Loaded event with a custom condition that checks for data layer readiness.


Platform-Specific Considerations

AEM as a Cloud Service vs. AEM 6.5 -- Cloud Service deployments are immutable. You cannot modify Dispatcher configuration at runtime; all changes go through Cloud Manager. On 6.5, Dispatcher is managed manually and you have more flexibility with caching rules for analytics endpoints.

Component-level tracking -- AEM's component architecture means each component can emit its own analytics events. Use a consistent pattern: each component's clientlib pushes to adobeDataLayer when the component renders or when a user interacts with it.

// Component-level event in a clientlib
document.querySelectorAll('[data-cmp-type="accordion"]').forEach(el => {
  el.addEventListener('click', function(e) {
    window.adobeDataLayer = window.adobeDataLayer || [];
    window.adobeDataLayer.push({
      event: 'component-click',
      component: {
        type: 'accordion',
        id: el.dataset.cmpId,
        title: el.querySelector('.cmp-accordion__title')?.textContent
      }
    });
  });
});

Author vs. Publish -- Analytics should only fire on Publish instances. Check the run mode in your Sling Model or use a clientlib category that is only included in Publish-mode templates. The WCM mode can be checked in HTL:

<sly data-sly-test="${!wcmmode.edit && !wcmmode.preview}">
  <!-- Analytics scripts here -->
</sly>

Multisite with Language Copies -- AEM's MSM (Multi-Site Manager) creates language copies that share structure but differ in content. Ensure your data layer includes the hreflang and site section so analytics can segment by locale without cross-contamination.