/**
* Infralytiqs bootstrap for an AEM as a Cloud Service published site.
*
* Deployed as a clientlib and referenced from the TenantService factory
* configuration's infralytiqsClientBootstrapLibs mapping, e.g.
* www.acme.com:/etc.clientlibs/acme/clientlibs/infralytiqs/bootstrap.js
*
* The SDK injects this file itself once window.IL_CLIENT_BOOTSTRAP_LIB is set,
* so by the time this runs, window.Infralytiqs is available.
*/
(function (window, document) {
'use strict';
// The HTL fragment renders these. Bail out rather than guessing: an event
// sent with the wrong tenant is worse than no event at all.
var serverUrl = window.IL_SERVER_URL;
var tenantId = window.IL_TENANT_ID;
var siteId = window.IL_SITE_ID;
if (!window.Infralytiqs || !serverUrl || !tenantId || !siteId) {
return;
}
/** Read a data attribute from the clicked element or its nearest ancestor. */
function closestAttr(el, attr) {
var match = el && el.closest ? el.closest('[' + attr + ']') : null;
return match ? (match.getAttribute(attr) || '') : '';
}
Infralytiqs.init({
serverUrl: serverUrl,
tenantId: tenantId,
siteId: siteId,
dbName: window.IL_DB_NAME || undefined,
// Attach the AEM page context to every event, so a report can group by
// template or by the content path without parsing URLs.
globalDimensions: {
aem_template: document.documentElement.getAttribute('data-aem-template') || '',
aem_page_path: document.documentElement.getAttribute('data-aem-page-path') || '',
aem_language: document.documentElement.lang || '',
aem_environment: window.IL_AEM_ENV || 'publish'
},
// Auto-captured clicks resolve these into custom_dimensions. Values may be
// a CSS selector or a function receiving the matched element.
evarMap: {
// Core Components render data-cmp-* attributes; lean on them rather than
// inventing a parallel markup contract.
component_type: function (el) { return closestAttr(el, 'data-cmp-data-layer') ? 'core' : ''; },
component_id: function (el) {
var host = el.closest('[id][class*="cmp-"]');
return host ? host.id : '';
},
link_text: function (el) { return (el.textContent || '').trim().slice(0, 120); },
link_href: function (el) { return el.getAttribute('href') || ''; },
asset_id: '[data-asset-id]',
cta_name: '[data-il-cta]'
},
propMap: {
// Scroll depth at the moment of the click, as a whole percentage.
scroll_depth: function () {
var h = document.documentElement;
var scrollable = h.scrollHeight - h.clientHeight;
return scrollable > 0 ? Math.round((h.scrollTop / scrollable) * 100) : 0;
}
},
debug: /[?&]ilDebug=1/.test(window.location.search)
});
// ── AEM-specific hooks ────────────────────────────────────────────────
// Everything below is behaviour the generic SDK cannot infer.
// 1. DAM asset downloads from the published site. Publish-side downloads are
// invisible to the author-side filter, so capture them here.
document.addEventListener('click', function (event) {
var link = event.target.closest && event.target.closest('a[href*="/content/dam/"]');
if (!link) { return; }
var href = link.getAttribute('href') || '';
Infralytiqs.track('asset_download', {
asset_path: href.split('?')[0],
asset_format: (href.split('?')[0].split('.').pop() || '').toLowerCase(),
download_source: 'publish',
link_text: (link.textContent || '').trim().slice(0, 120)
}, { asset_download_count: 1 }, 'asset_download_status_initiated');
}, { capture: true, passive: true });
// 2. Site search. The dimension key matters: search_term is MATERIALIZED
// into its own ClickHouse column, so it is cheap to group by.
var searchForm = document.querySelector('form[data-il-search], .cmp-search form');
if (searchForm) {
searchForm.addEventListener('submit', function () {
var input = searchForm.querySelector('input[type="search"], input[name="q"], input[name="fulltext"]');
var term = input && input.value ? input.value.trim() : '';
if (term) {
Infralytiqs.track('site_search', { search_term: term.slice(0, 200) });
}
});
}
// 3. Adaptive Forms and Core Component forms.
Array.prototype.forEach.call(
document.querySelectorAll('form[data-cmp-is="form"], form.cmp-form'),
function (form) {
form.addEventListener('submit', function () {
Infralytiqs.track('form_submit', {
form_name: form.getAttribute('name') || form.id || 'unnamed',
form_action: form.getAttribute('action') || ''
}, { form_field_count: form.elements.length });
});
}
);
// 4. Experience Fragment and content-block visibility. Useful for measuring
// whether a campaign block was ever actually seen.
if ('IntersectionObserver' in window) {
var seen = new WeakSet();
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting || seen.has(entry.target)) { return; }
seen.add(entry.target);
Infralytiqs.track('block_view', {
block_name: entry.target.getAttribute('data-il-block') || entry.target.id || 'unnamed',
block_type: entry.target.getAttribute('data-il-block-type') || ''
});
});
}, { threshold: 0.5 });
Array.prototype.forEach.call(
document.querySelectorAll('[data-il-block]'),
function (node) { observer.observe(node); }
);
}
// 5. Identify the visitor when AEM knows who they are — a closed user group,
// or a personalisation context. Never send an email address or any other
// direct identifier here; send the opaque id you already use.
if (window.IL_USER_ID) {
Infralytiqs.identify(window.IL_USER_ID);
}
// 6. Flush on the way out so the last interaction of a session is not lost.
// pagehide is more reliable than unload on mobile Safari.
window.addEventListener('pagehide', function () {
Infralytiqs.flush();
});
})(window, document);
package com.yourproject.core.models;
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.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import com.adobexp.infralytiqs.service.TenantService;
import com.adobexp.infralytiqs.service.TenantServiceManager;
/**
* Resolves the Infralytiqs tuple for the current page from the TenantService
* factory configuration whose rootPath is the closest match, so one codebase
* can serve several brands without any brand-specific Java.
*/
@Model(adaptables = SlingHttpServletRequest.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class InfralytiqsBootstrapModel {
@SlingObject
private Resource resource;
@SlingObject
private SlingHttpServletRequest request;
@OSGiService
private TenantServiceManager tenantServiceManager;
private TenantService tenant;
@PostConstruct
protected void init() {
if (tenantServiceManager != null && resource != null) {
tenant = tenantServiceManager.getConfigForPath(resource.getPath());
}
}
/** False when no factory configuration covers this content root. */
public boolean isEnabled() {
return tenant != null
&& isNotBlank(tenant.getTenantId())
&& isNotBlank(tenant.getSiteId());
}
public String getAnalyticsServerUrl() {
return tenant == null ? "" : nullSafe(tenant.getAnalyticsServerUrl());
}
public String getTenantId() {
return tenant == null ? "" : nullSafe(tenant.getTenantId());
}
public String getSiteId() {
return tenant == null ? "" : nullSafe(tenant.getSiteId());
}
public String getDbName() {
return tenant == null ? "" : nullSafe(tenant.getDbName());
}
/**
* The per-host bootstrap path from infralytiqsClientBootstrapLibs. Resolved
* against the request's Host header, so a single configuration can serve
* several domains with different bootstrap scripts.
*/
public String getClientBootstrapLib() {
if (tenant == null) {
return "";
}
return nullSafe(tenant.getClientBootstrapLib(request.getServerName()));
}
private static String nullSafe(String value) {
return value == null ? "" : value;
}
private static boolean isNotBlank(String value) {
return value != null && !value.trim().isEmpty();
}
}
<!--/*
apps/<yourproject>/components/structure/page/customfooterlibs.html
Hook the fragment in at the end of the page. Loading it here rather than in
the head means the SDK never blocks first paint, and the DOM the bootstrap
queries for forms and blocks already exists.
*/-->
<sly data-sly-include="partials/Infralytiqs.html"/>
<!--/*
While authoring, the fragment renders nothing at all — the wcmmode test in
Infralytiqs.html suppresses it in edit and preview. Author-side activity is
the OSGi bundle's job, and double-counting an author previewing a page as
published-site traffic would quietly corrupt your numbers.
*/-->